diff --git a/dev/.documenter-siteinfo.json b/dev/.documenter-siteinfo.json index 8282950e..5009c5f1 100644 --- a/dev/.documenter-siteinfo.json +++ b/dev/.documenter-siteinfo.json @@ -1 +1 @@ -{"documenter":{"julia_version":"1.11.2","generation_timestamp":"2024-12-21T07:27:33","documenter_version":"1.8.0"}} \ No newline at end of file +{"documenter":{"julia_version":"1.11.2","generation_timestamp":"2024-12-21T09:28:19","documenter_version":"1.8.0"}} \ No newline at end of file diff --git a/dev/adaptive_kalmanfilter/307702a0.svg b/dev/adaptive_kalmanfilter/8d916370.svg similarity index 86% rename from dev/adaptive_kalmanfilter/307702a0.svg rename to dev/adaptive_kalmanfilter/8d916370.svg index 6f9754b8..62fd0b63 100644 --- a/dev/adaptive_kalmanfilter/307702a0.svg +++ b/dev/adaptive_kalmanfilter/8d916370.svg @@ -1,44 +1,44 @@ - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/adaptive_kalmanfilter/bfc60acd.svg b/dev/adaptive_kalmanfilter/ac409e6f.svg similarity index 86% rename from dev/adaptive_kalmanfilter/bfc60acd.svg rename to dev/adaptive_kalmanfilter/ac409e6f.svg index 30786956..0bc14ccf 100644 --- a/dev/adaptive_kalmanfilter/bfc60acd.svg +++ b/dev/adaptive_kalmanfilter/ac409e6f.svg @@ -1,74 +1,74 @@ - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/adaptive_kalmanfilter/8c81adda.svg b/dev/adaptive_kalmanfilter/b5045006.svg similarity index 88% rename from dev/adaptive_kalmanfilter/8c81adda.svg rename to dev/adaptive_kalmanfilter/b5045006.svg index 58aa53b5..2861f9f5 100644 --- a/dev/adaptive_kalmanfilter/8c81adda.svg +++ b/dev/adaptive_kalmanfilter/b5045006.svg @@ -1,48 +1,48 @@ - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/adaptive_kalmanfilter/59e85a4f.svg b/dev/adaptive_kalmanfilter/dbe90c0d.svg similarity index 87% rename from dev/adaptive_kalmanfilter/59e85a4f.svg rename to dev/adaptive_kalmanfilter/dbe90c0d.svg index 5de94659..4dd619e6 100644 --- a/dev/adaptive_kalmanfilter/59e85a4f.svg +++ b/dev/adaptive_kalmanfilter/dbe90c0d.svg @@ -1,46 +1,46 @@ - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/adaptive_kalmanfilter/c6b6935d.svg b/dev/adaptive_kalmanfilter/df5b83a3.svg similarity index 87% rename from dev/adaptive_kalmanfilter/c6b6935d.svg rename to dev/adaptive_kalmanfilter/df5b83a3.svg index 1c462fe5..21b7d423 100644 --- a/dev/adaptive_kalmanfilter/c6b6935d.svg +++ b/dev/adaptive_kalmanfilter/df5b83a3.svg @@ -1,46 +1,46 @@ - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/adaptive_kalmanfilter/1797bee1.svg b/dev/adaptive_kalmanfilter/ee8991d8.svg similarity index 87% rename from dev/adaptive_kalmanfilter/1797bee1.svg rename to dev/adaptive_kalmanfilter/ee8991d8.svg index a837ed59..f83c9c38 100644 --- a/dev/adaptive_kalmanfilter/1797bee1.svg +++ b/dev/adaptive_kalmanfilter/ee8991d8.svg @@ -1,46 +1,46 @@ - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/adaptive_kalmanfilter/index.html b/dev/adaptive_kalmanfilter/index.html index 04066783..1b46d8d9 100644 --- a/dev/adaptive_kalmanfilter/index.html +++ b/dev/adaptive_kalmanfilter/index.html @@ -9,7 +9,7 @@ x = [zeros(50); 0:100] T = length(x) Y = x + randn(T) -plot([Y x], lab=["Measurement" "True state to be tracked"], c=[1 :purple])Example block output

Simple Kalman filtering

We will use a Kalman filter to perform the filtering. The model is a double integrator, i.e., a constant-acceleration model. The state vector is thus $x = [p, v]^T$, where $p$ is the position and $v$ is the velocity. When designing a Kalman filter, we need to specify the noise covariances $R_1$ and $R_2$. While it's often easy to measure the covariance of the measurement noise, $R_2$, it can be quite difficult to know ahead of time what the dynamics noise covariance, $R_1$, should be. In this example, we will use an adaptive filter, where we will increase the dynamics noise covariance if the filter prediction error is too large. However, we first run the filter twice, once with a large $R_1$ and once with a small $R_1$ to illustrate the difference.

y = [[y] for y in Y] # create a vector of vectors for the KF
+plot([Y x], lab=["Measurement" "True state to be tracked"], c=[1 :purple])
Example block output

Simple Kalman filtering

We will use a Kalman filter to perform the filtering. The model is a double integrator, i.e., a constant-acceleration model. The state vector is thus $x = [p, v]^T$, where $p$ is the position and $v$ is the velocity. When designing a Kalman filter, we need to specify the noise covariances $R_1$ and $R_2$. While it's often easy to measure the covariance of the measurement noise, $R_2$, it can be quite difficult to know ahead of time what the dynamics noise covariance, $R_1$, should be. In this example, we will use an adaptive filter, where we will increase the dynamics noise covariance if the filter prediction error is too large. However, we first run the filter twice, once with a large $R_1$ and once with a small $R_1$ to illustrate the difference.

y = [[y] for y in Y] # create a vector of vectors for the KF
 u = fill([], T) # No inputs in this example :(
 
 # Define the model
@@ -38,7 +38,7 @@
     Yh = reduce(hcat, yh)
     plot!(Yh', lab="Estimate \$σ_w\$ = $σw")
 end
-fig
Example block output

When $R_1$ is small (controlled by $σ_w$), we get a nice and smooth filter estimate, but this estimate clearly lags behind the true state. When $R_1$ is large, the filter estimate is much more responsive, but it also has a lot of noise.

Adaptive noise covariance

Below, we will implement an adaptive filter, where we keep the dynamics noise covariance low by default, but increase it if the filter prediction error is too large. We will use a Z-score to determine if the prediction error is too large. The Z-score is defined as the number of standard deviations the prediction error is away from the estimated mean. This time around we use separate correct! and predict! calls, so that we can access the prediction error as well as the prior covariance of the prediction error, $S$. $S$ (or the Cholesky factor $Sᵪ$) will be used to compute the Z-score.

σw = 1e-5 # Set the covariance to a low value by default
+fig
Example block output

When $R_1$ is small (controlled by $σ_w$), we get a nice and smooth filter estimate, but this estimate clearly lags behind the true state. When $R_1$ is large, the filter estimate is much more responsive, but it also has a lot of noise.

Adaptive noise covariance

Below, we will implement an adaptive filter, where we keep the dynamics noise covariance low by default, but increase it if the filter prediction error is too large. We will use a Z-score to determine if the prediction error is too large. The Z-score is defined as the number of standard deviations the prediction error is away from the estimated mean. This time around we use separate correct! and predict! calls, so that we can access the prediction error as well as the prior covariance of the prediction error, $S$. $S$ (or the Cholesky factor $Sᵪ$) will be used to compute the Z-score.

σw = 1e-5 # Set the covariance to a low value by default
 R1 = σw*[Ts^3/3 Ts^2/2; Ts^2/2 Ts]
 kf = KalmanFilter(A, B, C, D, R1, R2)
 measure = LowLevelParticleFilters.measurement(kf)
@@ -64,7 +64,7 @@
 end
 
 Yh = reduce(hcat, yh)
-plot([Y Yh'], lab=["Measurement" "Adaptive estimate"])
Example block output

Not too bad! This time the filter estimate is much more responsive during the transition, but exhibits favorable noise properties during the stationary phases. We can also plot the prediction error and the Z-score to see how the filter adapts to the dynamics noise covariance.

plot([es σs], lab=["Prediction error" "Z-score"])
Example block output

Notice how the prediction errors, that should ideally be centered around zero, remain predominantly negative for a long time interval after the transition. This can be attributed to an overshoot in the velocity state of the estimator, but the rapid decrease of the covariance after the transition makes the filter slow at correcting its overshoot. If we want, we could mitigate this and make the adaptation even more sophisticated by letting the covariance remain large for a while after a transition in operating mode has been detected. Below, we implement a simple version of this, where we use a multiplier $σ_{wt}$ that defaults to 1, but is increase to a very large value of 1000 if a transition is detected. When no transition is detected, $σ_{wt}$ is decreased exponentially back down to 1.

σw  = 1e-5 # Set the covariance to a low value by default
+plot([Y Yh'], lab=["Measurement" "Adaptive estimate"])
Example block output

Not too bad! This time the filter estimate is much more responsive during the transition, but exhibits favorable noise properties during the stationary phases. We can also plot the prediction error and the Z-score to see how the filter adapts to the dynamics noise covariance.

plot([es σs], lab=["Prediction error" "Z-score"])
Example block output

Notice how the prediction errors, that should ideally be centered around zero, remain predominantly negative for a long time interval after the transition. This can be attributed to an overshoot in the velocity state of the estimator, but the rapid decrease of the covariance after the transition makes the filter slow at correcting its overshoot. If we want, we could mitigate this and make the adaptation even more sophisticated by letting the covariance remain large for a while after a transition in operating mode has been detected. Below, we implement a simple version of this, where we use a multiplier $σ_{wt}$ that defaults to 1, but is increase to a very large value of 1000 if a transition is detected. When no transition is detected, $σ_{wt}$ is decreased exponentially back down to 1.

σw  = 1e-5 # Set the covariance to a low value by default
 σwt = 1.0
 R1  = σw*[Ts^3/3 Ts^2/2; Ts^2/2 Ts]
 kf  = KalmanFilter(A, B, C, D, R1, R2)
@@ -94,4 +94,4 @@
 end
 
 Yh = reduce(hcat, yh)
-plot([Y Yh'], lab=["Measurement" "Adaptive estimate"])
Example block output
plot([es σs σwts], lab=["Prediction error" "Z-score" "\$σ_{wt}\$ multiplier"], layout=2, sp=[1 1 2])
Example block output

This time, the prediction errors look more like white noise centered around zero after the initial transient caused by the transition.

Summary

This tutorial demonstrated simple Kalman filtering for a double integrator without control inputs. We saw how the filtering estimate could be improved by playing around with the covariance matrices of the estimator, helping it catch up to fast changes in the behavior of the system without sacrificing steady-state noise properties.

In this case, we handled the modification of $R_1$ outside of the filter, implementing our own filtering loop. Some applications get away with instead providing time-varying matrices in the form of a 3-dimension array, where the third dimension corresponds to time, or instead of providing a matrix, providing a function $R_1(x, u, p, t)$ allows the matrix to be a function of state, input, parameters and time. These options apply to all matrices in the filter, including the dynamics matrices, $A,B,C,D$.

Lastly, we mention the ability of the KalmanFilter to act like a recursive least-squares estimator, by setting the "forgetting factor $α>1$ when creating the KalmanFilter. $α>1$ will cause the filter will exhibit exponential forgetting similar to an RLS estimator, in addition to the covariance inflation due to R1. It is thus possible to get a RLS-like algorithm by setting $R_1 = 0, R_2 = 1/α$ and $α > 1$.

Disturbance modeling and noise tuning

See this notebook for a blog post about disturbance modeling and noise tuning using LowLevelParticleFilter.jl

+plot([Y Yh'], lab=["Measurement" "Adaptive estimate"])Example block output
plot([es σs σwts], lab=["Prediction error" "Z-score" "\$σ_{wt}\$ multiplier"], layout=2, sp=[1 1 2])
Example block output

This time, the prediction errors look more like white noise centered around zero after the initial transient caused by the transition.

Summary

This tutorial demonstrated simple Kalman filtering for a double integrator without control inputs. We saw how the filtering estimate could be improved by playing around with the covariance matrices of the estimator, helping it catch up to fast changes in the behavior of the system without sacrificing steady-state noise properties.

In this case, we handled the modification of $R_1$ outside of the filter, implementing our own filtering loop. Some applications get away with instead providing time-varying matrices in the form of a 3-dimension array, where the third dimension corresponds to time, or instead of providing a matrix, providing a function $R_1(x, u, p, t)$ allows the matrix to be a function of state, input, parameters and time. These options apply to all matrices in the filter, including the dynamics matrices, $A,B,C,D$.

Lastly, we mention the ability of the KalmanFilter to act like a recursive least-squares estimator, by setting the "forgetting factor $α>1$ when creating the KalmanFilter. $α>1$ will cause the filter will exhibit exponential forgetting similar to an RLS estimator, in addition to the covariance inflation due to R1. It is thus possible to get a RLS-like algorithm by setting $R_1 = 0, R_2 = 1/α$ and $α > 1$.

Disturbance modeling and noise tuning

See this notebook for a blog post about disturbance modeling and noise tuning using LowLevelParticleFilter.jl

diff --git a/dev/api/index.html b/dev/api/index.html index b42b483c..eb08bae6 100644 --- a/dev/api/index.html +++ b/dev/api/index.html @@ -1,13 +1,13 @@ -API · LowLevelParticleFilters Documentation

Exported functions and types

Index

LowLevelParticleFilters.AdvancedParticleFilterMethod
AdvancedParticleFilter(N::Integer, dynamics::Function, measurement::Function, measurement_likelihood, dynamics_density, initial_density; p = NullParameters(), threads = false, kwargs...)

This type represents a standard particle filter but affords extra flexibility compared to the ParticleFilter type, e.g., non-additive noise in the dynamics and measurement functions.

See the docs for more information: https://baggepinnen.github.io/LowLevelParticleFilters.jl/stable/#AdvancedParticleFilter-1

Arguments:

  • N: Number of particles
  • dynamics: A discrete-time dynamics function (x, u, p, t, noise=false) -> x⁺. It's important that the noise argument defaults to false.
  • measurement: A measurement function (x, u, p, t, noise=false) -> y. It's important that the noise argument defaults to false.
  • measurement_likelihood: A function (x, u, y, p, t)->logl to evaluate the log-likelihood of a measurement.
  • dynamics_density: This field is not used by the advanced filter and can be set to nothing.
  • initial_density: The distribution of the initial state.
  • threads: use threads to propagate particles in parallel. Only activate this if your dynamics is thread-safe. SeeToDee.SimpleColloc is not thread-safe by default due to the use of internal caches, but SeeToDee.Rk4 is.

Extended help

Multiple measurement models

The measurement_likelihood function is used to evaluate the likelihood of a measurement. If you have multiple sensors and want to perform individual correct! steps for each, call correct!(..., g = custom_likelihood_function).

source
LowLevelParticleFilters.CompositeMeasurementModelMethod
CompositeMeasurementModel(model1, model2, ...)

A composite measurement model that combines multiple measurement models. This model acts as all component models concatenated. The tuple returned from correct! will be

  • ll: The sum of the log-likelihood of all component models
  • e: The concatenated innovation vector
  • S: A vector of the innovation covariance matrices of the component models
  • Sᵪ: A vector of the Cholesky factorizations of the innovation covariance matrices of the component models
  • K: A vector of the Kalman gains of the component models

If all sensors operate on at the same rate, and all measurement models are of the same type, it's more efficient to use a single measurement model with a vector-valued measurement function.

Fields:

  • models: A tuple of measurement models
source
LowLevelParticleFilters.EKFMeasurementModelMethod
EKFMeasurementModel{IPM}(measurement, R2, ny, Cjac, cache = nothing)

A measurement model for the Extended Kalman Filter.

Arguments:

  • IPM: A boolean indicating if the measurement function is inplace
  • measurement: The measurement function y = h(x, u, p, t)
  • R2: The measurement noise covariance matrix
  • ny: The number of measurement variables
  • Cjac: The Jacobian of the measurement function Cjac(x, u, p, t). If none is provided, ForwardDiff will be used.
source
LowLevelParticleFilters.ExtendedKalmanFilterType
ExtendedKalmanFilter(kf, dynamics, measurement; Ajac, Cjac)
+API · LowLevelParticleFilters Documentation

Exported functions and types

Index

LowLevelParticleFilters.AdvancedParticleFilterMethod
AdvancedParticleFilter(N::Integer, dynamics::Function, measurement::Function, measurement_likelihood, dynamics_density, initial_density; p = NullParameters(), threads = false, kwargs...)

This type represents a standard particle filter but affords extra flexibility compared to the ParticleFilter type, e.g., non-additive noise in the dynamics and measurement functions.

See the docs for more information: https://baggepinnen.github.io/LowLevelParticleFilters.jl/stable/#AdvancedParticleFilter-1

Arguments:

  • N: Number of particles
  • dynamics: A discrete-time dynamics function (x, u, p, t, noise=false) -> x⁺. It's important that the noise argument defaults to false.
  • measurement: A measurement function (x, u, p, t, noise=false) -> y. It's important that the noise argument defaults to false.
  • measurement_likelihood: A function (x, u, y, p, t)->logl to evaluate the log-likelihood of a measurement.
  • dynamics_density: This field is not used by the advanced filter and can be set to nothing.
  • initial_density: The distribution of the initial state.
  • threads: use threads to propagate particles in parallel. Only activate this if your dynamics is thread-safe. SeeToDee.SimpleColloc is not thread-safe by default due to the use of internal caches, but SeeToDee.Rk4 is.

Extended help

Multiple measurement models

The measurement_likelihood function is used to evaluate the likelihood of a measurement. If you have multiple sensors and want to perform individual correct! steps for each, call correct!(..., g = custom_likelihood_function).

source
LowLevelParticleFilters.CompositeMeasurementModelMethod
CompositeMeasurementModel(model1, model2, ...)

A composite measurement model that combines multiple measurement models. This model acts as all component models concatenated. The tuple returned from correct! will be

  • ll: The sum of the log-likelihood of all component models
  • e: The concatenated innovation vector
  • S: A vector of the innovation covariance matrices of the component models
  • Sᵪ: A vector of the Cholesky factorizations of the innovation covariance matrices of the component models
  • K: A vector of the Kalman gains of the component models

If all sensors operate on at the same rate, and all measurement models are of the same type, it's more efficient to use a single measurement model with a vector-valued measurement function.

Fields:

  • models: A tuple of measurement models
source
LowLevelParticleFilters.EKFMeasurementModelMethod
EKFMeasurementModel{IPM}(measurement, R2, ny, Cjac, cache = nothing)

A measurement model for the Extended Kalman Filter.

Arguments:

  • IPM: A boolean indicating if the measurement function is inplace
  • measurement: The measurement function y = h(x, u, p, t)
  • R2: The measurement noise covariance matrix
  • ny: The number of measurement variables
  • Cjac: The Jacobian of the measurement function Cjac(x, u, p, t). If none is provided, ForwardDiff will be used.
source
LowLevelParticleFilters.ExtendedKalmanFilterType
ExtendedKalmanFilter(kf, dynamics, measurement; Ajac, Cjac)
 ExtendedKalmanFilter(dynamics, measurement, R1,R2,d0=MvNormal(Matrix(R1)); nu::Int, p = NullParameters(), α = 1.0, check = true)

A nonlinear state estimator propagating uncertainty using linearization.

The constructor to the extended Kalman filter takes dynamics and measurement functions, and either covariance matrices, or a KalmanFilter. If the former constructor is used, the number of inputs to the system dynamics, nu, must be explicitly provided with a keyword argument.

By default, the filter will internally linearize the dynamics using ForwardDiff. User provided Jacobian functions can be provided as keyword arguments Ajac and Cjac. These functions should have the signature (x,u,p,t)::AbstractMatrix where x is the state, u is the input, p is the parameters, and t is the time.

The dynamics and measurement function are on the following form

x(t+1) = dynamics(x, u, p, t) + w
-y      = measurement(x, u, p, t) + e

where w ~ N(0, R1), e ~ N(0, R2) and x(0) ~ d0

See also UnscentedKalmanFilter which is typically more accurate than ExtendedKalmanFilter. See KalmanFilter for detailed instructions on how to set up a Kalman filter kf.

source
LowLevelParticleFilters.IMMMethod
IMM(models, P, μ; check = true, p = NullParameters(), interact = true)

Interacting Multiple Model (IMM) filter. This filter is a combination of multiple Kalman-type filters, each with its own state and covariance. The IMM filter is a probabilistically weighted average of the states and covariances of the individual filters. The weights are determined by the probability matrix P and the mixing probabilities μ.

Experimental

This filter is currently considered experimental and the user interface may change in the future without respecting semantic versioning.

In addition to the predict! and correct! steps, the IMM filter has an interact! method that updates the states and covariances of the individual filters based on the mixing probabilities. The combine! method combines the states and covariances of the individual filters into a single state and covariance. These four functions are typically called in either of the orders

  • correct!, combine!, interact!, predict! (as is done in update!)
  • interact!, predict!, correct!, combine! (as is done in the reference cited below)

These two orders are cyclic permutations of each other, and the order used in update! is chosen to align with the order used in the other filters, where the initial condition is corrected using the first measurement, i.e., we assume the first measurement updates $x(0|-1)$ to $x(0|0)$.

The initial (combined) state and covariance of the IMM filter is made up of the weighted average of the states and covariances of the individual filters. The weights are the initial mixing probabilities μ.

Ref: "Interacting multiple model methods in target tracking: a survey", E. Mazor; A. Averbuch; Y. Bar-Shalom; J. Dayan

Arguments:

  • models: An array of Kalman-type filters, such as KalmanFilter, ExtendedKalmanFilter, UnscentedKalmanFilter, etc. The state of each model must have the same meaning, such that forming a weighted average makes sense.
  • P: The mode-transition probability matrix. P[i,j] is the probability of transitioning from mode i to mode j (each row must sum to one).
  • μ: The initial mixing probabilities. μ[i] is the probability of being in mode i at the initial contidion (must sum to one).
  • check: If true, check that the inputs are valid. If false, skip the checks.
  • p: Parameters for the filter. NOTE: this p is shared among all internal filters. The internal p of each filter will be overridden by this one.
  • interact: If true, the filter will run the interaction as part of update! and forward_trajectory. If false, the filter will not run the interaction step. This choice can be overridden by passing the keyword argument interact to the respective functions.
source
LowLevelParticleFilters.KalmanFilterType
KalmanFilter(A,B,C,D,R1,R2,d0=MvNormal(R1); p = NullParameters(), α=1, check=true)

The matrices A,B,C,D define the dynamics

x' = Ax + Bu + w
-y  = Cx + Du + e

where w ~ N(0, R1), e ~ N(0, R2) and x(0) ~ d0

The matrices can be time varying such that, e.g., A[:, :, t] contains the $A$ matrix at time index t. They can also be given as functions on the form

Afun(x, u, p, t) -> A

For maximum performance, provide statically sized matrices from StaticArrays.jl

α is an optional "forgetting factor", if this is set to a value > 1, such as 1.01-1.2, the filter will, in addition to the covariance inflation due to $R_1$, exhibit "exponential forgetting" similar to a Recursive Least-Squares (RLS) estimator. It is thus possible to get a RLS-like algorithm by setting $R_1=0, R_2 = 1/α$ and $α > 1$ ($α$ is the inverse of the traditional RLS parameter $α = 1/λ$). The exact form of the covariance update is

\[R(t+1|t) = α AR(t)A^T + R_1\]

If check = true (default) the function will check that the eigenvalues of A are less than 2 in absolute value. Large eigenvalues may be an indication that the system matrices are representing a continuous-time system and the user has forgotten to discretize it. Turn off this check by setting check = false.

Tutorials on Kalman filtering

The tutorial "How to tune a Kalman filter" details how to figure out appropriate covariance matrices for the Kalman filter, as well as how to add disturbance models to the system model. See also the tutorial in the documentation

source
LowLevelParticleFilters.KalmanFilteringSolutionType
KalmanFilteringSolution{Tx,Txt,TR,TRt,Tll} <: AbstractFilteringSolution

Fields

  • x: predictions $x(t+1|t)$ (plotted if plotx=true)
  • xt: filtered estimates $x(t|t)$ (plotted if plotxt=true)
  • R: predicted covariance matrices $R(t+1|t)$ (plotted if plotR=true)
  • Rt: filter covariances $R(t|t)$ (plotted if plotRt=true)
  • ll: loglikelihood
  • e: prediction errors $e(t|t-1) = y - ŷ(t|t-1)$ (plotted if plote=true)

Plot

The solution object can be plotted

plot(sol, plotx=true, plotxt=true, plotR=true, plotRt=true, plote=true, plotu=true, ploty=true, plotyh=true, plotyht=true, name="")

where

  • plotx: Plot the predictions x(t|t-1)
  • plotxt: Plot the filtered estimates x(t|t)
  • plotR: Plot the predicted covariances R(t|t-1) as ribbons at ±2σ (1.96 σ to be precise)
  • plotRt: Plot the filter covariances R(t|t) as ribbons at ±2σ (1.96 σ to be precise)
  • plote: Plot the prediction errors e(t|t-1) = y - ŷ(t|t-1)
  • plotu: Plot the input
  • ploty: Plot the measurements
  • plotyh: Plot the predicted measurements ŷ(t|t-1)
  • plotyht: Plot the filtered measurements ŷ(t|t)
  • name: a string that is prepended to the labels of the plots, which is useful when plotting multiple solutions in the same plot.
source
LowLevelParticleFilters.ParticleFilterMethod
ParticleFilter(N::Integer, dynamics, measurement, dynamics_density, measurement_density, initial_density; threads = false, p = NullParameters(), kwargs...)

See the docs for more information: https://baggepinnen.github.io/LowLevelParticleFilters.jl/stable/#Particle-filter-1

Arguments:

  • N: Number of particles
  • dynamics: A discrete-time dynamics function (x, u, p, t) -> x⁺
  • measurement: A measurement function (x, u, p, t) -> y
  • dynamics_density: A probability-density function for additive noise in the dynamics. Use AdvancedParticleFilter for non-additive noise.
  • measurement_density: A probability-density function for additive measurement noise. Use AdvancedParticleFilter for non-additive noise.
  • initial_density: Distribution of the initial state.
source
LowLevelParticleFilters.ParticleFilteringSolutionType
ParticleFilteringSolution{F, Tu, Ty, Tx, Tw, Twe, Tll} <: AbstractFilteringSolution

Fields:

  • f: The filter used to produce the solution.
  • u: Input
  • y: Output / measurements
  • x: Particles, the size of this array is (N,T), where N is the number of particles and T is the number of time steps. Each element represents a weighted state hypothesis with weight given by we.
  • w: Weights (log space). These are used for internal computations.
  • we: Weights (exponentiated / original space). These are the ones to use to compute weighted means etc., they sum to one for each time step.
  • ll: Log likelihood

Plot

The solution object can be plotted

plot(sol; nbinsy=30, xreal=nothing, dim=nothing, ploty=true)
source
LowLevelParticleFilters.SqKalmanFilterType
SqKalmanFilter(A,B,C,D,R1,R2,d0=MvNormal(R1); p = NullParameters(), α=1)

A standard Kalman filter on square-root form. This filter may have better numerical performance when the covariance matrices are ill-conditioned.

The matrices A,B,C,D define the dynamics

x' = Ax + Bu + w
-y  = Cx + Du + e

where w ~ N(0, R1), e ~ N(0, R2) and x(0) ~ d0

The matrices can be time varying such that, e.g., A[:, :, t] contains the $A$ matrix at time index t. They can also be given as functions on the form

Afun(x, u, p, t) -> A

The internal fields storing covariance matrices are for this filter storing the upper-triangular Cholesky factor.

α is an optional "forgetting factor", if this is set to a value > 1, such as 1.01-1.2, the filter will, in addition to the covariance inflation due to $R_1$, exhibit "exponential forgetting" similar to a Recursive Least-Squares (RLS) estimator. It is thus possible to get a RLS-like algorithm by setting $R_1=0, R_2 = 1/α$ and $α > 1$ ($α$ is the inverse of the traditional RLS parameter $α = 1/λ$). The form of the covariance update is

\[R(t+1|t) = α AR(t)A^T + R_1\]

Ref: "A Square-Root Kalman Filter Using Only QR Decompositions", Kevin Tracy https://arxiv.org/abs/2208.06452

source
LowLevelParticleFilters.UKFMeasurementModelMethod
UKFMeasurementModel{inplace_measurement,augmented_measurement}(measurement, R2, ny, ne, innovation, mean, cov, cross_cov, cache = nothing)

A measurement model for the Unscented Kalman Filter.

Arguments:

  • measurement: The measurement function y = h(x, u, p, t)
  • R2: The measurement noise covariance matrix
  • ny: The number of measurement variables
  • ne: If augmented_measurement is true, the number of measurement noise variables
  • innovation(y::AbstractVector, yh::AbstractVector) where the arguments represent (measured output, predicted output)
  • mean(ys::AbstractVector{<:AbstractVector}): computes the mean of the vector of vectors of output sigma points.
  • cov(ys::AbstractVector{<:AbstractVector}, y::AbstractVector): computes the covariance matrix of the output sigma points.
  • cross_cov(xs::AbstractVector{<:AbstractVector}, x::AbstractVector, ys::AbstractVector{<:AbstractVector}, y::AbstractVector) where the arguments represents (state sigma points, mean state, output sigma points, mean output). The function should return the cross-covariance matrix between the state and output sigma points.
source
LowLevelParticleFilters.UKFMeasurementModelMethod
UKFMeasurementModel{T,IPM,AUGM}(measurement, R2; nx, ny, ne = nothing, innovation = -, mean = safe_mean, cov = safe_cov, cross_cov = cross_cov, static = nothing)
  • T is the element type used for arrays
  • IPM is a boolean indicating if the measurement function is inplace
  • AUGM is a boolean indicating if the measurement model is augmented
source
LowLevelParticleFilters.IMMMethod
IMM(models, P, μ; check = true, p = NullParameters(), interact = true)

Interacting Multiple Model (IMM) filter. This filter is a combination of multiple Kalman-type filters, each with its own state and covariance. The IMM filter is a probabilistically weighted average of the states and covariances of the individual filters. The weights are determined by the probability matrix P and the mixing probabilities μ.

Experimental

This filter is currently considered experimental and the user interface may change in the future without respecting semantic versioning.

In addition to the predict! and correct! steps, the IMM filter has an interact! method that updates the states and covariances of the individual filters based on the mixing probabilities. The combine! method combines the states and covariances of the individual filters into a single state and covariance. These four functions are typically called in either of the orders

  • correct!, combine!, interact!, predict! (as is done in update!)
  • interact!, predict!, correct!, combine! (as is done in the reference cited below)

These two orders are cyclic permutations of each other, and the order used in update! is chosen to align with the order used in the other filters, where the initial condition is corrected using the first measurement, i.e., we assume the first measurement updates $x(0|-1)$ to $x(0|0)$.

The initial (combined) state and covariance of the IMM filter is made up of the weighted average of the states and covariances of the individual filters. The weights are the initial mixing probabilities μ.

Ref: "Interacting multiple model methods in target tracking: a survey", E. Mazor; A. Averbuch; Y. Bar-Shalom; J. Dayan

Arguments:

  • models: An array of Kalman-type filters, such as KalmanFilter, ExtendedKalmanFilter, UnscentedKalmanFilter, etc. The state of each model must have the same meaning, such that forming a weighted average makes sense.
  • P: The mode-transition probability matrix. P[i,j] is the probability of transitioning from mode i to mode j (each row must sum to one).
  • μ: The initial mixing probabilities. μ[i] is the probability of being in mode i at the initial contidion (must sum to one).
  • check: If true, check that the inputs are valid. If false, skip the checks.
  • p: Parameters for the filter. NOTE: this p is shared among all internal filters. The internal p of each filter will be overridden by this one.
  • interact: If true, the filter will run the interaction as part of update! and forward_trajectory. If false, the filter will not run the interaction step. This choice can be overridden by passing the keyword argument interact to the respective functions.
source
LowLevelParticleFilters.KalmanFilterType
KalmanFilter(A,B,C,D,R1,R2,d0=MvNormal(R1); p = NullParameters(), α=1, check=true)

The matrices A,B,C,D define the dynamics

x' = Ax + Bu + w
+y  = Cx + Du + e

where w ~ N(0, R1), e ~ N(0, R2) and x(0) ~ d0

The matrices can be time varying such that, e.g., A[:, :, t] contains the $A$ matrix at time index t. They can also be given as functions on the form

Afun(x, u, p, t) -> A

For maximum performance, provide statically sized matrices from StaticArrays.jl

α is an optional "forgetting factor", if this is set to a value > 1, such as 1.01-1.2, the filter will, in addition to the covariance inflation due to $R_1$, exhibit "exponential forgetting" similar to a Recursive Least-Squares (RLS) estimator. It is thus possible to get a RLS-like algorithm by setting $R_1=0, R_2 = 1/α$ and $α > 1$ ($α$ is the inverse of the traditional RLS parameter $α = 1/λ$). The exact form of the covariance update is

\[R(t+1|t) = α AR(t)A^T + R_1\]

If check = true (default) the function will check that the eigenvalues of A are less than 2 in absolute value. Large eigenvalues may be an indication that the system matrices are representing a continuous-time system and the user has forgotten to discretize it. Turn off this check by setting check = false.

Tutorials on Kalman filtering

The tutorial "How to tune a Kalman filter" details how to figure out appropriate covariance matrices for the Kalman filter, as well as how to add disturbance models to the system model. See also the tutorial in the documentation

source
LowLevelParticleFilters.KalmanFilteringSolutionType
KalmanFilteringSolution{Tx,Txt,TR,TRt,Tll} <: AbstractFilteringSolution

Fields

  • x: predictions $x(t+1|t)$ (plotted if plotx=true)
  • xt: filtered estimates $x(t|t)$ (plotted if plotxt=true)
  • R: predicted covariance matrices $R(t+1|t)$ (plotted if plotR=true)
  • Rt: filter covariances $R(t|t)$ (plotted if plotRt=true)
  • ll: loglikelihood
  • e: prediction errors $e(t|t-1) = y - ŷ(t|t-1)$ (plotted if plote=true)

Plot

The solution object can be plotted

plot(sol, plotx=true, plotxt=true, plotR=true, plotRt=true, plote=true, plotu=true, ploty=true, plotyh=true, plotyht=true, name="")

where

  • plotx: Plot the predictions x(t|t-1)
  • plotxt: Plot the filtered estimates x(t|t)
  • plotR: Plot the predicted covariances R(t|t-1) as ribbons at ±2σ (1.96 σ to be precise)
  • plotRt: Plot the filter covariances R(t|t) as ribbons at ±2σ (1.96 σ to be precise)
  • plote: Plot the prediction errors e(t|t-1) = y - ŷ(t|t-1)
  • plotu: Plot the input
  • ploty: Plot the measurements
  • plotyh: Plot the predicted measurements ŷ(t|t-1)
  • plotyht: Plot the filtered measurements ŷ(t|t)
  • name: a string that is prepended to the labels of the plots, which is useful when plotting multiple solutions in the same plot.
source
LowLevelParticleFilters.ParticleFilterMethod
ParticleFilter(N::Integer, dynamics, measurement, dynamics_density, measurement_density, initial_density; threads = false, p = NullParameters(), kwargs...)

See the docs for more information: https://baggepinnen.github.io/LowLevelParticleFilters.jl/stable/#Particle-filter-1

Arguments:

  • N: Number of particles
  • dynamics: A discrete-time dynamics function (x, u, p, t) -> x⁺
  • measurement: A measurement function (x, u, p, t) -> y
  • dynamics_density: A probability-density function for additive noise in the dynamics. Use AdvancedParticleFilter for non-additive noise.
  • measurement_density: A probability-density function for additive measurement noise. Use AdvancedParticleFilter for non-additive noise.
  • initial_density: Distribution of the initial state.
source
LowLevelParticleFilters.ParticleFilteringSolutionType
ParticleFilteringSolution{F, Tu, Ty, Tx, Tw, Twe, Tll} <: AbstractFilteringSolution

Fields:

  • f: The filter used to produce the solution.
  • u: Input
  • y: Output / measurements
  • x: Particles, the size of this array is (N,T), where N is the number of particles and T is the number of time steps. Each element represents a weighted state hypothesis with weight given by we.
  • w: Weights (log space). These are used for internal computations.
  • we: Weights (exponentiated / original space). These are the ones to use to compute weighted means etc., they sum to one for each time step.
  • ll: Log likelihood

Plot

The solution object can be plotted

plot(sol; nbinsy=30, xreal=nothing, dim=nothing, ploty=true)
source
LowLevelParticleFilters.SqKalmanFilterType
SqKalmanFilter(A,B,C,D,R1,R2,d0=MvNormal(R1); p = NullParameters(), α=1)

A standard Kalman filter on square-root form. This filter may have better numerical performance when the covariance matrices are ill-conditioned.

The matrices A,B,C,D define the dynamics

x' = Ax + Bu + w
+y  = Cx + Du + e

where w ~ N(0, R1), e ~ N(0, R2) and x(0) ~ d0

The matrices can be time varying such that, e.g., A[:, :, t] contains the $A$ matrix at time index t. They can also be given as functions on the form

Afun(x, u, p, t) -> A

The internal fields storing covariance matrices are for this filter storing the upper-triangular Cholesky factor.

α is an optional "forgetting factor", if this is set to a value > 1, such as 1.01-1.2, the filter will, in addition to the covariance inflation due to $R_1$, exhibit "exponential forgetting" similar to a Recursive Least-Squares (RLS) estimator. It is thus possible to get a RLS-like algorithm by setting $R_1=0, R_2 = 1/α$ and $α > 1$ ($α$ is the inverse of the traditional RLS parameter $α = 1/λ$). The form of the covariance update is

\[R(t+1|t) = α AR(t)A^T + R_1\]

Ref: "A Square-Root Kalman Filter Using Only QR Decompositions", Kevin Tracy https://arxiv.org/abs/2208.06452

source
LowLevelParticleFilters.UKFMeasurementModelMethod
UKFMeasurementModel{inplace_measurement,augmented_measurement}(measurement, R2, ny, ne, innovation, mean, cov, cross_cov, cache = nothing)

A measurement model for the Unscented Kalman Filter.

Arguments:

  • measurement: The measurement function y = h(x, u, p, t)
  • R2: The measurement noise covariance matrix
  • ny: The number of measurement variables
  • ne: If augmented_measurement is true, the number of measurement noise variables
  • innovation(y::AbstractVector, yh::AbstractVector) where the arguments represent (measured output, predicted output)
  • mean(ys::AbstractVector{<:AbstractVector}): computes the mean of the vector of vectors of output sigma points.
  • cov(ys::AbstractVector{<:AbstractVector}, y::AbstractVector): computes the covariance matrix of the output sigma points.
  • cross_cov(xs::AbstractVector{<:AbstractVector}, x::AbstractVector, ys::AbstractVector{<:AbstractVector}, y::AbstractVector) where the arguments represents (state sigma points, mean state, output sigma points, mean output). The function should return the cross-covariance matrix between the state and output sigma points.
source
LowLevelParticleFilters.UKFMeasurementModelMethod
UKFMeasurementModel{T,IPM,AUGM}(measurement, R2; nx, ny, ne = nothing, innovation = -, mean = safe_mean, cov = safe_cov, cross_cov = cross_cov, static = nothing)
  • T is the element type used for arrays
  • IPM is a boolean indicating if the measurement function is inplace
  • AUGM is a boolean indicating if the measurement model is augmented
source
LowLevelParticleFilters.UnscentedKalmanFilterMethod
UnscentedKalmanFilter(dynamics, measurement, R1, R2, d0=MvNormal(Matrix(R1)); p = NullParameters(), ny, nu)
 UnscentedKalmanFilter{IPD,IPM,AUGD,AUGM}(dynamics, measurement_model::AbstractMeasurementModel, R1, d0=SimpleMvNormal(R1); p=NullParameters(), nu)

A nonlinear state estimator propagating uncertainty using the unscented transform.

The dynamics and measurement function are on either of the following forms

x' = dynamics(x, u, p, t) + w
 y  = measurement(x, u, p, t) + e
x' = dynamics(x, u, p, t, w)
-y  = measurement(x, u, p, t, e)

where w ~ N(0, R1), e ~ N(0, R2) and x(0) ~ d0. The former (default) assums that the noise is additive and added after the dynamics and measurement updates, while the latter assumes that the dynamics functions take an additional argument corresponding to the noise term. The latter form (sometimes refered to as the "augmented" form) is useful when the noise is multiplicative or when the noise is added before the dynamics and measurement updates. See "Augmented UKF" below for more details on how to use this form.

The matrices R1, R2 can be time varying such that, e.g., R1[:, :, t] contains the $R_1$ matrix at time index t. They can also be given as functions on the form

Rfun(x, u, p, t) -> R

For maximum performance, provide statically sized matrices from StaticArrays.jl

ny, nu indicate the number of outputs and inputs.

Custom type of u

The input u may be of any type, e.g., a named tuple or a custom struct. The u provided in the input data is passed directly to the dynamics and measurement functions, so as long as the type is compatible with the dynamics it will work out. The one exception where this will not work is when calling simulate, which assumes that u is an array.

Augmented UKF

If the noise is not additive, one may use the augmented form of the UKF. In this form, the dynamics functions take additional input arguments that correspond to the noise terms. To enable this form, the typed constructor

UnscentedKalmanFilter{inplace_dynamics,inplace_measurement,augmented_dynamics,augmented_measurement}(...)

is used, where the Boolean type parameters have the following meaning

  • inplace_dynamics: If true, the dynamics function operates in-place, i.e., it modifies the first argument in dynamics(dx, x, u, p, t). Default is false.
  • inplace_measurement: If true, the measurement function operates in-place, i.e., it modifies the first argument in measurement(y, x, u, p, t). Default is false.
  • augmented_dynamics: If true the dynamics function is augmented with an additional noise input w, i.e., dynamics(x, u, p, t, w). Default is false.
  • augmented_measurement: If true the measurement function is agumented with an additional noise input e, i.e., measurement(x, u, p, t, e). Default is false. (If the measurement noise has fewer degrees of freedom than the number of measurements, you may failure in Cholesky factorizations, see "Custom Cholesky factorization" below).

Use of augmented dynamics incurs extra computational cost. The number of sigma points used is 2L+1 where L is the length of the augmented state vector. Without augmentation, L = nx, with augmentation L = nx + nw and L = nx + ne for dynamics and measurement, respectively.

Sigma-point rejection

For problems with challenging dynamics, a mechanism for rejection of sigma points after the dynamics update is provided. A function reject(x) -> Bool can be provided through the keyword argument reject that returns true if a sigma point for $x(t+1)$ should be rejected, e.g., if an instability or non-finite number is detected. A rejected point is replaced by the propagated mean point (the mean point cannot be rejected). This function may be provided either to the constructor of the UKF or passed to the predict! function.

Custom measurement models

By default, standard arithmetic mean and e(y, yh) = y - yh are used as mean and innovation functions.

By passing and explicitly created UKFMeasurementModel, one may provide custom functions that compute the mean, the covariance and the innovation. This is useful in situations where the state or a measurement lives on a manifold. One may further override the mean and covariance functions for the state sigma points by passing the keyword arguments state_mean and state_cov to the constructor.

  • state_mean(xs::AbstractVector{<:AbstractVector}) computes the mean of the vector of vectors of state sigma points.
  • state_cov(xs::AbstractVector{<:AbstractVector}, m = mean(xs)) where the first argument represent state sigma points and the second argument, which must be optional, represents the mean of those points. The function should return the covariance matrix of the state sigma points.

See UKFMeasurementModel for more details on how to set up a custom measurement model. Pass the custom measurement model as the second argument to the UKF constructor.

Custom Cholesky factorization

The UnscentedKalmanFilter supports providing a custom function to compute the Cholesky factorization of the covariance matrices for use in sigma-point generation.

If either of the following conditions are met, you may experience failure in internal Cholesky factorizations:

  • The dynamics noise or measurement noise covariance matrices ($R_1, R_2$) are singular
  • The measurement is augmented and the measurement noise has fewer degrees of freedom than the number of measurements
  • (Under specific technical conditions) The dynamics is augmented and the dynamics noise has fewer degrees of freedom than the number of state variables. The technical conditions are easiest to understand in the linear-systems case, where it corresponds to the Riccati equation associated with the Kalman gain not having a solution. This may happen when the pair $(A, R1)$ has uncontrollable modes on the unit circle, for example, when there are integrating modes that are not affected through the noise.

The error message may look like

ERROR: PosDefException: matrix is not positive definite; Factorization failed.

In such situations, it is advicable to reconsider the noise model and covariance matrices, alternatively, you may provide a custom Cholesky factorization function to the UKF constructor through the keyword argument cholesky!. The function should have the signature cholesky!(A::AbstractMatrix)::Cholesky. A useful alternative factorizaiton when covariance matrices are expected to be singular is cholesky! = R->cholesky!(Positive, Matrix(R)) where the "positive" Cholesky factorization is provided by the package PositiveFactorizations.jl, which must be manually installed and loaded by the user.

source
LowLevelParticleFilters.combine!Method
combine!(imm::IMM)

Combine the models of the IMM filter into a single state imm.x and covariance imm.R. This is done by taking a weighted average of the states and covariances of the individual models, where the weights are the mixing probabilities μ.

source
LowLevelParticleFilters.commandplotFunction
commandplot(pf, u, y, p=parameters(pf); kwargs...)

Produce a helpful plot. For customization options (kwargs...), see ?pplot. After each time step, a command from the user is requested.

  • q: quit
  • s n: step n steps
Note

This function requires using Plots to be called before it is used.

source
LowLevelParticleFilters.correct!Function
(; ll, e, S, Sᵪ, K) = correct!(kf::AbstractKalmanFilter, u, y, p = parameters(kf), t::Integer = index(kf), R2)

The correct step for a Kalman filter returns not only the log likelihood ll and the prediction error e, but also the covariance of the output S, its Cholesky factor Sᵪ and the Kalman gain K.

If R2 stored in kf is a function R2(x, u, p, t), this function is evaluated at the state before the correction is performed. The measurement noise covariance matrix R2 stored in the filter object can optionally be overridden by passing the argument R2, in this case R2 must be a matrix.

Extended help

To perform separate measurement updates for different sensors, see the "Measurement models" in the documentation.

source
LowLevelParticleFilters.correct!Function
ll, e = correct!(pf, u, y, p = parameters(f), t = index(f))

Update state/weights based on measurement y, returns log-likelihood and prediction error (the error is always 0 for particle filters).

Extended help

To perform separate measurement updates for different sensors, see the "Measurement models" in the documentation. For AdvancedParticleFilter, this can be realized by passing a custom measurement_likelihood function as the keyword argument g to correct!, or by calling the lower-level function measurement_equation! with a custom measurement_likelihood.

source
LowLevelParticleFilters.correct!Function
correct!(kf::SqKalmanFilter, u, y, p = parameters(kf), t::Real = index(kf); R2 = get_mat(kf.R2, kf.x, u, p, t))

For the square-root Kalman filter, a custom provided R2 must be the upper triangular Cholesky factor of the covariance matrix of the measurement noise.

source
LowLevelParticleFilters.correct!Method
ll, lls, rest = correct!(imm::IMM, u, y, args; kwargs)

The correct step of the IMM filter corrects each model with the measurements y and control input u. The mixing probabilities imm.μ are updated based on the likelihood of each model given the measurements and the transition probability matrix P.

The returned tuple consists of the sum of the log-likelihood of all models, the vector of individual log-likelihoods and an array of the rest of the return values from the correct step of each model.

source
LowLevelParticleFilters.correct!Method
correct!(ukf::UnscentedKalmanFilter{IPD, IPM, AUGD, AUGM}, u, y, p = parameters(ukf), t::Real = index(ukf) * ukf.Ts; R2 = get_mat(ukf.R2, ukf.x, u, p, t), mean, cross_cov, innovation)

The correction step for an UnscentedKalmanFilter allows the user to override, R2, mean, cross_cov, innovation.

Arguments:

  • u: The input
  • y: The measurement
  • p: The parameters
  • t: The current time
  • R2: The measurement noise covariance matrix, or a function that returns the covariance matrix (x,u,p,t)->R2.
  • mean: The function that computes the mean of the output sigma points.
  • cross_cov: The function that computes the cross-covariance of the state and output sigma points.
  • innovation: The function that computes the innovation between the measured output and the predicted output.

Extended help

To perform separate measurement updates for different sensors, see the "Measurement models" in the documentation

source
LowLevelParticleFilters.debugplotFunction
debugplot(pf, u, y, p=parameters(pf); runall=false, kwargs...)

Produce a helpful plot. For customization options (kwargs...), see ?pplot.

  • runall=false: if true, runs all time steps befor displaying (faster), if false, displays the plot after each time step.

The generated plot becomes quite heavy. Initially, try limiting your input to 100 time steps to verify that it doesn't crash.

Note

This function requires using Plots to be called before it is used.

source
LowLevelParticleFilters.forward_trajectoryFunction
sol = forward_trajectory(pf, u::AbstractVector, y::AbstractVector, p=parameters(pf))

Run the particle filter for a sequence of inputs and measurements (offline / batch filtering). Return a solution with x,w,we,ll = particles, weights, expweights and loglikelihood

If MonteCarloMeasurements.jl is loaded, you may transform the output particles to Matrix{MonteCarloMeasurements.Particles} using Particles(x,we). Internally, the particles are then resampled such that they all have unit weight. This is conventient for making use of the plotting facilities of MonteCarloMeasurements.jl.

sol can be plotted

plot(sol::ParticleFilteringSolution; nbinsy=30, xreal=nothing, dim=nothing)
source
LowLevelParticleFilters.forward_trajectoryFunction
forward_trajectory(imm::IMM, u, y, p = parameters(imm); interact = true)

When performing batch filtering using an IMM filter, one may

  • Override the interact parameter of the filter
  • Access the mode probabilities along the trajectory as the sol.extra field. This is a matrix of size (n_modes, T) where T is the length of the trajectory (length of u and y).

The returned solution object is of type KalmanFilteringSolution and has the following fields:

source
LowLevelParticleFilters.forward_trajectoryFunction
sol = forward_trajectory(kf::AbstractKalmanFilter, u::Vector, y::Vector, p=parameters(kf))

Run a Kalman filter forward to perform (offline / batch) filtering along an entire trajectory u, y.

Returns a KalmanFilteringSolution: with the following

  • x: predictions $x(t|t-1)$
  • xt: filtered estimates $x(t|t)$
  • R: predicted covariance matrices $R(t|t-1)$
  • Rt: filter covariances $R(t|t)$
  • ll: loglik

sol can be plotted

plot(sol::KalmanFilteringSolution; plotx = true, plotxt=true, plotu=true, ploty=true)

See KalmanFilteringSolution for more details.

Extended help

Very large systems

If your system is very large, i.e., the dimension of the state is very large, and the arrays u,y are long, this function may use a lot of memory to store all covariance matrices R, Rt. If you do not need all the information retained by this function, you may opt to call one of the functions

That store significantly less information. The amount of computation performed by all of these functions is identical, the only difference lies in what is stored and returned.

source
LowLevelParticleFilters.interact!Method
interact!(imm::IMM)

The interaction step of the IMM filter updates the state and covariance of each internal model based on the mixing probabilities imm.μ and the transition probability matrix imm.P.

Models with small mixing probabilities will have their states and covariances updated more towards the states and covariances of models with higher mixing probabilities, and vice versa.

source
LowLevelParticleFilters.logsumexp!Function
ll = logsumexp!(w, we [, maxw])

Normalizes the weight vector w and returns the weighted log-likelihood

https://arxiv.org/pdf/1412.8695.pdf eq 3.8 for p(y) https://discourse.julialang.org/t/fast-logsumexp/22827/7?u=baggepinnen for stable logsumexp

source
LowLevelParticleFilters.mean_trajectoryMethod
mean_trajectory(sol::ParticleFilteringSolution)
-mean_trajectory(x::AbstractMatrix, we::AbstractMatrix)

Compute the weighted mean along the trajectory of a particle-filter solution. Returns a matrix of size T × nx. If x and we are supplied, the weights are expected to be in the original space (not log space).

source
LowLevelParticleFilters.metropolisFunction
metropolis(ll::Function(θ), R::Int, θ₀::Vector, draw::Function(θ) = naive_sampler(θ₀))

Performs MCMC sampling using the marginal Metropolis (-Hastings) algorithm draw = θ -> θ' samples a new parameter vector given an old parameter vector. The distribution must be symmetric, e.g., a Gaussian. R is the number of iterations. See log_likelihood_fun

Example:

filter_from_parameters(θ) = ParticleFilter(N, dynamics, measurement, MvNormal(n,exp(θ[1])), MvNormal(p,exp(θ[2])), d0)
+y  = measurement(x, u, p, t, e)

where w ~ N(0, R1), e ~ N(0, R2) and x(0) ~ d0. The former (default) assums that the noise is additive and added after the dynamics and measurement updates, while the latter assumes that the dynamics functions take an additional argument corresponding to the noise term. The latter form (sometimes refered to as the "augmented" form) is useful when the noise is multiplicative or when the noise is added before the dynamics and measurement updates. See "Augmented UKF" below for more details on how to use this form.

The matrices R1, R2 can be time varying such that, e.g., R1[:, :, t] contains the $R_1$ matrix at time index t. They can also be given as functions on the form

Rfun(x, u, p, t) -> R

For maximum performance, provide statically sized matrices from StaticArrays.jl

ny, nu indicate the number of outputs and inputs.

Custom type of u

The input u may be of any type, e.g., a named tuple or a custom struct. The u provided in the input data is passed directly to the dynamics and measurement functions, so as long as the type is compatible with the dynamics it will work out. The one exception where this will not work is when calling simulate, which assumes that u is an array.

Augmented UKF

If the noise is not additive, one may use the augmented form of the UKF. In this form, the dynamics functions take additional input arguments that correspond to the noise terms. To enable this form, the typed constructor

UnscentedKalmanFilter{inplace_dynamics,inplace_measurement,augmented_dynamics,augmented_measurement}(...)

is used, where the Boolean type parameters have the following meaning

  • inplace_dynamics: If true, the dynamics function operates in-place, i.e., it modifies the first argument in dynamics(dx, x, u, p, t). Default is false.
  • inplace_measurement: If true, the measurement function operates in-place, i.e., it modifies the first argument in measurement(y, x, u, p, t). Default is false.
  • augmented_dynamics: If true the dynamics function is augmented with an additional noise input w, i.e., dynamics(x, u, p, t, w). Default is false.
  • augmented_measurement: If true the measurement function is agumented with an additional noise input e, i.e., measurement(x, u, p, t, e). Default is false. (If the measurement noise has fewer degrees of freedom than the number of measurements, you may failure in Cholesky factorizations, see "Custom Cholesky factorization" below).

Use of augmented dynamics incurs extra computational cost. The number of sigma points used is 2L+1 where L is the length of the augmented state vector. Without augmentation, L = nx, with augmentation L = nx + nw and L = nx + ne for dynamics and measurement, respectively.

Sigma-point rejection

For problems with challenging dynamics, a mechanism for rejection of sigma points after the dynamics update is provided. A function reject(x) -> Bool can be provided through the keyword argument reject that returns true if a sigma point for $x(t+1)$ should be rejected, e.g., if an instability or non-finite number is detected. A rejected point is replaced by the propagated mean point (the mean point cannot be rejected). This function may be provided either to the constructor of the UKF or passed to the predict! function.

Custom measurement models

By default, standard arithmetic mean and e(y, yh) = y - yh are used as mean and innovation functions.

By passing and explicitly created UKFMeasurementModel, one may provide custom functions that compute the mean, the covariance and the innovation. This is useful in situations where the state or a measurement lives on a manifold. One may further override the mean and covariance functions for the state sigma points by passing the keyword arguments state_mean and state_cov to the constructor.

  • state_mean(xs::AbstractVector{<:AbstractVector}) computes the mean of the vector of vectors of state sigma points.
  • state_cov(xs::AbstractVector{<:AbstractVector}, m = mean(xs)) where the first argument represent state sigma points and the second argument, which must be optional, represents the mean of those points. The function should return the covariance matrix of the state sigma points.

See UKFMeasurementModel for more details on how to set up a custom measurement model. Pass the custom measurement model as the second argument to the UKF constructor.

Custom Cholesky factorization

The UnscentedKalmanFilter supports providing a custom function to compute the Cholesky factorization of the covariance matrices for use in sigma-point generation.

If either of the following conditions are met, you may experience failure in internal Cholesky factorizations:

  • The dynamics noise or measurement noise covariance matrices ($R_1, R_2$) are singular
  • The measurement is augmented and the measurement noise has fewer degrees of freedom than the number of measurements
  • (Under specific technical conditions) The dynamics is augmented and the dynamics noise has fewer degrees of freedom than the number of state variables. The technical conditions are easiest to understand in the linear-systems case, where it corresponds to the Riccati equation associated with the Kalman gain not having a solution. This may happen when the pair $(A, R1)$ has uncontrollable modes on the unit circle, for example, when there are integrating modes that are not affected through the noise.

The error message may look like

ERROR: PosDefException: matrix is not positive definite; Factorization failed.

In such situations, it is advicable to reconsider the noise model and covariance matrices, alternatively, you may provide a custom Cholesky factorization function to the UKF constructor through the keyword argument cholesky!. The function should have the signature cholesky!(A::AbstractMatrix)::Cholesky. A useful alternative factorizaiton when covariance matrices are expected to be singular is cholesky! = R->cholesky!(Positive, Matrix(R)) where the "positive" Cholesky factorization is provided by the package PositiveFactorizations.jl, which must be manually installed and loaded by the user.

source
LowLevelParticleFilters.combine!Method
combine!(imm::IMM)

Combine the models of the IMM filter into a single state imm.x and covariance imm.R. This is done by taking a weighted average of the states and covariances of the individual models, where the weights are the mixing probabilities μ.

source
LowLevelParticleFilters.commandplotFunction
commandplot(pf, u, y, p=parameters(pf); kwargs...)

Produce a helpful plot. For customization options (kwargs...), see ?pplot. After each time step, a command from the user is requested.

  • q: quit
  • s n: step n steps
Note

This function requires using Plots to be called before it is used.

source
LowLevelParticleFilters.correct!Function
(; ll, e, S, Sᵪ, K) = correct!(kf::AbstractKalmanFilter, u, y, p = parameters(kf), t::Integer = index(kf), R2)

The correct step for a Kalman filter returns not only the log likelihood ll and the prediction error e, but also the covariance of the output S, its Cholesky factor Sᵪ and the Kalman gain K.

If R2 stored in kf is a function R2(x, u, p, t), this function is evaluated at the state before the correction is performed. The measurement noise covariance matrix R2 stored in the filter object can optionally be overridden by passing the argument R2, in this case R2 must be a matrix.

Extended help

To perform separate measurement updates for different sensors, see the "Measurement models" in the documentation.

source
LowLevelParticleFilters.correct!Function
ll, e = correct!(pf, u, y, p = parameters(f), t = index(f))

Update state/weights based on measurement y, returns log-likelihood and prediction error (the error is always 0 for particle filters).

Extended help

To perform separate measurement updates for different sensors, see the "Measurement models" in the documentation. For AdvancedParticleFilter, this can be realized by passing a custom measurement_likelihood function as the keyword argument g to correct!, or by calling the lower-level function measurement_equation! with a custom measurement_likelihood.

source
LowLevelParticleFilters.correct!Function
correct!(kf::SqKalmanFilter, u, y, p = parameters(kf), t::Real = index(kf); R2 = get_mat(kf.R2, kf.x, u, p, t))

For the square-root Kalman filter, a custom provided R2 must be the upper triangular Cholesky factor of the covariance matrix of the measurement noise.

source
LowLevelParticleFilters.correct!Method
ll, lls, rest = correct!(imm::IMM, u, y, args; kwargs)

The correct step of the IMM filter corrects each model with the measurements y and control input u. The mixing probabilities imm.μ are updated based on the likelihood of each model given the measurements and the transition probability matrix P.

The returned tuple consists of the sum of the log-likelihood of all models, the vector of individual log-likelihoods and an array of the rest of the return values from the correct step of each model.

source
LowLevelParticleFilters.correct!Method
correct!(ukf::UnscentedKalmanFilter{IPD, IPM, AUGD, AUGM}, u, y, p = parameters(ukf), t::Real = index(ukf) * ukf.Ts; R2 = get_mat(ukf.R2, ukf.x, u, p, t), mean, cross_cov, innovation)

The correction step for an UnscentedKalmanFilter allows the user to override, R2, mean, cross_cov, innovation.

Arguments:

  • u: The input
  • y: The measurement
  • p: The parameters
  • t: The current time
  • R2: The measurement noise covariance matrix, or a function that returns the covariance matrix (x,u,p,t)->R2.
  • mean: The function that computes the mean of the output sigma points.
  • cross_cov: The function that computes the cross-covariance of the state and output sigma points.
  • innovation: The function that computes the innovation between the measured output and the predicted output.

Extended help

To perform separate measurement updates for different sensors, see the "Measurement models" in the documentation

source
LowLevelParticleFilters.debugplotFunction
debugplot(pf, u, y, p=parameters(pf); runall=false, kwargs...)

Produce a helpful plot. For customization options (kwargs...), see ?pplot.

  • runall=false: if true, runs all time steps befor displaying (faster), if false, displays the plot after each time step.

The generated plot becomes quite heavy. Initially, try limiting your input to 100 time steps to verify that it doesn't crash.

Note

This function requires using Plots to be called before it is used.

source
LowLevelParticleFilters.forward_trajectoryFunction
sol = forward_trajectory(kf::AbstractKalmanFilter, u::Vector, y::Vector, p=parameters(kf))

Run a Kalman filter forward to perform (offline / batch) filtering along an entire trajectory u, y.

Returns a KalmanFilteringSolution: with the following

  • x: predictions $x(t|t-1)$
  • xt: filtered estimates $x(t|t)$
  • R: predicted covariance matrices $R(t|t-1)$
  • Rt: filter covariances $R(t|t)$
  • ll: loglik

sol can be plotted

plot(sol::KalmanFilteringSolution; plotx = true, plotxt=true, plotu=true, ploty=true)

See KalmanFilteringSolution for more details.

Extended help

Very large systems

If your system is very large, i.e., the dimension of the state is very large, and the arrays u,y are long, this function may use a lot of memory to store all covariance matrices R, Rt. If you do not need all the information retained by this function, you may opt to call one of the functions

That store significantly less information. The amount of computation performed by all of these functions is identical, the only difference lies in what is stored and returned.

source
LowLevelParticleFilters.forward_trajectoryFunction
sol = forward_trajectory(pf, u::AbstractVector, y::AbstractVector, p=parameters(pf))

Run the particle filter for a sequence of inputs and measurements (offline / batch filtering). Return a solution with x,w,we,ll = particles, weights, expweights and loglikelihood

If MonteCarloMeasurements.jl is loaded, you may transform the output particles to Matrix{MonteCarloMeasurements.Particles} using Particles(x,we). Internally, the particles are then resampled such that they all have unit weight. This is conventient for making use of the plotting facilities of MonteCarloMeasurements.jl.

sol can be plotted

plot(sol::ParticleFilteringSolution; nbinsy=30, xreal=nothing, dim=nothing)
source
LowLevelParticleFilters.forward_trajectoryFunction
forward_trajectory(imm::IMM, u, y, p = parameters(imm); interact = true)

When performing batch filtering using an IMM filter, one may

  • Override the interact parameter of the filter
  • Access the mode probabilities along the trajectory as the sol.extra field. This is a matrix of size (n_modes, T) where T is the length of the trajectory (length of u and y).

The returned solution object is of type KalmanFilteringSolution and has the following fields:

source
LowLevelParticleFilters.interact!Method
interact!(imm::IMM)

The interaction step of the IMM filter updates the state and covariance of each internal model based on the mixing probabilities imm.μ and the transition probability matrix imm.P.

Models with small mixing probabilities will have their states and covariances updated more towards the states and covariances of models with higher mixing probabilities, and vice versa.

source
LowLevelParticleFilters.logsumexp!Function
ll = logsumexp!(w, we [, maxw])

Normalizes the weight vector w and returns the weighted log-likelihood

https://arxiv.org/pdf/1412.8695.pdf eq 3.8 for p(y) https://discourse.julialang.org/t/fast-logsumexp/22827/7?u=baggepinnen for stable logsumexp

source
LowLevelParticleFilters.mean_trajectoryMethod
mean_trajectory(sol::ParticleFilteringSolution)
+mean_trajectory(x::AbstractMatrix, we::AbstractMatrix)

Compute the weighted mean along the trajectory of a particle-filter solution. Returns a matrix of size T × nx. If x and we are supplied, the weights are expected to be in the original space (not log space).

source
LowLevelParticleFilters.metropolisFunction
metropolis(ll::Function(θ), R::Int, θ₀::Vector, draw::Function(θ) = naive_sampler(θ₀))

Performs MCMC sampling using the marginal Metropolis (-Hastings) algorithm draw = θ -> θ' samples a new parameter vector given an old parameter vector. The distribution must be symmetric, e.g., a Gaussian. R is the number of iterations. See log_likelihood_fun

Example:

filter_from_parameters(θ) = ParticleFilter(N, dynamics, measurement, MvNormal(n,exp(θ[1])), MvNormal(p,exp(θ[2])), d0)
 priors = [Normal(0,0.1),Normal(0,0.1)]
 ll     = log_likelihood_fun(filter_from_parameters,priors,u,y,1)
 θ₀ = log.([1.,1.]) # Initial point
@@ -17,9 +17,9 @@
 # thetam = reduce(hcat, theta)'
 @time thetalls = LowLevelParticleFilters.metropolis_threaded(burnin, ll, 5000, θ₀, draw) # run on all threads, will provide (2000-burnin)*nthreads() samples
 histogram(exp.(thetalls[:,1:2]), layout=3)
-plot!(thetalls[:,3], subplot=3) # if threaded call, log likelihoods are in the last column
source
LowLevelParticleFilters.simulateFunction
x,u,y = simulate(f::AbstractFilter, T::Int, du::Distribution, p=parameters(f), [N]; dynamics_noise=true, measurement_noise=true)
-x,u,y = simulate(f::AbstractFilter, u, p=parameters(f); dynamics_noise=true, measurement_noise=true)

Simulate dynamical system forward in time T steps, or for the duration of u. Returns state sequence, inputs and measurements.

  • u is an input-signal trajectory, alternatively, du is a distribution of random inputs.

A simulation can be considered a draw from the prior distribution over the evolution of the system implied by the selected noise models. Such a simulation is useful in order to evaluate whether or not the noise models are reasonable.

If MonteCarloMeasurements.jl is loaded, the argument N::Int can be supplied, in which case N simulations are done and the result is returned in the form of Vector{MonteCarloMeasurements.Particles}.

source
LowLevelParticleFilters.simulateFunction
x,u,y = simulate(f::AbstractFilter, T::Int, du::Distribution, p=parameters(f), [N]; dynamics_noise=true, measurement_noise=true)
+x,u,y = simulate(f::AbstractFilter, u, p=parameters(f); dynamics_noise=true, measurement_noise=true)

Simulate dynamical system forward in time T steps, or for the duration of u. Returns state sequence, inputs and measurements.

  • u is an input-signal trajectory, alternatively, du is a distribution of random inputs.

A simulation can be considered a draw from the prior distribution over the evolution of the system implied by the selected noise models. Such a simulation is useful in order to evaluate whether or not the noise models are reasonable.

If MonteCarloMeasurements.jl is loaded, the argument N::Int can be supplied, in which case N simulations are done and the result is returned in the form of Vector{MonteCarloMeasurements.Particles}.

source
LowLevelParticleFilters.smoothFunction
xT,RT,ll = smooth(sol, kf)
 xT,RT,ll = smooth(kf::KalmanFilter, u::Vector, y::Vector, p=parameters(kf))
-xT,RT,ll = smooth(kf::ExtendedKalmanFilter, u::Vector, y::Vector, p=parameters(kf))

Returns smoothed estimates of state x and covariance R given all input output data u,y or an existing solution sol obtained from forward_trajectory.

source
LowLevelParticleFilters.update!Function
ll, e = update!(f::AbstractFilter, u, y, p = parameters(f), t = index(f))

Perform one step of predict! and correct!, returns log-likelihood and prediction error

source
LowLevelParticleFilters.update!Method
update!(imm::IMM, u, y, p, t; correct_kwargs = (;), predict_kwargs = (;), interact = true)

The combined udpate for an IMM filter performs the following steps:

  1. Correct each model with the measurements y and control input u.
  2. Combine the models into a single state and covariance.
  3. Interact the models to update their respective state and covariance.
  4. Predict each model to the next time step.

This differs slightly from the udpate step of other filters, where at the end of an update the state of the filter is the one-step ahead predicted value, whereas here each individual filter has a predicted state, but the combine! step of the IMM filter hasn't been performed on the predictions yet. The state of the IMM filter is thus $x(t|t)$ and not $x(t+1|t)$ like it is for other filters, and each filter internal to the IMM.

Arguments:

  • correct_kwargs: An optional named tuple of keyword arguments that are sent to correct!.
  • predict_kwargs: An optional named tuple of keyword arguments that are sent to predict!.
  • interact: Whether or not to run the interaction step.
source
StatsAPI.predict!Function
predict!(f, u, p = parameters(f), t = index(f))

Move filter state forward in time using dynamics equation and input vector u.

source
StatsAPI.predict!Function
predict!(kf::SqKalmanFilter, u, p = parameters(kf), t::Real = index(kf); R1 = get_mat(kf.R1, kf.x, u, p, t), α = kf.α)

For the square-root Kalman filter, a custom provided R1 must be the upper triangular Cholesky factor of the covariance matrix of the process noise.

source
StatsAPI.predict!Function
predict!(kf::AbstractKalmanFilter, u, p = parameters(kf), t::Integer = index(kf); R1, α = kf.α)

Perform the prediction step (updating the state estimate to $x(t+1|t)$). If R1 stored in kf is a function R1(x, u, p, t), this function is evaluated at the state before the prediction is performed. The dynamics noise covariance matrix R1 stored in kf can optionally be overridden by passing the argument R1, in this case R1 must be a matrix.

source
StatsAPI.predict!Method
predict!(ukf::UnscentedKalmanFilter, u, p = parameters(ukf), t::Real = index(ukf) * ukf.Ts; R1 = get_mat(ukf.R1, ukf.x, u, p, t), reject, mean, cov, dynamics)

The prediction step for an UnscentedKalmanFilter allows the user to override, R1 and any of the functions, reject, mean, cov, dynamics`.

Arguments:

  • u: The input
  • p: The parameters
  • t: The current time
  • R1: The dynamics noise covariance matrix, or a function that returns the covariance matrix.
  • reject: A function that takes a sigma point and returns true if it should be rejected.
  • mean: The function that computes the mean of the state sigma points.
  • cov: The function that computes the covariance of the state sigma points.
source
LowLevelParticleFilters.prediction_errors!Function
prediction_errors!(res, f::AbstractFilter, u, y, p = parameters(f), λ = 1)

Calculate the prediction errors and store the result in res. Similar to sse, this function is useful for sum-of-squares optimization. In contrast to sse, this function returns the residuals themselves rather than their sum of squares. This is useful for Gauss-Newton style optimizers, such as LeastSquaresOptim.LevenbergMarquardt.

Arguments:

  • res: A vector of length ny*length(y). Note, for each datapoint in u and u, there are ny outputs, and thus ny residuals.
  • f: Any filter
  • λ: A weighting factor to minimize dot(e, λ, e). A commonly used metric is λ = Diagonal(1 ./ (mag.^2)), where mag is a vector of the "typical magnitude" of each output. Internally, the square root of W = sqrt(λ) is calculated so that the residuals stored in res are W*e.

See example in Solving using Gauss-Newton optimization.

source
+xT,RT,ll = smooth(kf::ExtendedKalmanFilter, u::Vector, y::Vector, p=parameters(kf))

Returns smoothed estimates of state x and covariance R given all input output data u,y or an existing solution sol obtained from forward_trajectory.

source
LowLevelParticleFilters.update!Function
ll, e = update!(f::AbstractFilter, u, y, p = parameters(f), t = index(f))

Perform one step of predict! and correct!, returns log-likelihood and prediction error

source
LowLevelParticleFilters.update!Method
update!(imm::IMM, u, y, p, t; correct_kwargs = (;), predict_kwargs = (;), interact = true)

The combined udpate for an IMM filter performs the following steps:

  1. Correct each model with the measurements y and control input u.
  2. Combine the models into a single state and covariance.
  3. Interact the models to update their respective state and covariance.
  4. Predict each model to the next time step.

This differs slightly from the udpate step of other filters, where at the end of an update the state of the filter is the one-step ahead predicted value, whereas here each individual filter has a predicted state, but the combine! step of the IMM filter hasn't been performed on the predictions yet. The state of the IMM filter is thus $x(t|t)$ and not $x(t+1|t)$ like it is for other filters, and each filter internal to the IMM.

Arguments:

  • correct_kwargs: An optional named tuple of keyword arguments that are sent to correct!.
  • predict_kwargs: An optional named tuple of keyword arguments that are sent to predict!.
  • interact: Whether or not to run the interaction step.
source
StatsAPI.predict!Function
predict!(f, u, p = parameters(f), t = index(f))

Move filter state forward in time using dynamics equation and input vector u.

source
StatsAPI.predict!Function
predict!(kf::SqKalmanFilter, u, p = parameters(kf), t::Real = index(kf); R1 = get_mat(kf.R1, kf.x, u, p, t), α = kf.α)

For the square-root Kalman filter, a custom provided R1 must be the upper triangular Cholesky factor of the covariance matrix of the process noise.

source
StatsAPI.predict!Function
predict!(kf::AbstractKalmanFilter, u, p = parameters(kf), t::Integer = index(kf); R1, α = kf.α)

Perform the prediction step (updating the state estimate to $x(t+1|t)$). If R1 stored in kf is a function R1(x, u, p, t), this function is evaluated at the state before the prediction is performed. The dynamics noise covariance matrix R1 stored in kf can optionally be overridden by passing the argument R1, in this case R1 must be a matrix.

source
StatsAPI.predict!Method
predict!(ukf::UnscentedKalmanFilter, u, p = parameters(ukf), t::Real = index(ukf) * ukf.Ts; R1 = get_mat(ukf.R1, ukf.x, u, p, t), reject, mean, cov, dynamics)

The prediction step for an UnscentedKalmanFilter allows the user to override, R1 and any of the functions, reject, mean, cov, dynamics`.

Arguments:

  • u: The input
  • p: The parameters
  • t: The current time
  • R1: The dynamics noise covariance matrix, or a function that returns the covariance matrix.
  • reject: A function that takes a sigma point and returns true if it should be rejected.
  • mean: The function that computes the mean of the state sigma points.
  • cov: The function that computes the covariance of the state sigma points.
source
LowLevelParticleFilters.prediction_errors!Function
prediction_errors!(res, f::AbstractFilter, u, y, p = parameters(f), λ = 1)

Calculate the prediction errors and store the result in res. Similar to sse, this function is useful for sum-of-squares optimization. In contrast to sse, this function returns the residuals themselves rather than their sum of squares. This is useful for Gauss-Newton style optimizers, such as LeastSquaresOptim.LevenbergMarquardt.

Arguments:

  • res: A vector of length ny*length(y). Note, for each datapoint in u and u, there are ny outputs, and thus ny residuals.
  • f: Any filter
  • λ: A weighting factor to minimize dot(e, λ, e). A commonly used metric is λ = Diagonal(1 ./ (mag.^2)), where mag is a vector of the "typical magnitude" of each output. Internally, the square root of W = sqrt(λ) is calculated so that the residuals stored in res are W*e.

See example in Solving using Gauss-Newton optimization.

source
diff --git a/dev/beetle_example/04bb4f6c.png b/dev/beetle_example/04bb4f6c.png deleted file mode 100644 index 9ad77ed8..00000000 Binary files a/dev/beetle_example/04bb4f6c.png and /dev/null differ diff --git a/dev/beetle_example/0c78f876.png b/dev/beetle_example/0c78f876.png new file mode 100644 index 00000000..1402c948 Binary files /dev/null and b/dev/beetle_example/0c78f876.png differ diff --git a/dev/beetle_example/18eca807.png b/dev/beetle_example/18eca807.png new file mode 100644 index 00000000..37f4dbea Binary files /dev/null and b/dev/beetle_example/18eca807.png differ diff --git a/dev/beetle_example/3079f958.svg b/dev/beetle_example/3079f958.svg new file mode 100644 index 00000000..5b0400fd --- /dev/null +++ b/dev/beetle_example/3079f958.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/beetle_example/3551aa42.png b/dev/beetle_example/3551aa42.png deleted file mode 100644 index 70244590..00000000 Binary files a/dev/beetle_example/3551aa42.png and /dev/null differ diff --git a/dev/beetle_example/3da61457.png b/dev/beetle_example/3da61457.png new file mode 100644 index 00000000..d772729e Binary files /dev/null and b/dev/beetle_example/3da61457.png differ diff --git a/dev/beetle_example/6cda41b0.svg b/dev/beetle_example/6cda41b0.svg new file mode 100644 index 00000000..cfba2a13 --- /dev/null +++ b/dev/beetle_example/6cda41b0.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/beetle_example/7d56539f.png b/dev/beetle_example/7d56539f.png deleted file mode 100644 index 8c71a3b6..00000000 Binary files a/dev/beetle_example/7d56539f.png and /dev/null differ diff --git a/dev/beetle_example/c17cca23.svg b/dev/beetle_example/831112a0.svg similarity index 88% rename from dev/beetle_example/c17cca23.svg rename to dev/beetle_example/831112a0.svg index 5a5b09c7..5f3761a6 100644 --- a/dev/beetle_example/c17cca23.svg +++ b/dev/beetle_example/831112a0.svg @@ -1,55 +1,55 @@ - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/beetle_example/9887f561.svg b/dev/beetle_example/9887f561.svg deleted file mode 100644 index 8805c239..00000000 --- a/dev/beetle_example/9887f561.svg +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/dev/beetle_example/e74a81d4.svg b/dev/beetle_example/e74a81d4.svg deleted file mode 100644 index dbd46fb9..00000000 --- a/dev/beetle_example/e74a81d4.svg +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/dev/beetle_example/index.html b/dev/beetle_example/index.html index 4cec25a4..b411e672 100644 --- a/dev/beetle_example/index.html +++ b/dev/beetle_example/index.html @@ -49,7 +49,7 @@ T = length(y) sol = forward_trajectory(pf,u[1:T],y[1:T]) (; x,w,we,ll) = sol -plot(sol, markerstrokecolor=:auto, m=(2,0.5))Example block output

We can clearly see when the beetle switched mode (state variable 5). This corresponds well to annotations provided by a biologist and is the fundamental question we want to answer with the filtering procedure.

We can plot the mean of the filtered trajectory as well

xh = mean_trajectory(x,we)
+plot(sol, markerstrokecolor=:auto, m=(2,0.5))
Example block output

We can clearly see when the beetle switched mode (state variable 5). This corresponds well to annotations provided by a biologist and is the fundamental question we want to answer with the filtering procedure.

We can plot the mean of the filtered trajectory as well

xh = mean_trajectory(x,we)
 
 "plotting helper function"
 function to1series(x::AbstractVector, y)
@@ -61,10 +61,10 @@
 to1series(y) = to1series(1:size(y,1),y)
 
 fig1 = plot(xh[:,1],xh[:,2], c=:blue, lab="estimate", legend=:bottomleft)
-plot!(xyt[:,1],xyt[:,2], c=:red, lab="measurement")
Example block output

as well as the angle state variable (we subsample the particles to not get sluggish plots)

fig2 = scatter(to1series(ϕ.(x)'[:,1:2:end])..., m=(:black, 0.03, 2), lab="", size=(500,300))
-plot!(identity.(xh[:,4]), lab="Filtered angle", legend=:topleft, ylims=(-30, 70))
Example block output

The particle plot above indicate that the posterior is multimodal. This phenomenon arises due to the simple model that uses an angle that is allowed to leave the interval $0-2π$ rad. In this example, we are not interested in the angle, but rather when the beetle switches mode. The filtering distribution above gives a hint at when this happens, but we will not plot the mode trajectory until we have explored smoothing as well.

Smoothing

The filtering results above does not use all the available information when trying to figure out the state trajectory. To do this, we may call a smoother. We use a particle smoother and compute 10 smoothing trajectories.

M = 10 # Number of smoothing trajectories, NOTE: if this is set higher, the result will be better at the expense of linear scaling of the computational cost.
+plot!(xyt[:,1],xyt[:,2], c=:red, lab="measurement")
Example block output

as well as the angle state variable (we subsample the particles to not get sluggish plots)

fig2 = scatter(to1series(ϕ.(x)'[:,1:2:end])..., m=(:black, 0.03, 2), lab="", size=(500,300))
+plot!(identity.(xh[:,4]), lab="Filtered angle", legend=:topleft, ylims=(-30, 70))
Example block output

The particle plot above indicate that the posterior is multimodal. This phenomenon arises due to the simple model that uses an angle that is allowed to leave the interval $0-2π$ rad. In this example, we are not interested in the angle, but rather when the beetle switches mode. The filtering distribution above gives a hint at when this happens, but we will not plot the mode trajectory until we have explored smoothing as well.

Smoothing

The filtering results above does not use all the available information when trying to figure out the state trajectory. To do this, we may call a smoother. We use a particle smoother and compute 10 smoothing trajectories.

M = 10 # Number of smoothing trajectories, NOTE: if this is set higher, the result will be better at the expense of linear scaling of the computational cost.
 sb,ll = smooth(pf, M, u, y) # Sample smoothing particles (b for backward-trajectory)
 sbm = smoothed_mean(sb)     # Calculate the mean of smoothing trajectories
 sbt = smoothed_trajs(sb)    # Get smoothing trajectories
-plot!(fig1, sbm[1,:],sbm[2,:], lab="xs")
Example block output
plot!(fig2, identity.(sbm'[:,4]), lab="smoothed")
Example block output

We see that the smoothed trajectory may look very different from the filter trajectory. This is an indication that it's hard to tell what state the beetle is currently in, but easier to look back and tell what state the beetle must have been in at a historical point.

We can also visualize the mode state

plot(xh[:,5], lab="Filtering")
-plot!(to1series(sbt[5,:,:]')..., lab="Smoothing", title="Mode trajectories", l=(:black,0.2))
Example block output

also this state variable indicates that it's hard to tell what state the beetle is in during filtering, but obvious with hindsight (smoothing). The mode switch occurs when the filtering distribution of the angle becomes drastically wider, indicating that increased dynamics noise is required in order to describe the motion of the beetle.

Summary

This example has demonstrated filtering and smoothing in an advanced application that includes manual control over noise, mixed continuous and discrete state.

+plot!(fig1, sbm[1,:],sbm[2,:], lab="xs")Example block output
plot!(fig2, identity.(sbm'[:,4]), lab="smoothed")
Example block output

We see that the smoothed trajectory may look very different from the filter trajectory. This is an indication that it's hard to tell what state the beetle is currently in, but easier to look back and tell what state the beetle must have been in at a historical point.

We can also visualize the mode state

plot(xh[:,5], lab="Filtering")
+plot!(to1series(sbt[5,:,:]')..., lab="Smoothing", title="Mode trajectories", l=(:black,0.2))
Example block output

also this state variable indicates that it's hard to tell what state the beetle is in during filtering, but obvious with hindsight (smoothing). The mode switch occurs when the filtering distribution of the angle becomes drastically wider, indicating that increased dynamics noise is required in order to describe the motion of the beetle.

Summary

This example has demonstrated filtering and smoothing in an advanced application that includes manual control over noise, mixed continuous and discrete state.

diff --git a/dev/beetle_example_imm/1f6e463e.svg b/dev/beetle_example_imm/1f6e463e.svg deleted file mode 100644 index c4f57061..00000000 --- a/dev/beetle_example_imm/1f6e463e.svg +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/dev/beetle_example_imm/4396de8a.png b/dev/beetle_example_imm/4396de8a.png deleted file mode 100644 index eb618f20..00000000 Binary files a/dev/beetle_example_imm/4396de8a.png and /dev/null differ diff --git a/dev/beetle_example_imm/5525986c.svg b/dev/beetle_example_imm/5525986c.svg new file mode 100644 index 00000000..4c672691 --- /dev/null +++ b/dev/beetle_example_imm/5525986c.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/beetle_example_imm/68a796f9.png b/dev/beetle_example_imm/68a796f9.png new file mode 100644 index 00000000..919ac77d Binary files /dev/null and b/dev/beetle_example_imm/68a796f9.png differ diff --git a/dev/beetle_example_imm/index.html b/dev/beetle_example_imm/index.html index ace05a5b..f4fbb8b4 100644 --- a/dev/beetle_example_imm/index.html +++ b/dev/beetle_example_imm/index.html @@ -17,7 +17,7 @@ @inline ϕ(s) = s[4]

We then define the probability distributions we need. The IMM filter takes a transition-probability matrix, $P$, and an initial mixing probability, $μ$. $P$ is a Markov (stochastic) matrix, where each row sums to one, and P[i, j] is the probability of switching from mode i to mode j. μ is a vector of probabilities, where μ[i] is the probability of starting in mode i. We also define the noise distributions for the dynamics and the measurements. The dynamics noise is modeled as a Gaussian distribution with a standard deviation of dvσ for the velocity and ϕσ for the angle. The measurement noise is modeled as a Gaussian distribution with a standard deviation of dgσ. The initial state is modeled as a Gaussian distribution with a mean at the first measurement and a standard deviation of d0.

dgσ = 1.0 # the deviation of the measurement noise distribution
 dvσ = 0.3 # the deviation of the dynamics noise distribution
 ϕσ  = 0.5
-P = [0.995 0.005; 0.0 1] # Transition probability matrix, we model the search mode as "almost terminal"
+P = [0.995 0.005; 0.0 1] # Transition probability matrix, we model the search mode as "terminal"
 μ = [1.0, 0.0] # Initial mixing probabilities
 R1 = Diagonal([1e-1, 1e-1, dvσ, ϕσ].^2)
 R2 = dgσ^2*I(ny) # Measurement noise covariance matrix
@@ -45,14 +45,14 @@
 sol = forward_trajectory(imm, u, y, interact=true)
 figx = plot(sol, plotu=false, plotRt=true)
 figmode = plot(sol.extra', title="Mode")
-plot(figx, figmode)
Example block output

If you have followed the particle filter tutorial Smoothing the track of a moving beetle, you will notice that the result here is much worse. We used noise parameters similar to in the particle-gilter example, but those were tuned fo the particle filter. Below, we will attempt to optimize the performance of the IMM filter.

Tuning by optimization

We will attempt to optimize the dynamics and measurement noise covariance matrices and the modegain parameter. We code this up in two functions, one that takes the parameter vector and returns an IMM filter, and one that calculates the loss given the filter. We will optimize the log-likelihood of the data given the filter.

params = [log10.(diag(R1)); log10(1); log10(10)]
+plot(figx, figmode)
Example block output

If you have followed the particle filter tutorial Smoothing the track of a moving beetle, you will notice that the result here is much worse. We used noise parameters similar to in the particle-gilter example, but those were tuned fo the particle filter. Below, we will attempt to optimize the performance of the IMM filter.

Tuning by optimization

We will attempt to optimize the dynamics and measurement noise covariance matrices and the modegain parameter. We code this up in two functions, one that takes the parameter vector and returns an IMM filter, and one that calculates the loss given the filter. We will optimize the log-likelihood of the data given the filter.

params = [log10.(diag(R1)); log10(1); log10(10)]
 
 function get_opt_kf(p)
     T = eltype(p)
     R1i = Diagonal(SVector{4}(exp10.(p[1:4])))
     R2i = SMatrix{2,2}(exp10(p[5])*R2)
     d0i = MvNormal(SVector{4, T}(T.(d0.μ)), SMatrix{4,4}(T.(d0.Σ)))
-    modegain = 5+exp10(p[6])
+    modegain = 2+exp10(p[6])
     Pi = SMatrix{2,2, Float64,4}(P)
     # sigmoid(x) = 1/(1+exp(-x))
     # switch_prob = sigmoid(p[7])
@@ -65,7 +65,8 @@
 function cost(pars)
     try
         imm = get_opt_kf(pars)
-        ll = loglik(imm, u, y, interact=true) - 1/2*logdet(imm.models[1].R1)
+        T = length(y)
+        ll = loglik(imm, u[1:T], y[1:T], interact=true) - 1/2*logdet(imm.models[1].R1)
         return -ll
     catch e
         # rethrow() # If you only get Inf, you can uncomment this line to see the error message
@@ -78,15 +79,16 @@
 res = Optim.optimize(
     cost,
     params,
-    ParticleSwarm(),
+    ParticleSwarm(), # Use a gradient-free optimizer. ForwardDiff works, but the algorithm is numerically difficult to compute gradients through and may suffer from overflows in the gradient computation
     Optim.Options(
         show_trace        = true,
         show_every        = 5,
-        iterations        = 200,
+        iterations        = 100,
+        time_limit        = 30,
     ),
 )
 
 imm = get_opt_kf(res.minimizer)
 
 sol = forward_trajectory(imm, u, y)
-plot(sol.extra', title="Mode (optimized filter)")
Example block output

If it went well, the filter should be in mode 1 (the false mode) from the start until just before 200 time steps, at which point it should switch to model 2 (true). This method of detecting the mode switch of the beetle appears to be somewhat less robust than the particle filter, but is significantly cheaper computationally.

The IMM filter does not stick in mode 2 perpetually after having reached it since it never actually becomes fully confident that mode 2 has been reached, but detecting the first switch is sufficient to know that the switch has occurred.

+plot(sol.extra', title="Mode (optimized filter)")Example block output

If it went well, the filter should be in mode 1 (the false mode) from the start until around 200 time steps, at which point it should switch to model 2 (true). This method of detecting the mode switch of the beetle appears to be somewhat less robust than the particle filter, but is significantly cheaper computationally.

The IMM filter does not stick in mode 2 perpetually after having reached it since it never actually becomes fully confident that mode 2 has been reached, but detecting the first switch is sufficient to know that the switch has occurred.

The log-likelihood of the solution

sol.ll
-1660.8132416661117

should be similar to that of the particle-filter in the tutorial Smoothing the track of a moving beetle, which was around -1660.

diff --git a/dev/benchmark/index.html b/dev/benchmark/index.html index 69e713b3..235a3ca1 100644 --- a/dev/benchmark/index.html +++ b/dev/benchmark/index.html @@ -36,4 +36,4 @@ particle_count = [10, 20, 50, 100, 200, 500, 1000] nT = length(time_steps) leg = reshape(["$(time_steps[i]) time steps" for i = 1:nT], 1,:) -plot(particle_count,RMSE,xscale=:log10, ylabel="RMS errors", xlabel=" Number of particles", lab=leg)

window

+plot(particle_count,RMSE,xscale=:log10, ylabel="RMS errors", xlabel=" Number of particles", lab=leg)

window

diff --git a/dev/dae/index.html b/dev/dae/index.html index 3e7fa660..f1a35ff9 100644 --- a/dev/dae/index.html +++ b/dev/dae/index.html @@ -1,2 +1,2 @@ -State estimation for DAE systems · LowLevelParticleFilters Documentation +State estimation for DAE systems · LowLevelParticleFilters Documentation diff --git a/dev/discretization/index.html b/dev/discretization/index.html index 0a57ce16..d1180cf5 100644 --- a/dev/discretization/index.html +++ b/dev/discretization/index.html @@ -36,4 +36,4 @@ Nd = sys_wd.B # The discretized noise input matrix R1d = Nd*Nd' # The final discrete-time covariance matrix
2×2 Matrix{Float64}:
  6.25e-6   0.000125
- 0.000125  0.0025

We can verify that the matrix we computed corresponds to the theoretical covariance matrix for a discrete-time double integrator:

R1d ≈ σ1^2*[Ts^2 / 2; Ts]*[Ts^2 / 2; Ts]'
true

For a nonlinear system, we could adopt a similar strategy by first linearizing the system around a suitable operating point. Alternatively, we could make use of the fact that some of the state estimators in this package allows the covariance matrices to be functions of the state, and thus compute a new discretized covariance matrix using a linearization around the current state.

Non-uniform sample rates

Special care is needed if the sample rate is not constant, i.e., the time interval between measurements varies.

Dropped samples

A common case is that the sample rate is constant, but some measurements are lost. This case is very easy to handle; the filter loop iterates between two steps

  1. Prediction using predict!(filter, x, u, p, t)
  2. Correction using
    • correct!(f, u, y, p, t) if using the standard measurement model of the filter
    • correct!(f, mm, u, y, p, t, mm) to use a custom measurement model mm

If a measurement y is lacking, one simply skips the corresponding call to correct! where y is missing. Repeated calls to predict! corresponds to simulating the system without any feedback from measurements, like if an ODE was solved. Internally, the filter will keep track of the covariance of the estimate, which is likely to grow if no measurements are used to inform the filter about the state of the system.

Sensors with different sample rates

For Kalman-type filters, it is possible to construct custom measurement models, and pass an instance of a measurement model as the second argument to correct!. This allows for sensor fusion with sensors operating at different rates, or when parts of the measurement model are linear, and other parts are nonlinear. See examples in Measurement models for how to construct explicit measurement models.

A video demonstrating the use of multiple measurement models running at different rates is available on YouTube:

Stochastic sample rate

In some situations, such as in event-based systems, the sample rate is truly stochastic. There is no single correct way of handling this, and we instead outline some alternative approaches.

  • If the filtering is performed offline on a batch of data, time-varying dynamics can be used, for instance by supplying matrices to a KalmanFilter on the form A[:, :, t]. Each A is then computed as the discretization with the sample time given as the time between measurement t and measurement t+1.
  • A conceptually simple approach is to choose a very small sample interval $T_s$ which is smaller than the smallest occuring sample interval in the data, and approximate each sample interval by rounding it to the nearest integer multiple of $T_s$. This transforms the problem to an instance of the "dropped samples" problem described above.
  • Make use of an adaptive integrator instead of the fixed-step rk4 supplied in this package, and manually keep track of the step length that needs to be taken.
+ 0.000125 0.0025

We can verify that the matrix we computed corresponds to the theoretical covariance matrix for a discrete-time double integrator:

R1d ≈ σ1^2*[Ts^2 / 2; Ts]*[Ts^2 / 2; Ts]'
true

For a nonlinear system, we could adopt a similar strategy by first linearizing the system around a suitable operating point. Alternatively, we could make use of the fact that some of the state estimators in this package allows the covariance matrices to be functions of the state, and thus compute a new discretized covariance matrix using a linearization around the current state.

Non-uniform sample rates

Special care is needed if the sample rate is not constant, i.e., the time interval between measurements varies.

Dropped samples

A common case is that the sample rate is constant, but some measurements are lost. This case is very easy to handle; the filter loop iterates between two steps

  1. Prediction using predict!(filter, x, u, p, t)
  2. Correction using
    • correct!(f, u, y, p, t) if using the standard measurement model of the filter
    • correct!(f, mm, u, y, p, t, mm) to use a custom measurement model mm

If a measurement y is lacking, one simply skips the corresponding call to correct! where y is missing. Repeated calls to predict! corresponds to simulating the system without any feedback from measurements, like if an ODE was solved. Internally, the filter will keep track of the covariance of the estimate, which is likely to grow if no measurements are used to inform the filter about the state of the system.

Sensors with different sample rates

For Kalman-type filters, it is possible to construct custom measurement models, and pass an instance of a measurement model as the second argument to correct!. This allows for sensor fusion with sensors operating at different rates, or when parts of the measurement model are linear, and other parts are nonlinear. See examples in Measurement models for how to construct explicit measurement models.

A video demonstrating the use of multiple measurement models running at different rates is available on YouTube:

Stochastic sample rate

In some situations, such as in event-based systems, the sample rate is truly stochastic. There is no single correct way of handling this, and we instead outline some alternative approaches.

  • If the filtering is performed offline on a batch of data, time-varying dynamics can be used, for instance by supplying matrices to a KalmanFilter on the form A[:, :, t]. Each A is then computed as the discretization with the sample time given as the time between measurement t and measurement t+1.
  • A conceptually simple approach is to choose a very small sample interval $T_s$ which is smaller than the smallest occuring sample interval in the data, and approximate each sample interval by rounding it to the nearest integer multiple of $T_s$. This transforms the problem to an instance of the "dropped samples" problem described above.
  • Make use of an adaptive integrator instead of the fixed-step rk4 supplied in this package, and manually keep track of the step length that needs to be taken.
diff --git a/dev/distributions/index.html b/dev/distributions/index.html index 7a39edb3..5a371cc9 100644 --- a/dev/distributions/index.html +++ b/dev/distributions/index.html @@ -9,4 +9,4 @@ @btime logpdf($dm,$(Vector(sv))) # 11.392 ns (0 allocations: 0 bytes)
@btime logpdf($d,$sv)  # 13.964 ns (0 allocations: 0 bytes)
 @btime logpdf($dt,$sv) # 12.817 ns (0 allocations: 0 bytes)
 @btime logpdf($dm,$sv) # 8.383  ns (0 allocations: 0 bytes)

Without loading LowLevelParticleFilters, the timing for the native distributions are the following

@btime logpdf($d,$sv)  # 18.040 ns (0 allocations: 0 bytes)
-@btime logpdf($dm,$sv) # 9.938  ns (0 allocations: 0 bytes)
+@btime logpdf($dm,$sv) # 9.938 ns (0 allocations: 0 bytes) diff --git a/dev/fault_detection/index.html b/dev/fault_detection/index.html index 4a7f667b..e0a8b4f4 100644 --- a/dev/fault_detection/index.html +++ b/dev/fault_detection/index.html @@ -1,2 +1,2 @@ -Fault detection · LowLevelParticleFilters Documentation +Fault detection · LowLevelParticleFilters Documentation diff --git a/dev/index-33d29abe.png b/dev/index-33d29abe.png new file mode 100644 index 00000000..7d830ddc Binary files /dev/null and b/dev/index-33d29abe.png differ diff --git a/dev/index-388f16d4.png b/dev/index-388f16d4.png deleted file mode 100644 index d4d58f51..00000000 Binary files a/dev/index-388f16d4.png and /dev/null differ diff --git a/dev/index-4b9768e5.png b/dev/index-4b9768e5.png new file mode 100644 index 00000000..5b608486 Binary files /dev/null and b/dev/index-4b9768e5.png differ diff --git a/dev/index-4cc3cda1.svg b/dev/index-4cc3cda1.svg new file mode 100644 index 00000000..e2cc7445 --- /dev/null +++ b/dev/index-4cc3cda1.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/index-62cb66c7.png b/dev/index-62cb66c7.png new file mode 100644 index 00000000..5e5d2526 Binary files /dev/null and b/dev/index-62cb66c7.png differ diff --git a/dev/index-6ac96477.png b/dev/index-6ac96477.png deleted file mode 100644 index dacb8edb..00000000 Binary files a/dev/index-6ac96477.png and /dev/null differ diff --git a/dev/index-80b9d4e0.png b/dev/index-80b9d4e0.png deleted file mode 100644 index 33909e04..00000000 Binary files a/dev/index-80b9d4e0.png and /dev/null differ diff --git a/dev/index-894e6729.svg b/dev/index-894e6729.svg deleted file mode 100644 index d56ec761..00000000 --- a/dev/index-894e6729.svg +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/dev/index-8fbe54cb.svg b/dev/index-8fbe54cb.svg new file mode 100644 index 00000000..30435dfd --- /dev/null +++ b/dev/index-8fbe54cb.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/index-96104843.svg b/dev/index-96104843.svg deleted file mode 100644 index c4ad4661..00000000 --- a/dev/index-96104843.svg +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/dev/index-b37be692.png b/dev/index-b37be692.png deleted file mode 100644 index c71a121d..00000000 Binary files a/dev/index-b37be692.png and /dev/null differ diff --git a/dev/index-e6146f98.png b/dev/index-e6146f98.png new file mode 100644 index 00000000..e224336b Binary files /dev/null and b/dev/index-e6146f98.png differ diff --git a/dev/index.html b/dev/index.html index 7f30a038..3d5f2110 100644 --- a/dev/index.html +++ b/dev/index.html @@ -35,10 +35,10 @@ pf(u[1], y[1]) # Perform one filtering step using input u and measurement y particles(pf) # Query the filter for particles, try weights(pf) or expweights(pf) as well x̂ = weighted_mean(pf) # using the current state
2-element Vector{Float64}:
- -0.9019972897330913
-  0.09993085374393618

If you want to perform batch filtering using an existing trajectory consisting of vectors of inputs and measurements, try any of the functions forward_trajectory, mean_trajectory:

sol = forward_trajectory(pf, u, y) # Filter whole trajectories at once
+ -0.1341748822051022
+ -0.31374513551312594

If you want to perform batch filtering using an existing trajectory consisting of vectors of inputs and measurements, try any of the functions forward_trajectory, mean_trajectory:

sol = forward_trajectory(pf, u, y) # Filter whole trajectories at once
 x̂,ll = mean_trajectory(pf, u, y)
-plot(sol, xreal=xs, markersize=2)
Example block output

u ad y are then assumed to be vectors of vectors. StaticArrays is recommended for maximum performance.

If MonteCarloMeasurements.jl is loaded, you may transform the output particles to Matrix{MonteCarloMeasurements.Particles} with the layout T × n_state using Particles(x,we). Internally, the particles are then resampled such that they all have unit weight. This is conventient for making use of the plotting facilities of MonteCarloMeasurements.jl.

For a full usage example, see the benchmark section below or example_lineargaussian.jl

Resampling

The particle filter will perform a resampling step whenever the distribution of the weights has become degenerate. The resampling is triggered when the effective number of samples is smaller than pf.resample_threshold $\in [0, 1]$, this value can be set when constructing the filter. How the resampling is done is governed by pf.resampling_strategy, we currently provide ResampleSystematic <: ResamplingStrategy as the only implemented strategy. See https://en.wikipedia.org/wiki/Particle_filter for more info.

Particle Smoothing

Smoothing is the process of finding the best state estimate given both past and future data. Smoothing is thus only possible in an offline setting. This package provides a particle smoother, based on forward filtering, backward simulation (FFBS), example usage follows:

N     = 2000 # Number of particles
+plot(sol, xreal=xs, markersize=2)
Example block output

u ad y are then assumed to be vectors of vectors. StaticArrays is recommended for maximum performance.

If MonteCarloMeasurements.jl is loaded, you may transform the output particles to Matrix{MonteCarloMeasurements.Particles} with the layout T × n_state using Particles(x,we). Internally, the particles are then resampled such that they all have unit weight. This is conventient for making use of the plotting facilities of MonteCarloMeasurements.jl.

For a full usage example, see the benchmark section below or example_lineargaussian.jl

Resampling

The particle filter will perform a resampling step whenever the distribution of the weights has become degenerate. The resampling is triggered when the effective number of samples is smaller than pf.resample_threshold $\in [0, 1]$, this value can be set when constructing the filter. How the resampling is done is governed by pf.resampling_strategy, we currently provide ResampleSystematic <: ResamplingStrategy as the only implemented strategy. See https://en.wikipedia.org/wiki/Particle_filter for more info.

Particle Smoothing

Smoothing is the process of finding the best state estimate given both past and future data. Smoothing is thus only possible in an offline setting. This package provides a particle smoother, based on forward filtering, backward simulation (FFBS), example usage follows:

N     = 2000 # Number of particles
 T     = 80   # Number of time steps
 M     = 100  # Number of smoothed backwards trajectories
 pf    = ParticleFilter(N, dynamics, measurement, df, dg, d0)
@@ -53,10 +53,10 @@
 xbt   = smoothed_trajs(xb)  # Get smoothing trajectories
 xbs   = [diag(xbc) for xbc in xbc] |> vecvec_to_mat .|> sqrt
 plot(xbm', ribbon=2xbs, lab="PF smooth")
-plot!(vecvec_to_mat(x), l=:dash, lab="True")
Example block output

We can plot the particles themselves as well

downsample = 5
+plot!(vecvec_to_mat(x), l=:dash, lab="True")
Example block output

We can plot the particles themselves as well

downsample = 5
 plot(vecvec_to_mat(x), l=(4,), layout=(2,1), show=false)
 scatter!(xbt[1, 1:downsample:end, :]', subplot=1, show=false, m=(1,:black, 0.5), lab="")
-scatter!(xbt[2, 1:downsample:end, :]', subplot=2, m=(1,:black, 0.5), lab="")
Example block output

Kalman filter

The KalmanFilter (wiki) assumes that $f$ and $g$ are linear functions, i.e., that they can be written on the form

\[\begin{aligned} +scatter!(xbt[2, 1:downsample:end, :]', subplot=2, m=(1,:black, 0.5), lab="")Example block output

Kalman filter

The KalmanFilter (wiki) assumes that $f$ and $g$ are linear functions, i.e., that they can be written on the form

\[\begin{aligned} x(t+1) &= Ax(t) + Bu(t) + w(t)\\ y(t) &= Cx(t) + Du(t) + e(t) \end{aligned}\]

for some matrices $A,B,C,D$ where $w \sim N(0, R_1)$ and $e \sim N(0, R_2)$ are zero mean and Gaussian. The Kalman filter represents the posterior distributions over $x$ by the mean and a covariance matrix. The magic behind the Kalman filter is that linear transformations of Gaussian distributions remain Gaussian, and we thus have a very efficient way of representing them.

A Kalman filter is easily created using the constructor KalmanFilter. Many of the functions defined for particle filters, are defined also for Kalman filters, e.g.:

R1 = cov(df)
@@ -72,7 +72,7 @@
 end

The matrices in the Kalman filter may be time varying, such that A[:, :, t] is $A(t)$. They may also be provided as functions on the form $A(t) = A(x, u, p, t)$. This works for both dynamics and covariance matrices.

The numeric type used in the Kalman filter is determined from the mean of the initial state distribution, so make sure that this has the correct type if you intend to use, e.g., Float32 or ForwardDiff.Dual for automatic differentiation.

Smoothing using KF

Kalman filters can also be used for smoothing

kf = KalmanFilter(A, B, C, 0, cov(df), cov(dg), d0)
 xT,R,lls = smooth(kf, u, y) # Returns smoothed state, smoothed cov, loglik

Plot and compare PF and KF

plot(vecvec_to_mat(xT), lab="Kalman smooth", layout=2)
 plot!(xbm', lab="pf smooth")
-plot!(vecvec_to_mat(x), lab="true")
Example block output

Kalman filter tuning tutorial

The tutorial "How to tune a Kalman filter" details how to figure out appropriate covariance matrices for the Kalman filter, as well as how to add disturbance models to the system model.

Unscented Kalman Filter

The UnscentedKalmanFilter represents posterior distributions over $x$ as Gaussian distributions just like the KalmanFilter, but propagates them through a nonlinear function $f$ by a deterministic sampling of a small number of particles called sigma points (this is referred to as the unscented transform). This UKF thus handles nonlinear functions $f,g$, but only Gaussian disturbances and unimodal posteriors. The UKF will by default treat the noise as additive, but by using the augmented UKF form, non-additive noise may be handled as well. See the docstring of UnscentedKalmanFilter for more details.

The UKF takes the same arguments as a regular KalmanFilter, but the matrices defining the dynamics are replaced by two functions, dynamics and measurement, working in the same way as for the ParticleFilter above (unless the augmented form is used).

ukf = UnscentedKalmanFilter(dynamics, measurement, cov(df), cov(dg), MvNormal(SA[1.,1.]); nu=nu, ny=ny)
UnscentedKalmanFilter{false, false, false, false, typeof(Main.dynamics), UKFMeasurementModel{false, false, typeof(Main.measurement), Matrix{Float64}, typeof(-), typeof(LowLevelParticleFilters.safe_mean), typeof(LowLevelParticleFilters.safe_cov), typeof(LowLevelParticleFilters.cross_cov), LowLevelParticleFilters.SigmaPointCache{Vector{StaticArraysCore.SVector{2, Float64}}, Vector{StaticArraysCore.SVector{1, Float64}}}}, Matrix{Float64}, Distributions.MvNormal{Float64, PDMats.PDiagMat{Float64, StaticArraysCore.SVector{2, Float64}}, FillArrays.Zeros{Float64, 1, Tuple{Base.OneTo{Int64}}}}, LowLevelParticleFilters.SigmaPointCache{Vector{StaticArraysCore.SVector{2, Float64}}, Vector{StaticArraysCore.SVector{2, Float64}}}, Vector{Float64}, Matrix{Float64}, LowLevelParticleFilters.NullParameters, Nothing, typeof(LowLevelParticleFilters.safe_mean), typeof(LowLevelParticleFilters.safe_cov), typeof(LinearAlgebra.cholesky!)}(Main.dynamics, UKFMeasurementModel{false, false, typeof(Main.measurement), Matrix{Float64}, typeof(-), typeof(LowLevelParticleFilters.safe_mean), typeof(LowLevelParticleFilters.safe_cov), typeof(LowLevelParticleFilters.cross_cov), LowLevelParticleFilters.SigmaPointCache{Vector{StaticArraysCore.SVector{2, Float64}}, Vector{StaticArraysCore.SVector{1, Float64}}}}(Main.measurement, [0.04000000000000001;;], 1, 0, -, LowLevelParticleFilters.safe_mean, LowLevelParticleFilters.safe_cov, LowLevelParticleFilters.cross_cov, LowLevelParticleFilters.SigmaPointCache{Vector{StaticArraysCore.SVector{2, Float64}}, Vector{StaticArraysCore.SVector{1, Float64}}}(StaticArraysCore.SVector{2, Float64}[[0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0]], StaticArraysCore.SVector{1, Float64}[[0.0], [0.0], [0.0], [0.0], [0.0]])), [0.010000000000000002 0.0; 0.0 0.010000000000000002], Distributions.MvNormal{Float64, PDMats.PDiagMat{Float64, StaticArraysCore.SVector{2, Float64}}, FillArrays.Zeros{Float64, 1, Tuple{Base.OneTo{Int64}}}}(
+plot!(vecvec_to_mat(x), lab="true")
Example block output

Kalman filter tuning tutorial

The tutorial "How to tune a Kalman filter" details how to figure out appropriate covariance matrices for the Kalman filter, as well as how to add disturbance models to the system model.

Unscented Kalman Filter

The UnscentedKalmanFilter represents posterior distributions over $x$ as Gaussian distributions just like the KalmanFilter, but propagates them through a nonlinear function $f$ by a deterministic sampling of a small number of particles called sigma points (this is referred to as the unscented transform). This UKF thus handles nonlinear functions $f,g$, but only Gaussian disturbances and unimodal posteriors. The UKF will by default treat the noise as additive, but by using the augmented UKF form, non-additive noise may be handled as well. See the docstring of UnscentedKalmanFilter for more details.

The UKF takes the same arguments as a regular KalmanFilter, but the matrices defining the dynamics are replaced by two functions, dynamics and measurement, working in the same way as for the ParticleFilter above (unless the augmented form is used).

ukf = UnscentedKalmanFilter(dynamics, measurement, cov(df), cov(dg), MvNormal(SA[1.,1.]); nu=nu, ny=ny)
UnscentedKalmanFilter{false, false, false, false, typeof(Main.dynamics), UKFMeasurementModel{false, false, typeof(Main.measurement), Matrix{Float64}, typeof(-), typeof(LowLevelParticleFilters.safe_mean), typeof(LowLevelParticleFilters.safe_cov), typeof(LowLevelParticleFilters.cross_cov), LowLevelParticleFilters.SigmaPointCache{Vector{StaticArraysCore.SVector{2, Float64}}, Vector{StaticArraysCore.SVector{1, Float64}}}}, Matrix{Float64}, Distributions.MvNormal{Float64, PDMats.PDiagMat{Float64, StaticArraysCore.SVector{2, Float64}}, FillArrays.Zeros{Float64, 1, Tuple{Base.OneTo{Int64}}}}, LowLevelParticleFilters.SigmaPointCache{Vector{StaticArraysCore.SVector{2, Float64}}, Vector{StaticArraysCore.SVector{2, Float64}}}, Vector{Float64}, Matrix{Float64}, LowLevelParticleFilters.NullParameters, Nothing, typeof(LowLevelParticleFilters.safe_mean), typeof(LowLevelParticleFilters.safe_cov), typeof(LinearAlgebra.cholesky!)}(Main.dynamics, UKFMeasurementModel{false, false, typeof(Main.measurement), Matrix{Float64}, typeof(-), typeof(LowLevelParticleFilters.safe_mean), typeof(LowLevelParticleFilters.safe_cov), typeof(LowLevelParticleFilters.cross_cov), LowLevelParticleFilters.SigmaPointCache{Vector{StaticArraysCore.SVector{2, Float64}}, Vector{StaticArraysCore.SVector{1, Float64}}}}(Main.measurement, [0.04000000000000001;;], 1, 0, -, LowLevelParticleFilters.safe_mean, LowLevelParticleFilters.safe_cov, LowLevelParticleFilters.cross_cov, LowLevelParticleFilters.SigmaPointCache{Vector{StaticArraysCore.SVector{2, Float64}}, Vector{StaticArraysCore.SVector{1, Float64}}}(StaticArraysCore.SVector{2, Float64}[[0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0]], StaticArraysCore.SVector{1, Float64}[[0.0], [0.0], [0.0], [0.0], [0.0]])), [0.010000000000000002 0.0; 0.0 0.010000000000000002], Distributions.MvNormal{Float64, PDMats.PDiagMat{Float64, StaticArraysCore.SVector{2, Float64}}, FillArrays.Zeros{Float64, 1, Tuple{Base.OneTo{Int64}}}}(
 dim: 2
 μ: Zeros(2)
 Σ: [1.0 0.0; 0.0 1.0]
@@ -103,27 +103,27 @@
   p: LowLevelParticleFilters.NullParameters LowLevelParticleFilters.NullParameters()
   threads: Bool false
   Ts: Float64 1.0
-, StaticArraysCore.SVector{1, Float64}[[-0.581146869379883], [-1.469770570308081], [-0.4665682508355672], [0.4234924411574639], [-1.2994733710630846], [0.19717030178114028], [0.5781200218992196], [0.06754736822207849], [-1.572069110356176], [0.17227229037093392]  …  [0.2769012811822157], [-0.5043025072555615], [1.0903524694417166], [0.9708346951972306], [0.2376545415920728], [-0.19697645842384984], [-1.276720957403689], [0.3164908801143953], [0.9957909432454998], [0.21073604411718913]], StaticArraysCore.SVector{1, Float64}[[0.5391939827750171], [0.18249931772160993], [0.10087755016317689], [-0.030646125661147258], [-0.06840802251253109], [-0.2754469405850646], [-0.6598459146381536], [-0.08451814678404623], [-0.8487502596864017], [-0.5813112714708646]  …  [0.1506382818748036], [0.6440322824183035], [0.1145393221188427], [0.17567522546691838], [0.46315975412284505], [0.16957810665037654], [-0.3111368843884259], [-0.6698000197360167], [-0.24455231528274332], [-0.1592922241654996]], StaticArraysCore.SVector{2, Float64}[[-3.3153421443122513, -2.4541217991834285] [0.25154080451826155, 0.41265079618577816] … [-0.6604198046223679, -0.2840031842113254] [-0.4858714356475497, -0.20358222568168374]; [0.4416529515995341, 0.47928403107595907] [0.19928333343389698, 0.6284416872336622] … [-0.8097845965671094, -0.5176163851766447] [-0.5381792825321053, -0.49309215041687404]; … ; [1.0298013271664521, -1.7041575966065925] [-1.287666187735087, 0.6136860145828382] … [-0.20231986795731988, -0.2424585032449692] [0.16054112245350213, -0.27018806758351616]; [-2.8475007636855683, -0.2986138941616716] [-1.3831915677243751, 0.835860591189961] … [-0.37642140979492167, -0.23009734942673565] [-0.14349861748985682, -0.35382671254377407]], [-117.36454134010754 -7.342943359561149 … -7.180603797284416 -6.57295337980245; -5.410164237496096 -9.166629533145915 … -8.093198987381932 -8.85380840153896; … ; -68.27312806751331 -9.004846665946825 … -7.161203959654783 -6.6827570903364375; -14.139324691803322 -12.016833992422935 … -7.163760984517893 -7.004636353714595], [1.0696146063254322e-51 0.0006471429406122278 … 0.0007612080879892544 0.0013976634795139352; 0.004470905860662188 0.00010446802302518174 … 0.0003056105474434297 0.00014283671953124513; … ; 2.235410147260729e-30 0.0001228131251115503 … 0.0007761195738376076 0.0012523204477937811; 7.233846826193592e-7 6.041646448073951e-6 … 0.0007741375519101658 0.0009076639438922475], -18.90520087624123)
plot(sol, xreal=x)
Example block output

We can even use this type as an AuxiliaryParticleFilter

apfa = AuxiliaryParticleFilter(apf)
+, StaticArraysCore.SVector{1, Float64}[[2.039129321735416], [1.151597694841343], [0.14053103072265363], [-0.5094480022037171], [-0.3264775442126382], [2.2765002943710693], [-1.0765221156821387], [0.32376782093108836], [-1.1401054540200994], [-1.7487208181298963]  …  [1.610889849566029], [-1.1038081711193932], [1.7951582478483346], [0.3627856246592035], [0.04464316800248958], [0.09932971641987426], [-0.7659901715531249], [0.8365073240852781], [-0.7153055161303419], [0.5024990644200305]], StaticArraysCore.SVector{1, Float64}[[-0.15195378778438828], [-0.136234692260716], [-0.04625962370970575], [-0.2656630013766956], [0.32226659370652505], [0.272079043578301], [0.1454060659305941], [0.9579799076845587], [0.46183980027537025], [0.21177737888290737]  …  [0.04836438156831484], [0.2053723445488685], [-0.19788624438997968], [-0.07036645109882726], [0.2198240373639479], [0.10496125015329888], [0.19987803633018758], [0.32072339879967576], [0.7749932994985786], [0.5883278851737495]], StaticArraysCore.SVector{2, Float64}[[-1.3590046681886658, -0.5237985500240129] [-1.0559797483754867, -0.7007290475151341] … [0.1710352000831514, -0.054895966666212014] [0.2963868760206542, 0.6029967049599272]; [-1.5931213264149418, 0.4188477218246225] [-1.4389746212444192, 0.17196304447145783] … [0.4165559095356379, 0.5554707276650006] [0.7367438753916016, 0.4505937180448987]; … ; [1.1509161965527337, -1.638603245343401] [-2.151531141273438, 0.08093842260054027] … [0.9483243270408499, 0.4595291741033511] [0.7502879718491563, 0.5326993592200591]; [2.9766025218569214, 1.2045253376049334] [0.12361187820727325, -0.6052850800417828] … [1.1354797552083327, 0.7223257093307399] [0.7129216256758648, 0.36275341094432334]], [-7.040807060082262 -10.974429961943297 … -16.717396041544564 -7.308329692607172; -9.385130012168295 -8.178579559602461 … -7.076744685633059 -7.542773774116657; … ; -32.93903309077555 -7.5808085207487865 … -6.9482772958112875 -7.344321675430435; -28.31289569023146 -9.741359826486645 … -12.766698885283327 -7.941688056981706], [0.000875419760736845 1.7134270761816663e-5 … 5.491957242211849e-8 0.00066993511563328; 8.396336273616395e-5 0.0002806002330926794 … 0.0008445178496110321 0.0005299256871800618; … ; 4.951761164623748e-15 0.0005101485905055395 … 0.0009602880217813548 0.0006462515876515632; 5.056681907476915e-13 5.8800521944811705e-5 … 2.8542573100480076e-6 0.00035560568870060826], -6.86404020850312)
plot(sol, xreal=x)
Example block output

We can even use this type as an AuxiliaryParticleFilter

apfa = AuxiliaryParticleFilter(apf)
 sol = forward_trajectory(apfa, u, y, ny)
-plot(sol, dim=1, xreal=x) # Same as above, but only plots a single dimension
Example block output

See the tutorials section for more advanced examples, including state estimation for DAE (Differential-Algebraic Equation) systems.

Troubleshooting and tuning

Tuning a particle filter can be quite the challenge. To assist with this, we provide som visualization tools

debugplot(pf,u[1:20],y[1:20], runall=true, xreal=x[1:20])
Time     Surviving    Effective nbr of particles
+plot(sol, dim=1, xreal=x) # Same as above, but only plots a single dimension
Example block output

See the tutorials section for more advanced examples, including state estimation for DAE (Differential-Algebraic Equation) systems.

Troubleshooting and tuning

Tuning a particle filter can be quite the challenge. To assist with this, we provide som visualization tools

debugplot(pf,u[1:20],y[1:20], runall=true, xreal=x[1:20])
Time     Surviving    Effective nbr of particles
 --------------------------------------------------------------
 t:     1   1.000    2000.0
-t:     2   1.000     296.0
-t:     3   0.147    2000.0
-t:     4   1.000    1468.7
-t:     5   1.000     974.4
-t:     6   1.000     680.1
-t:     7   1.000     514.6
-t:     8   1.000     353.6
-t:     9   0.208    2000.0
-t:    10   1.000    1020.6
-t:    11   1.000    1057.6
-t:    12   1.000     598.1
-t:    13   1.000     535.7
-t:    14   1.000     327.0
-t:    15   0.268    2000.0
-t:    16   1.000    1549.9
-t:    17   1.000     581.9
-t:    18   1.000     554.1
-t:    19   1.000     482.8
-t:    20   1.000     366.4

The plot displays all state variables and all measurements. The heatmap in the background represents the weighted particle distributions per time step. For the measurement sequences, the heatmap represent the distributions of predicted measurements. The blue dots corresponds to measured values. In this case, we simulated the data and we had access to the state as well, if we do not have that, just omit xreal. You can also manually step through the time-series using

  • commandplot(pf,u,y; kwargs...)

For options to the debug plots, see ?pplot.

Tuning noise parameters through optimization

See examples in Parameter Estimation.

Tuning through simulation

It is possible to sample from the Bayesian model implied by a filter and its parameters by calling the function simulate. A simple tuning strategy is to adjust the noise parameters such that a simulation looks "similar" to the data, i.e., the data must not be too unlikely under the model.

Videos

Several video tutorials using this package are available in the playlists

Some examples featuring this package in particular are


Using an optimizer to optimize the likelihood of an UnscentedKalmanFilter:


Estimation of time-varying parameters:


Adaptive control by means of estimation of time-varying parameters:

+t: 2 1.000 278.6 +t: 3 0.149 2000.0 +t: 4 1.000 1462.6 +t: 5 1.000 933.9 +t: 6 1.000 513.5 +t: 7 1.000 346.3 +t: 8 1.000 256.4 +t: 9 0.161 2000.0 +t: 10 1.000 1247.5 +t: 11 1.000 365.6 +t: 12 1.000 274.7 +t: 13 1.000 258.8 +t: 14 0.234 2000.0 +t: 15 1.000 1559.2 +t: 16 1.000 1090.9 +t: 17 1.000 536.3 +t: 18 1.000 300.5 +t: 19 1.000 337.4 +t: 20 0.279 2000.0

The plot displays all state variables and all measurements. The heatmap in the background represents the weighted particle distributions per time step. For the measurement sequences, the heatmap represent the distributions of predicted measurements. The blue dots corresponds to measured values. In this case, we simulated the data and we had access to the state as well, if we do not have that, just omit xreal. You can also manually step through the time-series using

  • commandplot(pf,u,y; kwargs...)

For options to the debug plots, see ?pplot.

Tuning noise parameters through optimization

See examples in Parameter Estimation.

Tuning through simulation

It is possible to sample from the Bayesian model implied by a filter and its parameters by calling the function simulate. A simple tuning strategy is to adjust the noise parameters such that a simulation looks "similar" to the data, i.e., the data must not be too unlikely under the model.

Videos

Several video tutorials using this package are available in the playlists

Some examples featuring this package in particular are


Using an optimizer to optimize the likelihood of an UnscentedKalmanFilter:


Estimation of time-varying parameters:


Adaptive control by means of estimation of time-varying parameters:

diff --git a/dev/measurement_models/index.html b/dev/measurement_models/index.html index 0b67e5eb..5fbc65ed 100644 --- a/dev/measurement_models/index.html +++ b/dev/measurement_models/index.html @@ -24,7 +24,7 @@ R2 = I(ny) mm_kf = LinearMeasurementModel(__C, 0, R2; nx, ny) -ukf = UnscentedKalmanFilter(dynamics_ip, mm_kf, R1; ny, nu)
UnscentedKalmanFilter{true, false, false, false, typeof(Main.dynamics_ip), LinearMeasurementModel{Matrix{Float64}, Int64, LinearAlgebra.Diagonal{Bool, Vector{Bool}}, Nothing}, LinearAlgebra.Diagonal{Bool, Vector{Bool}}, LowLevelParticleFilters.SimpleMvNormal{Vector{Float64}, LinearAlgebra.Diagonal{Bool, Vector{Bool}}}, LowLevelParticleFilters.SigmaPointCache{Vector{Vector{Float64}}, Vector{Vector{Float64}}}, Vector{Float64}, Matrix{Float64}, LowLevelParticleFilters.NullParameters, Nothing, typeof(LowLevelParticleFilters.safe_mean), typeof(LowLevelParticleFilters.safe_cov), typeof(LinearAlgebra.cholesky!)}(Main.dynamics_ip, LinearMeasurementModel{Matrix{Float64}, Int64, LinearAlgebra.Diagonal{Bool, Vector{Bool}}, Nothing}([-3.4336509571765053 0.7848257286874997 … 1.7409551744781047 0.28442432033664633; -0.9068377917702121 1.9162540186985246 … 1.8788277264178899 0.7201981001313605; … ; 0.715220244669793 1.5217859431962695 … -0.18597339767327933 1.001546765688925; 1.0692606615098486 0.2156844795608287 … 0.3688366360641461 -1.1181842094069676], 0, Bool[1 0 … 0 0; 0 1 … 0 0; … ; 0 0 … 1 0; 0 0 … 0 1], 90, nothing), Bool[1 0 … 0 0; 0 1 … 0 0; … ; 0 0 … 1 0; 0 0 … 0 1], LowLevelParticleFilters.SimpleMvNormal{Vector{Float64}, LinearAlgebra.Diagonal{Bool, Vector{Bool}}}([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], Bool[1 0 … 0 0; 0 1 … 0 0; … ; 0 0 … 1 0; 0 0 … 0 1]), LowLevelParticleFilters.SigmaPointCache{Vector{Vector{Float64}}, Vector{Vector{Float64}}}([[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  …  [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]], [[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  …  [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]]), [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [1.0 0.0 … 0.0 0.0; 0.0 1.0 … 0.0 0.0; … ; 0.0 0.0 … 1.0 0.0; 0.0 0.0 … 0.0 1.0], 0, 1.0, 90, 2, LowLevelParticleFilters.NullParameters(), nothing, LowLevelParticleFilters.safe_mean, LowLevelParticleFilters.safe_cov, LinearAlgebra.cholesky!)

When we create the filter with the custom measurement model, we do not pass the arguments that are associated with the measurement model to the filter constructor, i.e., we do not pass any measurement function, and not the measurement covariance matrix $R_2$.

Sensor fusion: Using several different measurement models

Above we constructed a filter with a custom measurement model, we can also pass a custom measurement model when we call correct!. This may be useful when, e.g., performing sensor fusion with sensors operating at different sample rates, or when parts of the measurement model are linear, and other parts are nonlinear.

The following example instantiates three different filters and three different measurement models. Each filter is updated with each measurement model, demonstrating that any combination of filter and measurement model can be used together.

using LowLevelParticleFilters, LinearAlgebra
+ukf = UnscentedKalmanFilter(dynamics_ip, mm_kf, R1; ny, nu)
UnscentedKalmanFilter{true, false, false, false, typeof(Main.dynamics_ip), LinearMeasurementModel{Matrix{Float64}, Int64, LinearAlgebra.Diagonal{Bool, Vector{Bool}}, Nothing}, LinearAlgebra.Diagonal{Bool, Vector{Bool}}, LowLevelParticleFilters.SimpleMvNormal{Vector{Float64}, LinearAlgebra.Diagonal{Bool, Vector{Bool}}}, LowLevelParticleFilters.SigmaPointCache{Vector{Vector{Float64}}, Vector{Vector{Float64}}}, Vector{Float64}, Matrix{Float64}, LowLevelParticleFilters.NullParameters, Nothing, typeof(LowLevelParticleFilters.safe_mean), typeof(LowLevelParticleFilters.safe_cov), typeof(LinearAlgebra.cholesky!)}(Main.dynamics_ip, LinearMeasurementModel{Matrix{Float64}, Int64, LinearAlgebra.Diagonal{Bool, Vector{Bool}}, Nothing}([0.45563868255969786 1.2767221563269395 … 0.5664561128334656 0.633487302084747; -1.0309093438610097 -0.8365799692760448 … -1.2792629123847703 -0.2654701159186893; … ; -0.7528181076204443 0.7769090438716332 … 0.4549191393234413 -0.6703097540313754; 0.8326613494926483 -1.4716719378245975 … 1.1337257013525612 1.0103431316009601], 0, Bool[1 0 … 0 0; 0 1 … 0 0; … ; 0 0 … 1 0; 0 0 … 0 1], 90, nothing), Bool[1 0 … 0 0; 0 1 … 0 0; … ; 0 0 … 1 0; 0 0 … 0 1], LowLevelParticleFilters.SimpleMvNormal{Vector{Float64}, LinearAlgebra.Diagonal{Bool, Vector{Bool}}}([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], Bool[1 0 … 0 0; 0 1 … 0 0; … ; 0 0 … 1 0; 0 0 … 0 1]), LowLevelParticleFilters.SigmaPointCache{Vector{Vector{Float64}}, Vector{Vector{Float64}}}([[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  …  [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]], [[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  …  [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]]), [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [1.0 0.0 … 0.0 0.0; 0.0 1.0 … 0.0 0.0; … ; 0.0 0.0 … 1.0 0.0; 0.0 0.0 … 0.0 1.0], 0, 1.0, 90, 2, LowLevelParticleFilters.NullParameters(), nothing, LowLevelParticleFilters.safe_mean, LowLevelParticleFilters.safe_cov, LinearAlgebra.cholesky!)

When we create the filter with the custom measurement model, we do not pass the arguments that are associated with the measurement model to the filter constructor, i.e., we do not pass any measurement function, and not the measurement covariance matrix $R_2$.

Sensor fusion: Using several different measurement models

Above we constructed a filter with a custom measurement model, we can also pass a custom measurement model when we call correct!. This may be useful when, e.g., performing sensor fusion with sensors operating at different sample rates, or when parts of the measurement model are linear, and other parts are nonlinear.

The following example instantiates three different filters and three different measurement models. Each filter is updated with each measurement model, demonstrating that any combination of filter and measurement model can be used together.

using LowLevelParticleFilters, LinearAlgebra
 nx = 10    # Dimension of state
 nu = 2     # Dimension of input
 ny = 9     # Dimension of measurements
@@ -83,4 +83,4 @@
 [ Info: Updating ExtendedKalmanFilter with measurement model UKFMeasurementModel
 [ Info: Updating UnscentedKalmanFilter with measurement model UKFMeasurementModel

Since the dynamics in this particular example is in fact linear, we should get identical results for all three filters.

using Test
 @test kf.x ≈ ekf.x ≈ ukf.x
-@test kf.R ≈ ekf.R ≈ ukf.R
Test Passed

Video tutorial

A video demonstrating the use of multiple measurement models in a sensor-fusion context is available on YouTube:

+@test kf.R ≈ ekf.R ≈ ukf.R
Test Passed

Video tutorial

A video demonstrating the use of multiple measurement models in a sensor-fusion context is available on YouTube:

diff --git a/dev/neural_network/0c9f13e1.svg b/dev/neural_network/34a9b0f3.svg similarity index 96% rename from dev/neural_network/0c9f13e1.svg rename to dev/neural_network/34a9b0f3.svg index cdf94b11..91f8aff0 100644 --- a/dev/neural_network/0c9f13e1.svg +++ b/dev/neural_network/34a9b0f3.svg @@ -1,396 +1,396 @@ - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/neural_network/index.html b/dev/neural_network/index.html index f4c42b52..f3b9f3a8 100644 --- a/dev/neural_network/index.html +++ b/dev/neural_network/index.html @@ -107,7 +107,7 @@ plot(0:Ts:4000, reduce(hcat, xTe)'[:, nx+1:end], title="EKF parameters", c=1, alpha=0.2), plot(0:Ts:4000, reduce(hcat, xTu)'[:, nx+1:end], title="UKF parameters", c=1, alpha=0.2), legend = false, -)Example block output

Benchmarking

The neural network used in this example has

length(parr)
164

parameters, and the length of the data is

length(data.u)
134

Performing the estimation using the Extended Kalman Filter took

using BenchmarkTools
+)
Example block output

Benchmarking

The neural network used in this example has

length(parr)
164

parameters, and the length of the data is

length(data.u)
134

Performing the estimation using the Extended Kalman Filter took

using BenchmarkTools
 @btime forward_trajectory(ekf, data.u, data.x);
   # 46.034 ms (77872 allocations: 123.45 MiB)

and with the Unscented Kalman Filter

@btime forward_trajectory(ukf, data.u, data.x);
-  # 142.608 ms (2134370 allocations: 224.82 MiB)

The EKF is a bit faster, which is to be expected. Both methods are very fast from a neural-network training perspective, but the performance will not scale favorably to very large network sizes.

Closing remarks

We have seen how to estimate train a black-box neural network dynamics model by treating the parameter estimation as a state-estimation problem. This example is very simple and leaves a lot of room for improvement, such as

  • We assumed very little prior knowledge of the dynamics. In practice, we may want to model as much as possible from first principles and add a neural network to capture only the residuals that our first-principles model cannot capture.
  • We started the training of the network weights directly from a random initialization. In practice, we may want to pre-train the network on a large offline dataset before updating the weights adaptively in real-time.
  • We used forward-mode AD to compute the Jacobian. The Jacobian of the dynamics has dense rows, which means that it's theoretically favorable to use reverse-mode AD to compute it. This is possible using Zygote.jl, but Zygote does not handle array mutation, and one must thus avoid the in-place version of the dynamics. Since the number of parameters in this example is small, sparse forward mode AD ended up being slightly faster.
+ # 142.608 ms (2134370 allocations: 224.82 MiB)

The EKF is a bit faster, which is to be expected. Both methods are very fast from a neural-network training perspective, but the performance will not scale favorably to very large network sizes.

Closing remarks

We have seen how to estimate train a black-box neural network dynamics model by treating the parameter estimation as a state-estimation problem. This example is very simple and leaves a lot of room for improvement, such as

  • We assumed very little prior knowledge of the dynamics. In practice, we may want to model as much as possible from first principles and add a neural network to capture only the residuals that our first-principles model cannot capture.
  • We started the training of the network weights directly from a random initialization. In practice, we may want to pre-train the network on a large offline dataset before updating the weights adaptively in real-time.
  • We used forward-mode AD to compute the Jacobian. The Jacobian of the dynamics has dense rows, which means that it's theoretically favorable to use reverse-mode AD to compute it. This is possible using Zygote.jl, but Zygote does not handle array mutation, and one must thus avoid the in-place version of the dynamics. Since the number of parameters in this example is small, sparse forward mode AD ended up being slightly faster.
diff --git a/dev/noisetuning/index.html b/dev/noisetuning/index.html index 53ddff4c..1d30969d 100644 --- a/dev/noisetuning/index.html +++ b/dev/noisetuning/index.html @@ -1,2 +1,2 @@ -Noise tuning and disturbance modeling for Kalman filtering · LowLevelParticleFilters Documentation +Noise tuning and disturbance modeling for Kalman filtering · LowLevelParticleFilters Documentation diff --git a/dev/parameter_estimation/0ce988d8.svg b/dev/parameter_estimation/0ce988d8.svg deleted file mode 100644 index 1db38020..00000000 --- a/dev/parameter_estimation/0ce988d8.svg +++ /dev/null @@ -1,301 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/dev/parameter_estimation/0d315cae.svg b/dev/parameter_estimation/0d315cae.svg deleted file mode 100644 index ed78c4ab..00000000 --- a/dev/parameter_estimation/0d315cae.svg +++ /dev/null @@ -1,248 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/dev/parameter_estimation/1fbc1cbb.svg b/dev/parameter_estimation/1fbc1cbb.svg new file mode 100644 index 00000000..5135cc6e --- /dev/null +++ b/dev/parameter_estimation/1fbc1cbb.svg @@ -0,0 +1,248 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/parameter_estimation/23dbe7cf.svg b/dev/parameter_estimation/23dbe7cf.svg new file mode 100644 index 00000000..8cc3e7df --- /dev/null +++ b/dev/parameter_estimation/23dbe7cf.svg @@ -0,0 +1,245 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/parameter_estimation/24f69092.svg b/dev/parameter_estimation/24f69092.svg new file mode 100644 index 00000000..acc5f930 --- /dev/null +++ b/dev/parameter_estimation/24f69092.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/parameter_estimation/43f4c869.svg b/dev/parameter_estimation/43f4c869.svg deleted file mode 100644 index 5163b124..00000000 --- a/dev/parameter_estimation/43f4c869.svg +++ /dev/null @@ -1,257 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/dev/parameter_estimation/5afd5f48.svg b/dev/parameter_estimation/5afd5f48.svg deleted file mode 100644 index c829b099..00000000 --- a/dev/parameter_estimation/5afd5f48.svg +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/dev/parameter_estimation/99d67e60.svg b/dev/parameter_estimation/99d67e60.svg new file mode 100644 index 00000000..511fee1b --- /dev/null +++ b/dev/parameter_estimation/99d67e60.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/parameter_estimation/c5d789ad.svg b/dev/parameter_estimation/b3acc360.svg similarity index 93% rename from dev/parameter_estimation/c5d789ad.svg rename to dev/parameter_estimation/b3acc360.svg index d1787a3a..a1e30662 100644 --- a/dev/parameter_estimation/c5d789ad.svg +++ b/dev/parameter_estimation/b3acc360.svg @@ -1,88 +1,88 @@ - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/parameter_estimation/b61366d9.svg b/dev/parameter_estimation/b61366d9.svg new file mode 100644 index 00000000..c5a0a8fb --- /dev/null +++ b/dev/parameter_estimation/b61366d9.svg @@ -0,0 +1,304 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/parameter_estimation/b91de95b.svg b/dev/parameter_estimation/b69d40b1.svg similarity index 93% rename from dev/parameter_estimation/b91de95b.svg rename to dev/parameter_estimation/b69d40b1.svg index 11c030e7..8c01a774 100644 --- a/dev/parameter_estimation/b91de95b.svg +++ b/dev/parameter_estimation/b69d40b1.svg @@ -1,92 +1,92 @@ - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/parameter_estimation/c319f8f0.svg b/dev/parameter_estimation/c319f8f0.svg deleted file mode 100644 index 2b8e2081..00000000 --- a/dev/parameter_estimation/c319f8f0.svg +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/dev/parameter_estimation/175e5dc8.svg b/dev/parameter_estimation/d91b6330.svg similarity index 66% rename from dev/parameter_estimation/175e5dc8.svg rename to dev/parameter_estimation/d91b6330.svg index 55ed63e0..0f393c54 100644 --- a/dev/parameter_estimation/175e5dc8.svg +++ b/dev/parameter_estimation/d91b6330.svg @@ -1,189 +1,149 @@ - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - - - - - - + + + + + + + diff --git a/dev/parameter_estimation/index.html b/dev/parameter_estimation/index.html index 20d544ff..fa62acc6 100644 --- a/dev/parameter_estimation/index.html +++ b/dev/parameter_estimation/index.html @@ -17,7 +17,7 @@ measurement(x,u,p,t) = C*x vecvec_to_mat(x) = copy(reduce(hcat, x)') # Helper function pf = ParticleFilter(N, dynamics, measurement, df, dg, d0) -xs,u,y = simulate(pf,300,df)
([[1.0812304200971112, -0.14562935751734435], [2.3116314798731947, 0.8082328770481344], [2.149967983738722, 2.541312262560706], [2.872559969263048, 3.125774767585316], [1.2847604308632985, 3.594221579226578], [1.7057197186301083, 1.8021199812015898], [1.2521647148100503, 3.7759747591461132], [2.916252681282151, 5.527520598681996], [3.8655354764711896, 5.694599692041501], [3.336783380763536, 5.69541365597537]  …  [-40.057822375546, 0.10357520800534564], [-40.79781396078199, -0.14891820535533207], [-40.998735577498536, -1.3313676138345785], [-42.305070570706356, -2.4752799199342475], [-40.175733821663414, -0.9221917889417495], [-40.83920051568449, -2.3416262383382276], [-41.11639931687991, -2.930835927025443], [-42.65241345816076, -3.8233874837439936], [-44.10458147517731, -2.663071035334208], [-44.9401935069422, -2.5067551899020923]], [[0.5839054062927054, -0.27929477378843304], [0.41007457728427377, 0.9184433161348705], [0.11204639752074251, 0.5631568392262732], [1.1255022555445502, -0.432131932236531], [-1.4609285400734122, -1.0047900574943391], [1.658369563599552, 0.41173127501428186], [0.080280121082907, -1.6750679461296805], [0.47545184209756375, 0.9200875097747347], [-0.8745692970385783, -0.6063211557138892], [-0.6371267005592348, 0.3182510062201885]  …  [-1.2489711048850263, 0.5737070285485795], [-0.5339829136703733, 0.2608634380802061], [-0.24862328178704068, 0.9549561375433364], [-0.16281638770760531, 1.0145987581223301], [-1.9226803890658999, 0.8827977883629121], [-0.858732315998032, -0.8824817581291353], [-0.6331406379760188, 0.42937840647202086], [-0.4115253891654016, -0.3459220491586696], [1.0877107905147378, -1.2027615350296437], [0.5789458507677563, 1.2073250722043054]], [[1.1322693119104885, 1.2029078202705352], [2.415363163655064, 1.695904961850642], [1.615759267659335, 3.5222811498451456], [2.143408989541812, 4.449899931909501], [0.8136966133619441, 4.499992159132988], [1.5599254636028799, 2.3315079458814703], [1.2799763549358372, 4.997067283150275], [2.606702355083542, 5.166990151336355], [3.4065631365369033, 6.56772913959163], [4.298954711495609, 6.490182843053165]  …  [-38.92772845267652, -1.4240826458928186], [-39.247146472512256, -2.3438403741212466], [-38.68000016203556, 0.35067589772821006], [-40.746955577248805, -4.369904724612883], [-39.41991512287349, -0.9956165897938583], [-40.73730497150801, -1.6295350451398427], [-42.87587778055609, -2.542375460317303], [-42.671747208920145, -2.9776133707209542], [-43.409959422929205, -3.849652637003693], [-43.03840912826101, -5.067597518916225]])

Compute likelihood for various values of the parameters

p = nothing
+xs,u,y = simulate(pf,300,df)
([[1.0812304200971112, -0.14562935751734435], [0.4607585860405089, -0.26877726154333453], [0.1746695304331622, 0.8583346015504947], [-0.9214627126928474, 0.8064840415028459], [-0.7914529733164749, 1.2766786662910583], [-0.08231328525055592, -0.12651336412522596], [0.4548121977246394, -0.016090288673307418], [1.1437185445779767, 1.0102618637859997], [0.41378790949518307, 0.9733010427501054], [-0.34959947631249844, 2.0102727426935676]  …  [-396.0793383516089, -7.14883185708867], [-398.1453577469821, -8.978199939476177], [-399.5537380277289, -9.132101219748684], [-400.6604900815941, -9.481225612192988], [-403.42205151747856, -9.940164431721728], [-402.98615320505536, -10.912036937439861], [-403.52125776769185, -12.542770762621664], [-403.7363348486577, -14.491275899498842], [-405.41913093283154, -16.6631983440307], [-406.5442261933284, -15.784503607517209]], [[0.5839054062927054, -0.27929477378843304], [0.41007457728427377, 0.9184433161348705], [0.11204639752074251, 0.5631568392262732], [1.1255022555445502, -0.432131932236531], [-1.4609285400734122, -1.0047900574943391], [1.658369563599552, 0.41173127501428186], [0.080280121082907, -1.6750679461296805], [0.47545184209756375, 0.9200875097747347], [-0.8745692970385783, -0.6063211557138892], [-0.6371267005592348, 0.3182510062201885]  …  [-1.2489711048850263, 0.5737070285485795], [-0.5339829136703733, 0.2608634380802061], [-0.24862328178704068, 0.9549561375433364], [-0.16281638770760531, 1.0145987581223301], [-1.9226803890658999, 0.8827977883629121], [-0.858732315998032, -0.8824817581291353], [-0.6331406379760188, 0.42937840647202086], [-0.4115253891654016, -0.3459220491586696], [1.0877107905147378, -1.2027615350296437], [0.5789458507677563, 1.2073250722043054]], [[3.826379477846361, 1.106346131653314], [-0.5299524866050582, 0.029844978891187146], [-1.9262924115894797, 2.4575180042692497], [-0.335419945580939, 0.4248124748336408], [-1.6779522529226032, 0.48425158180477057], [0.20170143674813212, 1.0435377872168172], [0.832460087218006, -0.9740279880423295], [-0.10327968004895771, 0.8192117715711238], [0.5070534631332806, 0.41553019255392354], [0.4600219090084662, 0.8696004125230792]  …  [-396.74634382146576, -7.151126612740699], [-398.3362017506925, -7.528928595515266], [-399.327259973, -9.310502658656317], [-399.88209093374587, -8.504114595664538], [-403.6130400910793, -8.898714242989328], [-402.87182523413037, -11.642104259408015], [-403.0100033389447, -11.02070063527221], [-404.12759639635897, -15.24432040373154], [-405.39332832867996, -17.435338403058026], [-405.62937020884544, -15.012921996736385]])

Compute likelihood for various values of the parameters

p = nothing
 svec = exp10.(LinRange(-0.8, 1.2, 60))
 llspf = map(svec) do s
     df = MvNormal(nx,s)
@@ -30,13 +30,13 @@
     xlabel = "Dynamics noise standard deviation",
     lab = "PF",
 )
-vline!([svec[findmax(llspf)[2]]], l=(:dash,:blue), primary=false)
Example block output

the correct value for the simulated data is 1 (the simulated system is the same as on the front page of the docs).

We can do the same with a Kalman filter

eye(n) = SMatrix{n,n}(1.0I(n))
+vline!([svec[findmax(llspf)[2]]], l=(:dash,:blue), primary=false)
Example block output

the correct value for the simulated data is 1 (the simulated system is the same as on the front page of the docs).

We can do the same with a Kalman filter

eye(n) = SMatrix{n,n}(1.0I(n))
 llskf = map(svec) do s
     kfs = KalmanFilter(A, B, C, 0, s^2*eye(nx), eye(ny), d0)
     loglik(kfs, u, y, p)
 end
 plot!(svec, llskf, yscale=:identity, xscale=:log10, lab="Kalman", c=:red)
-vline!([svec[findmax(llskf)[2]]], l=(:dash,:red), primary=false)
Example block output

the result can be quite noisy due to the stochastic nature of particle filtering. The particle filter likelihood agrees with the Kalman-filter estimate, which is optimal for the linear example system we are simulating here, apart for when the noise variance is small. Due to particle depletion, particle filters often struggle when dynamics-noise is too small. This problem is mitigated by using a greater number of particles, or simply by not using a too small covariance.

MAP estimation

In this example, we will estimate the variance of the noises in the dynamics and the measurement functions.

To solve a MAP estimation problem, we need to define a function that takes a parameter vector and returns a filter, the parameters are used to construct the covariance matrices:

filter_from_parameters(θ, pf = nothing) = KalmanFilter(A, B, C, 0, exp(θ[1])^2*eye(nx), exp(θ[2])^2*eye(ny), d0) # Works with particle filters as well

The call to exp on the parameters is so that we can define log-normal priors

priors = [Normal(0,2),Normal(0,2)]
2-element Vector{Distributions.Normal{Float64}}:
+vline!([svec[findmax(llskf)[2]]], l=(:dash,:red), primary=false)
Example block output

the result can be quite noisy due to the stochastic nature of particle filtering. The particle filter likelihood agrees with the Kalman-filter estimate, which is optimal for the linear example system we are simulating here, apart for when the noise variance is small. Due to particle depletion, particle filters often struggle when dynamics-noise is too small. This problem is mitigated by using a greater number of particles, or simply by not using a too small covariance.

MAP estimation

In this example, we will estimate the variance of the noises in the dynamics and the measurement functions.

To solve a MAP estimation problem, we need to define a function that takes a parameter vector and returns a filter, the parameters are used to construct the covariance matrices:

filter_from_parameters(θ, pf = nothing) = KalmanFilter(A, B, C, 0, exp(θ[1])^2*eye(nx), exp(θ[2])^2*eye(ny), d0) # Works with particle filters as well

The call to exp on the parameters is so that we can define log-normal priors

priors = [Normal(0,2),Normal(0,2)]
2-element Vector{Distributions.Normal{Float64}}:
  Distributions.Normal{Float64}(μ=0.0, σ=2.0)
  Distributions.Normal{Float64}(μ=0.0, σ=2.0)

Now we call the function log_likelihood_fun that returns a function to be minimized

ll = log_likelihood_fun(filter_from_parameters, priors, u, y, p)

Since this is a low-dimensional problem, we can plot the LL on a 2d-grid

function meshgrid(a,b)
     grid_a = [i for i in a, j in b]
@@ -54,16 +54,16 @@
     yticks = (1:Nv, round.(v, digits = 2)),
     xlabel = "sigma v",
     ylabel = "sigma w",
-) # Yes, labels are reversed
Example block output

For higher-dimensional problems, we may estimate the parameters using an optimizer, e.g., Optim.jl.

Bayesian inference using PMMH

We proceed like we did for MAP above, but when calling the function metropolis, we will get the entire posterior distribution of the parameter vector, for the small cost of a massive increase in the amount of computations. metropolis runs the Metropolis Hastings algorithm, or more precisely if a particle filter is used, the "Particle Marginal Metropolis Hastings" (PMMH) algorithm. Here we use the Kalman filter simply to have the documentation build a bit faster, it can be quite heavy to run.

filter_from_parameters(θ, pf = nothing) = KalmanFilter(A, B, C, 0, exp(θ[1])^2*I(nx), exp(θ[2])^2*I(ny), d0) # Works with particle filters as well

The call to exp on the parameters is so that we can define log-normal priors

priors = [Normal(0,2),Normal(0,2)]
+) # Yes, labels are reversed
Example block output

For higher-dimensional problems, we may estimate the parameters using an optimizer, e.g., Optim.jl.

Bayesian inference using PMMH

We proceed like we did for MAP above, but when calling the function metropolis, we will get the entire posterior distribution of the parameter vector, for the small cost of a massive increase in the amount of computations. metropolis runs the Metropolis Hastings algorithm, or more precisely if a particle filter is used, the "Particle Marginal Metropolis Hastings" (PMMH) algorithm. Here we use the Kalman filter simply to have the documentation build a bit faster, it can be quite heavy to run.

filter_from_parameters(θ, pf = nothing) = KalmanFilter(A, B, C, 0, exp(θ[1])^2*I(nx), exp(θ[2])^2*I(ny), d0) # Works with particle filters as well

The call to exp on the parameters is so that we can define log-normal priors

priors = [Normal(0,2),Normal(0,2)]
 ll     = log_likelihood_fun(filter_from_parameters, priors, u, y, p)
 θ₀     = log.([1.0, 1.0]) # Starting point

We also need to define a function that suggests a new point from the "proposal distribution". This can be pretty much anything, but it has to be symmetric since I was lazy and simplified an equation.

draw   = θ -> θ .+ 0.05 .* randn.() # This function dictates how new proposal parameters are being generated.
 burnin = 200 # remove this many initial samples ("burn-in period")
 @info "Starting Metropolis algorithm"
 @time theta, lls = metropolis(ll, 2200, θ₀, draw) # Run PMMH for 2200  iterations
 thetam = reduce(hcat, theta)'[burnin+1:end,:] # Build a matrix of the output
-histogram(exp.(thetam), layout=(3,1), lab=["R1" "R2"]); plot!(lls[burnin+1:end], subplot=3, lab="log likelihood") # Visualize
Example block output

In this example, we initialize the MH algorithm on the correct value θ₀, in general, you'd see a period in the beginning where the likelihood (bottom plot) is much lower than during the rest of the sampling, this is the reason we remove a number of samples in the beginning, typically referred to as "burn in".

If you are lucky, you can run the above threaded as well. I tried my best to make particle filters thread safe with their own rngs etc., but your milage may vary. For threading to help, the dynamics must be non-allocating, e.g., by using StaticArrays etc.

@time thetalls = LowLevelParticleFilters.metropolis_threaded(burnin, ll, 2200, θ₀, draw, nthreads=2)
+histogram(exp.(thetam), layout=(3,1), lab=["R1" "R2"]); plot!(lls[burnin+1:end], subplot=3, lab="log likelihood") # Visualize
Example block output

In this example, we initialize the MH algorithm on the correct value θ₀, in general, you'd see a period in the beginning where the likelihood (bottom plot) is much lower than during the rest of the sampling, this is the reason we remove a number of samples in the beginning, typically referred to as "burn in".

If you are lucky, you can run the above threaded as well. I tried my best to make particle filters thread safe with their own rngs etc., but your milage may vary. For threading to help, the dynamics must be non-allocating, e.g., by using StaticArrays etc.

@time thetalls = LowLevelParticleFilters.metropolis_threaded(burnin, ll, 2200, θ₀, draw, nthreads=2)
 histogram(exp.(thetalls[:,1:2]), layout=3)
-plot!(thetalls[:,3], subplot=3)
Example block output

Bayesian inference using DynamicHMC.jl

The following snippet of code performs the same estimation as above, but uses the much more sophisticated HMC sampler in DynamicHMC.jl rather than the PMMH sampler above. This package requires the log-likelihood function to be wrapped in a custom struct that implements the LogDensityProblems interface, which is done below. We also indicate that we want to use ForwardDiff.jl to compute the gradients for fast sampling.

using DynamicHMC, LogDensityProblemsAD, ForwardDiff, LogDensityProblems, LinearAlgebra, Random
+plot!(thetalls[:,3], subplot=3)
Example block output

Bayesian inference using DynamicHMC.jl

The following snippet of code performs the same estimation as above, but uses the much more sophisticated HMC sampler in DynamicHMC.jl rather than the PMMH sampler above. This package requires the log-likelihood function to be wrapped in a custom struct that implements the LogDensityProblems interface, which is done below. We also indicate that we want to use ForwardDiff.jl to compute the gradients for fast sampling.

using DynamicHMC, LogDensityProblemsAD, ForwardDiff, LogDensityProblems, LinearAlgebra, Random
 
 struct LogTargetDensity{F}
     ll::F
@@ -135,7 +135,7 @@
 plot(
     plot(reduce(hcat, x)', title="State"),
     plot(reduce(hcat, u)', title="Inputs")
-)
Example block output

To perform the joint state and parameter estimation, we define a version of the dynamics that contains an extra state, corresponding to the unknown or time varying parameter, in this case $a_1$. We do not have any apriori information about how this parameter changes, so we say that its derivative is 0 and it's thus only driven by noise:

function quadtank_paramest(h, u, p, t)
+)
Example block output

To perform the joint state and parameter estimation, we define a version of the dynamics that contains an extra state, corresponding to the unknown or time varying parameter, in this case $a_1$. We do not have any apriori information about how this parameter changes, so we say that its derivative is 0 and it's thus only driven by noise:

function quadtank_paramest(h, u, p, t)
     kc = 0.5
     k1, k2, g = 1.6, 1.6, 9.81
     A1 = A3 = A2 = A4 = 4.9
@@ -164,7 +164,7 @@
 
 sol = forward_trajectory(kf, u, y)
 plot(sol, plotx=false, plotxt=true, plotu=false, ploty=true, legend=:bottomright)
-plot!([0,500,500,1000], [0.03, 0.03, 0.06, 0.06], l=(:dash, :black), sp=5, lab="True param")
Example block output

as we can see, the correct value of the parameter is quickly found ($x_5$), and it also adapts at $t=500$ when the parameter value changes. The speed with which the parameter adapts to changes is determined by the covariance matrix $R_1$, a higher value results in faster adaptation, but also higher sensitivity to noise.

If adaptive parameter estimation is coupled with a model-based controller, we get an adaptive controller! Note: the state that corresponds to the estimated parameter is typically not controllable, a fact that may require some special care for some control methods.

We may ask ourselves, what's the difference between a parameter and a state variable if we can add parameters as state variables? Typically, parameters do not vary with time, and if they do, they vary significantly slower than the state variables. State variables also have dynamics associate with them, whereas we often have no idea about how the parameters vary other than that they vary slowly.

Abrupt changes to the dynamics like in the example above can happen in practice, for instance, due to equipment failure or change of operating mode. This can be treated as a scenario with time-varying parameters that are continuously estimated.

Using an optimizer

The state estimators in this package are all statistically motivated and thus compute things like the likelihood of the data as a by-product of the estimation. Maximum-likelihood or prediction-error estimation is thus very straight-forward by simply calling a gradient-based optimizer with gradients provided by differentiating through the state estimator using automatic differentiation. In this example, we will continue the example from above, but now estimate all the parameters of the quad-tank process. This time, they will not vary with time. We will first use a standard optimization algorithm from Optim.jl to minimize the cost function based on the prediction error, and then use a Gauss-Newton optimizer.

We now define the dynamics function such that it takes its parameters from the p input argument. We also define a variable p_true that contains the true values that we will use to simulate some estimation data

function quadtank(h, u, p, t)
+plot!([0,500,500,1000], [0.03, 0.03, 0.06, 0.06], l=(:dash, :black), sp=5, lab="True param")
Example block output

as we can see, the correct value of the parameter is quickly found ($x_5$), and it also adapts at $t=500$ when the parameter value changes. The speed with which the parameter adapts to changes is determined by the covariance matrix $R_1$, a higher value results in faster adaptation, but also higher sensitivity to noise.

If adaptive parameter estimation is coupled with a model-based controller, we get an adaptive controller! Note: the state that corresponds to the estimated parameter is typically not controllable, a fact that may require some special care for some control methods.

We may ask ourselves, what's the difference between a parameter and a state variable if we can add parameters as state variables? Typically, parameters do not vary with time, and if they do, they vary significantly slower than the state variables. State variables also have dynamics associate with them, whereas we often have no idea about how the parameters vary other than that they vary slowly.

Abrupt changes to the dynamics like in the example above can happen in practice, for instance, due to equipment failure or change of operating mode. This can be treated as a scenario with time-varying parameters that are continuously estimated.

Using an optimizer

The state estimators in this package are all statistically motivated and thus compute things like the likelihood of the data as a by-product of the estimation. Maximum-likelihood or prediction-error estimation is thus very straight-forward by simply calling a gradient-based optimizer with gradients provided by differentiating through the state estimator using automatic differentiation. In this example, we will continue the example from above, but now estimate all the parameters of the quad-tank process. This time, they will not vary with time. We will first use a standard optimization algorithm from Optim.jl to minimize the cost function based on the prediction error, and then use a Gauss-Newton optimizer.

We now define the dynamics function such that it takes its parameters from the p input argument. We also define a variable p_true that contains the true values that we will use to simulate some estimation data

function quadtank(h, u, p, t)
     kc = p[1]
     k1, k2, g = p[2], p[3], 9.81
     A1 = A3 = A2 = A4 = p[4]
@@ -195,7 +195,7 @@
 plot(
     plot(reduce(hcat, x)', title="State"),
     plot(reduce(hcat, u)', title="Inputs")
-)
Example block output

This time, we define a cost function for the optimizer to optimize, we'll use the sum of squared errors (sse). It's important to define the UKF with an initial state distribution with the same element type as the parameter vector so that automatic differentiation through the state estimator works, hence the explicit casting T.(x0) and T.(R1). We also make sure to use StaticArrays for the covariance matrices and the initial condition for performance reasons (optional).

nx = 4
+)
Example block output

This time, we define a cost function for the optimizer to optimize, we'll use the sum of squared errors (sse). It's important to define the UKF with an initial state distribution with the same element type as the parameter vector so that automatic differentiation through the state estimator works, hence the explicit casting T.(x0) and T.(R1). We also make sure to use StaticArrays for the covariance matrices and the initial condition for performance reasons (optional).

nx = 4
 R1 = SMatrix{nx,nx}(Diagonal([0.1, 0.1, 0.1, 0.1])) # Use of StaticArrays is generally good for performance
 R2 = SMatrix{ny,ny}(Diagonal((1e-2)^2 * ones(ny)))
 x0 = SA[2.0, 2, 3, 3]
@@ -280,4 +280,4 @@
   h3  => 0
   h1  => 1
   A1  => 0
-  a2  => 0

indicating that we can not hope to resolve all of the parameters. However, using appropriate regularization from prior information, we might still recover a lot of information about the system. Regularization could easily be added to the function cost above, e.g., using a penalty like (p-p_guess)'Γ*(p-p_guess) for some matrix $\Gamma$, to indicate our confidence in the initial guess.

Videos

Examples of parameter estimation are available here

By using an optimizer to optimize the likelihood of an UnscentedKalmanFilter:

Estimation of time-varying parameters:

Adaptive control by means of estimation of time-varying parameters:

+ a2 => 0

indicating that we can not hope to resolve all of the parameters. However, using appropriate regularization from prior information, we might still recover a lot of information about the system. Regularization could easily be added to the function cost above, e.g., using a penalty like (p-p_guess)'Γ*(p-p_guess) for some matrix $\Gamma$, to indicate our confidence in the initial guess.

Videos

Examples of parameter estimation are available here

By using an optimizer to optimize the likelihood of an UnscentedKalmanFilter:

Estimation of time-varying parameters:

Adaptive control by means of estimation of time-varying parameters:

diff --git a/dev/search_index.js b/dev/search_index.js index 290294d7..33c011c9 100644 --- a/dev/search_index.js +++ b/dev/search_index.js @@ -1,3 +1,3 @@ var documenterSearchIndex = {"docs": -[{"location":"distributions/#Performance-tips","page":"Performance tips","title":"Performance tips","text":"","category":"section"},{"location":"distributions/","page":"Performance tips","title":"Performance tips","text":"Use of StaticArrays.jl is recommended for optimal performance when the state dimension is small, e.g., less than about 10-15 for Kalman filters and less than about 100 for particle filters. In the section Parameter optimization we demonstrate one workflow that makes use of StaticArrays everywhere it is needed for an UnscentedKalmanFilter in order to get a completely allocation free filter. The following arrays must be static for this to hold","category":"page"},{"location":"distributions/","page":"Performance tips","title":"Performance tips","text":"The initial state distribution (the vector and matrix passed to d0 = MvNormal(μ, Σ) for Kalman filters). If you are performing parameter optimization with gradients derived using ForwardDiff.jl, these must further have the correct element type. How to achieve this is demonstrated in the liked example above.\nInputs u measured outputs y.\nIn case of Kalman filters, the dynamic model matrices A, B, C, D and the covariance matrices R1, R2.\nThe dynamics functions for UnscentedKalmanFilter and particle filters must further return static arrays when passed static arrays as inputs.","category":"page"},{"location":"distributions/#Analysis-using-JET","page":"Performance tips","title":"Analysis using JET","text":"","category":"section"},{"location":"distributions/","page":"Performance tips","title":"Performance tips","text":"All flavors of Kalman filters are analyzed for potential runtime dispatch using JET.jl. This analysis is performed in the tests and generally requires a completely static filter using static arrays internally. See the tests for an example of how to set a filter up this way.","category":"page"},{"location":"distributions/#High-performance-Distributions","page":"Performance tips","title":"High performance Distributions","text":"","category":"section"},{"location":"distributions/","page":"Performance tips","title":"Performance tips","text":"When using LowLevelParticleFilters, a number of methods related to distributions are defined for static arrays, making logpdf etc. faster. We also provide a new kind of distribution: TupleProduct <: MultivariateDistribution that behaves similarly to the Product distribution. The TupleProduct however stores the individual distributions in a tuple, has compile-time known length and supports Mixed <: ValueSupport, meaning that it can be a product of both Continuous and Discrete dimensions, something not supported by the standard Product. Example","category":"page"},{"location":"distributions/","page":"Performance tips","title":"Performance tips","text":"using BenchmarkTools, LowLevelParticleFilters, Distributions, StaticArrays\ndt = TupleProduct((Normal(0,2), Normal(0,2), Binomial())) # Mixed value support","category":"page"},{"location":"distributions/","page":"Performance tips","title":"Performance tips","text":"A small benchmark","category":"page"},{"location":"distributions/","page":"Performance tips","title":"Performance tips","text":"sv = @SVector randn(2)\nd = Distributions.Product([Normal(0,2), Normal(0,2)])\ndt = TupleProduct((Normal(0,2), Normal(0,2)))\ndm = MvNormal(2, 2)\n@btime logpdf($d,$(Vector(sv))) # 19.536 ns (0 allocations: 0 bytes)\n@btime logpdf($dt,$(Vector(sv))) # 13.742 ns (0 allocations: 0 bytes)\n@btime logpdf($dm,$(Vector(sv))) # 11.392 ns (0 allocations: 0 bytes)","category":"page"},{"location":"distributions/","page":"Performance tips","title":"Performance tips","text":"@btime logpdf($d,$sv) # 13.964 ns (0 allocations: 0 bytes)\n@btime logpdf($dt,$sv) # 12.817 ns (0 allocations: 0 bytes)\n@btime logpdf($dm,$sv) # 8.383 ns (0 allocations: 0 bytes)","category":"page"},{"location":"distributions/","page":"Performance tips","title":"Performance tips","text":"Without loading LowLevelParticleFilters, the timing for the native distributions are the following","category":"page"},{"location":"distributions/","page":"Performance tips","title":"Performance tips","text":"@btime logpdf($d,$sv) # 18.040 ns (0 allocations: 0 bytes)\n@btime logpdf($dm,$sv) # 9.938 ns (0 allocations: 0 bytes)","category":"page"},{"location":"parameter_estimation/#Parameter-Estimation","page":"Parameter estimation","title":"Parameter Estimation","text":"","category":"section"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"State estimation is an integral part of many parameter-estimation methods. Below, we will illustrate several different methods of performing parameter estimation. We can roughly divide the methods into two camps","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"Methods that optimize prediction error or likelihood by tweaking model parameters.\nMethods that add the parameters to be estimated as state variables in the model and estimate them using standard state estimation. ","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"From the first camp, we provide som basic functionality for maximum-likelihood estimation and MAP estimation, described below. An example of (2), joint state and parameter estimation, is provided in Joint state and parameter estimation.","category":"page"},{"location":"parameter_estimation/#Maximum-likelihood-estimation","page":"Parameter estimation","title":"Maximum-likelihood estimation","text":"","category":"section"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"Filters calculate the likelihood and prediction errors while performing filtering, this can be used to perform maximum likelihood estimation or prediction-error minimization. One can estimate all kinds of parameters using this method, in the example below, we will estimate the noise covariance. We may for example plot likelihood as function of the variance of the dynamics noise like this:","category":"page"},{"location":"parameter_estimation/#Generate-data-by-simulation","page":"Parameter estimation","title":"Generate data by simulation","text":"","category":"section"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"This simulates the same linear system as on the index page of the documentation","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"using LowLevelParticleFilters, LinearAlgebra, StaticArrays, Distributions, Plots\nnx = 2 # Dimension of state\nnu = 2 # Dimension of input\nny = 2 # Dimension of measurements\nN = 2000 # Number of particles\n\nconst dg = MvNormal(ny,1.0) # Measurement noise Distribution\nconst df = MvNormal(nx,1.0) # Dynamics noise Distribution\nconst d0 = MvNormal(@SVector(randn(nx)),2.0) # Initial state Distribution\n\nconst A = SA[1 0.1; 0 1]\nconst B = @SMatrix [0.0 0.1; 1 0.1]\nconst C = @SMatrix [1.0 0; 0 1]\n\ndynamics(x,u,p,t) = A*x .+ B*u \nmeasurement(x,u,p,t) = C*x\nvecvec_to_mat(x) = copy(reduce(hcat, x)') # Helper function\npf = ParticleFilter(N, dynamics, measurement, df, dg, d0)\nxs,u,y = simulate(pf,300,df)","category":"page"},{"location":"parameter_estimation/#Compute-likelihood-for-various-values-of-the-parameters","page":"Parameter estimation","title":"Compute likelihood for various values of the parameters","text":"","category":"section"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"p = nothing\nsvec = exp10.(LinRange(-0.8, 1.2, 60))\nllspf = map(svec) do s\n df = MvNormal(nx,s)\n pfs = ParticleFilter(N, dynamics, measurement, df, dg, d0)\n loglik(pfs, u, y, p)\nend\nplot( svec, llspf,\n xscale = :log10,\n title = \"Log-likelihood\",\n xlabel = \"Dynamics noise standard deviation\",\n lab = \"PF\",\n)\nvline!([svec[findmax(llspf)[2]]], l=(:dash,:blue), primary=false)","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"the correct value for the simulated data is 1 (the simulated system is the same as on the front page of the docs).","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"We can do the same with a Kalman filter","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"eye(n) = SMatrix{n,n}(1.0I(n))\nllskf = map(svec) do s\n kfs = KalmanFilter(A, B, C, 0, s^2*eye(nx), eye(ny), d0)\n loglik(kfs, u, y, p)\nend\nplot!(svec, llskf, yscale=:identity, xscale=:log10, lab=\"Kalman\", c=:red)\nvline!([svec[findmax(llskf)[2]]], l=(:dash,:red), primary=false)","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"the result can be quite noisy due to the stochastic nature of particle filtering. The particle filter likelihood agrees with the Kalman-filter estimate, which is optimal for the linear example system we are simulating here, apart for when the noise variance is small. Due to particle depletion, particle filters often struggle when dynamics-noise is too small. This problem is mitigated by using a greater number of particles, or simply by not using a too small covariance.","category":"page"},{"location":"parameter_estimation/#MAP-estimation","page":"Parameter estimation","title":"MAP estimation","text":"","category":"section"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"In this example, we will estimate the variance of the noises in the dynamics and the measurement functions.","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"To solve a MAP estimation problem, we need to define a function that takes a parameter vector and returns a filter, the parameters are used to construct the covariance matrices:","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"filter_from_parameters(θ, pf = nothing) = KalmanFilter(A, B, C, 0, exp(θ[1])^2*eye(nx), exp(θ[2])^2*eye(ny), d0) # Works with particle filters as well\nnothing # hide","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"The call to exp on the parameters is so that we can define log-normal priors","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"priors = [Normal(0,2),Normal(0,2)]","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"Now we call the function log_likelihood_fun that returns a function to be minimized","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"ll = log_likelihood_fun(filter_from_parameters, priors, u, y, p)\nnothing # hide","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"Since this is a low-dimensional problem, we can plot the LL on a 2d-grid","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"function meshgrid(a,b)\n grid_a = [i for i in a, j in b]\n grid_b = [j for i in a, j in b]\n grid_a, grid_b\nend\nNv = 20\nv = LinRange(-0.7,1,Nv)\nllxy = (x,y) -> ll([x;y])\nVGx, VGy = meshgrid(v,v)\nVGz = llxy.(VGx, VGy)\nheatmap(\n VGz,\n xticks = (1:Nv, round.(v, digits = 2)),\n yticks = (1:Nv, round.(v, digits = 2)),\n xlabel = \"sigma v\",\n ylabel = \"sigma w\",\n) # Yes, labels are reversed","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"For higher-dimensional problems, we may estimate the parameters using an optimizer, e.g., Optim.jl.","category":"page"},{"location":"parameter_estimation/#Bayesian-inference-using-PMMH","page":"Parameter estimation","title":"Bayesian inference using PMMH","text":"","category":"section"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"We proceed like we did for MAP above, but when calling the function metropolis, we will get the entire posterior distribution of the parameter vector, for the small cost of a massive increase in the amount of computations. metropolis runs the Metropolis Hastings algorithm, or more precisely if a particle filter is used, the \"Particle Marginal Metropolis Hastings\" (PMMH) algorithm. Here we use the Kalman filter simply to have the documentation build a bit faster, it can be quite heavy to run.","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"filter_from_parameters(θ, pf = nothing) = KalmanFilter(A, B, C, 0, exp(θ[1])^2*I(nx), exp(θ[2])^2*I(ny), d0) # Works with particle filters as well\nnothing # hide","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"The call to exp on the parameters is so that we can define log-normal priors","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"priors = [Normal(0,2),Normal(0,2)]\nll = log_likelihood_fun(filter_from_parameters, priors, u, y, p)\nθ₀ = log.([1.0, 1.0]) # Starting point\nnothing # hide","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"We also need to define a function that suggests a new point from the \"proposal distribution\". This can be pretty much anything, but it has to be symmetric since I was lazy and simplified an equation.","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"draw = θ -> θ .+ 0.05 .* randn.() # This function dictates how new proposal parameters are being generated. \nburnin = 200 # remove this many initial samples (\"burn-in period\")\n@info \"Starting Metropolis algorithm\"\n@time theta, lls = metropolis(ll, 2200, θ₀, draw) # Run PMMH for 2200 iterations\nthetam = reduce(hcat, theta)'[burnin+1:end,:] # Build a matrix of the output\nhistogram(exp.(thetam), layout=(3,1), lab=[\"R1\" \"R2\"]); plot!(lls[burnin+1:end], subplot=3, lab=\"log likelihood\") # Visualize","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"In this example, we initialize the MH algorithm on the correct value θ₀, in general, you'd see a period in the beginning where the likelihood (bottom plot) is much lower than during the rest of the sampling, this is the reason we remove a number of samples in the beginning, typically referred to as \"burn in\".","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"If you are lucky, you can run the above threaded as well. I tried my best to make particle filters thread safe with their own rngs etc., but your milage may vary. For threading to help, the dynamics must be non-allocating, e.g., by using StaticArrays etc.","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"@time thetalls = LowLevelParticleFilters.metropolis_threaded(burnin, ll, 2200, θ₀, draw, nthreads=2)\nhistogram(exp.(thetalls[:,1:2]), layout=3)\nplot!(thetalls[:,3], subplot=3)","category":"page"},{"location":"parameter_estimation/#Bayesian-inference-using-DynamicHMC.jl","page":"Parameter estimation","title":"Bayesian inference using DynamicHMC.jl","text":"","category":"section"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"The following snippet of code performs the same estimation as above, but uses the much more sophisticated HMC sampler in DynamicHMC.jl rather than the PMMH sampler above. This package requires the log-likelihood function to be wrapped in a custom struct that implements the LogDensityProblems interface, which is done below. We also indicate that we want to use ForwardDiff.jl to compute the gradients for fast sampling.","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"using DynamicHMC, LogDensityProblemsAD, ForwardDiff, LogDensityProblems, LinearAlgebra, Random\n\nstruct LogTargetDensity{F}\n ll::F\n dim::Int\nend\nLogDensityProblems.logdensity(p::LogTargetDensity, θ) = p.ll(θ)\nLogDensityProblems.dimension(p::LogTargetDensity) = p.dim\nLogDensityProblems.capabilities(::Type{LogTargetDensity}) = LogDensityProblems.LogDensityOrder{0}()\n\nfunction filter_from_parameters(θ, pf = nothing)\n # It's important that the distribution of the initial state has the same\n # element type as the parameters. DynamicHMC will use Dual numbers for differentiation,\n # hence, we make sure that d0 has `eltype(d0) = eltype(θ)`\n T = eltype(θ)\n d0 = MvNormal(T.(d0.μ), T.(d0.Σ))\n KalmanFilter(A, B, C, 0, exp(θ[1])^2*eye(nx), exp(θ[2])^2*eye(ny), d0) \nend\nll = log_likelihood_fun(filter_from_parameters, priors, u, y, p)\n\nD = length(θ₀)\nℓπ = LogTargetDensity(ll, D)\n∇P = ADgradient(:ForwardDiff, ℓπ)\n\nresults = mcmc_with_warmup(Random.default_rng(), ∇P, 3000)\nDynamicHMC.Diagnostics.summarize_tree_statistics(results.tree_statistics)\nlls = [ts.π for ts in results.tree_statistics]\n\nhistogram(exp.(results.posterior_matrix)', layout=(3,1), lab=[\"R1\" \"R2\"])\nplot!(lls, subplot=3, lab=\"log likelihood\") # Visualize","category":"page"},{"location":"parameter_estimation/#Joint-state-and-parameter-estimation","page":"Parameter estimation","title":"Joint state and parameter estimation","text":"","category":"section"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"In this example, we'll show how to perform parameter estimation by treating a parameter as a state variable. This method can not only estimate constant parameters, but also time-varying parameters. The system we will consider is a quadruple tank, where two upper tanks feed into two lower tanks. The outlet for tank 1 can vary in size, simulating, e.g., that something partially blocks the outlet. We start by defining the dynamics on a form that changes the outlet area a_1 at time t=500:","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"using LowLevelParticleFilters\nusing SeeToDee\nusing Distributions\nusing StaticArrays\nusing Plots, LinearAlgebra\n\nfunction quadtank(h,u,p,t)\n kc = 0.5\n k1, k2, g = 1.6, 1.6, 9.81\n A1 = A3 = A2 = A4 = 4.9\n a1, a3, a2, a4 = 0.03, 0.03, 0.03, 0.03\n γ1, γ2 = 0.2, 0.2\n\n if t > 500\n a1 *= 2 # Change the parameter at t = 500\n end\n\n ssqrt(x) = √(max(x, zero(x)) + 1e-3) # For numerical robustness at x = 0\n \n SA[\n -a1/A1 * ssqrt(2g*h[1]) + a3/A1*ssqrt(2g*h[3]) + γ1*k1/A1 * u[1]\n -a2/A2 * ssqrt(2g*h[2]) + a4/A2*ssqrt(2g*h[4]) + γ2*k2/A2 * u[2]\n -a3/A3*ssqrt(2g*h[3]) + (1-γ2)*k2/A3 * u[2]\n -a4/A4*ssqrt(2g*h[4]) + (1-γ1)*k1/A4 * u[1]\n ]\nend\n\nnu = 2 # number of control inputs\nnx = 4 # number of state variables\nny = 2 # number of measured outputs\nTs = 1 # sample time\nnothing # hide","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"We then define a measurement function, we measure the levels of tanks 1 and 2, and discretize the continuous-time dynamics using a Runge-Kutta 4 integrator SeeToDee.Rk4:","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"measurement(x,u,p,t) = SA[x[1], x[2]]\ndiscrete_dynamics = SeeToDee.Rk4(quadtank, Ts)\nnothing # hide","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"We simulate the system using the rollout function and add some noise to the measurements. The inputs in this case are just square waves.","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"Tperiod = 200\nt = 0:Ts:1000\nu = vcat.(0.25 .* sign.(sin.(2pi/Tperiod .* t)) .+ 0.25)\nu = SVector{nu}.(vcat.(u,u))\nx0 = Float64[2,2,3,3]\nx = LowLevelParticleFilters.rollout(discrete_dynamics, x0, u)[1:end-1]\ny = measurement.(x, u, 0, 0)\ny = [y .+ 0.01.*randn.() for y in y]\n\nplot(\n plot(reduce(hcat, x)', title=\"State\"),\n plot(reduce(hcat, u)', title=\"Inputs\")\n)","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"To perform the joint state and parameter estimation, we define a version of the dynamics that contains an extra state, corresponding to the unknown or time varying parameter, in this case a_1. We do not have any apriori information about how this parameter changes, so we say that its derivative is 0 and it's thus only driven by noise:","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"function quadtank_paramest(h, u, p, t)\n kc = 0.5\n k1, k2, g = 1.6, 1.6, 9.81\n A1 = A3 = A2 = A4 = 4.9\n a3, a2, a4 = 0.03, 0.03, 0.03\n γ1, γ2 = 0.2, 0.2\n\n a1 = h[5] # the a1 parameter is a state\n\n ssqrt(x) = √(max(x, zero(x)) + 1e-3) # For numerical robustness at x = 0\n \n SA[\n -a1/A1 * ssqrt(2g*h[1]) + a3/A1*ssqrt(2g*h[3]) + γ1*k1/A1 * u[1]\n -a2/A2 * ssqrt(2g*h[2]) + a4/A2*ssqrt(2g*h[4]) + γ2*k2/A2 * u[2]\n -a3/A3*ssqrt(2g*h[3]) + (1-γ2)*k2/A3 * u[2]\n -a4/A4*ssqrt(2g*h[4]) + (1-γ1)*k1/A4 * u[1]\n 0 # the state is only driven by noise\n ]\nend\n\ndiscrete_dynamics_params = SeeToDee.Rk4(quadtank_paramest, Ts)\nnothing # hide","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"We then define a nonlinear state estimator, we will use the UnscentedKalmanFilter, and solve the filtering problem. We start by an initial state estimate x_0 that is slightly off for the parameter a_1","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"nx = 5\nR1 = SMatrix{nx,nx}(Diagonal([0.1, 0.1, 0.1, 0.1, 0.0001])) # Use of StaticArrays is generally good for performance\nR2 = SMatrix{ny,ny}(Diagonal((1e-2)^2 * ones(ny)))\nx0 = SA[2, 2, 3, 3, 0.02] # The SA prefix makes the array static, which is good for performance\n\nkf = UnscentedKalmanFilter(discrete_dynamics_params, measurement, R1, R2, MvNormal(x0, R1); ny, nu, Ts)\n\nsol = forward_trajectory(kf, u, y)\nplot(sol, plotx=false, plotxt=true, plotu=false, ploty=true, legend=:bottomright)\nplot!([0,500,500,1000], [0.03, 0.03, 0.06, 0.06], l=(:dash, :black), sp=5, lab=\"True param\")","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"as we can see, the correct value of the parameter is quickly found (x_5), and it also adapts at t=500 when the parameter value changes. The speed with which the parameter adapts to changes is determined by the covariance matrix R_1, a higher value results in faster adaptation, but also higher sensitivity to noise. ","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"If adaptive parameter estimation is coupled with a model-based controller, we get an adaptive controller! Note: the state that corresponds to the estimated parameter is typically not controllable, a fact that may require some special care for some control methods.","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"We may ask ourselves, what's the difference between a parameter and a state variable if we can add parameters as state variables? Typically, parameters do not vary with time, and if they do, they vary significantly slower than the state variables. State variables also have dynamics associate with them, whereas we often have no idea about how the parameters vary other than that they vary slowly.","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"Abrupt changes to the dynamics like in the example above can happen in practice, for instance, due to equipment failure or change of operating mode. This can be treated as a scenario with time-varying parameters that are continuously estimated. ","category":"page"},{"location":"parameter_estimation/#Using-an-optimizer","page":"Parameter estimation","title":"Using an optimizer","text":"","category":"section"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"The state estimators in this package are all statistically motivated and thus compute things like the likelihood of the data as a by-product of the estimation. Maximum-likelihood or prediction-error estimation is thus very straight-forward by simply calling a gradient-based optimizer with gradients provided by differentiating through the state estimator using automatic differentiation. In this example, we will continue the example from above, but now estimate all the parameters of the quad-tank process. This time, they will not vary with time. We will first use a standard optimization algorithm from Optim.jl to minimize the cost function based on the prediction error, and then use a Gauss-Newton optimizer.","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"We now define the dynamics function such that it takes its parameters from the p input argument. We also define a variable p_true that contains the true values that we will use to simulate some estimation data","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"function quadtank(h, u, p, t)\n kc = p[1]\n k1, k2, g = p[2], p[3], 9.81\n A1 = A3 = A2 = A4 = p[4]\n a1 = a3 = a2 = a4 = p[5]\n γ1 = γ2 = p[6]\n\n ssqrt(x) = √(max(x, zero(x)) + 1e-3) # For numerical robustness at x = 0\n \n SA[\n -a1/A1 * ssqrt(2g*h[1]) + a3/A1*ssqrt(2g*h[3]) + γ1*k1/A1 * u[1]\n -a2/A2 * ssqrt(2g*h[2]) + a4/A2*ssqrt(2g*h[4]) + γ2*k2/A2 * u[2]\n -a3/A3*ssqrt(2g*h[3]) + (1-γ2)*k2/A3 * u[2]\n -a4/A4*ssqrt(2g*h[4]) + (1-γ1)*k1/A4 * u[1]\n ]\nend\n\ndiscrete_dynamics = SeeToDee.Rk4(quadtank, Ts) # Discretize the dynamics using a 4:th order Runge-Kutta integrator\np_true = [0.5, 1.6, 1.6, 4.9, 0.03, 0.2]\nnothing # hide","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"Similar to previous example, we simulate the system, this time using a more exciting input in order to be able to identify several parameters","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"using Random; Random.seed!(1) # hide\nTperiod = 200\nt = 0:Ts:1000\nu1 = vcat.(0.25 .* sign.(sin.(2pi/Tperiod .* (t ./ 40).^2)) .+ 0.25)\nu2 = vcat.(0.25 .* sign.(sin.(2pi/Tperiod .* (t ./ 40).^2 .+ pi/2)) .+ 0.25)\nu = SVector{nu}.(vcat.(u1,u2))\nx0 = SA[2.0,2,3,3] # Initial condition, static array for performance\nx = LowLevelParticleFilters.rollout(discrete_dynamics, x0, u, p_true)[1:end-1]\ny = measurement.(x, u, 0, 0)\ny = [y .+ 0.01 .* randn.() for y in y]\n\nplot(\n plot(reduce(hcat, x)', title=\"State\"),\n plot(reduce(hcat, u)', title=\"Inputs\")\n)","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"This time, we define a cost function for the optimizer to optimize, we'll use the sum of squared errors (sse). It's important to define the UKF with an initial state distribution with the same element type as the parameter vector so that automatic differentiation through the state estimator works, hence the explicit casting T.(x0) and T.(R1). We also make sure to use StaticArrays for the covariance matrices and the initial condition for performance reasons (optional).","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"nx = 4\nR1 = SMatrix{nx,nx}(Diagonal([0.1, 0.1, 0.1, 0.1])) # Use of StaticArrays is generally good for performance\nR2 = SMatrix{ny,ny}(Diagonal((1e-2)^2 * ones(ny)))\nx0 = SA[2.0, 2, 3, 3]\n\nfunction cost(p::Vector{T}) where T\n kf = UnscentedKalmanFilter(discrete_dynamics, measurement, R1, R2, MvNormal(T.(x0), T.(R1)); ny, nu, Ts)\n LowLevelParticleFilters.sse(kf, u, y, p) # Sum of squared prediction errors\nend\nnothing # hide","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"We generate a random initial guess for the estimation problem","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"p_guess = p_true .+ 0.1*p_true .* randn(length(p_true))","category":"page"},{"location":"parameter_estimation/#Solving-using-Optim","page":"Parameter estimation","title":"Solving using Optim","text":"","category":"section"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"We first minimize the cost using the BFGS optimization algorithm from Optim.jl","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"using Optim\nres = Optim.optimize(\n cost,\n p_guess,\n BFGS(),\n Optim.Options(\n show_trace = true,\n show_every = 5,\n iterations = 100,\n time_limit = 30,\n ),\n autodiff = :forward, # Indicate that we want to use forward-mode AD to derive gradients\n)","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"We started out with a normalized parameter error of","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"using LinearAlgebra\nnorm(p_true - p_guess) / norm(p_true)","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"and ended with","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"p_opt = res.minimizer\nnorm(p_true - p_opt) / norm(p_true)","category":"page"},{"location":"parameter_estimation/#Solving-using-Gauss-Newton-optimization","page":"Parameter estimation","title":"Solving using Gauss-Newton optimization","text":"","category":"section"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"Below, we optimize the sum of squared residuals again, but this time we do it using a Gauss-Newton style algorithm (Levenberg Marquardt). These algorithms want the entire residual vector rather than the sum of squares of the residuals, so we define an alternative \"cost function\" called residuals that calls the lower-level function LowLevelParticleFilters.prediction_errors!","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"using LeastSquaresOptim\n\nfunction residuals!(res, p::Vector{T}) where T\n kf = UnscentedKalmanFilter(discrete_dynamics, measurement, R1, R2, MvNormal(T.(x0), T.(R1)); ny, nu, Ts)\n LowLevelParticleFilters.prediction_errors!(res, kf, u, y, p) \nend\n\nres_gn = optimize!(LeastSquaresProblem(x = copy(p_guess), f! = residuals!, output_length = length(y)*ny, autodiff = :forward), LevenbergMarquardt())\n\np_opt_gn = res_gn.minimizer\nnorm(p_true - p_opt_gn) / norm(p_true)","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"Gauss-Newton algorithms are often more efficient at sum-of-squares minimization than the more generic BFGS optimizer. This form of Gauss-Newton optimization of prediction errors is also available through ControlSystemIdentification.jl, which uses this package undernath the hood.","category":"page"},{"location":"parameter_estimation/#Identifiability","page":"Parameter estimation","title":"Identifiability","text":"","category":"section"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"There is no guarantee that we will recover the true parameters for this system, especially not if the input excitation is poor, but we will generally find parameters that results in a good predictor for the system (this is after all what we're optimizing for). A tool like StructuralIdentifiability.jl may be used to determine the identifiability of parameters and state variables, something that for this system could look like","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"using StructuralIdentifiability\n\node = @ODEmodel(\n h1'(t) = -a1/A1 * h1(t) + a3/A1*h3(t) + gam*k1/A1 * u1(t),\n h2'(t) = -a2/A2 * h2(t) + a4/A2*h4(t) + gam*k2/A2 * u2(t),\n h3'(t) = -a3/A3*h3(t) + (1-gam)*k2/A3 * u2(t),\n h4'(t) = -a4/A4*h4(t) + (1-gam)*k1/A4 * u1(t),\n\ty1(t) = h1(t),\n y2(t) = h2(t),\n)\n\nlocal_id = assess_local_identifiability(ode, 0.99)","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"where we have made the substitution sqrt h rightarrow h due to a limitation of the tool. The output of the above analysis is ","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"julia> local_id = assess_local_identifiability(ode, 0.99)\nDict{Nemo.fmpq_mpoly, Bool} with 15 entries:\n a3 => 0\n gam => 1\n k2 => 0\n A4 => 0\n h4 => 0\n h2 => 1\n A3 => 0\n a1 => 0\n A2 => 0\n k1 => 0\n a4 => 0\n h3 => 0\n h1 => 1\n A1 => 0\n a2 => 0","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"indicating that we can not hope to resolve all of the parameters. However, using appropriate regularization from prior information, we might still recover a lot of information about the system. Regularization could easily be added to the function cost above, e.g., using a penalty like (p-p_guess)'Γ*(p-p_guess) for some matrix Gamma, to indicate our confidence in the initial guess.","category":"page"},{"location":"parameter_estimation/#Videos","page":"Parameter estimation","title":"Videos","text":"","category":"section"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"Examples of parameter estimation are available here","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"By using an optimizer to optimize the likelihood of an UnscentedKalmanFilter:","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"Estimation of time-varying parameters:","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"Adaptive control by means of estimation of time-varying parameters:","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"","category":"page"},{"location":"neural_network/#Adaptive-Neural-Network-training","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"","category":"section"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"In this example, we will demonstrate how we can take the estimation of time-varying parameters to the extreme, and use a nonlinear state estimator to estimate the weights in a neural-network model of a dynamical system. ","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"In the tutorial Joint state and parameter estimation, we demonstrated how we can add a parameter as a state variable and let the state estimator estimate this alongside the state. In this example, we will try to learn an entire black-box model of the system dynamics using a neural network, and treat the network weights as time-varying parameters by adding them to the state.","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"We start by generating some data from a simple dynamical system, we will continue to use the quadruple-tank system from Joint state and parameter estimation.","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"using LowLevelParticleFilters, Lux, Random, SeeToDee, StaticArrays, Plots, LinearAlgebra, ComponentArrays, DifferentiationInterface, SparseMatrixColorings\nusing SparseConnectivityTracer: TracerSparsityDetector\nusing DisplayAs # hide\n\nusing LowLevelParticleFilters: SimpleMvNormal\n\nfunction quadtank(h,u,p,t)\n kc = 0.5\n k1, k2, g = 1.6, 1.6, 9.81\n A1 = A3 = A2 = A4 = 4.9\n a1, a3, a2, a4 = 0.03, 0.03, 0.03, 0.03\n γ1, γ2 = 0.2, 0.2\n\n if t > 2000\n a1 *= 1.5 # Change the parameter at t = 2000\n end\n\n ssqrt(x) = √(max(x, zero(x)) + 1e-3) # For numerical robustness at x = 0\n\n SA[\n -a1/A1 * ssqrt(2g*h[1]) + a3/A1*ssqrt(2g*h[3]) + γ1*k1/A1 * u[1]\n -a2/A2 * ssqrt(2g*h[2]) + a4/A2*ssqrt(2g*h[4]) + γ2*k2/A2 * u[2]\n -a3/A3*ssqrt(2g*h[3]) + (1-γ2)*k2/A3 * u[2]\n -a4/A4*ssqrt(2g*h[4]) + (1-γ1)*k1/A4 * u[1]\n ]\nend\n\nTs = 30 # sample time\ndiscrete_dynamics = SeeToDee.Rk4(quadtank, Ts) # Discretize dynamics\nnu = 2 # number of control inputs\nnx = 4 # number of state variables\nny = 4 # number of measured outputs\n\nfunction generate_data() \n measurement(x,u,p,t) = x#SA[x[1], x[2]]\n Tperiod = 200\n t = 0:Ts:4000\n u = vcat.((0.25 .* sign.(sin.(2pi/Tperiod .* t)) .+ 0.25) .* sqrt.(rand.()))\n u = SVector{nu, Float32}.(vcat.(u,u))\n x0 = Float32[2,2,3,3]\n x = LowLevelParticleFilters.rollout(discrete_dynamics, x0, u)[1:end-1]\n y = measurement.(x, u, 0, 0)\n y = [Float32.(y .+ 0.01.*randn.()) for y in y] # Add some noise to the measurement\n\n (; x, u, y, nx, nu, ny, Ts)\nend\n\nrng = Random.default_rng()\nRandom.seed!(rng, 1)\ndata = generate_data()\nnothing # hide","category":"page"},{"location":"neural_network/#Neural-network-dynamics","page":"Adaptive Neural-Network training","title":"Neural network dynamics","text":"","category":"section"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"Our neural network will be a small feedforward network built using the package Lux.jl. ","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"ni = ny + nu\nnhidden = 8\nconst model_ = Chain(Dense(ni, nhidden, tanh), Dense(nhidden, nhidden, tanh), Dense(nhidden, ny))","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"Since the network is rather small, we will train on the CPU only, this will be fast enough for this use case. We may extract the parameters of the network using the function Lux.setup, and convert them to a ComponentArray to make it easier to refer to different parts of the combined state vector.","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"dev = cpu_device()\nps, st = Lux.setup(rng, model_) |> dev\nparr = ComponentArray(ps)\nnothing # hide","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"The dynamics of our black-box model will call the neural network to predict the next state given the current state and input. We bias the dynamics towards low frequencies by adding a multiple of the current state to the prediction of the next state, 0.95*x. We also add a small amount of weight decay to the parameters of the neural network for regularization, 0.995*p.","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"function dynamics(out0, xp0, u, _, t)\n xp = ComponentArray(xp0, getaxes(s0))\n out = ComponentArray(out0, getaxes(s0))\n x = xp.x\n p = xp.p\n xp, _ = Lux.apply(model_, [x; u], p, st)\n @. out.x = 0.95f0*x+xp\n @. out.p = 0.995f0*p\n nothing\nend\n\n@views measurement(out, x, _, _, _) = out .= x[1:nx] # Assume measurement of the full state vector\nnothing # hide","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"For simplicity, we have assumed here that we have access to measurements of the entire state vector of the original process. This is many times unrealistic, and if we do not have such access, we may instead augment the measured signals with delayed versions of themselves (sometimes called a delay embedding). This is a common technique in discrete-time system identification, used in e.g., ControlSystemIdentification.arx and subspaceid.","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"The initial state of the process x0 and the initial parameters of the neural network parr can now be concatenated to form the initial augmented state s0.","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"x0 = Float32[2; 2; 3; 3]\ns0 = ComponentVector(; x=x0, p=parr)\nnothing # hide","category":"page"},{"location":"neural_network/#Kalman-filter-setup","page":"Adaptive Neural-Network training","title":"Kalman filter setup","text":"","category":"section"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"We will estimate the parameters using two different nonlinear Kalman filters, the ExtendedKalmanFilter and the UnscentedKalmanFilter. The covariance matrices for the filters, R1, R2, may be tuned such that we get the desired learning speed of the weights, where larger covariance for the network weights will allow for faster learning, but also more noise in the estimates. ","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"R1 = Diagonal([0.1ones(nx); 0.01ones(length(parr))]) .|> Float32\nR2 = Diagonal((1e-2)^2 * ones(ny)) .|> Float32\nnothing # hide","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"The ExtendedKalmanFilter uses Jacobians of the dynamics and measurement model, and if we do not provide those functions they will be automatically computed using ForwardDiff.jl. Since our Jacobians will be relatively large but sparse in this example, we will make use of the sparsity-aware features of DifferentiationInterface.jl in order to get efficient Jacobian computations. ","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"function Ajacfun(x,u,p,t) # Function that returns a function for the Jacobian of the dynamics\n # For large neural networks, it might be faster to use an OOP formulation with Zygote instead of ForwardDiff. Zygote does not handle the in-place version\n backend = AutoSparse(\n AutoForwardDiff(),\n # AutoZygote(),\n sparsity_detector=TracerSparsityDetector(),\n coloring_algorithm=GreedyColoringAlgorithm(),\n )\n out = similar(getdata(x))\n inner = (out,x)->dynamics(out,x,u,p,t)\n prep = prepare_jacobian(inner, out, backend, getdata(x))\n jac = one(eltype(x)) .* sparsity_pattern(prep)\n function (x,u,p,t)\n inner2 = (out,x)->dynamics(out,x,u,p,t)\n DifferentiationInterface.jacobian!(inner2, out, jac, prep, backend, x)\n end\nend\n\nAjac = Ajacfun(s0, data.u[1], nothing, 0)\n\nconst CJ_ = [I(nx) zeros(Float32, nx, length(parr))] # The jacobian of the measurement model is constant\nCjac(x,u,p,t) = CJ_\nnothing # hide","category":"page"},{"location":"neural_network/#Estimation","page":"Adaptive Neural-Network training","title":"Estimation","text":"","category":"section"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"We may now initialize our filters and perform the estimation. Here, we use the function forward_trajectory to perform filtering along the entire data trajectory at once, but we may use this in a streaming fashion as well, as more data becomes available in real time.","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"We plot the one-step ahead prediction of the outputs and compare to the \"measured\" data.","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"ekf = ExtendedKalmanFilter(dynamics, measurement, R1, R2, SimpleMvNormal(s0, 100R1); nu, check=false, Ajac, Cjac, Ts)\nukf = UnscentedKalmanFilter(dynamics, measurement, R1, R2, SimpleMvNormal(s0, 100R1); nu, ny, Ts)\n\n@time sole = forward_trajectory(ekf, data.u, data.x)\n@time solu = forward_trajectory(ukf, data.u, data.x)\n\nplot(sole, plotx=false, plotxt=false, plotyh=true, plotyht=false, plotu=false, plote=true, name=\"EKF\", layout=(nx, 1), size=(1200, 1500))\nplot!(solu, plotx=false, plotxt=false, plotyh=true, plotyht=false, plotu=false, plote=true, name=\"UKF\", ploty=false)\nDisplayAs.PNG(Plots.current()) # hide","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"We see that prediction errors, e, are large in the beginning when the network weights are randomly initialized, but after about half the trajectory the errors are significantly reduced. Just like in the tutorial Joint state and parameter estimation, we modified the true dynamics after some time, at t=2000, and we see that the filters are able to adapt to this change after a transient increase in prediction error variance.","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"We may also plot the evolution of the neural-network weights over time, and see how the filters adapt to the changing dynamics of the system.","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"plot(\n plot(0:Ts:4000, reduce(hcat, sole.xt)'[:, nx+1:end], title=\"EKF parameters\"),\n plot(0:Ts:4000, reduce(hcat, solu.xt)'[:, nx+1:end], title=\"UKF parameters\"),\n legend = false,\n)\nDisplayAs.PNG(Plots.current()) # hide","category":"page"},{"location":"neural_network/#Smoothing","page":"Adaptive Neural-Network training","title":"Smoothing","text":"","category":"section"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"@time xTe,RTe = smooth(sole, ekf)\n@time xTu,RTu = smooth(solu, ukf)\nplot(\n plot(0:Ts:4000, reduce(hcat, xTe)'[:, nx+1:end], title=\"EKF parameters\", c=1, alpha=0.2),\n plot(0:Ts:4000, reduce(hcat, xTu)'[:, nx+1:end], title=\"UKF parameters\", c=1, alpha=0.2),\n legend = false,\n)","category":"page"},{"location":"neural_network/#Benchmarking","page":"Adaptive Neural-Network training","title":"Benchmarking","text":"","category":"section"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"The neural network used in this example has","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"length(parr)","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"parameters, and the length of the data is","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"length(data.u)","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"Performing the estimation using the Extended Kalman Filter took","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"using BenchmarkTools\n@btime forward_trajectory(ekf, data.u, data.x);\n # 46.034 ms (77872 allocations: 123.45 MiB)","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"and with the Unscented Kalman Filter","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"@btime forward_trajectory(ukf, data.u, data.x);\n # 142.608 ms (2134370 allocations: 224.82 MiB)","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"The EKF is a bit faster, which is to be expected. Both methods are very fast from a neural-network training perspective, but the performance will not scale favorably to very large network sizes.","category":"page"},{"location":"neural_network/#Closing-remarks","page":"Adaptive Neural-Network training","title":"Closing remarks","text":"","category":"section"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"We have seen how to estimate train a black-box neural network dynamics model by treating the parameter estimation as a state-estimation problem. This example is very simple and leaves a lot of room for improvement, such as","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"We assumed very little prior knowledge of the dynamics. In practice, we may want to model as much as possible from first principles and add a neural network to capture only the residuals that our first-principles model cannot capture.\nWe started the training of the network weights directly from a random initialization. In practice, we may want to pre-train the network on a large offline dataset before updating the weights adaptively in real-time.\nWe used forward-mode AD to compute the Jacobian. The Jacobian of the dynamics has dense rows, which means that it's theoretically favorable to use reverse-mode AD to compute it. This is possible using Zygote.jl, but Zygote does not handle array mutation, and one must thus avoid the in-place version of the dynamics. Since the number of parameters in this example is small, sparse forward mode AD ended up being slightly faster.","category":"page"},{"location":"adaptive_kalmanfilter/#Noise-adaptive-Kalman-filter","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Noise-adaptive Kalman filter","text":"","category":"section"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"In this tutorial we will consider filtering of a 1D position track, similar in spirit to what one could have obtained from a GPS device, but limited to 1D for easier visualization. We will use a constant-velocity model, i.e., use a double integrator,","category":"page"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"beginaligned\nx_k+1 = beginbmatrix 1 T_s 0 1 endbmatrix x_k + beginbmatrix T_s^22 T_s endbmatrix w_k \ny_k = beginbmatrix 1 0 endbmatrix x_k + v_k\nendaligned","category":"page"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"where w_k sim mathcalN(0 σ_w) is the process noise, and v_k sim mathcalN(0 R_2) is the measurement noise, and illustrate how we can make use of an adaptive noise covariance to improve the filter performance.","category":"page"},{"location":"adaptive_kalmanfilter/#Data-generation","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Data generation","text":"","category":"section"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"We start by generating some position data that we want to perform filtering on. The \"object\" we want to track is initially stationary, and transitions to moving with a constant velocity after a while. ","category":"page"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"using LowLevelParticleFilters, Plots, Random\nRandom.seed!(1)\n\n# Create a time series for filtering\nx = [zeros(50); 0:100]\nT = length(x)\nY = x + randn(T)\nplot([Y x], lab=[\"Measurement\" \"True state to be tracked\"], c=[1 :purple])","category":"page"},{"location":"adaptive_kalmanfilter/#Simple-Kalman-filtering","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Simple Kalman filtering","text":"","category":"section"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"We will use a Kalman filter to perform the filtering. The model is a double integrator, i.e., a constant-acceleration model. The state vector is thus x = p v^T, where p is the position and v is the velocity. When designing a Kalman filter, we need to specify the noise covariances R_1 and R_2. While it's often easy to measure the covariance of the measurement noise, R_2, it can be quite difficult to know ahead of time what the dynamics noise covariance, R_1, should be. In this example, we will use an adaptive filter, where we will increase the dynamics noise covariance if the filter prediction error is too large. However, we first run the filter twice, once with a large R_1 and once with a small R_1 to illustrate the difference.","category":"page"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"y = [[y] for y in Y] # create a vector of vectors for the KF\nu = fill([], T) # No inputs in this example :(\n\n# Define the model\nTs = 1\nA = [1 Ts; 0 1]\nB = zeros(2, 0)\nC = [1 0]\nD = zeros(0, 0)\nR2 = [1;;]\n\nσws = [1e-2, 1e-5] # Dynamics noise standard deviations\n\nfig = plot(Y, lab=\"Measurement\")\nfor σw in σws\n R1 = σw*[Ts^3/3 Ts^2/2; Ts^2/2 Ts] # The dynamics noise covariance matrix is σw*Bw*Bw' where Bw = [Ts^2/2; Ts]\n kf = KalmanFilter(A, B, C, D, R1, R2)\n yh = []\n measure = LowLevelParticleFilters.measurement(kf)\n for t = 1:T # Main filter loop\n kf(u[t], y[t]) # Performs both prediction and correction\n xh = state(kf)\n yht = measure(xh, u[t], nothing, t)\n push!(yh, yht)\n end\n\n Yh = reduce(hcat, yh)\n plot!(Yh', lab=\"Estimate \\$σ_w\\$ = $σw\")\nend\nfig","category":"page"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"When R_1 is small (controlled by σ_w), we get a nice and smooth filter estimate, but this estimate clearly lags behind the true state. When R_1 is large, the filter estimate is much more responsive, but it also has a lot of noise.","category":"page"},{"location":"adaptive_kalmanfilter/#Adaptive-noise-covariance","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Adaptive noise covariance","text":"","category":"section"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"Below, we will implement an adaptive filter, where we keep the dynamics noise covariance low by default, but increase it if the filter prediction error is too large. We will use a Z-score to determine if the prediction error is too large. The Z-score is defined as the number of standard deviations the prediction error is away from the estimated mean. This time around we use separate correct! and predict! calls, so that we can access the prediction error as well as the prior covariance of the prediction error, S. S (or the Cholesky factor Sᵪ) will be used to compute the Z-score.","category":"page"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"σw = 1e-5 # Set the covariance to a low value by default\nR1 = σw*[Ts^3/3 Ts^2/2; Ts^2/2 Ts]\nkf = KalmanFilter(A, B, C, D, R1, R2)\nmeasure = LowLevelParticleFilters.measurement(kf)\n\n# Some arrays to store simulation data\nyh = []\nes = Float64[]\nσs = Float64[]\nfor t = 1:T # Main filter loop\n ll, e, S, Sᵪ = correct!(kf, u[t], y[t], nothing, t) # Manually call the prediction step\n σ = √(e'*(Sᵪ\\e)) # Compute the Z-score\n push!(es, e[]) # Save for plotting\n push!(σs, σ)\n if σ > 3 # If the Z-score is too high\n # we temporarily increase the dynamics noise covariance by 1000x to adapt faster\n predict!(kf, u[t], nothing, t; R1 = 1000kf.R1) \n else\n predict!(kf, u[t], nothing, t)\n end\n xh = state(kf)\n yht = measure(xh, u[t], nothing, t)\n push!(yh, yht)\nend\n\nYh = reduce(hcat, yh)\nplot([Y Yh'], lab=[\"Measurement\" \"Adaptive estimate\"])","category":"page"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"Not too bad! This time the filter estimate is much more responsive during the transition, but exhibits favorable noise properties during the stationary phases. We can also plot the prediction error and the Z-score to see how the filter adapts to the dynamics noise covariance.","category":"page"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"plot([es σs], lab=[\"Prediction error\" \"Z-score\"])","category":"page"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"Notice how the prediction errors, that should ideally be centered around zero, remain predominantly negative for a long time interval after the transition. This can be attributed to an overshoot in the velocity state of the estimator, but the rapid decrease of the covariance after the transition makes the filter slow at correcting its overshoot. If we want, we could mitigate this and make the adaptation even more sophisticated by letting the covariance remain large for a while after a transition in operating mode has been detected. Below, we implement a simple version of this, where we use a multiplier σ_wt that defaults to 1, but is increase to a very large value of 1000 if a transition is detected. When no transition is detected, σ_wt is decreased exponentially back down to 1.","category":"page"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"σw = 1e-5 # Set the covariance to a low value by default\nσwt = 1.0\nR1 = σw*[Ts^3/3 Ts^2/2; Ts^2/2 Ts]\nkf = KalmanFilter(A, B, C, D, R1, R2)\nmeasure = LowLevelParticleFilters.measurement(kf)\n\n# Some arrays to store simulation data\nyh = []\nes = Float64[]\nσs = Float64[]\nσwts = Float64[]\nfor t = 1:T # Main filter loop\n global σwt # Note, do not make use of global variables in performance critical code\n ll, e, S, Sᵪ = correct!(kf, u[t], y[t], nothing, t) # Manually call the prediction step\n σ = √(e'*(Sᵪ\\e)) # Compute the Z-score\n push!(es, e[]) # Save for plotting\n push!(σs, σ)\n if σ > 3 # If the Z-score is too high\n σwt = 1000.0 # Set the R1 multiplier to a very large value\n else\n σwt = max(0.7σwt, 1.0) # Decrease exponentially back to 1\n end\n push!(σwts, σwt)\n predict!(kf, u[t], nothing, t; R1 = σwt*kf.R1) \n xh = state(kf)\n yht = measure(xh, u[t], nothing, t)\n push!(yh, yht)\nend\n\nYh = reduce(hcat, yh)\nplot([Y Yh'], lab=[\"Measurement\" \"Adaptive estimate\"])","category":"page"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"plot([es σs σwts], lab=[\"Prediction error\" \"Z-score\" \"\\$σ_{wt}\\$ multiplier\"], layout=2, sp=[1 1 2])","category":"page"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"This time, the prediction errors look more like white noise centered around zero after the initial transient caused by the transition.","category":"page"},{"location":"adaptive_kalmanfilter/#Summary","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Summary","text":"","category":"section"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"This tutorial demonstrated simple Kalman filtering for a double integrator without control inputs. We saw how the filtering estimate could be improved by playing around with the covariance matrices of the estimator, helping it catch up to fast changes in the behavior of the system without sacrificing steady-state noise properties.","category":"page"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"In this case, we handled the modification of R_1 outside of the filter, implementing our own filtering loop. Some applications get away with instead providing time-varying matrices in the form of a 3-dimension array, where the third dimension corresponds to time, or instead of providing a matrix, providing a function R_1(x u p t) allows the matrix to be a function of state, input, parameters and time. These options apply to all matrices in the filter, including the dynamics matrices, ABCD.","category":"page"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"Lastly, we mention the ability of the KalmanFilter to act like a recursive least-squares estimator, by setting the \"forgetting factor α1 when creating the KalmanFilter. α1 will cause the filter will exhibit exponential forgetting similar to an RLS estimator, in addition to the covariance inflation due to R1. It is thus possible to get a RLS-like algorithm by setting R_1 = 0 R_2 = 1α and α 1.","category":"page"},{"location":"adaptive_kalmanfilter/#Disturbance-modeling-and-noise-tuning","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Disturbance modeling and noise tuning","text":"","category":"section"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"See this notebook for a blog post about disturbance modeling and noise tuning using LowLevelParticleFilter.jl","category":"page"},{"location":"dae/#State-estimation-for-high-index-DAEs","page":"State estimation for DAE systems","title":"State estimation for high-index DAEs","text":"","category":"section"},{"location":"dae/","page":"State estimation for DAE systems","title":"State estimation for DAE systems","text":"This tutorial is hosted as a notebook.","category":"page"},{"location":"measurement_models/#measurement_models","page":"Multiple measurement models","title":"Measurement models","text":"","category":"section"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"The Kalman-type filters","category":"page"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"KalmanFilter\nExtendedKalmanFilter\nUnscentedKalmanFilter","category":"page"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"each come with their own built-in measurement model, e.g., the standard KalmanFilter uses the linear measurement model y = Cx + Du + e, while the ExtendedKalmanFilter and UnscentedKalmanFilter use the nonlinear measurement model y = h(xupt) + e or y = h(xupte). For covariance propagation, the ExtendedKalmanFilter uses linearization to approximate the nonlinear measurement model, while the UnscentedKalmanFilter uses the unscented transform.","category":"page"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"It is sometimes useful to mix and match dynamics and measurement models. For example, using the unscented transform from the UKF for the dynamics update (predict!), but the linear measurement model from the standard KalmanFilter for the measurement update (correct!) if the measurement model is linear.","category":"page"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"This is possible by constructing a filter with an explicitly created measurement model. The available measurement models are","category":"page"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"LinearMeasurementModel performs linear propagation of covariance (as is done in KalmanFilter).\nEKFMeasurementModel uses linearization to propagate covariance (as is done in ExtendedKalmanFilter).\nUKFMeasurementModel uses the unscented transform to propagate covariance (as is done in UnscentedKalmanFilter).\nCompositeMeasurementModel combines multiple measurement models.","category":"page"},{"location":"measurement_models/#Constructing-a-filter-with-a-custom-measurement-model","page":"Multiple measurement models","title":"Constructing a filter with a custom measurement model","text":"","category":"section"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"Constructing a Kalman-type filter automatically creates a measurement model of the corresponding type, given the functions/matrices passed to the filter constructor. To construct a filter with a non-standard measurement model, e.g., and UKF with a KF measurement model, manually create the desired measurement model and pass it as the second argument to the constructor. For example, to construct an UKF with a linear measurement model, we do","category":"page"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"using LowLevelParticleFilters, LinearAlgebra\nnx = 100 # Dimension of state\nnu = 2 # Dimension of input\nny = 90 # Dimension of measurements\n\n# Define linear state-space system\nconst __A = 0.1*randn(nx, nx)\nconst __B = randn(nx, nu)\nconst __C = randn(ny,nx)\nfunction dynamics_ip(dx,x,u,p,t)\n # __A*x .+ __B*u\n mul!(dx, __A, x)\n mul!(dx, __B, u, 1.0, 1.0)\n nothing\nend\nfunction measurement_ip(y,x,u,p,t)\n # __C*x\n mul!(y, __C, x)\n nothing\nend\n\nR1 = I(nx)\nR2 = I(ny)\n\nmm_kf = LinearMeasurementModel(__C, 0, R2; nx, ny)\nukf = UnscentedKalmanFilter(dynamics_ip, mm_kf, R1; ny, nu)","category":"page"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"When we create the filter with the custom measurement model, we do not pass the arguments that are associated with the measurement model to the filter constructor, i.e., we do not pass any measurement function, and not the measurement covariance matrix R_2.","category":"page"},{"location":"measurement_models/#Sensor-fusion:-Using-several-different-measurement-models","page":"Multiple measurement models","title":"Sensor fusion: Using several different measurement models","text":"","category":"section"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"Above we constructed a filter with a custom measurement model, we can also pass a custom measurement model when we call correct!. This may be useful when, e.g., performing sensor fusion with sensors operating at different sample rates, or when parts of the measurement model are linear, and other parts are nonlinear.","category":"page"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"The following example instantiates three different filters and three different measurement models. Each filter is updated with each measurement model, demonstrating that any combination of filter and measurement model can be used together.","category":"page"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"using LowLevelParticleFilters, LinearAlgebra\nnx = 10 # Dimension of state\nnu = 2 # Dimension of input\nny = 9 # Dimension of measurements\n\n# Define linear state-space system\nconst __A = 0.1*randn(nx, nx)\nconst __B = randn(nx, nu)\nconst __C = randn(ny,nx)\nfunction dynamics_ip(dx,x,u,p,t)\n # __A*x .+ __B*u\n mul!(dx, __A, x)\n mul!(dx, __B, u, 1.0, 1.0)\n nothing\nend\nfunction measurement_ip(y,x,u,p,t)\n # __C*x\n mul!(y, __C, x)\n nothing\nend\n\nR1 = I(nx) # Covariance matrices\nR2 = I(ny)\n\n# Construct three different filters\nkf = KalmanFilter(__A, __B, __C, 0, R1, R2)\nukf = UnscentedKalmanFilter(dynamics_ip, measurement_ip, R1, R2; ny, nu)\nekf = ExtendedKalmanFilter(dynamics_ip, measurement_ip, R1, R2; nu)\n\n# Simulate some data\nT = 200 # Number of time steps\nU = [randn(nu) for _ in 1:T]\nx,u,y = LowLevelParticleFilters.simulate(kf, U) # Simulate trajectory using the model in the filter\n\n# Construct three different measurement models\nmm_kf = LinearMeasurementModel(__C, 0, R2; nx, ny)\nmm_ekf = EKFMeasurementModel{Float64, true}(measurement_ip, R2; nx, ny)\nmm_ukf = UKFMeasurementModel{Float64, true, false}(measurement_ip, R2; nx, ny)\n\n\nmms = [mm_kf, mm_ekf, mm_ukf]\nfilters = [kf, ekf, ukf]\n\nfor mm in mms, filter in filters\n @info \"Updating $(nameof(typeof(filter))) with measurement model $(nameof(typeof(mm)))\"\n correct!(filter, mm, u[1], y[1]) # Pass the measurement model as the second argument to the correct! function if not using the measurement model built into the filter\nend\nnothing # hide","category":"page"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"Since the dynamics in this particular example is in fact linear, we should get identical results for all three filters.","category":"page"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"using Test\n@test kf.x ≈ ekf.x ≈ ukf.x\n@test kf.R ≈ ekf.R ≈ ukf.R","category":"page"},{"location":"measurement_models/#Video-tutorial","page":"Multiple measurement models","title":"Video tutorial","text":"","category":"section"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"A video demonstrating the use of multiple measurement models in a sensor-fusion context is available on YouTube:","category":"page"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"","category":"page"},{"location":"api/#Exported-functions-and-types","page":"API","title":"Exported functions and types","text":"","category":"section"},{"location":"api/#Index","page":"API","title":"Index","text":"","category":"section"},{"location":"api/","page":"API","title":"API","text":"","category":"page"},{"location":"api/","page":"API","title":"API","text":"Modules = [LowLevelParticleFilters]\nPrivate = false","category":"page"},{"location":"api/#LowLevelParticleFilters.AdvancedParticleFilter-Tuple{Integer, Function, Function, Any, Any, Any}","page":"API","title":"LowLevelParticleFilters.AdvancedParticleFilter","text":"AdvancedParticleFilter(N::Integer, dynamics::Function, measurement::Function, measurement_likelihood, dynamics_density, initial_density; p = NullParameters(), threads = false, kwargs...)\n\nThis type represents a standard particle filter but affords extra flexibility compared to the ParticleFilter type, e.g., non-additive noise in the dynamics and measurement functions.\n\nSee the docs for more information: https://baggepinnen.github.io/LowLevelParticleFilters.jl/stable/#AdvancedParticleFilter-1\n\nArguments:\n\nN: Number of particles\ndynamics: A discrete-time dynamics function (x, u, p, t, noise=false) -> x⁺. It's important that the noise argument defaults to false.\nmeasurement: A measurement function (x, u, p, t, noise=false) -> y. It's important that the noise argument defaults to false.\nmeasurement_likelihood: A function (x, u, y, p, t)->logl to evaluate the log-likelihood of a measurement.\ndynamics_density: This field is not used by the advanced filter and can be set to nothing.\ninitial_density: The distribution of the initial state.\nthreads: use threads to propagate particles in parallel. Only activate this if your dynamics is thread-safe. SeeToDee.SimpleColloc is not thread-safe by default due to the use of internal caches, but SeeToDee.Rk4 is.\n\nExtended help\n\nMultiple measurement models\n\nThe measurement_likelihood function is used to evaluate the likelihood of a measurement. If you have multiple sensors and want to perform individual correct! steps for each, call correct!(..., g = custom_likelihood_function).\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.AuxiliaryParticleFilter-Tuple","page":"API","title":"LowLevelParticleFilters.AuxiliaryParticleFilter","text":"AuxiliaryParticleFilter(args...; kwargs...)\n\nTakes exactly the same arguments as ParticleFilter, or an instance of ParticleFilter.\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.CompositeMeasurementModel-Tuple{Any, Vararg{Any}}","page":"API","title":"LowLevelParticleFilters.CompositeMeasurementModel","text":"CompositeMeasurementModel(model1, model2, ...)\n\nA composite measurement model that combines multiple measurement models. This model acts as all component models concatenated. The tuple returned from correct! will be\n\nll: The sum of the log-likelihood of all component models\ne: The concatenated innovation vector\nS: A vector of the innovation covariance matrices of the component models\nSᵪ: A vector of the Cholesky factorizations of the innovation covariance matrices of the component models\nK: A vector of the Kalman gains of the component models\n\nIf all sensors operate on at the same rate, and all measurement models are of the same type, it's more efficient to use a single measurement model with a vector-valued measurement function.\n\nFields:\n\nmodels: A tuple of measurement models\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.EKFMeasurementModel-Union{Tuple{IPM}, NTuple{4, Any}, NTuple{5, Any}} where IPM","page":"API","title":"LowLevelParticleFilters.EKFMeasurementModel","text":"EKFMeasurementModel{IPM}(measurement, R2, ny, Cjac, cache = nothing)\n\nA measurement model for the Extended Kalman Filter.\n\nArguments:\n\nIPM: A boolean indicating if the measurement function is inplace\nmeasurement: The measurement function y = h(x, u, p, t)\nR2: The measurement noise covariance matrix\nny: The number of measurement variables\nCjac: The Jacobian of the measurement function Cjac(x, u, p, t). If none is provided, ForwardDiff will be used.\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.EKFMeasurementModel-Union{Tuple{M}, Tuple{IPM}, Tuple{T}, Tuple{M, Any}} where {T, IPM, M}","page":"API","title":"LowLevelParticleFilters.EKFMeasurementModel","text":"EKFMeasurementModel{T,IPM}(measurement::M, R2; nx, ny, Cjac = nothing)\n\nT is the element type used for arrays\nIPM is a boolean indicating if the measurement function is inplace\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.ExtendedKalmanFilter","page":"API","title":"LowLevelParticleFilters.ExtendedKalmanFilter","text":"ExtendedKalmanFilter(kf, dynamics, measurement; Ajac, Cjac)\nExtendedKalmanFilter(dynamics, measurement, R1,R2,d0=MvNormal(Matrix(R1)); nu::Int, p = NullParameters(), α = 1.0, check = true)\n\nA nonlinear state estimator propagating uncertainty using linearization.\n\nThe constructor to the extended Kalman filter takes dynamics and measurement functions, and either covariance matrices, or a KalmanFilter. If the former constructor is used, the number of inputs to the system dynamics, nu, must be explicitly provided with a keyword argument.\n\nBy default, the filter will internally linearize the dynamics using ForwardDiff. User provided Jacobian functions can be provided as keyword arguments Ajac and Cjac. These functions should have the signature (x,u,p,t)::AbstractMatrix where x is the state, u is the input, p is the parameters, and t is the time.\n\nThe dynamics and measurement function are on the following form\n\nx(t+1) = dynamics(x, u, p, t) + w\ny = measurement(x, u, p, t) + e\n\nwhere w ~ N(0, R1), e ~ N(0, R2) and x(0) ~ d0\n\nSee also UnscentedKalmanFilter which is typically more accurate than ExtendedKalmanFilter. See KalmanFilter for detailed instructions on how to set up a Kalman filter kf.\n\n\n\n\n\n","category":"type"},{"location":"api/#LowLevelParticleFilters.IMM-Tuple{Any, AbstractMatrix, AbstractVector}","page":"API","title":"LowLevelParticleFilters.IMM","text":"IMM(models, P, μ; check = true, p = NullParameters(), interact = true)\n\nInteracting Multiple Model (IMM) filter. This filter is a combination of multiple Kalman-type filters, each with its own state and covariance. The IMM filter is a probabilistically weighted average of the states and covariances of the individual filters. The weights are determined by the probability matrix P and the mixing probabilities μ.\n\nwarning: Experimental\nThis filter is currently considered experimental and the user interface may change in the future without respecting semantic versioning.\n\nIn addition to the predict! and correct! steps, the IMM filter has an interact! method that updates the states and covariances of the individual filters based on the mixing probabilities. The combine! method combines the states and covariances of the individual filters into a single state and covariance. These four functions are typically called in either of the orders\n\ncorrect!, combine!, interact!, predict! (as is done in update!)\ninteract!, predict!, correct!, combine! (as is done in the reference cited below)\n\nThese two orders are cyclic permutations of each other, and the order used in update! is chosen to align with the order used in the other filters, where the initial condition is corrected using the first measurement, i.e., we assume the first measurement updates x(0-1) to x(00).\n\nThe initial (combined) state and covariance of the IMM filter is made up of the weighted average of the states and covariances of the individual filters. The weights are the initial mixing probabilities μ.\n\nRef: \"Interacting multiple model methods in target tracking: a survey\", E. Mazor; A. Averbuch; Y. Bar-Shalom; J. Dayan\n\nArguments:\n\nmodels: An array of Kalman-type filters, such as KalmanFilter, ExtendedKalmanFilter, UnscentedKalmanFilter, etc. The state of each model must have the same meaning, such that forming a weighted average makes sense.\nP: The mode-transition probability matrix. P[i,j] is the probability of transitioning from mode i to mode j (each row must sum to one).\nμ: The initial mixing probabilities. μ[i] is the probability of being in mode i at the initial contidion (must sum to one).\ncheck: If true, check that the inputs are valid. If false, skip the checks.\np: Parameters for the filter. NOTE: this p is shared among all internal filters. The internal p of each filter will be overridden by this one.\ninteract: If true, the filter will run the interaction as part of update! and forward_trajectory. If false, the filter will not run the interaction step. This choice can be overridden by passing the keyword argument interact to the respective functions.\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.KalmanFilter","page":"API","title":"LowLevelParticleFilters.KalmanFilter","text":"KalmanFilter(A,B,C,D,R1,R2,d0=MvNormal(R1); p = NullParameters(), α=1, check=true)\n\nThe matrices A,B,C,D define the dynamics\n\nx' = Ax + Bu + w\ny = Cx + Du + e\n\nwhere w ~ N(0, R1), e ~ N(0, R2) and x(0) ~ d0\n\nThe matrices can be time varying such that, e.g., A[:, :, t] contains the A matrix at time index t. They can also be given as functions on the form\n\nAfun(x, u, p, t) -> A\n\nFor maximum performance, provide statically sized matrices from StaticArrays.jl\n\nα is an optional \"forgetting factor\", if this is set to a value > 1, such as 1.01-1.2, the filter will, in addition to the covariance inflation due to R_1, exhibit \"exponential forgetting\" similar to a Recursive Least-Squares (RLS) estimator. It is thus possible to get a RLS-like algorithm by setting R_1=0 R_2 = 1α and α 1 (α is the inverse of the traditional RLS parameter α = 1λ). The exact form of the covariance update is\n\nR(t+1t) = α AR(t)A^T + R_1\n\nIf check = true (default) the function will check that the eigenvalues of A are less than 2 in absolute value. Large eigenvalues may be an indication that the system matrices are representing a continuous-time system and the user has forgotten to discretize it. Turn off this check by setting check = false.\n\nTutorials on Kalman filtering\n\nThe tutorial \"How to tune a Kalman filter\" details how to figure out appropriate covariance matrices for the Kalman filter, as well as how to add disturbance models to the system model. See also the tutorial in the documentation\n\n\n\n\n\n","category":"type"},{"location":"api/#LowLevelParticleFilters.KalmanFilteringSolution","page":"API","title":"LowLevelParticleFilters.KalmanFilteringSolution","text":"KalmanFilteringSolution{Tx,Txt,TR,TRt,Tll} <: AbstractFilteringSolution\n\nFields\n\nx: predictions x(t+1t) (plotted if plotx=true)\nxt: filtered estimates x(tt) (plotted if plotxt=true)\nR: predicted covariance matrices R(t+1t) (plotted if plotR=true)\nRt: filter covariances R(tt) (plotted if plotRt=true)\nll: loglikelihood\ne: prediction errors e(tt-1) = y - y(tt-1) (plotted if plote=true)\n\nPlot\n\nThe solution object can be plotted\n\nplot(sol, plotx=true, plotxt=true, plotR=true, plotRt=true, plote=true, plotu=true, ploty=true, plotyh=true, plotyht=true, name=\"\")\n\nwhere\n\nplotx: Plot the predictions x(t|t-1)\nplotxt: Plot the filtered estimates x(t|t)\nplotR: Plot the predicted covariances R(t|t-1) as ribbons at ±2σ (1.96 σ to be precise)\nplotRt: Plot the filter covariances R(t|t) as ribbons at ±2σ (1.96 σ to be precise)\nplote: Plot the prediction errors e(t|t-1) = y - ŷ(t|t-1)\nplotu: Plot the input\nploty: Plot the measurements\nplotyh: Plot the predicted measurements ŷ(t|t-1)\nplotyht: Plot the filtered measurements ŷ(t|t)\nname: a string that is prepended to the labels of the plots, which is useful when plotting multiple solutions in the same plot.\n\n\n\n\n\n","category":"type"},{"location":"api/#LowLevelParticleFilters.LinearMeasurementModel","page":"API","title":"LowLevelParticleFilters.LinearMeasurementModel","text":"LinearMeasurementModel{CT, DT, RT, CAT}\n\nA linear measurement model y = C*x + D*u + e.\n\nFields:\n\nC \nD\nR2: The measurement noise covariance matrix\nny: The number of measurement variables\n\n\n\n\n\n","category":"type"},{"location":"api/#LowLevelParticleFilters.ParticleFilter-Tuple{Integer, Function, Function, Any, Any, Any}","page":"API","title":"LowLevelParticleFilters.ParticleFilter","text":"ParticleFilter(N::Integer, dynamics, measurement, dynamics_density, measurement_density, initial_density; threads = false, p = NullParameters(), kwargs...)\n\nSee the docs for more information: https://baggepinnen.github.io/LowLevelParticleFilters.jl/stable/#Particle-filter-1\n\nArguments:\n\nN: Number of particles\ndynamics: A discrete-time dynamics function (x, u, p, t) -> x⁺\nmeasurement: A measurement function (x, u, p, t) -> y\ndynamics_density: A probability-density function for additive noise in the dynamics. Use AdvancedParticleFilter for non-additive noise.\nmeasurement_density: A probability-density function for additive measurement noise. Use AdvancedParticleFilter for non-additive noise.\ninitial_density: Distribution of the initial state.\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.ParticleFilteringSolution","page":"API","title":"LowLevelParticleFilters.ParticleFilteringSolution","text":"ParticleFilteringSolution{F, Tu, Ty, Tx, Tw, Twe, Tll} <: AbstractFilteringSolution\n\nFields:\n\nf: The filter used to produce the solution.\nu: Input\ny: Output / measurements\nx: Particles, the size of this array is (N,T), where N is the number of particles and T is the number of time steps. Each element represents a weighted state hypothesis with weight given by we.\nw: Weights (log space). These are used for internal computations.\nwe: Weights (exponentiated / original space). These are the ones to use to compute weighted means etc., they sum to one for each time step.\nll: Log likelihood\n\nPlot\n\nThe solution object can be plotted\n\nplot(sol; nbinsy=30, xreal=nothing, dim=nothing, ploty=true)\n\n\n\n\n\n","category":"type"},{"location":"api/#LowLevelParticleFilters.SqKalmanFilter","page":"API","title":"LowLevelParticleFilters.SqKalmanFilter","text":"SqKalmanFilter(A,B,C,D,R1,R2,d0=MvNormal(R1); p = NullParameters(), α=1)\n\nA standard Kalman filter on square-root form. This filter may have better numerical performance when the covariance matrices are ill-conditioned.\n\nThe matrices A,B,C,D define the dynamics\n\nx' = Ax + Bu + w\ny = Cx + Du + e\n\nwhere w ~ N(0, R1), e ~ N(0, R2) and x(0) ~ d0\n\nThe matrices can be time varying such that, e.g., A[:, :, t] contains the A matrix at time index t. They can also be given as functions on the form\n\nAfun(x, u, p, t) -> A\n\nThe internal fields storing covariance matrices are for this filter storing the upper-triangular Cholesky factor.\n\nα is an optional \"forgetting factor\", if this is set to a value > 1, such as 1.01-1.2, the filter will, in addition to the covariance inflation due to R_1, exhibit \"exponential forgetting\" similar to a Recursive Least-Squares (RLS) estimator. It is thus possible to get a RLS-like algorithm by setting R_1=0 R_2 = 1α and α 1 (α is the inverse of the traditional RLS parameter α = 1λ). The form of the covariance update is\n\nR(t+1t) = α AR(t)A^T + R_1\n\nRef: \"A Square-Root Kalman Filter Using Only QR Decompositions\", Kevin Tracy https://arxiv.org/abs/2208.06452\n\n\n\n\n\n","category":"type"},{"location":"api/#LowLevelParticleFilters.UKFMeasurementModel-Union{Tuple{AUGM}, Tuple{IPM}, NTuple{8, Any}, NTuple{9, Any}} where {IPM, AUGM}","page":"API","title":"LowLevelParticleFilters.UKFMeasurementModel","text":"UKFMeasurementModel{inplace_measurement,augmented_measurement}(measurement, R2, ny, ne, innovation, mean, cov, cross_cov, cache = nothing)\n\nA measurement model for the Unscented Kalman Filter.\n\nArguments:\n\nmeasurement: The measurement function y = h(x, u, p, t)\nR2: The measurement noise covariance matrix\nny: The number of measurement variables\nne: If augmented_measurement is true, the number of measurement noise variables\ninnovation(y::AbstractVector, yh::AbstractVector) where the arguments represent (measured output, predicted output)\nmean(ys::AbstractVector{<:AbstractVector}): computes the mean of the vector of vectors of output sigma points.\ncov(ys::AbstractVector{<:AbstractVector}, y::AbstractVector): computes the covariance matrix of the output sigma points.\ncross_cov(xs::AbstractVector{<:AbstractVector}, x::AbstractVector, ys::AbstractVector{<:AbstractVector}, y::AbstractVector) where the arguments represents (state sigma points, mean state, output sigma points, mean output). The function should return the cross-covariance matrix between the state and output sigma points.\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.UKFMeasurementModel-Union{Tuple{AUGM}, Tuple{IPM}, Tuple{T}, Tuple{Any, Any}} where {T, IPM, AUGM}","page":"API","title":"LowLevelParticleFilters.UKFMeasurementModel","text":"UKFMeasurementModel{T,IPM,AUGM}(measurement, R2; nx, ny, ne = nothing, innovation = -, mean = safe_mean, cov = safe_cov, cross_cov = cross_cov, static = nothing)\n\nT is the element type used for arrays\nIPM is a boolean indicating if the measurement function is inplace\nAUGM is a boolean indicating if the measurement model is augmented\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.UnscentedKalmanFilter-Union{Tuple{AUGM}, Tuple{AUGD}, Tuple{IPM}, Tuple{IPD}, Tuple{Any, LowLevelParticleFilters.AbstractMeasurementModel, Any}, Tuple{Any, LowLevelParticleFilters.AbstractMeasurementModel, Any, Any}} where {IPD, IPM, AUGD, AUGM}","page":"API","title":"LowLevelParticleFilters.UnscentedKalmanFilter","text":"UnscentedKalmanFilter(dynamics, measurement, R1, R2, d0=MvNormal(Matrix(R1)); p = NullParameters(), ny, nu)\nUnscentedKalmanFilter{IPD,IPM,AUGD,AUGM}(dynamics, measurement_model::AbstractMeasurementModel, R1, d0=SimpleMvNormal(R1); p=NullParameters(), nu)\n\nA nonlinear state estimator propagating uncertainty using the unscented transform.\n\nThe dynamics and measurement function are on either of the following forms\n\nx' = dynamics(x, u, p, t) + w\ny = measurement(x, u, p, t) + e\n\nx' = dynamics(x, u, p, t, w)\ny = measurement(x, u, p, t, e)\n\nwhere w ~ N(0, R1), e ~ N(0, R2) and x(0) ~ d0. The former (default) assums that the noise is additive and added after the dynamics and measurement updates, while the latter assumes that the dynamics functions take an additional argument corresponding to the noise term. The latter form (sometimes refered to as the \"augmented\" form) is useful when the noise is multiplicative or when the noise is added before the dynamics and measurement updates. See \"Augmented UKF\" below for more details on how to use this form.\n\nThe matrices R1, R2 can be time varying such that, e.g., R1[:, :, t] contains the R_1 matrix at time index t. They can also be given as functions on the form\n\nRfun(x, u, p, t) -> R\n\nFor maximum performance, provide statically sized matrices from StaticArrays.jl\n\nny, nu indicate the number of outputs and inputs.\n\nCustom type of u\n\nThe input u may be of any type, e.g., a named tuple or a custom struct. The u provided in the input data is passed directly to the dynamics and measurement functions, so as long as the type is compatible with the dynamics it will work out. The one exception where this will not work is when calling simulate, which assumes that u is an array.\n\nAugmented UKF\n\nIf the noise is not additive, one may use the augmented form of the UKF. In this form, the dynamics functions take additional input arguments that correspond to the noise terms. To enable this form, the typed constructor\n\nUnscentedKalmanFilter{inplace_dynamics,inplace_measurement,augmented_dynamics,augmented_measurement}(...)\n\nis used, where the Boolean type parameters have the following meaning\n\ninplace_dynamics: If true, the dynamics function operates in-place, i.e., it modifies the first argument in dynamics(dx, x, u, p, t). Default is false.\ninplace_measurement: If true, the measurement function operates in-place, i.e., it modifies the first argument in measurement(y, x, u, p, t). Default is false.\naugmented_dynamics: If true the dynamics function is augmented with an additional noise input w, i.e., dynamics(x, u, p, t, w). Default is false.\naugmented_measurement: If true the measurement function is agumented with an additional noise input e, i.e., measurement(x, u, p, t, e). Default is false. (If the measurement noise has fewer degrees of freedom than the number of measurements, you may failure in Cholesky factorizations, see \"Custom Cholesky factorization\" below).\n\nUse of augmented dynamics incurs extra computational cost. The number of sigma points used is 2L+1 where L is the length of the augmented state vector. Without augmentation, L = nx, with augmentation L = nx + nw and L = nx + ne for dynamics and measurement, respectively.\n\nSigma-point rejection\n\nFor problems with challenging dynamics, a mechanism for rejection of sigma points after the dynamics update is provided. A function reject(x) -> Bool can be provided through the keyword argument reject that returns true if a sigma point for x(t+1) should be rejected, e.g., if an instability or non-finite number is detected. A rejected point is replaced by the propagated mean point (the mean point cannot be rejected). This function may be provided either to the constructor of the UKF or passed to the predict! function.\n\nCustom measurement models\n\nBy default, standard arithmetic mean and e(y, yh) = y - yh are used as mean and innovation functions.\n\nBy passing and explicitly created UKFMeasurementModel, one may provide custom functions that compute the mean, the covariance and the innovation. This is useful in situations where the state or a measurement lives on a manifold. One may further override the mean and covariance functions for the state sigma points by passing the keyword arguments state_mean and state_cov to the constructor.\n\nstate_mean(xs::AbstractVector{<:AbstractVector}) computes the mean of the vector of vectors of state sigma points.\nstate_cov(xs::AbstractVector{<:AbstractVector}, m = mean(xs)) where the first argument represent state sigma points and the second argument, which must be optional, represents the mean of those points. The function should return the covariance matrix of the state sigma points.\n\nSee UKFMeasurementModel for more details on how to set up a custom measurement model. Pass the custom measurement model as the second argument to the UKF constructor.\n\nCustom Cholesky factorization\n\nThe UnscentedKalmanFilter supports providing a custom function to compute the Cholesky factorization of the covariance matrices for use in sigma-point generation.\n\nIf either of the following conditions are met, you may experience failure in internal Cholesky factorizations:\n\nThe dynamics noise or measurement noise covariance matrices (R_1 R_2) are singular\nThe measurement is augmented and the measurement noise has fewer degrees of freedom than the number of measurements\n(Under specific technical conditions) The dynamics is augmented and the dynamics noise has fewer degrees of freedom than the number of state variables. The technical conditions are easiest to understand in the linear-systems case, where it corresponds to the Riccati equation associated with the Kalman gain not having a solution. This may happen when the pair (A R1) has uncontrollable modes on the unit circle, for example, when there are integrating modes that are not affected through the noise.\n\nThe error message may look like\n\nERROR: PosDefException: matrix is not positive definite; Factorization failed.\n\nIn such situations, it is advicable to reconsider the noise model and covariance matrices, alternatively, you may provide a custom Cholesky factorization function to the UKF constructor through the keyword argument cholesky!. The function should have the signature cholesky!(A::AbstractMatrix)::Cholesky. A useful alternative factorizaiton when covariance matrices are expected to be singular is cholesky! = R->cholesky!(Positive, Matrix(R)) where the \"positive\" Cholesky factorization is provided by the package PositiveFactorizations.jl, which must be manually installed and loaded by the user.\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.combine!-Tuple{IMM}","page":"API","title":"LowLevelParticleFilters.combine!","text":"combine!(imm::IMM)\n\nCombine the models of the IMM filter into a single state imm.x and covariance imm.R. This is done by taking a weighted average of the states and covariances of the individual models, where the weights are the mixing probabilities μ.\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.commandplot","page":"API","title":"LowLevelParticleFilters.commandplot","text":"commandplot(pf, u, y, p=parameters(pf); kwargs...)\n\nProduce a helpful plot. For customization options (kwargs...), see ?pplot. After each time step, a command from the user is requested.\n\nq: quit\ns n: step n steps\n\nnote: Note\nThis function requires using Plots to be called before it is used.\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.correct!","page":"API","title":"LowLevelParticleFilters.correct!","text":"(; ll, e, S, Sᵪ, K) = correct!(kf::AbstractKalmanFilter, u, y, p = parameters(kf), t::Integer = index(kf), R2)\n\nThe correct step for a Kalman filter returns not only the log likelihood ll and the prediction error e, but also the covariance of the output S, its Cholesky factor Sᵪ and the Kalman gain K.\n\nIf R2 stored in kf is a function R2(x, u, p, t), this function is evaluated at the state before the correction is performed. The measurement noise covariance matrix R2 stored in the filter object can optionally be overridden by passing the argument R2, in this case R2 must be a matrix.\n\nExtended help\n\nTo perform separate measurement updates for different sensors, see the \"Measurement models\" in the documentation.\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.correct!-2","page":"API","title":"LowLevelParticleFilters.correct!","text":"ll, e = correct!(pf, u, y, p = parameters(f), t = index(f))\n\nUpdate state/weights based on measurement y, returns log-likelihood and prediction error (the error is always 0 for particle filters).\n\nExtended help\n\nTo perform separate measurement updates for different sensors, see the \"Measurement models\" in the documentation. For AdvancedParticleFilter, this can be realized by passing a custom measurement_likelihood function as the keyword argument g to correct!, or by calling the lower-level function measurement_equation! with a custom measurement_likelihood.\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.correct!-3","page":"API","title":"LowLevelParticleFilters.correct!","text":"correct!(kf::SqKalmanFilter, u, y, p = parameters(kf), t::Real = index(kf); R2 = get_mat(kf.R2, kf.x, u, p, t))\n\nFor the square-root Kalman filter, a custom provided R2 must be the upper triangular Cholesky factor of the covariance matrix of the measurement noise.\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.correct!-Tuple{IMM, Any, Any, Vararg{Any}}","page":"API","title":"LowLevelParticleFilters.correct!","text":"ll, lls, rest = correct!(imm::IMM, u, y, args; kwargs)\n\nThe correct step of the IMM filter corrects each model with the measurements y and control input u. The mixing probabilities imm.μ are updated based on the likelihood of each model given the measurements and the transition probability matrix P.\n\nThe returned tuple consists of the sum of the log-likelihood of all models, the vector of individual log-likelihoods and an array of the rest of the return values from the correct step of each model.\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.correct!-Tuple{UnscentedKalmanFilter, Any, Any, Any, Real}","page":"API","title":"LowLevelParticleFilters.correct!","text":"correct!(ukf::UnscentedKalmanFilter{IPD, IPM, AUGD, AUGM}, u, y, p = parameters(ukf), t::Real = index(ukf) * ukf.Ts; R2 = get_mat(ukf.R2, ukf.x, u, p, t), mean, cross_cov, innovation)\n\nThe correction step for an UnscentedKalmanFilter allows the user to override, R2, mean, cross_cov, innovation.\n\nArguments:\n\nu: The input\ny: The measurement\np: The parameters\nt: The current time\nR2: The measurement noise covariance matrix, or a function that returns the covariance matrix (x,u,p,t)->R2.\nmean: The function that computes the mean of the output sigma points.\ncross_cov: The function that computes the cross-covariance of the state and output sigma points.\ninnovation: The function that computes the innovation between the measured output and the predicted output.\n\nExtended help\n\nTo perform separate measurement updates for different sensors, see the \"Measurement models\" in the documentation\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.debugplot","page":"API","title":"LowLevelParticleFilters.debugplot","text":"debugplot(pf, u, y, p=parameters(pf); runall=false, kwargs...)\n\nProduce a helpful plot. For customization options (kwargs...), see ?pplot.\n\nrunall=false: if true, runs all time steps befor displaying (faster), if false, displays the plot after each time step.\n\nThe generated plot becomes quite heavy. Initially, try limiting your input to 100 time steps to verify that it doesn't crash.\n\nnote: Note\nThis function requires using Plots to be called before it is used.\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.densityplot","page":"API","title":"LowLevelParticleFilters.densityplot","text":"densityplot(x,[w])\n\nPlot (weighted) particles densities\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.forward_trajectory","page":"API","title":"LowLevelParticleFilters.forward_trajectory","text":"sol = forward_trajectory(pf, u::AbstractVector, y::AbstractVector, p=parameters(pf))\n\nRun the particle filter for a sequence of inputs and measurements (offline / batch filtering). Return a solution with x,w,we,ll = particles, weights, expweights and loglikelihood\n\nIf MonteCarloMeasurements.jl is loaded, you may transform the output particles to Matrix{MonteCarloMeasurements.Particles} using Particles(x,we). Internally, the particles are then resampled such that they all have unit weight. This is conventient for making use of the plotting facilities of MonteCarloMeasurements.jl.\n\nsol can be plotted\n\nplot(sol::ParticleFilteringSolution; nbinsy=30, xreal=nothing, dim=nothing)\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.forward_trajectory-2","page":"API","title":"LowLevelParticleFilters.forward_trajectory","text":"forward_trajectory(imm::IMM, u, y, p = parameters(imm); interact = true)\n\nWhen performing batch filtering using an IMM filter, one may\n\nOverride the interact parameter of the filter\nAccess the mode probabilities along the trajectory as the sol.extra field. This is a matrix of size (n_modes, T) where T is the length of the trajectory (length of u and y).\n\nThe returned solution object is of type KalmanFilteringSolution and has the following fields:\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.forward_trajectory-3","page":"API","title":"LowLevelParticleFilters.forward_trajectory","text":"sol = forward_trajectory(kf::AbstractKalmanFilter, u::Vector, y::Vector, p=parameters(kf))\n\nRun a Kalman filter forward to perform (offline / batch) filtering along an entire trajectory u, y.\n\nReturns a KalmanFilteringSolution: with the following\n\nx: predictions x(tt-1)\nxt: filtered estimates x(tt)\nR: predicted covariance matrices R(tt-1)\nRt: filter covariances R(tt)\nll: loglik\n\nsol can be plotted\n\nplot(sol::KalmanFilteringSolution; plotx = true, plotxt=true, plotu=true, ploty=true)\n\nSee KalmanFilteringSolution for more details.\n\nExtended help\n\nVery large systems\n\nIf your system is very large, i.e., the dimension of the state is very large, and the arrays u,y are long, this function may use a lot of memory to store all covariance matrices R, Rt. If you do not need all the information retained by this function, you may opt to call one of the functions\n\nloglik\nLowLevelParticleFilters.sse\nLowLevelParticleFilters.prediction_errors!\n\nThat store significantly less information. The amount of computation performed by all of these functions is identical, the only difference lies in what is stored and returned.\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.interact!-Tuple{IMM}","page":"API","title":"LowLevelParticleFilters.interact!","text":"interact!(imm::IMM)\n\nThe interaction step of the IMM filter updates the state and covariance of each internal model based on the mixing probabilities imm.μ and the transition probability matrix imm.P.\n\nModels with small mixing probabilities will have their states and covariances updated more towards the states and covariances of models with higher mixing probabilities, and vice versa.\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.log_likelihood_fun-Tuple{Any, AbstractVector, Any, Any, Any}","page":"API","title":"LowLevelParticleFilters.log_likelihood_fun","text":"ll(θ) = log_likelihood_fun(filter_from_parameters(θ::Vector)::Function, priors::Vector{Distribution}, u, y, p)\n\nreturns function θ -> p(y|θ)p(θ)\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.loglik","page":"API","title":"LowLevelParticleFilters.loglik","text":"ll = loglik(filter, u, y, p=parameters(filter))\n\nCalculate log-likelihood for entire sequences u,y\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.logsumexp!","page":"API","title":"LowLevelParticleFilters.logsumexp!","text":"ll = logsumexp!(w, we [, maxw])\n\nNormalizes the weight vector w and returns the weighted log-likelihood\n\nhttps://arxiv.org/pdf/1412.8695.pdf eq 3.8 for p(y) https://discourse.julialang.org/t/fast-logsumexp/22827/7?u=baggepinnen for stable logsumexp\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.mean_trajectory-Tuple{Any, Vector, Vector}","page":"API","title":"LowLevelParticleFilters.mean_trajectory","text":"x,ll = mean_trajectory(pf, u::Vector{Vector}, y::Vector{Vector}, p=parameters(pf))\n\nThis method resets the particle filter to the initial state distribution upon start\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.mean_trajectory-Tuple{ParticleFilteringSolution}","page":"API","title":"LowLevelParticleFilters.mean_trajectory","text":"mean_trajectory(sol::ParticleFilteringSolution)\nmean_trajectory(x::AbstractMatrix, we::AbstractMatrix)\n\nCompute the weighted mean along the trajectory of a particle-filter solution. Returns a matrix of size T × nx. If x and we are supplied, the weights are expected to be in the original space (not log space).\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.metropolis","page":"API","title":"LowLevelParticleFilters.metropolis","text":"metropolis(ll::Function(θ), R::Int, θ₀::Vector, draw::Function(θ) = naive_sampler(θ₀))\n\nPerforms MCMC sampling using the marginal Metropolis (-Hastings) algorithm draw = θ -> θ' samples a new parameter vector given an old parameter vector. The distribution must be symmetric, e.g., a Gaussian. R is the number of iterations. See log_likelihood_fun\n\nExample:\n\nfilter_from_parameters(θ) = ParticleFilter(N, dynamics, measurement, MvNormal(n,exp(θ[1])), MvNormal(p,exp(θ[2])), d0)\npriors = [Normal(0,0.1),Normal(0,0.1)]\nll = log_likelihood_fun(filter_from_parameters,priors,u,y,1)\nθ₀ = log.([1.,1.]) # Initial point\ndraw = θ -> θ .+ rand(MvNormal(0.1ones(2))) # Function that proposes new parameters (has to be symmetric)\nburnin = 200 # If using threaded call, provide number of burnin iterations\n# @time theta, lls = metropolis(ll, 2000, θ₀, draw) # Run single threaded\n# thetam = reduce(hcat, theta)'\n@time thetalls = LowLevelParticleFilters.metropolis_threaded(burnin, ll, 5000, θ₀, draw) # run on all threads, will provide (2000-burnin)*nthreads() samples\nhistogram(exp.(thetalls[:,1:2]), layout=3)\nplot!(thetalls[:,3], subplot=3) # if threaded call, log likelihoods are in the last column\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.reset!-Tuple{LowLevelParticleFilters.AbstractKalmanFilter}","page":"API","title":"LowLevelParticleFilters.reset!","text":"reset!(kf::AbstractKalmanFilter; x0)\n\nReset the initial distribution of the state. Optionally, a new mean vector x0 can be provided.\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.reset!-Tuple{LowLevelParticleFilters.AbstractParticleFilter}","page":"API","title":"LowLevelParticleFilters.reset!","text":"Reset the filter to initial state and covariance/distribution\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.reset!-Tuple{SqKalmanFilter}","page":"API","title":"LowLevelParticleFilters.reset!","text":"reset!(kf::SqKalmanFilter; x0)\n\nReset the initial distribution of the state. Optionally, a new mean vector x0 can be provided.\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.simulate","page":"API","title":"LowLevelParticleFilters.simulate","text":"x,u,y = simulate(f::AbstractFilter, T::Int, du::Distribution, p=parameters(f), [N]; dynamics_noise=true, measurement_noise=true)\nx,u,y = simulate(f::AbstractFilter, u, p=parameters(f); dynamics_noise=true, measurement_noise=true)\n\nSimulate dynamical system forward in time T steps, or for the duration of u. Returns state sequence, inputs and measurements.\n\nu is an input-signal trajectory, alternatively, du is a distribution of random inputs.\n\nA simulation can be considered a draw from the prior distribution over the evolution of the system implied by the selected noise models. Such a simulation is useful in order to evaluate whether or not the noise models are reasonable.\n\nIf MonteCarloMeasurements.jl is loaded, the argument N::Int can be supplied, in which case N simulations are done and the result is returned in the form of Vector{MonteCarloMeasurements.Particles}.\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.smooth","page":"API","title":"LowLevelParticleFilters.smooth","text":"xb,ll = smooth(pf, M, u, y, p=parameters(pf))\nxb,ll = smooth(pf, xf, wf, wef, ll, M, u, y, p=parameters(pf))\n\nPerform particle smoothing using forward-filtering, backward simulation. Return smoothed particles and loglikelihood. See also smoothed_trajs, smoothed_mean, smoothed_cov\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.smooth-2","page":"API","title":"LowLevelParticleFilters.smooth","text":"xT,RT,ll = smooth(sol, kf)\nxT,RT,ll = smooth(kf::KalmanFilter, u::Vector, y::Vector, p=parameters(kf))\nxT,RT,ll = smooth(kf::ExtendedKalmanFilter, u::Vector, y::Vector, p=parameters(kf))\n\nReturns smoothed estimates of state x and covariance R given all input output data u,y or an existing solution sol obtained from forward_trajectory.\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.smoothed_cov-Tuple{Any}","page":"API","title":"LowLevelParticleFilters.smoothed_cov","text":"smoothed_cov(xb)\n\nHelper function to calculate the covariance of smoothed particle trajectories\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.smoothed_mean-Tuple{Any}","page":"API","title":"LowLevelParticleFilters.smoothed_mean","text":"smoothed_mean(xb)\n\nHelper function to calculate the mean of smoothed particle trajectories\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.smoothed_trajs-Tuple{Any}","page":"API","title":"LowLevelParticleFilters.smoothed_trajs","text":"smoothed_trajs(xb)\n\nHelper function to get particle trajectories as a 3-dimensions array (N,M,T) instead of matrix of vectors.\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.update!","page":"API","title":"LowLevelParticleFilters.update!","text":"ll, e = update!(f::AbstractFilter, u, y, p = parameters(f), t = index(f))\n\nPerform one step of predict! and correct!, returns log-likelihood and prediction error\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.update!-Tuple{IMM, Any, Any, Vararg{Any}}","page":"API","title":"LowLevelParticleFilters.update!","text":"update!(imm::IMM, u, y, p, t; correct_kwargs = (;), predict_kwargs = (;), interact = true)\n\nThe combined udpate for an IMM filter performs the following steps:\n\nCorrect each model with the measurements y and control input u.\nCombine the models into a single state and covariance.\nInteract the models to update their respective state and covariance.\nPredict each model to the next time step.\n\nThis differs slightly from the udpate step of other filters, where at the end of an update the state of the filter is the one-step ahead predicted value, whereas here each individual filter has a predicted state, but the combine! step of the IMM filter hasn't been performed on the predictions yet. The state of the IMM filter is thus x(tt) and not x(t+1t) like it is for other filters, and each filter internal to the IMM.\n\nArguments:\n\ncorrect_kwargs: An optional named tuple of keyword arguments that are sent to correct!.\npredict_kwargs: An optional named tuple of keyword arguments that are sent to predict!.\ninteract: Whether or not to run the interaction step.\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.weighted_cov-Tuple{Any, Any}","page":"API","title":"LowLevelParticleFilters.weighted_cov","text":"weighted_cov(x,we)\n\nSimilar to weighted_mean, but returns covariances\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.weighted_mean-Tuple{Any, AbstractVector}","page":"API","title":"LowLevelParticleFilters.weighted_mean","text":"x̂ = weighted_mean(x,we)\n\nCalculated weighted mean of particle trajectories. we are expweights.\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.weighted_mean-Tuple{Any}","page":"API","title":"LowLevelParticleFilters.weighted_mean","text":"x̂ = weighted_mean(pf)\nx̂ = weighted_mean(s::PFstate)\n\n\n\n\n\n","category":"method"},{"location":"api/#StatsAPI.predict!","page":"API","title":"StatsAPI.predict!","text":"predict!(f, u, p = parameters(f), t = index(f))\n\nMove filter state forward in time using dynamics equation and input vector u.\n\n\n\n\n\n","category":"function"},{"location":"api/#StatsAPI.predict!-2","page":"API","title":"StatsAPI.predict!","text":"predict!(kf::SqKalmanFilter, u, p = parameters(kf), t::Real = index(kf); R1 = get_mat(kf.R1, kf.x, u, p, t), α = kf.α)\n\nFor the square-root Kalman filter, a custom provided R1 must be the upper triangular Cholesky factor of the covariance matrix of the process noise.\n\n\n\n\n\n","category":"function"},{"location":"api/#StatsAPI.predict!-3","page":"API","title":"StatsAPI.predict!","text":"predict!(kf::AbstractKalmanFilter, u, p = parameters(kf), t::Integer = index(kf); R1, α = kf.α)\n\nPerform the prediction step (updating the state estimate to x(t+1t)). If R1 stored in kf is a function R1(x, u, p, t), this function is evaluated at the state before the prediction is performed. The dynamics noise covariance matrix R1 stored in kf can optionally be overridden by passing the argument R1, in this case R1 must be a matrix.\n\n\n\n\n\n","category":"function"},{"location":"api/#StatsAPI.predict!-Union{Tuple{AUGM}, Tuple{AUGD}, Tuple{IPM}, Tuple{IPD}, Tuple{UnscentedKalmanFilter{IPD, IPM, AUGD, AUGM}, Any}, Tuple{UnscentedKalmanFilter{IPD, IPM, AUGD, AUGM}, Any, Any}, Tuple{UnscentedKalmanFilter{IPD, IPM, AUGD, AUGM}, Any, Any, Real}} where {IPD, IPM, AUGD, AUGM}","page":"API","title":"StatsAPI.predict!","text":"predict!(ukf::UnscentedKalmanFilter, u, p = parameters(ukf), t::Real = index(ukf) * ukf.Ts; R1 = get_mat(ukf.R1, ukf.x, u, p, t), reject, mean, cov, dynamics)\n\nThe prediction step for an UnscentedKalmanFilter allows the user to override, R1 and any of the functions, reject, mean, cov, dynamics`.\n\nArguments:\n\nu: The input\np: The parameters\nt: The current time\nR1: The dynamics noise covariance matrix, or a function that returns the covariance matrix.\nreject: A function that takes a sigma point and returns true if it should be rejected.\nmean: The function that computes the mean of the state sigma points.\ncov: The function that computes the covariance of the state sigma points.\n\n\n\n\n\n","category":"method"},{"location":"api/","page":"API","title":"API","text":"LowLevelParticleFilters.prediction_errors!","category":"page"},{"location":"api/#LowLevelParticleFilters.prediction_errors!","page":"API","title":"LowLevelParticleFilters.prediction_errors!","text":"prediction_errors!(res, f::AbstractFilter, u, y, p = parameters(f), λ = 1)\n\nCalculate the prediction errors and store the result in res. Similar to sse, this function is useful for sum-of-squares optimization. In contrast to sse, this function returns the residuals themselves rather than their sum of squares. This is useful for Gauss-Newton style optimizers, such as LeastSquaresOptim.LevenbergMarquardt.\n\nArguments:\n\nres: A vector of length ny*length(y). Note, for each datapoint in u and u, there are ny outputs, and thus ny residuals.\nf: Any filter\nλ: A weighting factor to minimize dot(e, λ, e). A commonly used metric is λ = Diagonal(1 ./ (mag.^2)), where mag is a vector of the \"typical magnitude\" of each output. Internally, the square root of W = sqrt(λ) is calculated so that the residuals stored in res are W*e.\n\nSee example in Solving using Gauss-Newton optimization.\n\n\n\n\n\n","category":"function"},{"location":"beetle_example/#Smoothing-the-track-of-a-moving-beetle","page":"Particle-filter tutorial","title":"Smoothing the track of a moving beetle","text":"","category":"section"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"This is an example of smoothing the 2-dimensional trajectory of a moving dung beetle. The example spurred off of this Discourse topic. For more information about the research behind this example, see Artificial light disrupts dung beetles’ sense of direction and A dung beetle that path integrates without the use of landmarks. Special thanks to Yakir Gagnon for providing this example.","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"In this example we will describe the position coordinates, x and y, of the beetle as functions of its velocity, v_t, and direction, θ_t:","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"beginaligned\nx_t+1 = x_t + cos(θ_t)v_t \ny_t+1 = y_t + sin(θ_t)v_t \nv_t+1 = v_t + e_t \nθ_t+1 = θ_t + w_t\nendaligned","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"where e_t N(0σ_e) w_t N(0σ_w) The beetle further has two \"modes\", one where it's moving towards a goal, and one where it's searching in a more erratic manner. Figuring out when this mode switch occurs is the goal of the filtering. The mode will be encoded as a state variable, and used to determine the amount of dynamic noise affecting the angle of the beetle, i.e., in the searching mode, the beetle has more angle noise. The mode switching is modeled as a stochastic process with a binomial distribution (coin flip) describing the likelihood of a switch from mode 0 (moving to goal) and mode 1 (searching). Once the beetle has started searching, it stays in that mode, i.e., the searching mode is \"sticky\" or \"terminal\".","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"The image below shows an example video from which the data is obtained (Image: Bettle)","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"We load a single experiment from file for the purpose of this example (in practice, there may be hundreds of experiments)","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"using LowLevelParticleFilters, LinearAlgebra, StaticArrays, Distributions, Plots, Random\nusing DisplayAs # hide\nusing DelimitedFiles\npath = \"../track.csv\"\nxyt = readdlm(path)\ntosvec(y) = reinterpret(SVector{length(y[1]),Float64}, reduce(hcat,y))[:] |> copy # helper function\ny = tosvec(collect(eachrow(xyt[:,1:2])))\nnothing # hide","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"We then define some properties of the dynamics and the filter. We will use an AdvancedParticleFilter since we want to have fine-grained control over the noise sampling for the mode switch.","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"N = 2000 # Number of particles in the particle filter\nn = 4 # Dimension of state: we have position (2D), speed and angle\np = 2 # Dimension of measurements, we can measure the x and the y\n@inline pos(s) = s[SVector(1,2)]\n@inline vel(s) = s[3]\n@inline ϕ(s) = s[4]\n@inline mode(s) = s[5]\nnothing # hide","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"We then define the probability distributions we need.","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"dgσ = 1 # the deviation of the measurement noise distribution\ndvσ = 0.3 # the deviation of the dynamics noise distribution\nϕσ = 0.5\nconst switch_prob = 0.03 # Probability of mode switch\nconst dg = MvNormal(@SVector(zeros(p)), dgσ^2) # Measurement noise Distribution\nconst df = LowLevelParticleFilters.TupleProduct((Normal.(0,[1e-1, 1e-1, dvσ, ϕσ])...,Binomial(1,switch_prob)))\nd0 = MvNormal(SVector(y[1]..., 0.5, atan((y[2]-y[1])...), 0), [3.,3,2,2,0])\nconst noisevec = zeros(5) # cache vector\nnothing # hide","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"We now define the dynamics, since we use the advanced filter, we include the noise=false argument. The dynamics is directly defined in discrete time.","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"@inline function dynamics(s,u,p,t,noise=false)\n # current state\n m = mode(s)\n v = vel(s)\n a = ϕ(s)\n p = pos(s)\n # get noise\n if noise\n y_noise, x_noise, v_noise, ϕ_noise,_ = rand!(df, noisevec)\n else\n y_noise, x_noise, v_noise, ϕ_noise = 0.,0.,0.,0.\n end\n # next state\n v⁺ = max(0.999v + v_noise, 0.0)\n m⁺ = Float64(m == 0 ? rand() < switch_prob : true)\n a⁺ = a + (ϕ_noise*(1 + m*10))/(1 + v⁺) # next state velocity is used here\n p⁺ = p + SVector(y_noise, x_noise) + SVector(sincos(a))*v⁺ # current angle but next velocity\n SVector{5,Float64}(p⁺[1], p⁺[2], v⁺, a⁺, m⁺) # all next state\nend\nfunction measurement_likelihood(s,u,y,p,t)\n logpdf(dg, pos(s)-y) # A simple linear measurement model with normal additive noise\nend\n@inline measurement(s,u,p,t,noise=false) = s[SVector(1,2)] + noise*rand(dg) # We observe the position coordinates with the measurement\nnothing # hide","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"In this example, we have no control inputs, we thus define a vector of only zeros. We then solve the forward filtering problem and plot the results.","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"u = zeros(length(y))\npf = AuxiliaryParticleFilter(AdvancedParticleFilter(N, dynamics, measurement, measurement_likelihood, df, d0))\nT = length(y)\nsol = forward_trajectory(pf,u[1:T],y[1:T])\n(; x,w,we,ll) = sol\nplot(sol, markerstrokecolor=:auto, m=(2,0.5))\nDisplayAs.PNG(Plots.current()) # hide","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"We can clearly see when the beetle switched mode (state variable 5). This corresponds well to annotations provided by a biologist and is the fundamental question we want to answer with the filtering procedure.","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"We can plot the mean of the filtered trajectory as well","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"xh = mean_trajectory(x,we)\n\n\"plotting helper function\"\nfunction to1series(x::AbstractVector, y)\n r,c = size(y)\n y2 = vec([y; fill(Inf, 1, c)])\n x2 = repeat([x; Inf], c)\n x2,y2\nend\nto1series(y) = to1series(1:size(y,1),y)\n\nfig1 = plot(xh[:,1],xh[:,2], c=:blue, lab=\"estimate\", legend=:bottomleft)\nplot!(xyt[:,1],xyt[:,2], c=:red, lab=\"measurement\")","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"as well as the angle state variable (we subsample the particles to not get sluggish plots)","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"fig2 = scatter(to1series(ϕ.(x)'[:,1:2:end])..., m=(:black, 0.03, 2), lab=\"\", size=(500,300))\nplot!(identity.(xh[:,4]), lab=\"Filtered angle\", legend=:topleft, ylims=(-30, 70))\nDisplayAs.PNG(fig2) # hide","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"The particle plot above indicate that the posterior is multimodal. This phenomenon arises due to the simple model that uses an angle that is allowed to leave the interval 0-2π rad. In this example, we are not interested in the angle, but rather when the beetle switches mode. The filtering distribution above gives a hint at when this happens, but we will not plot the mode trajectory until we have explored smoothing as well.","category":"page"},{"location":"beetle_example/#Smoothing","page":"Particle-filter tutorial","title":"Smoothing","text":"","category":"section"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"The filtering results above does not use all the available information when trying to figure out the state trajectory. To do this, we may call a smoother. We use a particle smoother and compute 10 smoothing trajectories.","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"M = 10 # Number of smoothing trajectories, NOTE: if this is set higher, the result will be better at the expense of linear scaling of the computational cost.\nsb,ll = smooth(pf, M, u, y) # Sample smoothing particles (b for backward-trajectory)\nsbm = smoothed_mean(sb) # Calculate the mean of smoothing trajectories\nsbt = smoothed_trajs(sb) # Get smoothing trajectories\nplot!(fig1, sbm[1,:],sbm[2,:], lab=\"xs\")","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"plot!(fig2, identity.(sbm'[:,4]), lab=\"smoothed\")\nDisplayAs.PNG(fig2) # hide","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"We see that the smoothed trajectory may look very different from the filter trajectory. This is an indication that it's hard to tell what state the beetle is currently in, but easier to look back and tell what state the beetle must have been in at a historical point. ","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"We can also visualize the mode state","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"plot(xh[:,5], lab=\"Filtering\")\nplot!(to1series(sbt[5,:,:]')..., lab=\"Smoothing\", title=\"Mode trajectories\", l=(:black,0.2))","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"also this state variable indicates that it's hard to tell what state the beetle is in during filtering, but obvious with hindsight (smoothing). The mode switch occurs when the filtering distribution of the angle becomes drastically wider, indicating that increased dynamics noise is required in order to describe the motion of the beetle.","category":"page"},{"location":"beetle_example/#Summary","page":"Particle-filter tutorial","title":"Summary","text":"","category":"section"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"This example has demonstrated filtering and smoothing in an advanced application that includes manual control over noise, mixed continuous and discrete state.","category":"page"},{"location":"beetle_example_imm/#Filtering-the-track-of-a-moving-beetle-using-IMM","page":"IMM-filter tutorial","title":"Filtering the track of a moving beetle using IMM","text":"","category":"section"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"This tutorial is very similar to Smoothing the track of a moving beetle, but uses an Interacting Multiple Models (IMM) filter to model the mode switching of the beetle. The IMM filter is a mixture model, in this case with internal Unscented Kalman filters, where each Kalman filter represents a different mode of the system. The IMM filter is able to switch between these modes based on the likelihood of the mode given the data.","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"This is an example of smoothing the 2-dimensional trajectory of a moving dung beetle. The example spurred off of this Discourse topic. For more information about the research behind this example, see Artificial light disrupts dung beetles’ sense of direction and A dung beetle that path integrates without the use of landmarks. Special thanks to Yakir Gagnon for providing this example.","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"In this example we will describe the position coordinates, x and y, of the beetle as functions of its velocity, v_t, and direction, θ_t:","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"beginaligned\nx_t+1 = x_t + cos(θ_t)v_t \ny_t+1 = y_t + sin(θ_t)v_t \nv_t+1 = v_t + e_t \nθ_t+1 = θ_t + w_t\nendaligned","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"where e_t N(0σ_e) w_t N(0σ_w) The beetle further has two \"modes\", one where it's moving towards a goal, and one where it's searching in a more erratic manner. Figuring out when this mode switch occurs is the goal of the filtering. The mode will be encoded as two different models, where the difference between the models lies in the amount of dynamic noise affecting the angle of the beetle, i.e., in the searching mode, the beetle has more angle noise. The mode switching is modeled as a stochastic process with a binomial distribution (coin flip) describing the likelihood of a switch from mode 0 (moving to goal) and mode 1 (searching). Once the beetle has started searching, it stays in that mode, i.e., the searching mode is \"sticky\" or \"terminal\".","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"The image below shows an example video from which the data is obtained (Image: Bettle)","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"We load a single experiment from file for the purpose of this example (in practice, there may be hundreds of experiments)","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"using LowLevelParticleFilters, LinearAlgebra, StaticArrays, Distributions, Plots, Random\nusing DisplayAs # hide\nusing DelimitedFiles\ncd(@__DIR__)\npath = \"../track.csv\"\nxyt = readdlm(path)\ntosvec(y) = reinterpret(SVector{length(y[1]),Float64}, reduce(hcat,y))[:] |> copy # helper function\ny = tosvec(collect(eachrow(xyt[:,1:2])))\nnothing # hide","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"We then define some properties of the dynamics and the filter. We will use an AdvancedParticleFilter since we want to have fine-grained control over the noise sampling for the mode switch.","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"nx = 4 # Dimension of state: we have position (2d), speed and angle\nny = 2 # Dimension of measurements, we can measure the x and the y\n@inline pos(s) = s[SVector(1,2)]\n@inline vel(s) = s[3]\n@inline ϕ(s) = s[4]\nnothing # hide","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"We then define the probability distributions we need. The IMM filter takes a transition-probability matrix, P, and an initial mixing probability, μ. P is a Markov (stochastic) matrix, where each row sums to one, and P[i, j] is the probability of switching from mode i to mode j. μ is a vector of probabilities, where μ[i] is the probability of starting in mode i. We also define the noise distributions for the dynamics and the measurements. The dynamics noise is modeled as a Gaussian distribution with a standard deviation of dvσ for the velocity and ϕσ for the angle. The measurement noise is modeled as a Gaussian distribution with a standard deviation of dgσ. The initial state is modeled as a Gaussian distribution with a mean at the first measurement and a standard deviation of d0.","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"dgσ = 1.0 # the deviation of the measurement noise distribution\ndvσ = 0.3 # the deviation of the dynamics noise distribution\nϕσ = 0.5\nP = [0.995 0.005; 0.0 1] # Transition probability matrix, we model the search mode as \"almost terminal\"\nμ = [1.0, 0.0] # Initial mixing probabilities\nR1 = Diagonal([1e-1, 1e-1, dvσ, ϕσ].^2)\nR2 = dgσ^2*I(ny) # Measurement noise covariance matrix\nd0 = MvNormal(SVector(y[1]..., 0.5, atan((y[2]-y[1])...)), [3.,3,2,2])\nnothing # hide","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"We now define the dynamics, which is directly defined in discrete time. The third argument is a parameter we call modegain, which is used to scale the amount of noise in the angle of the beetle depending on the mode in which it is in. The last argument is a boolean that tells the dynamics function which mode it is in, we will close over this argument when defining the dynamics for the individual Kalman filters that are part of the IMM, one will use m = false and one will use m = true.","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"@inline function dynamics(s,_,modegain,t,w,m)\n # current state\n v = vel(s)\n a = ϕ(s)\n p = pos(s)\n\n y_noise, x_noise, v_noise, ϕ_noise = w\n\n # next state\n v⁺ = max(0.999v + v_noise, 0.0)\n a⁺ = a + (ϕ_noise*(1 + m*modegain))/(1 + v⁺) # next state velocity is used here\n p⁺ = p + SVector(y_noise, x_noise) + SVector(sincos(a))*v⁺ # current angle but next velocity\n SVector(p⁺[1], p⁺[2], v⁺, a⁺) # all next state\nend\n@inline measurement(s,u,p,t) = s[SVector(1,2)] # We observe the position coordinates with the measurement\nnothing # hide","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"In this example, we have no control inputs, we thus define a vector of only zeros. We then solve the forward filtering problem and plot the results.","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"u = zeros(length(y)) # no control inputs\nkffalse = UnscentedKalmanFilter{false,false,true,false}((x,u,p,t,w)->dynamics(x,u,p,t,w,false), measurement, R1, R2, d0; ny, nu=0, p=10)\nkftrue = UnscentedKalmanFilter{false,false,true,false}((x,u,p,t,w)->dynamics(x,u,p,t,w,true), measurement, R1, R2, d0; ny, nu=0, p=10)\n\nimm = IMM([kffalse, kftrue], P, μ; p = 10)\n\nT = length(y)\nsol = forward_trajectory(imm, u, y, interact=true)\nfigx = plot(sol, plotu=false, plotRt=true)\nfigmode = plot(sol.extra', title=\"Mode\")\nplot(figx, figmode)\nDisplayAs.PNG(Plots.current()) # hide","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"If you have followed the particle filter tutorial Smoothing the track of a moving beetle, you will notice that the result here is much worse. We used noise parameters similar to in the particle-gilter example, but those were tuned fo the particle filter. Below, we will attempt to optimize the performance of the IMM filter.","category":"page"},{"location":"beetle_example_imm/#Tuning-by-optimization","page":"IMM-filter tutorial","title":"Tuning by optimization","text":"","category":"section"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"We will attempt to optimize the dynamics and measurement noise covariance matrices and the modegain parameter. We code this up in two functions, one that takes the parameter vector and returns an IMM filter, and one that calculates the loss given the filter. We will optimize the log-likelihood of the data given the filter.","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"params = [log10.(diag(R1)); log10(1); log10(10)]\n\nfunction get_opt_kf(p)\n T = eltype(p)\n R1i = Diagonal(SVector{4}(exp10.(p[1:4])))\n R2i = SMatrix{2,2}(exp10(p[5])*R2)\n d0i = MvNormal(SVector{4, T}(T.(d0.μ)), SMatrix{4,4}(T.(d0.Σ)))\n modegain = 5+exp10(p[6])\n Pi = SMatrix{2,2, Float64,4}(P)\n # sigmoid(x) = 1/(1+exp(-x))\n # switch_prob = sigmoid(p[7])\n # Pi = [1-switch_prob switch_prob; 0 1]\n kffalse = UnscentedKalmanFilter{false,false,true,false}((x,u,p,t,w)->dynamics(x,u,p,t,w,false), measurement, R1i, R2i, d0i; ny, nu=0)\n kftrue = UnscentedKalmanFilter{false,false,true,false}((x,u,p,t,w)->dynamics(x,u,p,t,w,true), measurement, R1i, R2i, d0i; ny, nu=0)\n\n IMM([kffalse, kftrue], Pi, T.(μ), p=modegain)\nend\nfunction cost(pars)\n try\n imm = get_opt_kf(pars)\n ll = loglik(imm, u, y, interact=true) - 1/2*logdet(imm.models[1].R1)\n return -ll\n catch e\n # rethrow() # If you only get Inf, you can uncomment this line to see the error message\n return eltype(pars)(Inf)\n\tend\nend\n\nusing Optim\nRandom.seed!(0)\nres = Optim.optimize(\n cost,\n params,\n ParticleSwarm(),\n Optim.Options(\n show_trace = true,\n show_every = 5,\n iterations = 200,\n ),\n)\n\nimm = get_opt_kf(res.minimizer)\nimm = get_opt_kf([-0.23848249020342335, 0.09510413594186848, -2.7540206342539832, -0.026720713351238334, -5.596193009305194, -25.37645648820617]) #make sure it goes well # hide\n\nsol = forward_trajectory(imm, u, y)\nplot(sol.extra', title=\"Mode (optimized filter)\")","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"If it went well, the filter should be in mode 1 (the false mode) from the start until just before 200 time steps, at which point it should switch to model 2 (true). This method of detecting the mode switch of the beetle appears to be somewhat less robust than the particle filter, but is significantly cheaper computationally. ","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"The IMM filter does not stick in mode 2 perpetually after having reached it since it never actually becomes fully confident that mode 2 has been reached, but detecting the first switch is sufficient to know that the switch has occurred. ","category":"page"},{"location":"noisetuning/#How-to-tune-a-Kalman-filter","page":"Noise tuning and disturbance modeling for Kalman filtering","title":"How to tune a Kalman filter","text":"","category":"section"},{"location":"noisetuning/","page":"Noise tuning and disturbance modeling for Kalman filtering","title":"Noise tuning and disturbance modeling for Kalman filtering","text":"This tutorial is hosted as a notebook.","category":"page"},{"location":"fault_detection/#Fault-detection","page":"Fault detection","title":"Fault detection","text":"","category":"section"},{"location":"fault_detection/","page":"Fault detection","title":"Fault detection","text":"This is a video tutorial, available below:","category":"page"},{"location":"fault_detection/","page":"Fault detection","title":"Fault detection","text":"","category":"page"},{"location":"fault_detection/","page":"Fault detection","title":"Fault detection","text":"The notebook used in the tutorial is available here:","category":"page"},{"location":"fault_detection/","page":"Fault detection","title":"Fault detection","text":"identification_12_fault_detection.jl on JuliaHub\nidentification_12_fault_detection.jl on GitHub","category":"page"},{"location":"discretization/#Discretization","page":"Discretization","title":"Discretization","text":"","category":"section"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"This package operates exclusively on discrete-time dynamics, and dynamics describing, e.g., ODE systems must thus be discretized. This page describes the details around discretization for nonlinear and linear systems, as well as how to discretize continuous-time noise processes. ","category":"page"},{"location":"discretization/#Nonlinear-ODEs","page":"Discretization","title":"Nonlinear ODEs","text":"","category":"section"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"Continuous-time dynamics functions on the form (x,u,p,t) -> ẋ can be discretized (integrated) using the function SeeToDee.Rk4, e.g.,","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"using SeeToDee\ndiscrete_dynamics = SeeToDee.Rk4(continuous_dynamics, sampletime; supersample=1)","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"where the integer supersample determines the number of RK4 steps that is taken internally for each change of the control signal (1 is often sufficient and is the default). The returned function discrete_dynamics is on the form (x,u,p,t) -> x⁺.","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"note: Note\nWhen solving state-estimation problems, accurate integration is often less important than during simulation. The motivations for this are severalThe dynamics model is often inaccurate, and solving an inaccurate model to high accuracy can be a waste of effort.\nThe performance is often dictated by the disturbances acting on the system.\nState-estimation enjoys feedback from measurements that corrects for slight errors due to integration.","category":"page"},{"location":"discretization/#Linear-systems","page":"Discretization","title":"Linear systems","text":"","category":"section"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"A linear system on the form ","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"beginaligned\ndotx(t) = Ax(t) + Bu(t)\ny(t) = Cx(t) + Du(t)\nendaligned","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"can be discretized using ControlSystems.c2d, which defaults to a zero-order hold discretization. See the example below for more info.","category":"page"},{"location":"discretization/#Covariance-matrices","page":"Discretization","title":"Covariance matrices","text":"","category":"section"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"Covariance matrices for continuous-time noise processes can also be discretized using ControlSystems.c2d","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"using ControlSystemIdentification\nR1d = c2d(sys::StateSpace{<:Discrete}, R1::Matrix)\nR1d, R2d = c2d(sys::StateSpace{<:Discrete}, R1::Matrix, R2::Matrix)","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"This samples a continuous-time covariance matrix to fit the provided discrete-time system sys.","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"The method used comes from theorem 5 in the reference below.","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"Ref: \"Discrete-time Solutions to the Continuous-time Differential Lyapunov Equation With Applications to Kalman Filtering\", Patrik Axelsson and Fredrik Gustafsson","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"On singular covariance matrices: The traditional double integrator with covariance matrix Q = diagm([0,σ²]) can not be sampled with this method. Instead, the input matrix (\"Cholesky factor\") of Q must be manually kept track of, e.g., the noise of variance σ² enters like N = [0, 1] which is sampled using ZoH and becomes Nd = [Ts^2 / 2; Ts] which results in the covariance matrix σ² * Nd * Nd' (see example below).","category":"page"},{"location":"discretization/#Example","page":"Discretization","title":"Example","text":"","category":"section"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"The following example will discretize a linear double integrator system. Double integrators arise when the position of an object is controlled by a force, i.e., when Newtons second law f = ma governs the dynamics. The system can be written on the form","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"beginaligned\ndot x(t) = Ax(t) + Bu(t) + Nw(t)\ny(t) = Cx(t) + e(t)\nendaligned","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"where N = B are both equal to [0, 1], indicating that the noise w(t) enters like a force (this could be for instance due to air resistance or friction).","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"We start by defining the system that takes u as an input and discretize that with a sample time of T_s = 01.","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"using ControlSystemsBase\nA = [0 1; 0 0]\nB = [0; 1;;]\nC = [1 0]\nD = 0\nTs = 0.1 # Sample time\n\nsys = ss(A,B,C,D)\nsysd = c2d(sys, Ts) # Discretize the dynamics","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"We then form another system, this time with w(t) as the input, and thus N as the input matrix instead of B. We assume that the noise has a standard deviation of sigma_1 = 05","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"σ1 = 0.5\nN = σ1*[0; 1;;]\nsys_w = ss(A,N,C,D)\nsys_wd = c2d(sys_w, Ts) # Discretize the noise system\nNd = sys_wd.B # The discretized noise input matrix\nR1d = Nd*Nd' # The final discrete-time covariance matrix","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"We can verify that the matrix we computed corresponds to the theoretical covariance matrix for a discrete-time double integrator:","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"R1d ≈ σ1^2*[Ts^2 / 2; Ts]*[Ts^2 / 2; Ts]'","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"For a nonlinear system, we could adopt a similar strategy by first linearizing the system around a suitable operating point. Alternatively, we could make use of the fact that some of the state estimators in this package allows the covariance matrices to be functions of the state, and thus compute a new discretized covariance matrix using a linearization around the current state.","category":"page"},{"location":"discretization/#Non-uniform-sample-rates","page":"Discretization","title":"Non-uniform sample rates","text":"","category":"section"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"Special care is needed if the sample rate is not constant, i.e., the time interval between measurements varies. ","category":"page"},{"location":"discretization/#Dropped-samples","page":"Discretization","title":"Dropped samples","text":"","category":"section"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"A common case is that the sample rate is constant, but some measurements are lost. This case is very easy to handle; the filter loop iterates between two steps","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"Prediction using predict!(filter, x, u, p, t)\nCorrection using\ncorrect!(f, u, y, p, t) if using the standard measurement model of the filter\ncorrect!(f, mm, u, y, p, t, mm) to use a custom measurement model mm","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"If a measurement y is lacking, one simply skips the corresponding call to correct! where y is missing. Repeated calls to predict! corresponds to simulating the system without any feedback from measurements, like if an ODE was solved. Internally, the filter will keep track of the covariance of the estimate, which is likely to grow if no measurements are used to inform the filter about the state of the system.","category":"page"},{"location":"discretization/#Sensors-with-different-sample-rates","page":"Discretization","title":"Sensors with different sample rates","text":"","category":"section"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"For Kalman-type filters, it is possible to construct custom measurement models, and pass an instance of a measurement model as the second argument to correct!. This allows for sensor fusion with sensors operating at different rates, or when parts of the measurement model are linear, and other parts are nonlinear. See examples in Measurement models for how to construct explicit measurement models.","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"A video demonstrating the use of multiple measurement models running at different rates is available on YouTube:","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"","category":"page"},{"location":"discretization/#Stochastic-sample-rate","page":"Discretization","title":"Stochastic sample rate","text":"","category":"section"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"In some situations, such as in event-based systems, the sample rate is truly stochastic. There is no single correct way of handling this, and we instead outline some alternative approaches.","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"If the filtering is performed offline on a batch of data, time-varying dynamics can be used, for instance by supplying matrices to a KalmanFilter on the form A[:, :, t]. Each A is then computed as the discretization with the sample time given as the time between measurement t and measurement t+1.\nA conceptually simple approach is to choose a very small sample interval T_s which is smaller than the smallest occuring sample interval in the data, and approximate each sample interval by rounding it to the nearest integer multiple of T_s. This transforms the problem to an instance of the \"dropped samples\" problem described above.\nMake use of an adaptive integrator instead of the fixed-step rk4 supplied in this package, and manually keep track of the step length that needs to be taken.","category":"page"},{"location":"benchmark/#Benchmark-test","page":"Benchmark","title":"Benchmark test","text":"","category":"section"},{"location":"benchmark/","page":"Benchmark","title":"Benchmark","text":"To see how the performance varies with the number of particles, we simulate several times. The following code simulates the system and performs filtering using the simulated measurements. We do this for varying number of time steps and varying number of particles.","category":"page"},{"location":"benchmark/","page":"Benchmark","title":"Benchmark","text":"note: Note\nTo run this code, see the bottom of src/example_lineargaussian.jl.","category":"page"},{"location":"benchmark/","page":"Benchmark","title":"Benchmark","text":"function run_test()\n particle_count = [10, 20, 50, 100, 200, 500, 1000]\n time_steps = [20, 100, 200]\n RMSE = zeros(length(particle_count),length(time_steps)) # Store the RMS errors\n propagated_particles = 0\n t = @elapsed for (Ti,T) = enumerate(time_steps)\n for (Ni,N) = enumerate(particle_count)\n montecarlo_runs = 2*maximum(particle_count)*maximum(time_steps) ÷ T ÷ N\n E = sum(1:montecarlo_runs) do mc_run\n pf = ParticleFilter(N, dynamics, measurement, df, dg, d0) # Create filter\n u = @SVector randn(2)\n x = SVector{2,Float64}(rand(rng, d0))\n y = SVector{2,Float64}(sample_measurement(pf,x,u,0,1))\n error = 0.0\n @inbounds for t = 1:T-1\n pf(u, y) # Update the particle filter\n x = dynamics(x,u,t) + SVector{2,Float64}(rand(rng, df)) # Simulate the true dynamics and add some noise\n y = SVector{2,Float64}(sample_measurement(pf,x,u,0,t)) # Simulate a measuerment\n u = @SVector randn(2) # draw a random control input\n error += sum(abs2,x-weighted_mean(pf))\n end # t\n √(error/T)\n end # MC\n RMSE[Ni,Ti] = E/montecarlo_runs\n propagated_particles += montecarlo_runs*N*T\n @show N\n end # N\n @show T\n end # T\n println(\"Propagated $propagated_particles particles in $t seconds for an average of $(propagated_particles/t/1000) particles per millisecond\")\n return RMSE\nend\n\n@time RMSE = run_test()","category":"page"},{"location":"benchmark/","page":"Benchmark","title":"Benchmark","text":"Propagated 8400000 particles in 1.140468043 seconds for an average of 7365.397085484139 particles per millisecond","category":"page"},{"location":"benchmark/","page":"Benchmark","title":"Benchmark","text":"We then plot the results","category":"page"},{"location":"benchmark/","page":"Benchmark","title":"Benchmark","text":"time_steps = [20, 100, 200]\nparticle_count = [10, 20, 50, 100, 200, 500, 1000]\nnT = length(time_steps)\nleg = reshape([\"$(time_steps[i]) time steps\" for i = 1:nT], 1,:)\nplot(particle_count,RMSE,xscale=:log10, ylabel=\"RMS errors\", xlabel=\" Number of particles\", lab=leg)","category":"page"},{"location":"benchmark/","page":"Benchmark","title":"Benchmark","text":"(Image: window)","category":"page"},{"location":"#LowLevelParticleFilters","page":"Home","title":"LowLevelParticleFilters","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"(Image: CI) (Image: codecov)","category":"page"},{"location":"","page":"Home","title":"Home","text":"This is a library for state estimation, that is, given measurements y(t) from a dynamical system, estimate the state vector x(t). Throughout, we assume dynamics on the form","category":"page"},{"location":"","page":"Home","title":"Home","text":"beginaligned\nx(t+1) = f(x(t) u(t) p t w(t))\ny(t) = g(x(t) u(t) p t e(t))\nendaligned","category":"page"},{"location":"","page":"Home","title":"Home","text":"or the linear version","category":"page"},{"location":"","page":"Home","title":"Home","text":"beginaligned\nx(t+1) = Ax(t) + Bu(t) + w(t)\ny(t) = Cx(t) + Du(t) + e(t)\nendaligned","category":"page"},{"location":"","page":"Home","title":"Home","text":"where x is the state vector, u an input, p some form of parameters, t is the time and we are disturbances (noise). Throughout the documentation, we often call the function f dynamics and the function g measurement.","category":"page"},{"location":"","page":"Home","title":"Home","text":"The dynamics above describe a discrete-time system, i.e., the function f takes the current state and produces the next state. This is in contrast to a continuous-time system, where f takes the current state but produces the time derivative of the state. A continuous-time system can be discretized, described in detail in Discretization.","category":"page"},{"location":"","page":"Home","title":"Home","text":"The parameters p can be anything, or left out. You may write the dynamics functions such that they depend on p and include parameters when you create a filter object. You may also override the parameters stored in the filter object when you call any function on the filter object. This behavior is modeled after the SciML ecosystem.","category":"page"},{"location":"","page":"Home","title":"Home","text":"Depending on the nature of f and g, the best method of estimating the state may vary. If fg are linear and the disturbances are additive and Gaussian, the KalmanFilter is an optimal state estimator. If any of the above assumptions fail to hold, we may need to resort to more advanced estimators. This package provides several filter types, outlined below.","category":"page"},{"location":"#Estimator-types","page":"Home","title":"Estimator types","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"We provide a number of filter types","category":"page"},{"location":"","page":"Home","title":"Home","text":"KalmanFilter. A standard Kalman filter. Is restricted to linear dynamics (possibly time varying) and Gaussian noise.\nSqKalmanFilter. A standard Kalman filter on square-root form (slightly slower but more numerically stable with ill-conditioned covariance).\nExtendedKalmanFilter: For nonlinear systems, the EKF runs a regular Kalman filter on linearized dynamics. Uses ForwardDiff.jl for linearization (or user provided). The noise model must still be Gaussian and additive.\nUnscentedKalmanFilter: The Unscented Kalman filter often performs slightly better than the Extended Kalman filter but may be slightly more computationally expensive. The UKF handles nonlinear dynamics and measurement models, but still requires a Gaussian noise model (may be non additive) and still assumes that all posterior distributions are Gaussian, i.e., can not handle multi-modal posteriors.\nParticleFilter: The particle filter is a nonlinear estimator. This version of the particle filter is simple to use and assumes that both dynamics noise and measurement noise are additive. Particle filters handle multi-modal posteriors.\nAdvancedParticleFilter: This filter gives you more flexibility, at the expense of having to define a few more functions. This filter does not require the noise to be additive and is thus the most flexible filter type.\nAuxiliaryParticleFilter: This filter is identical to ParticleFilter, but uses a slightly different proposal mechanism for new particles.\nIMM: (Currently considered experimental) The Interacting Multiple Models filter switches between multiple internal filters based on a hidden Markov model. This filter is useful when the system dynamics change over time and the change can be modeled as a discrete Markov chain, i.e., the system may switch between a small number of discrete \"modes\".","category":"page"},{"location":"#Functionality","page":"Home","title":"Functionality","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"This package provides ","category":"page"},{"location":"","page":"Home","title":"Home","text":"Filtering, estimating x(t) given measurements up to and including time t. We call the filtered estimate x(tt) (read as x at t given t).\nSmoothing, estimating x(t) given data up to T t, i.e., x(tT).\nParameter estimation.","category":"page"},{"location":"","page":"Home","title":"Home","text":"All filters work in two distinct steps.","category":"page"},{"location":"","page":"Home","title":"Home","text":"The prediction step (predict!). During prediction, we use the dynamics model to form x(tt-1) = f(x(t-1) )\nThe correction step (correct!). In this step, we adjust the predicted state x(tt-1) using the measurement y(t) to form x(tt).","category":"page"},{"location":"","page":"Home","title":"Home","text":"(The IMM filter is an exception to the above and has two additional steps, combine! and interact!)","category":"page"},{"location":"","page":"Home","title":"Home","text":"In general, all filters represent not only a point estimate of x(t), but a representation of the complete posterior probability distribution over x given all the data available up to time t. One major difference between different filter types is how they represent these probability distributions.","category":"page"},{"location":"#Particle-filter","page":"Home","title":"Particle filter","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"A particle filter represents the probability distribution over the state as a collection of samples, each sample is propagated through the dynamics function f individually. When a measurement becomes available, the samples, called particles, are given a weight based on how likely the particle is given the measurement. Each particle can thus be seen as representing a hypothesis about the current state of the system. After a few time steps, most weights are inevitably going to be extremely small, a manifestation of the curse of dimensionality, and a resampling step is incorporated to refresh the particle distribution and focus the particles on areas of the state space with high posterior probability.","category":"page"},{"location":"","page":"Home","title":"Home","text":"Defining a particle filter (ParticleFilter) is straightforward, one must define the distribution of the noise df in the dynamics function, dynamics(x,u,p,t) and the noise distribution dg in the measurement function measurement(x,u,p,t). Both of these noise sources are assumed to be additive, but can have any distribution (see AdvancedParticleFilter for non-additive noise). The distribution of the initial state estimate d0 must also be provided. In the example below, we use linear Gaussian dynamics so that we can easily compare both particle and Kalman filters. (If we have something close to linear Gaussian dynamics in practice, we should of course use a Kalman filter and not a particle filter.)","category":"page"},{"location":"","page":"Home","title":"Home","text":"using LowLevelParticleFilters, LinearAlgebra, StaticArrays, Distributions, Plots\nusing DisplayAs # hide","category":"page"},{"location":"","page":"Home","title":"Home","text":"Define problem","category":"page"},{"location":"","page":"Home","title":"Home","text":"nx = 2 # Dimension of state\nnu = 1 # Dimension of input\nny = 1 # Dimension of measurements\nN = 500 # Number of particles\n\nconst dg = MvNormal(ny,0.2) # Measurement noise Distribution\nconst df = MvNormal(nx,0.1) # Dynamics noise Distribution\nconst d0 = MvNormal(randn(nx),2.0) # Initial state Distribution\nnothing # hide","category":"page"},{"location":"","page":"Home","title":"Home","text":"Define linear state-space system (using StaticArrays for maximum performance)","category":"page"},{"location":"","page":"Home","title":"Home","text":"const A = SA[0.97043 -0.097368\n 0.09736 0.970437]\nconst B = SA[0.1; 0;;]\nconst C = SA[0 1.0]\nnothing # hide","category":"page"},{"location":"","page":"Home","title":"Home","text":"Next, we define the dynamics and measurement equations, they both take the signature (x,u,p,t) = (state, input, parameters, time) ","category":"page"},{"location":"","page":"Home","title":"Home","text":"dynamics(x,u,p,t) = A*x .+ B*u\nmeasurement(x,u,p,t) = C*x\nvecvec_to_mat(x) = copy(reduce(hcat, x)') # Helper function\nnothing # hide","category":"page"},{"location":"","page":"Home","title":"Home","text":"the parameter p can be anything, and is often optional. If p is not provided when performing operations on filters, any p stored in the filter objects (if supported) is used. The default if none is provided and none is stored in the filter is p = LowLevelParticleFilters.NullParameters().","category":"page"},{"location":"","page":"Home","title":"Home","text":"We are now ready to define and use a filter","category":"page"},{"location":"","page":"Home","title":"Home","text":"pf = ParticleFilter(N, dynamics, measurement, df, dg, d0)","category":"page"},{"location":"","page":"Home","title":"Home","text":"With the filter in hand, we can simulate from its dynamics and query some properties","category":"page"},{"location":"","page":"Home","title":"Home","text":"du = MvNormal(nu,1.0) # Random input distribution for simulation\nxs,u,y = simulate(pf,200,du) # We can simulate the model that the pf represents\npf(u[1], y[1]) # Perform one filtering step using input u and measurement y\nparticles(pf) # Query the filter for particles, try weights(pf) or expweights(pf) as well\nx̂ = weighted_mean(pf) # using the current state","category":"page"},{"location":"","page":"Home","title":"Home","text":"If you want to perform batch filtering using an existing trajectory consisting of vectors of inputs and measurements, try any of the functions forward_trajectory, mean_trajectory:","category":"page"},{"location":"","page":"Home","title":"Home","text":"sol = forward_trajectory(pf, u, y) # Filter whole trajectories at once\nx̂,ll = mean_trajectory(pf, u, y)\nplot(sol, xreal=xs, markersize=2)\nDisplayAs.PNG(Plots.current()) # hide","category":"page"},{"location":"","page":"Home","title":"Home","text":"u ad y are then assumed to be vectors of vectors. StaticArrays is recommended for maximum performance.","category":"page"},{"location":"","page":"Home","title":"Home","text":"If MonteCarloMeasurements.jl is loaded, you may transform the output particles to Matrix{MonteCarloMeasurements.Particles} with the layout T × n_state using Particles(x,we). Internally, the particles are then resampled such that they all have unit weight. This is conventient for making use of the plotting facilities of MonteCarloMeasurements.jl.","category":"page"},{"location":"","page":"Home","title":"Home","text":"For a full usage example, see the benchmark section below or example_lineargaussian.jl","category":"page"},{"location":"#Resampling","page":"Home","title":"Resampling","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"The particle filter will perform a resampling step whenever the distribution of the weights has become degenerate. The resampling is triggered when the effective number of samples is smaller than pf.resample_threshold in 0 1, this value can be set when constructing the filter. How the resampling is done is governed by pf.resampling_strategy, we currently provide ResampleSystematic <: ResamplingStrategy as the only implemented strategy. See https://en.wikipedia.org/wiki/Particle_filter for more info.","category":"page"},{"location":"#Particle-Smoothing","page":"Home","title":"Particle Smoothing","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"Smoothing is the process of finding the best state estimate given both past and future data. Smoothing is thus only possible in an offline setting. This package provides a particle smoother, based on forward filtering, backward simulation (FFBS), example usage follows:","category":"page"},{"location":"","page":"Home","title":"Home","text":"N = 2000 # Number of particles\nT = 80 # Number of time steps\nM = 100 # Number of smoothed backwards trajectories\npf = ParticleFilter(N, dynamics, measurement, df, dg, d0)\ndu = MvNormal(nu,1) # Control input distribution\nx,u,y = simulate(pf,T,du) # Simulate trajectory using the model in the filter\ntosvec(y) = reinterpret(SVector{length(y[1]),Float64}, reduce(hcat,y))[:] |> copy\nx,u,y = tosvec.((x,u,y)) # It's good for performance to use StaticArrays to the extent possible\n\nxb,ll = smooth(pf, M, u, y) # Sample smoothing particles\nxbm = smoothed_mean(xb) # Calculate the mean of smoothing trajectories\nxbc = smoothed_cov(xb) # And covariance\nxbt = smoothed_trajs(xb) # Get smoothing trajectories\nxbs = [diag(xbc) for xbc in xbc] |> vecvec_to_mat .|> sqrt\nplot(xbm', ribbon=2xbs, lab=\"PF smooth\")\nplot!(vecvec_to_mat(x), l=:dash, lab=\"True\")","category":"page"},{"location":"","page":"Home","title":"Home","text":"We can plot the particles themselves as well","category":"page"},{"location":"","page":"Home","title":"Home","text":"downsample = 5\nplot(vecvec_to_mat(x), l=(4,), layout=(2,1), show=false)\nscatter!(xbt[1, 1:downsample:end, :]', subplot=1, show=false, m=(1,:black, 0.5), lab=\"\")\nscatter!(xbt[2, 1:downsample:end, :]', subplot=2, m=(1,:black, 0.5), lab=\"\")\nDisplayAs.PNG(Plots.current()) # hide","category":"page"},{"location":"#Kalman-filter","page":"Home","title":"Kalman filter","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"The KalmanFilter (wiki) assumes that f and g are linear functions, i.e., that they can be written on the form","category":"page"},{"location":"","page":"Home","title":"Home","text":"beginaligned\nx(t+1) = Ax(t) + Bu(t) + w(t)\ny(t) = Cx(t) + Du(t) + e(t)\nendaligned","category":"page"},{"location":"","page":"Home","title":"Home","text":"for some matrices ABCD where w sim N(0 R_1) and e sim N(0 R_2) are zero mean and Gaussian. The Kalman filter represents the posterior distributions over x by the mean and a covariance matrix. The magic behind the Kalman filter is that linear transformations of Gaussian distributions remain Gaussian, and we thus have a very efficient way of representing them.","category":"page"},{"location":"","page":"Home","title":"Home","text":"A Kalman filter is easily created using the constructor KalmanFilter. Many of the functions defined for particle filters, are defined also for Kalman filters, e.g.:","category":"page"},{"location":"","page":"Home","title":"Home","text":"R1 = cov(df)\nR2 = cov(dg)\nkf = KalmanFilter(A, B, C, 0, R1, R2, d0)\nsol = forward_trajectory(kf, u, y) # sol contains filtered state, predictions, pred cov, filter cov, loglik\nnothing # hide","category":"page"},{"location":"","page":"Home","title":"Home","text":"It can also be called in a loop like the pf above","category":"page"},{"location":"","page":"Home","title":"Home","text":"for t = 1:T\n kf(u,y) # Performs both correct! and predict!\n # alternatively\n ll, e = correct!(kf, y, nothing, t) # Returns loglikelihood and prediction error (plus other things if you want)\n x = state(kf) # Access the state estimate\n R = covariance(kf) # Access the covariance of the estimate\n predict!(kf, u, nothing, t)\nend","category":"page"},{"location":"","page":"Home","title":"Home","text":"The matrices in the Kalman filter may be time varying, such that A[:, :, t] is A(t). They may also be provided as functions on the form A(t) = A(x u p t). This works for both dynamics and covariance matrices.","category":"page"},{"location":"","page":"Home","title":"Home","text":"The numeric type used in the Kalman filter is determined from the mean of the initial state distribution, so make sure that this has the correct type if you intend to use, e.g., Float32 or ForwardDiff.Dual for automatic differentiation.","category":"page"},{"location":"#Smoothing-using-KF","page":"Home","title":"Smoothing using KF","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"Kalman filters can also be used for smoothing ","category":"page"},{"location":"","page":"Home","title":"Home","text":"kf = KalmanFilter(A, B, C, 0, cov(df), cov(dg), d0)\nxT,R,lls = smooth(kf, u, y) # Returns smoothed state, smoothed cov, loglik\nnothing # hide","category":"page"},{"location":"","page":"Home","title":"Home","text":"Plot and compare PF and KF","category":"page"},{"location":"","page":"Home","title":"Home","text":"plot(vecvec_to_mat(xT), lab=\"Kalman smooth\", layout=2)\nplot!(xbm', lab=\"pf smooth\")\nplot!(vecvec_to_mat(x), lab=\"true\")","category":"page"},{"location":"#Kalman-filter-tuning-tutorial","page":"Home","title":"Kalman filter tuning tutorial","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"The tutorial \"How to tune a Kalman filter\" details how to figure out appropriate covariance matrices for the Kalman filter, as well as how to add disturbance models to the system model.","category":"page"},{"location":"#Unscented-Kalman-Filter","page":"Home","title":"Unscented Kalman Filter","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"The UnscentedKalmanFilter represents posterior distributions over x as Gaussian distributions just like the KalmanFilter, but propagates them through a nonlinear function f by a deterministic sampling of a small number of particles called sigma points (this is referred to as the unscented transform). This UKF thus handles nonlinear functions fg, but only Gaussian disturbances and unimodal posteriors. The UKF will by default treat the noise as additive, but by using the augmented UKF form, non-additive noise may be handled as well. See the docstring of UnscentedKalmanFilter for more details.","category":"page"},{"location":"","page":"Home","title":"Home","text":"The UKF takes the same arguments as a regular KalmanFilter, but the matrices defining the dynamics are replaced by two functions, dynamics and measurement, working in the same way as for the ParticleFilter above (unless the augmented form is used).","category":"page"},{"location":"","page":"Home","title":"Home","text":"ukf = UnscentedKalmanFilter(dynamics, measurement, cov(df), cov(dg), MvNormal(SA[1.,1.]); nu=nu, ny=ny)","category":"page"},{"location":"","page":"Home","title":"Home","text":"info: Info\nIf your function dynamics describes a continuous-time ODE, do not forget to discretize it before passing it to the UKF. See Discretization for more information.","category":"page"},{"location":"","page":"Home","title":"Home","text":"The UnscentedKalmanFilter has many customization options, see the docstring for more details. In particular, the UKF may be created with a linear measurement model as an optimization.","category":"page"},{"location":"#Extended-Kalman-Filter","page":"Home","title":"Extended Kalman Filter","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"The ExtendedKalmanFilter (EKF) is similar to the UKF, but propagates Gaussian distributions by linearizing the dynamics and using the formulas for linear systems similar to the standard Kalman filter. This can be slightly faster than the UKF (not always), but also less accurate for strongly nonlinear systems. The linearization is performed automatically using ForwardDiff.jl unless the user provides Jacobian functions that compute A and C. In general, the UKF is recommended over the EKF unless the EKF is faster and computational performance is the top priority.","category":"page"},{"location":"","page":"Home","title":"Home","text":"The EKF constructor has the following two signatures","category":"page"},{"location":"","page":"Home","title":"Home","text":"ExtendedKalmanFilter(dynamics, measurement, R1, R2, d0=MvNormal(R1); nu::Int, p = LowLevelParticleFilters.NullParameters(), α = 1.0, check = true, Ajac = nothing, Cjac = nothing)\nExtendedKalmanFilter(kf, dynamics, measurement; Ajac = nothing, Cjac = nothing)","category":"page"},{"location":"","page":"Home","title":"Home","text":"The first constructor takes all the arguments required to initialize the extended Kalman filter, while the second one takes an already defined standard Kalman filter. using the first constructor, the user must provide the number of inputs to the system, nu.","category":"page"},{"location":"","page":"Home","title":"Home","text":"where kf is a standard KalmanFilter from which the covariance properties are taken.","category":"page"},{"location":"","page":"Home","title":"Home","text":"info: Info\nIf your function dynamics describes a continuous-time ODE, do not forget to discretize it before passing it to the UKF. See Discretization for more information.","category":"page"},{"location":"#AdvancedParticleFilter","page":"Home","title":"AdvancedParticleFilter","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"The AdvancedParticleFilter works very much like the ParticleFilter, but admits more flexibility in its noise models.","category":"page"},{"location":"","page":"Home","title":"Home","text":"The AdvancedParticleFilter type requires you to implement the same functions as the regular ParticleFilter, but in this case you also need to handle sampling from the noise distributions yourself. The function dynamics must have a method signature like below. It must provide one method that accepts state vector, control vector, parameter, time and noise::Bool that indicates whether or not to add noise to the state. If noise should be added, this should be done inside dynamics An example is given below","category":"page"},{"location":"","page":"Home","title":"Home","text":"using Random\nconst rng = Random.Xoshiro()\nfunction dynamics(x, u, p, t, noise=false) # It's important that `noise` defaults to false\n x = A*x .+ B*u # A simple linear dynamics model in discrete time\n if noise\n x += rand(rng, df) # it's faster to supply your own rng\n end\n x\nend\nnothing # hide","category":"page"},{"location":"","page":"Home","title":"Home","text":"The measurement_likelihood function must have a method accepting state, input, measurement, parameter and time, and returning the log-likelihood of the measurement given the state, a simple example below:","category":"page"},{"location":"","page":"Home","title":"Home","text":"function measurement_likelihood(x, u, y, p, t)\n logpdf(dg, C*x-y) # An example of a simple linear measurement model with normal additive noise\nend\nnothing # hide","category":"page"},{"location":"","page":"Home","title":"Home","text":"This gives you very high flexibility. The noise model in either function can, for instance, be a function of the state, something that is not possible for the simple ParticleFilter. To be able to simulate the AdvancedParticleFilter like we did with the simple filter above, the measurement method with the signature measurement(x,u,p,t,noise=false) must be available and return a sample measurement given state (and possibly time). For our example measurement model above, this would look like this","category":"page"},{"location":"","page":"Home","title":"Home","text":"# This function is only required for simulation\nmeasurement(x, u, p, t, noise=false) = C*x + noise*rand(rng, dg)\nnothing # hide","category":"page"},{"location":"","page":"Home","title":"Home","text":"We now create the AdvancedParticleFilter and use it in the same way as the other filters:","category":"page"},{"location":"","page":"Home","title":"Home","text":"apf = AdvancedParticleFilter(N, dynamics, measurement, measurement_likelihood, df, d0)\nsol = forward_trajectory(apf, u, y, ny) # Perform batch filtering","category":"page"},{"location":"","page":"Home","title":"Home","text":"plot(sol, xreal=x)\nDisplayAs.PNG(Plots.current()) # hide","category":"page"},{"location":"","page":"Home","title":"Home","text":"We can even use this type as an AuxiliaryParticleFilter","category":"page"},{"location":"","page":"Home","title":"Home","text":"apfa = AuxiliaryParticleFilter(apf)\nsol = forward_trajectory(apfa, u, y, ny)\nplot(sol, dim=1, xreal=x) # Same as above, but only plots a single dimension\nDisplayAs.PNG(Plots.current()) # hide","category":"page"},{"location":"","page":"Home","title":"Home","text":"See the tutorials section for more advanced examples, including state estimation for DAE (Differential-Algebraic Equation) systems.","category":"page"},{"location":"#Troubleshooting-and-tuning","page":"Home","title":"Troubleshooting and tuning","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"Tuning a particle filter can be quite the challenge. To assist with this, we provide som visualization tools","category":"page"},{"location":"","page":"Home","title":"Home","text":"debugplot(pf,u[1:20],y[1:20], runall=true, xreal=x[1:20])","category":"page"},{"location":"","page":"Home","title":"Home","text":"The plot displays all state variables and all measurements. The heatmap in the background represents the weighted particle distributions per time step. For the measurement sequences, the heatmap represent the distributions of predicted measurements. The blue dots corresponds to measured values. In this case, we simulated the data and we had access to the state as well, if we do not have that, just omit xreal. You can also manually step through the time-series using","category":"page"},{"location":"","page":"Home","title":"Home","text":"commandplot(pf,u,y; kwargs...)","category":"page"},{"location":"","page":"Home","title":"Home","text":"For options to the debug plots, see ?pplot.","category":"page"},{"location":"#Tuning-noise-parameters-through-optimization","page":"Home","title":"Tuning noise parameters through optimization","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"See examples in Parameter Estimation.","category":"page"},{"location":"#Tuning-through-simulation","page":"Home","title":"Tuning through simulation","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"It is possible to sample from the Bayesian model implied by a filter and its parameters by calling the function simulate. A simple tuning strategy is to adjust the noise parameters such that a simulation looks \"similar\" to the data, i.e., the data must not be too unlikely under the model.","category":"page"},{"location":"#Videos","page":"Home","title":"Videos","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"Several video tutorials using this package are available in the playlists","category":"page"},{"location":"","page":"Home","title":"Home","text":"System identification in Julia\nControl systems in Julia","category":"page"},{"location":"","page":"Home","title":"Home","text":"Some examples featuring this package in particular are","category":"page"},{"location":"","page":"Home","title":"Home","text":"","category":"page"},{"location":"","page":"Home","title":"Home","text":"Using an optimizer to optimize the likelihood of an UnscentedKalmanFilter:","category":"page"},{"location":"","page":"Home","title":"Home","text":"","category":"page"},{"location":"","page":"Home","title":"Home","text":"","category":"page"},{"location":"","page":"Home","title":"Home","text":"Estimation of time-varying parameters:","category":"page"},{"location":"","page":"Home","title":"Home","text":"","category":"page"},{"location":"","page":"Home","title":"Home","text":"","category":"page"},{"location":"","page":"Home","title":"Home","text":"Adaptive control by means of estimation of time-varying parameters:","category":"page"},{"location":"","page":"Home","title":"Home","text":"","category":"page"}] +[{"location":"distributions/#Performance-tips","page":"Performance tips","title":"Performance tips","text":"","category":"section"},{"location":"distributions/","page":"Performance tips","title":"Performance tips","text":"Use of StaticArrays.jl is recommended for optimal performance when the state dimension is small, e.g., less than about 10-15 for Kalman filters and less than about 100 for particle filters. In the section Parameter optimization we demonstrate one workflow that makes use of StaticArrays everywhere it is needed for an UnscentedKalmanFilter in order to get a completely allocation free filter. The following arrays must be static for this to hold","category":"page"},{"location":"distributions/","page":"Performance tips","title":"Performance tips","text":"The initial state distribution (the vector and matrix passed to d0 = MvNormal(μ, Σ) for Kalman filters). If you are performing parameter optimization with gradients derived using ForwardDiff.jl, these must further have the correct element type. How to achieve this is demonstrated in the liked example above.\nInputs u measured outputs y.\nIn case of Kalman filters, the dynamic model matrices A, B, C, D and the covariance matrices R1, R2.\nThe dynamics functions for UnscentedKalmanFilter and particle filters must further return static arrays when passed static arrays as inputs.","category":"page"},{"location":"distributions/#Analysis-using-JET","page":"Performance tips","title":"Analysis using JET","text":"","category":"section"},{"location":"distributions/","page":"Performance tips","title":"Performance tips","text":"All flavors of Kalman filters are analyzed for potential runtime dispatch using JET.jl. This analysis is performed in the tests and generally requires a completely static filter using static arrays internally. See the tests for an example of how to set a filter up this way.","category":"page"},{"location":"distributions/#High-performance-Distributions","page":"Performance tips","title":"High performance Distributions","text":"","category":"section"},{"location":"distributions/","page":"Performance tips","title":"Performance tips","text":"When using LowLevelParticleFilters, a number of methods related to distributions are defined for static arrays, making logpdf etc. faster. We also provide a new kind of distribution: TupleProduct <: MultivariateDistribution that behaves similarly to the Product distribution. The TupleProduct however stores the individual distributions in a tuple, has compile-time known length and supports Mixed <: ValueSupport, meaning that it can be a product of both Continuous and Discrete dimensions, something not supported by the standard Product. Example","category":"page"},{"location":"distributions/","page":"Performance tips","title":"Performance tips","text":"using BenchmarkTools, LowLevelParticleFilters, Distributions, StaticArrays\ndt = TupleProduct((Normal(0,2), Normal(0,2), Binomial())) # Mixed value support","category":"page"},{"location":"distributions/","page":"Performance tips","title":"Performance tips","text":"A small benchmark","category":"page"},{"location":"distributions/","page":"Performance tips","title":"Performance tips","text":"sv = @SVector randn(2)\nd = Distributions.Product([Normal(0,2), Normal(0,2)])\ndt = TupleProduct((Normal(0,2), Normal(0,2)))\ndm = MvNormal(2, 2)\n@btime logpdf($d,$(Vector(sv))) # 19.536 ns (0 allocations: 0 bytes)\n@btime logpdf($dt,$(Vector(sv))) # 13.742 ns (0 allocations: 0 bytes)\n@btime logpdf($dm,$(Vector(sv))) # 11.392 ns (0 allocations: 0 bytes)","category":"page"},{"location":"distributions/","page":"Performance tips","title":"Performance tips","text":"@btime logpdf($d,$sv) # 13.964 ns (0 allocations: 0 bytes)\n@btime logpdf($dt,$sv) # 12.817 ns (0 allocations: 0 bytes)\n@btime logpdf($dm,$sv) # 8.383 ns (0 allocations: 0 bytes)","category":"page"},{"location":"distributions/","page":"Performance tips","title":"Performance tips","text":"Without loading LowLevelParticleFilters, the timing for the native distributions are the following","category":"page"},{"location":"distributions/","page":"Performance tips","title":"Performance tips","text":"@btime logpdf($d,$sv) # 18.040 ns (0 allocations: 0 bytes)\n@btime logpdf($dm,$sv) # 9.938 ns (0 allocations: 0 bytes)","category":"page"},{"location":"parameter_estimation/#Parameter-Estimation","page":"Parameter estimation","title":"Parameter Estimation","text":"","category":"section"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"State estimation is an integral part of many parameter-estimation methods. Below, we will illustrate several different methods of performing parameter estimation. We can roughly divide the methods into two camps","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"Methods that optimize prediction error or likelihood by tweaking model parameters.\nMethods that add the parameters to be estimated as state variables in the model and estimate them using standard state estimation. ","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"From the first camp, we provide som basic functionality for maximum-likelihood estimation and MAP estimation, described below. An example of (2), joint state and parameter estimation, is provided in Joint state and parameter estimation.","category":"page"},{"location":"parameter_estimation/#Maximum-likelihood-estimation","page":"Parameter estimation","title":"Maximum-likelihood estimation","text":"","category":"section"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"Filters calculate the likelihood and prediction errors while performing filtering, this can be used to perform maximum likelihood estimation or prediction-error minimization. One can estimate all kinds of parameters using this method, in the example below, we will estimate the noise covariance. We may for example plot likelihood as function of the variance of the dynamics noise like this:","category":"page"},{"location":"parameter_estimation/#Generate-data-by-simulation","page":"Parameter estimation","title":"Generate data by simulation","text":"","category":"section"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"This simulates the same linear system as on the index page of the documentation","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"using LowLevelParticleFilters, LinearAlgebra, StaticArrays, Distributions, Plots\nnx = 2 # Dimension of state\nnu = 2 # Dimension of input\nny = 2 # Dimension of measurements\nN = 2000 # Number of particles\n\nconst dg = MvNormal(ny,1.0) # Measurement noise Distribution\nconst df = MvNormal(nx,1.0) # Dynamics noise Distribution\nconst d0 = MvNormal(@SVector(randn(nx)),2.0) # Initial state Distribution\n\nconst A = SA[1 0.1; 0 1]\nconst B = @SMatrix [0.0 0.1; 1 0.1]\nconst C = @SMatrix [1.0 0; 0 1]\n\ndynamics(x,u,p,t) = A*x .+ B*u \nmeasurement(x,u,p,t) = C*x\nvecvec_to_mat(x) = copy(reduce(hcat, x)') # Helper function\npf = ParticleFilter(N, dynamics, measurement, df, dg, d0)\nxs,u,y = simulate(pf,300,df)","category":"page"},{"location":"parameter_estimation/#Compute-likelihood-for-various-values-of-the-parameters","page":"Parameter estimation","title":"Compute likelihood for various values of the parameters","text":"","category":"section"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"p = nothing\nsvec = exp10.(LinRange(-0.8, 1.2, 60))\nllspf = map(svec) do s\n df = MvNormal(nx,s)\n pfs = ParticleFilter(N, dynamics, measurement, df, dg, d0)\n loglik(pfs, u, y, p)\nend\nplot( svec, llspf,\n xscale = :log10,\n title = \"Log-likelihood\",\n xlabel = \"Dynamics noise standard deviation\",\n lab = \"PF\",\n)\nvline!([svec[findmax(llspf)[2]]], l=(:dash,:blue), primary=false)","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"the correct value for the simulated data is 1 (the simulated system is the same as on the front page of the docs).","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"We can do the same with a Kalman filter","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"eye(n) = SMatrix{n,n}(1.0I(n))\nllskf = map(svec) do s\n kfs = KalmanFilter(A, B, C, 0, s^2*eye(nx), eye(ny), d0)\n loglik(kfs, u, y, p)\nend\nplot!(svec, llskf, yscale=:identity, xscale=:log10, lab=\"Kalman\", c=:red)\nvline!([svec[findmax(llskf)[2]]], l=(:dash,:red), primary=false)","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"the result can be quite noisy due to the stochastic nature of particle filtering. The particle filter likelihood agrees with the Kalman-filter estimate, which is optimal for the linear example system we are simulating here, apart for when the noise variance is small. Due to particle depletion, particle filters often struggle when dynamics-noise is too small. This problem is mitigated by using a greater number of particles, or simply by not using a too small covariance.","category":"page"},{"location":"parameter_estimation/#MAP-estimation","page":"Parameter estimation","title":"MAP estimation","text":"","category":"section"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"In this example, we will estimate the variance of the noises in the dynamics and the measurement functions.","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"To solve a MAP estimation problem, we need to define a function that takes a parameter vector and returns a filter, the parameters are used to construct the covariance matrices:","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"filter_from_parameters(θ, pf = nothing) = KalmanFilter(A, B, C, 0, exp(θ[1])^2*eye(nx), exp(θ[2])^2*eye(ny), d0) # Works with particle filters as well\nnothing # hide","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"The call to exp on the parameters is so that we can define log-normal priors","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"priors = [Normal(0,2),Normal(0,2)]","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"Now we call the function log_likelihood_fun that returns a function to be minimized","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"ll = log_likelihood_fun(filter_from_parameters, priors, u, y, p)\nnothing # hide","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"Since this is a low-dimensional problem, we can plot the LL on a 2d-grid","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"function meshgrid(a,b)\n grid_a = [i for i in a, j in b]\n grid_b = [j for i in a, j in b]\n grid_a, grid_b\nend\nNv = 20\nv = LinRange(-0.7,1,Nv)\nllxy = (x,y) -> ll([x;y])\nVGx, VGy = meshgrid(v,v)\nVGz = llxy.(VGx, VGy)\nheatmap(\n VGz,\n xticks = (1:Nv, round.(v, digits = 2)),\n yticks = (1:Nv, round.(v, digits = 2)),\n xlabel = \"sigma v\",\n ylabel = \"sigma w\",\n) # Yes, labels are reversed","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"For higher-dimensional problems, we may estimate the parameters using an optimizer, e.g., Optim.jl.","category":"page"},{"location":"parameter_estimation/#Bayesian-inference-using-PMMH","page":"Parameter estimation","title":"Bayesian inference using PMMH","text":"","category":"section"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"We proceed like we did for MAP above, but when calling the function metropolis, we will get the entire posterior distribution of the parameter vector, for the small cost of a massive increase in the amount of computations. metropolis runs the Metropolis Hastings algorithm, or more precisely if a particle filter is used, the \"Particle Marginal Metropolis Hastings\" (PMMH) algorithm. Here we use the Kalman filter simply to have the documentation build a bit faster, it can be quite heavy to run.","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"filter_from_parameters(θ, pf = nothing) = KalmanFilter(A, B, C, 0, exp(θ[1])^2*I(nx), exp(θ[2])^2*I(ny), d0) # Works with particle filters as well\nnothing # hide","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"The call to exp on the parameters is so that we can define log-normal priors","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"priors = [Normal(0,2),Normal(0,2)]\nll = log_likelihood_fun(filter_from_parameters, priors, u, y, p)\nθ₀ = log.([1.0, 1.0]) # Starting point\nnothing # hide","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"We also need to define a function that suggests a new point from the \"proposal distribution\". This can be pretty much anything, but it has to be symmetric since I was lazy and simplified an equation.","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"draw = θ -> θ .+ 0.05 .* randn.() # This function dictates how new proposal parameters are being generated. \nburnin = 200 # remove this many initial samples (\"burn-in period\")\n@info \"Starting Metropolis algorithm\"\n@time theta, lls = metropolis(ll, 2200, θ₀, draw) # Run PMMH for 2200 iterations\nthetam = reduce(hcat, theta)'[burnin+1:end,:] # Build a matrix of the output\nhistogram(exp.(thetam), layout=(3,1), lab=[\"R1\" \"R2\"]); plot!(lls[burnin+1:end], subplot=3, lab=\"log likelihood\") # Visualize","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"In this example, we initialize the MH algorithm on the correct value θ₀, in general, you'd see a period in the beginning where the likelihood (bottom plot) is much lower than during the rest of the sampling, this is the reason we remove a number of samples in the beginning, typically referred to as \"burn in\".","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"If you are lucky, you can run the above threaded as well. I tried my best to make particle filters thread safe with their own rngs etc., but your milage may vary. For threading to help, the dynamics must be non-allocating, e.g., by using StaticArrays etc.","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"@time thetalls = LowLevelParticleFilters.metropolis_threaded(burnin, ll, 2200, θ₀, draw, nthreads=2)\nhistogram(exp.(thetalls[:,1:2]), layout=3)\nplot!(thetalls[:,3], subplot=3)","category":"page"},{"location":"parameter_estimation/#Bayesian-inference-using-DynamicHMC.jl","page":"Parameter estimation","title":"Bayesian inference using DynamicHMC.jl","text":"","category":"section"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"The following snippet of code performs the same estimation as above, but uses the much more sophisticated HMC sampler in DynamicHMC.jl rather than the PMMH sampler above. This package requires the log-likelihood function to be wrapped in a custom struct that implements the LogDensityProblems interface, which is done below. We also indicate that we want to use ForwardDiff.jl to compute the gradients for fast sampling.","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"using DynamicHMC, LogDensityProblemsAD, ForwardDiff, LogDensityProblems, LinearAlgebra, Random\n\nstruct LogTargetDensity{F}\n ll::F\n dim::Int\nend\nLogDensityProblems.logdensity(p::LogTargetDensity, θ) = p.ll(θ)\nLogDensityProblems.dimension(p::LogTargetDensity) = p.dim\nLogDensityProblems.capabilities(::Type{LogTargetDensity}) = LogDensityProblems.LogDensityOrder{0}()\n\nfunction filter_from_parameters(θ, pf = nothing)\n # It's important that the distribution of the initial state has the same\n # element type as the parameters. DynamicHMC will use Dual numbers for differentiation,\n # hence, we make sure that d0 has `eltype(d0) = eltype(θ)`\n T = eltype(θ)\n d0 = MvNormal(T.(d0.μ), T.(d0.Σ))\n KalmanFilter(A, B, C, 0, exp(θ[1])^2*eye(nx), exp(θ[2])^2*eye(ny), d0) \nend\nll = log_likelihood_fun(filter_from_parameters, priors, u, y, p)\n\nD = length(θ₀)\nℓπ = LogTargetDensity(ll, D)\n∇P = ADgradient(:ForwardDiff, ℓπ)\n\nresults = mcmc_with_warmup(Random.default_rng(), ∇P, 3000)\nDynamicHMC.Diagnostics.summarize_tree_statistics(results.tree_statistics)\nlls = [ts.π for ts in results.tree_statistics]\n\nhistogram(exp.(results.posterior_matrix)', layout=(3,1), lab=[\"R1\" \"R2\"])\nplot!(lls, subplot=3, lab=\"log likelihood\") # Visualize","category":"page"},{"location":"parameter_estimation/#Joint-state-and-parameter-estimation","page":"Parameter estimation","title":"Joint state and parameter estimation","text":"","category":"section"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"In this example, we'll show how to perform parameter estimation by treating a parameter as a state variable. This method can not only estimate constant parameters, but also time-varying parameters. The system we will consider is a quadruple tank, where two upper tanks feed into two lower tanks. The outlet for tank 1 can vary in size, simulating, e.g., that something partially blocks the outlet. We start by defining the dynamics on a form that changes the outlet area a_1 at time t=500:","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"using LowLevelParticleFilters\nusing SeeToDee\nusing Distributions\nusing StaticArrays\nusing Plots, LinearAlgebra\n\nfunction quadtank(h,u,p,t)\n kc = 0.5\n k1, k2, g = 1.6, 1.6, 9.81\n A1 = A3 = A2 = A4 = 4.9\n a1, a3, a2, a4 = 0.03, 0.03, 0.03, 0.03\n γ1, γ2 = 0.2, 0.2\n\n if t > 500\n a1 *= 2 # Change the parameter at t = 500\n end\n\n ssqrt(x) = √(max(x, zero(x)) + 1e-3) # For numerical robustness at x = 0\n \n SA[\n -a1/A1 * ssqrt(2g*h[1]) + a3/A1*ssqrt(2g*h[3]) + γ1*k1/A1 * u[1]\n -a2/A2 * ssqrt(2g*h[2]) + a4/A2*ssqrt(2g*h[4]) + γ2*k2/A2 * u[2]\n -a3/A3*ssqrt(2g*h[3]) + (1-γ2)*k2/A3 * u[2]\n -a4/A4*ssqrt(2g*h[4]) + (1-γ1)*k1/A4 * u[1]\n ]\nend\n\nnu = 2 # number of control inputs\nnx = 4 # number of state variables\nny = 2 # number of measured outputs\nTs = 1 # sample time\nnothing # hide","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"We then define a measurement function, we measure the levels of tanks 1 and 2, and discretize the continuous-time dynamics using a Runge-Kutta 4 integrator SeeToDee.Rk4:","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"measurement(x,u,p,t) = SA[x[1], x[2]]\ndiscrete_dynamics = SeeToDee.Rk4(quadtank, Ts)\nnothing # hide","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"We simulate the system using the rollout function and add some noise to the measurements. The inputs in this case are just square waves.","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"Tperiod = 200\nt = 0:Ts:1000\nu = vcat.(0.25 .* sign.(sin.(2pi/Tperiod .* t)) .+ 0.25)\nu = SVector{nu}.(vcat.(u,u))\nx0 = Float64[2,2,3,3]\nx = LowLevelParticleFilters.rollout(discrete_dynamics, x0, u)[1:end-1]\ny = measurement.(x, u, 0, 0)\ny = [y .+ 0.01.*randn.() for y in y]\n\nplot(\n plot(reduce(hcat, x)', title=\"State\"),\n plot(reduce(hcat, u)', title=\"Inputs\")\n)","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"To perform the joint state and parameter estimation, we define a version of the dynamics that contains an extra state, corresponding to the unknown or time varying parameter, in this case a_1. We do not have any apriori information about how this parameter changes, so we say that its derivative is 0 and it's thus only driven by noise:","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"function quadtank_paramest(h, u, p, t)\n kc = 0.5\n k1, k2, g = 1.6, 1.6, 9.81\n A1 = A3 = A2 = A4 = 4.9\n a3, a2, a4 = 0.03, 0.03, 0.03\n γ1, γ2 = 0.2, 0.2\n\n a1 = h[5] # the a1 parameter is a state\n\n ssqrt(x) = √(max(x, zero(x)) + 1e-3) # For numerical robustness at x = 0\n \n SA[\n -a1/A1 * ssqrt(2g*h[1]) + a3/A1*ssqrt(2g*h[3]) + γ1*k1/A1 * u[1]\n -a2/A2 * ssqrt(2g*h[2]) + a4/A2*ssqrt(2g*h[4]) + γ2*k2/A2 * u[2]\n -a3/A3*ssqrt(2g*h[3]) + (1-γ2)*k2/A3 * u[2]\n -a4/A4*ssqrt(2g*h[4]) + (1-γ1)*k1/A4 * u[1]\n 0 # the state is only driven by noise\n ]\nend\n\ndiscrete_dynamics_params = SeeToDee.Rk4(quadtank_paramest, Ts)\nnothing # hide","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"We then define a nonlinear state estimator, we will use the UnscentedKalmanFilter, and solve the filtering problem. We start by an initial state estimate x_0 that is slightly off for the parameter a_1","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"nx = 5\nR1 = SMatrix{nx,nx}(Diagonal([0.1, 0.1, 0.1, 0.1, 0.0001])) # Use of StaticArrays is generally good for performance\nR2 = SMatrix{ny,ny}(Diagonal((1e-2)^2 * ones(ny)))\nx0 = SA[2, 2, 3, 3, 0.02] # The SA prefix makes the array static, which is good for performance\n\nkf = UnscentedKalmanFilter(discrete_dynamics_params, measurement, R1, R2, MvNormal(x0, R1); ny, nu, Ts)\n\nsol = forward_trajectory(kf, u, y)\nplot(sol, plotx=false, plotxt=true, plotu=false, ploty=true, legend=:bottomright)\nplot!([0,500,500,1000], [0.03, 0.03, 0.06, 0.06], l=(:dash, :black), sp=5, lab=\"True param\")","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"as we can see, the correct value of the parameter is quickly found (x_5), and it also adapts at t=500 when the parameter value changes. The speed with which the parameter adapts to changes is determined by the covariance matrix R_1, a higher value results in faster adaptation, but also higher sensitivity to noise. ","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"If adaptive parameter estimation is coupled with a model-based controller, we get an adaptive controller! Note: the state that corresponds to the estimated parameter is typically not controllable, a fact that may require some special care for some control methods.","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"We may ask ourselves, what's the difference between a parameter and a state variable if we can add parameters as state variables? Typically, parameters do not vary with time, and if they do, they vary significantly slower than the state variables. State variables also have dynamics associate with them, whereas we often have no idea about how the parameters vary other than that they vary slowly.","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"Abrupt changes to the dynamics like in the example above can happen in practice, for instance, due to equipment failure or change of operating mode. This can be treated as a scenario with time-varying parameters that are continuously estimated. ","category":"page"},{"location":"parameter_estimation/#Using-an-optimizer","page":"Parameter estimation","title":"Using an optimizer","text":"","category":"section"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"The state estimators in this package are all statistically motivated and thus compute things like the likelihood of the data as a by-product of the estimation. Maximum-likelihood or prediction-error estimation is thus very straight-forward by simply calling a gradient-based optimizer with gradients provided by differentiating through the state estimator using automatic differentiation. In this example, we will continue the example from above, but now estimate all the parameters of the quad-tank process. This time, they will not vary with time. We will first use a standard optimization algorithm from Optim.jl to minimize the cost function based on the prediction error, and then use a Gauss-Newton optimizer.","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"We now define the dynamics function such that it takes its parameters from the p input argument. We also define a variable p_true that contains the true values that we will use to simulate some estimation data","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"function quadtank(h, u, p, t)\n kc = p[1]\n k1, k2, g = p[2], p[3], 9.81\n A1 = A3 = A2 = A4 = p[4]\n a1 = a3 = a2 = a4 = p[5]\n γ1 = γ2 = p[6]\n\n ssqrt(x) = √(max(x, zero(x)) + 1e-3) # For numerical robustness at x = 0\n \n SA[\n -a1/A1 * ssqrt(2g*h[1]) + a3/A1*ssqrt(2g*h[3]) + γ1*k1/A1 * u[1]\n -a2/A2 * ssqrt(2g*h[2]) + a4/A2*ssqrt(2g*h[4]) + γ2*k2/A2 * u[2]\n -a3/A3*ssqrt(2g*h[3]) + (1-γ2)*k2/A3 * u[2]\n -a4/A4*ssqrt(2g*h[4]) + (1-γ1)*k1/A4 * u[1]\n ]\nend\n\ndiscrete_dynamics = SeeToDee.Rk4(quadtank, Ts) # Discretize the dynamics using a 4:th order Runge-Kutta integrator\np_true = [0.5, 1.6, 1.6, 4.9, 0.03, 0.2]\nnothing # hide","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"Similar to previous example, we simulate the system, this time using a more exciting input in order to be able to identify several parameters","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"using Random; Random.seed!(1) # hide\nTperiod = 200\nt = 0:Ts:1000\nu1 = vcat.(0.25 .* sign.(sin.(2pi/Tperiod .* (t ./ 40).^2)) .+ 0.25)\nu2 = vcat.(0.25 .* sign.(sin.(2pi/Tperiod .* (t ./ 40).^2 .+ pi/2)) .+ 0.25)\nu = SVector{nu}.(vcat.(u1,u2))\nx0 = SA[2.0,2,3,3] # Initial condition, static array for performance\nx = LowLevelParticleFilters.rollout(discrete_dynamics, x0, u, p_true)[1:end-1]\ny = measurement.(x, u, 0, 0)\ny = [y .+ 0.01 .* randn.() for y in y]\n\nplot(\n plot(reduce(hcat, x)', title=\"State\"),\n plot(reduce(hcat, u)', title=\"Inputs\")\n)","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"This time, we define a cost function for the optimizer to optimize, we'll use the sum of squared errors (sse). It's important to define the UKF with an initial state distribution with the same element type as the parameter vector so that automatic differentiation through the state estimator works, hence the explicit casting T.(x0) and T.(R1). We also make sure to use StaticArrays for the covariance matrices and the initial condition for performance reasons (optional).","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"nx = 4\nR1 = SMatrix{nx,nx}(Diagonal([0.1, 0.1, 0.1, 0.1])) # Use of StaticArrays is generally good for performance\nR2 = SMatrix{ny,ny}(Diagonal((1e-2)^2 * ones(ny)))\nx0 = SA[2.0, 2, 3, 3]\n\nfunction cost(p::Vector{T}) where T\n kf = UnscentedKalmanFilter(discrete_dynamics, measurement, R1, R2, MvNormal(T.(x0), T.(R1)); ny, nu, Ts)\n LowLevelParticleFilters.sse(kf, u, y, p) # Sum of squared prediction errors\nend\nnothing # hide","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"We generate a random initial guess for the estimation problem","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"p_guess = p_true .+ 0.1*p_true .* randn(length(p_true))","category":"page"},{"location":"parameter_estimation/#Solving-using-Optim","page":"Parameter estimation","title":"Solving using Optim","text":"","category":"section"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"We first minimize the cost using the BFGS optimization algorithm from Optim.jl","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"using Optim\nres = Optim.optimize(\n cost,\n p_guess,\n BFGS(),\n Optim.Options(\n show_trace = true,\n show_every = 5,\n iterations = 100,\n time_limit = 30,\n ),\n autodiff = :forward, # Indicate that we want to use forward-mode AD to derive gradients\n)","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"We started out with a normalized parameter error of","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"using LinearAlgebra\nnorm(p_true - p_guess) / norm(p_true)","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"and ended with","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"p_opt = res.minimizer\nnorm(p_true - p_opt) / norm(p_true)","category":"page"},{"location":"parameter_estimation/#Solving-using-Gauss-Newton-optimization","page":"Parameter estimation","title":"Solving using Gauss-Newton optimization","text":"","category":"section"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"Below, we optimize the sum of squared residuals again, but this time we do it using a Gauss-Newton style algorithm (Levenberg Marquardt). These algorithms want the entire residual vector rather than the sum of squares of the residuals, so we define an alternative \"cost function\" called residuals that calls the lower-level function LowLevelParticleFilters.prediction_errors!","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"using LeastSquaresOptim\n\nfunction residuals!(res, p::Vector{T}) where T\n kf = UnscentedKalmanFilter(discrete_dynamics, measurement, R1, R2, MvNormal(T.(x0), T.(R1)); ny, nu, Ts)\n LowLevelParticleFilters.prediction_errors!(res, kf, u, y, p) \nend\n\nres_gn = optimize!(LeastSquaresProblem(x = copy(p_guess), f! = residuals!, output_length = length(y)*ny, autodiff = :forward), LevenbergMarquardt())\n\np_opt_gn = res_gn.minimizer\nnorm(p_true - p_opt_gn) / norm(p_true)","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"Gauss-Newton algorithms are often more efficient at sum-of-squares minimization than the more generic BFGS optimizer. This form of Gauss-Newton optimization of prediction errors is also available through ControlSystemIdentification.jl, which uses this package undernath the hood.","category":"page"},{"location":"parameter_estimation/#Identifiability","page":"Parameter estimation","title":"Identifiability","text":"","category":"section"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"There is no guarantee that we will recover the true parameters for this system, especially not if the input excitation is poor, but we will generally find parameters that results in a good predictor for the system (this is after all what we're optimizing for). A tool like StructuralIdentifiability.jl may be used to determine the identifiability of parameters and state variables, something that for this system could look like","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"using StructuralIdentifiability\n\node = @ODEmodel(\n h1'(t) = -a1/A1 * h1(t) + a3/A1*h3(t) + gam*k1/A1 * u1(t),\n h2'(t) = -a2/A2 * h2(t) + a4/A2*h4(t) + gam*k2/A2 * u2(t),\n h3'(t) = -a3/A3*h3(t) + (1-gam)*k2/A3 * u2(t),\n h4'(t) = -a4/A4*h4(t) + (1-gam)*k1/A4 * u1(t),\n\ty1(t) = h1(t),\n y2(t) = h2(t),\n)\n\nlocal_id = assess_local_identifiability(ode, 0.99)","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"where we have made the substitution sqrt h rightarrow h due to a limitation of the tool. The output of the above analysis is ","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"julia> local_id = assess_local_identifiability(ode, 0.99)\nDict{Nemo.fmpq_mpoly, Bool} with 15 entries:\n a3 => 0\n gam => 1\n k2 => 0\n A4 => 0\n h4 => 0\n h2 => 1\n A3 => 0\n a1 => 0\n A2 => 0\n k1 => 0\n a4 => 0\n h3 => 0\n h1 => 1\n A1 => 0\n a2 => 0","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"indicating that we can not hope to resolve all of the parameters. However, using appropriate regularization from prior information, we might still recover a lot of information about the system. Regularization could easily be added to the function cost above, e.g., using a penalty like (p-p_guess)'Γ*(p-p_guess) for some matrix Gamma, to indicate our confidence in the initial guess.","category":"page"},{"location":"parameter_estimation/#Videos","page":"Parameter estimation","title":"Videos","text":"","category":"section"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"Examples of parameter estimation are available here","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"By using an optimizer to optimize the likelihood of an UnscentedKalmanFilter:","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"Estimation of time-varying parameters:","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"Adaptive control by means of estimation of time-varying parameters:","category":"page"},{"location":"parameter_estimation/","page":"Parameter estimation","title":"Parameter estimation","text":"","category":"page"},{"location":"neural_network/#Adaptive-Neural-Network-training","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"","category":"section"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"In this example, we will demonstrate how we can take the estimation of time-varying parameters to the extreme, and use a nonlinear state estimator to estimate the weights in a neural-network model of a dynamical system. ","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"In the tutorial Joint state and parameter estimation, we demonstrated how we can add a parameter as a state variable and let the state estimator estimate this alongside the state. In this example, we will try to learn an entire black-box model of the system dynamics using a neural network, and treat the network weights as time-varying parameters by adding them to the state.","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"We start by generating some data from a simple dynamical system, we will continue to use the quadruple-tank system from Joint state and parameter estimation.","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"using LowLevelParticleFilters, Lux, Random, SeeToDee, StaticArrays, Plots, LinearAlgebra, ComponentArrays, DifferentiationInterface, SparseMatrixColorings\nusing SparseConnectivityTracer: TracerSparsityDetector\nusing DisplayAs # hide\n\nusing LowLevelParticleFilters: SimpleMvNormal\n\nfunction quadtank(h,u,p,t)\n kc = 0.5\n k1, k2, g = 1.6, 1.6, 9.81\n A1 = A3 = A2 = A4 = 4.9\n a1, a3, a2, a4 = 0.03, 0.03, 0.03, 0.03\n γ1, γ2 = 0.2, 0.2\n\n if t > 2000\n a1 *= 1.5 # Change the parameter at t = 2000\n end\n\n ssqrt(x) = √(max(x, zero(x)) + 1e-3) # For numerical robustness at x = 0\n\n SA[\n -a1/A1 * ssqrt(2g*h[1]) + a3/A1*ssqrt(2g*h[3]) + γ1*k1/A1 * u[1]\n -a2/A2 * ssqrt(2g*h[2]) + a4/A2*ssqrt(2g*h[4]) + γ2*k2/A2 * u[2]\n -a3/A3*ssqrt(2g*h[3]) + (1-γ2)*k2/A3 * u[2]\n -a4/A4*ssqrt(2g*h[4]) + (1-γ1)*k1/A4 * u[1]\n ]\nend\n\nTs = 30 # sample time\ndiscrete_dynamics = SeeToDee.Rk4(quadtank, Ts) # Discretize dynamics\nnu = 2 # number of control inputs\nnx = 4 # number of state variables\nny = 4 # number of measured outputs\n\nfunction generate_data() \n measurement(x,u,p,t) = x#SA[x[1], x[2]]\n Tperiod = 200\n t = 0:Ts:4000\n u = vcat.((0.25 .* sign.(sin.(2pi/Tperiod .* t)) .+ 0.25) .* sqrt.(rand.()))\n u = SVector{nu, Float32}.(vcat.(u,u))\n x0 = Float32[2,2,3,3]\n x = LowLevelParticleFilters.rollout(discrete_dynamics, x0, u)[1:end-1]\n y = measurement.(x, u, 0, 0)\n y = [Float32.(y .+ 0.01.*randn.()) for y in y] # Add some noise to the measurement\n\n (; x, u, y, nx, nu, ny, Ts)\nend\n\nrng = Random.default_rng()\nRandom.seed!(rng, 1)\ndata = generate_data()\nnothing # hide","category":"page"},{"location":"neural_network/#Neural-network-dynamics","page":"Adaptive Neural-Network training","title":"Neural network dynamics","text":"","category":"section"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"Our neural network will be a small feedforward network built using the package Lux.jl. ","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"ni = ny + nu\nnhidden = 8\nconst model_ = Chain(Dense(ni, nhidden, tanh), Dense(nhidden, nhidden, tanh), Dense(nhidden, ny))","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"Since the network is rather small, we will train on the CPU only, this will be fast enough for this use case. We may extract the parameters of the network using the function Lux.setup, and convert them to a ComponentArray to make it easier to refer to different parts of the combined state vector.","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"dev = cpu_device()\nps, st = Lux.setup(rng, model_) |> dev\nparr = ComponentArray(ps)\nnothing # hide","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"The dynamics of our black-box model will call the neural network to predict the next state given the current state and input. We bias the dynamics towards low frequencies by adding a multiple of the current state to the prediction of the next state, 0.95*x. We also add a small amount of weight decay to the parameters of the neural network for regularization, 0.995*p.","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"function dynamics(out0, xp0, u, _, t)\n xp = ComponentArray(xp0, getaxes(s0))\n out = ComponentArray(out0, getaxes(s0))\n x = xp.x\n p = xp.p\n xp, _ = Lux.apply(model_, [x; u], p, st)\n @. out.x = 0.95f0*x+xp\n @. out.p = 0.995f0*p\n nothing\nend\n\n@views measurement(out, x, _, _, _) = out .= x[1:nx] # Assume measurement of the full state vector\nnothing # hide","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"For simplicity, we have assumed here that we have access to measurements of the entire state vector of the original process. This is many times unrealistic, and if we do not have such access, we may instead augment the measured signals with delayed versions of themselves (sometimes called a delay embedding). This is a common technique in discrete-time system identification, used in e.g., ControlSystemIdentification.arx and subspaceid.","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"The initial state of the process x0 and the initial parameters of the neural network parr can now be concatenated to form the initial augmented state s0.","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"x0 = Float32[2; 2; 3; 3]\ns0 = ComponentVector(; x=x0, p=parr)\nnothing # hide","category":"page"},{"location":"neural_network/#Kalman-filter-setup","page":"Adaptive Neural-Network training","title":"Kalman filter setup","text":"","category":"section"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"We will estimate the parameters using two different nonlinear Kalman filters, the ExtendedKalmanFilter and the UnscentedKalmanFilter. The covariance matrices for the filters, R1, R2, may be tuned such that we get the desired learning speed of the weights, where larger covariance for the network weights will allow for faster learning, but also more noise in the estimates. ","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"R1 = Diagonal([0.1ones(nx); 0.01ones(length(parr))]) .|> Float32\nR2 = Diagonal((1e-2)^2 * ones(ny)) .|> Float32\nnothing # hide","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"The ExtendedKalmanFilter uses Jacobians of the dynamics and measurement model, and if we do not provide those functions they will be automatically computed using ForwardDiff.jl. Since our Jacobians will be relatively large but sparse in this example, we will make use of the sparsity-aware features of DifferentiationInterface.jl in order to get efficient Jacobian computations. ","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"function Ajacfun(x,u,p,t) # Function that returns a function for the Jacobian of the dynamics\n # For large neural networks, it might be faster to use an OOP formulation with Zygote instead of ForwardDiff. Zygote does not handle the in-place version\n backend = AutoSparse(\n AutoForwardDiff(),\n # AutoZygote(),\n sparsity_detector=TracerSparsityDetector(),\n coloring_algorithm=GreedyColoringAlgorithm(),\n )\n out = similar(getdata(x))\n inner = (out,x)->dynamics(out,x,u,p,t)\n prep = prepare_jacobian(inner, out, backend, getdata(x))\n jac = one(eltype(x)) .* sparsity_pattern(prep)\n function (x,u,p,t)\n inner2 = (out,x)->dynamics(out,x,u,p,t)\n DifferentiationInterface.jacobian!(inner2, out, jac, prep, backend, x)\n end\nend\n\nAjac = Ajacfun(s0, data.u[1], nothing, 0)\n\nconst CJ_ = [I(nx) zeros(Float32, nx, length(parr))] # The jacobian of the measurement model is constant\nCjac(x,u,p,t) = CJ_\nnothing # hide","category":"page"},{"location":"neural_network/#Estimation","page":"Adaptive Neural-Network training","title":"Estimation","text":"","category":"section"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"We may now initialize our filters and perform the estimation. Here, we use the function forward_trajectory to perform filtering along the entire data trajectory at once, but we may use this in a streaming fashion as well, as more data becomes available in real time.","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"We plot the one-step ahead prediction of the outputs and compare to the \"measured\" data.","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"ekf = ExtendedKalmanFilter(dynamics, measurement, R1, R2, SimpleMvNormal(s0, 100R1); nu, check=false, Ajac, Cjac, Ts)\nukf = UnscentedKalmanFilter(dynamics, measurement, R1, R2, SimpleMvNormal(s0, 100R1); nu, ny, Ts)\n\n@time sole = forward_trajectory(ekf, data.u, data.x)\n@time solu = forward_trajectory(ukf, data.u, data.x)\n\nplot(sole, plotx=false, plotxt=false, plotyh=true, plotyht=false, plotu=false, plote=true, name=\"EKF\", layout=(nx, 1), size=(1200, 1500))\nplot!(solu, plotx=false, plotxt=false, plotyh=true, plotyht=false, plotu=false, plote=true, name=\"UKF\", ploty=false)\nDisplayAs.PNG(Plots.current()) # hide","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"We see that prediction errors, e, are large in the beginning when the network weights are randomly initialized, but after about half the trajectory the errors are significantly reduced. Just like in the tutorial Joint state and parameter estimation, we modified the true dynamics after some time, at t=2000, and we see that the filters are able to adapt to this change after a transient increase in prediction error variance.","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"We may also plot the evolution of the neural-network weights over time, and see how the filters adapt to the changing dynamics of the system.","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"plot(\n plot(0:Ts:4000, reduce(hcat, sole.xt)'[:, nx+1:end], title=\"EKF parameters\"),\n plot(0:Ts:4000, reduce(hcat, solu.xt)'[:, nx+1:end], title=\"UKF parameters\"),\n legend = false,\n)\nDisplayAs.PNG(Plots.current()) # hide","category":"page"},{"location":"neural_network/#Smoothing","page":"Adaptive Neural-Network training","title":"Smoothing","text":"","category":"section"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"@time xTe,RTe = smooth(sole, ekf)\n@time xTu,RTu = smooth(solu, ukf)\nplot(\n plot(0:Ts:4000, reduce(hcat, xTe)'[:, nx+1:end], title=\"EKF parameters\", c=1, alpha=0.2),\n plot(0:Ts:4000, reduce(hcat, xTu)'[:, nx+1:end], title=\"UKF parameters\", c=1, alpha=0.2),\n legend = false,\n)","category":"page"},{"location":"neural_network/#Benchmarking","page":"Adaptive Neural-Network training","title":"Benchmarking","text":"","category":"section"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"The neural network used in this example has","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"length(parr)","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"parameters, and the length of the data is","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"length(data.u)","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"Performing the estimation using the Extended Kalman Filter took","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"using BenchmarkTools\n@btime forward_trajectory(ekf, data.u, data.x);\n # 46.034 ms (77872 allocations: 123.45 MiB)","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"and with the Unscented Kalman Filter","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"@btime forward_trajectory(ukf, data.u, data.x);\n # 142.608 ms (2134370 allocations: 224.82 MiB)","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"The EKF is a bit faster, which is to be expected. Both methods are very fast from a neural-network training perspective, but the performance will not scale favorably to very large network sizes.","category":"page"},{"location":"neural_network/#Closing-remarks","page":"Adaptive Neural-Network training","title":"Closing remarks","text":"","category":"section"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"We have seen how to estimate train a black-box neural network dynamics model by treating the parameter estimation as a state-estimation problem. This example is very simple and leaves a lot of room for improvement, such as","category":"page"},{"location":"neural_network/","page":"Adaptive Neural-Network training","title":"Adaptive Neural-Network training","text":"We assumed very little prior knowledge of the dynamics. In practice, we may want to model as much as possible from first principles and add a neural network to capture only the residuals that our first-principles model cannot capture.\nWe started the training of the network weights directly from a random initialization. In practice, we may want to pre-train the network on a large offline dataset before updating the weights adaptively in real-time.\nWe used forward-mode AD to compute the Jacobian. The Jacobian of the dynamics has dense rows, which means that it's theoretically favorable to use reverse-mode AD to compute it. This is possible using Zygote.jl, but Zygote does not handle array mutation, and one must thus avoid the in-place version of the dynamics. Since the number of parameters in this example is small, sparse forward mode AD ended up being slightly faster.","category":"page"},{"location":"adaptive_kalmanfilter/#Noise-adaptive-Kalman-filter","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Noise-adaptive Kalman filter","text":"","category":"section"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"In this tutorial we will consider filtering of a 1D position track, similar in spirit to what one could have obtained from a GPS device, but limited to 1D for easier visualization. We will use a constant-velocity model, i.e., use a double integrator,","category":"page"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"beginaligned\nx_k+1 = beginbmatrix 1 T_s 0 1 endbmatrix x_k + beginbmatrix T_s^22 T_s endbmatrix w_k \ny_k = beginbmatrix 1 0 endbmatrix x_k + v_k\nendaligned","category":"page"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"where w_k sim mathcalN(0 σ_w) is the process noise, and v_k sim mathcalN(0 R_2) is the measurement noise, and illustrate how we can make use of an adaptive noise covariance to improve the filter performance.","category":"page"},{"location":"adaptive_kalmanfilter/#Data-generation","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Data generation","text":"","category":"section"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"We start by generating some position data that we want to perform filtering on. The \"object\" we want to track is initially stationary, and transitions to moving with a constant velocity after a while. ","category":"page"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"using LowLevelParticleFilters, Plots, Random\nRandom.seed!(1)\n\n# Create a time series for filtering\nx = [zeros(50); 0:100]\nT = length(x)\nY = x + randn(T)\nplot([Y x], lab=[\"Measurement\" \"True state to be tracked\"], c=[1 :purple])","category":"page"},{"location":"adaptive_kalmanfilter/#Simple-Kalman-filtering","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Simple Kalman filtering","text":"","category":"section"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"We will use a Kalman filter to perform the filtering. The model is a double integrator, i.e., a constant-acceleration model. The state vector is thus x = p v^T, where p is the position and v is the velocity. When designing a Kalman filter, we need to specify the noise covariances R_1 and R_2. While it's often easy to measure the covariance of the measurement noise, R_2, it can be quite difficult to know ahead of time what the dynamics noise covariance, R_1, should be. In this example, we will use an adaptive filter, where we will increase the dynamics noise covariance if the filter prediction error is too large. However, we first run the filter twice, once with a large R_1 and once with a small R_1 to illustrate the difference.","category":"page"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"y = [[y] for y in Y] # create a vector of vectors for the KF\nu = fill([], T) # No inputs in this example :(\n\n# Define the model\nTs = 1\nA = [1 Ts; 0 1]\nB = zeros(2, 0)\nC = [1 0]\nD = zeros(0, 0)\nR2 = [1;;]\n\nσws = [1e-2, 1e-5] # Dynamics noise standard deviations\n\nfig = plot(Y, lab=\"Measurement\")\nfor σw in σws\n R1 = σw*[Ts^3/3 Ts^2/2; Ts^2/2 Ts] # The dynamics noise covariance matrix is σw*Bw*Bw' where Bw = [Ts^2/2; Ts]\n kf = KalmanFilter(A, B, C, D, R1, R2)\n yh = []\n measure = LowLevelParticleFilters.measurement(kf)\n for t = 1:T # Main filter loop\n kf(u[t], y[t]) # Performs both prediction and correction\n xh = state(kf)\n yht = measure(xh, u[t], nothing, t)\n push!(yh, yht)\n end\n\n Yh = reduce(hcat, yh)\n plot!(Yh', lab=\"Estimate \\$σ_w\\$ = $σw\")\nend\nfig","category":"page"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"When R_1 is small (controlled by σ_w), we get a nice and smooth filter estimate, but this estimate clearly lags behind the true state. When R_1 is large, the filter estimate is much more responsive, but it also has a lot of noise.","category":"page"},{"location":"adaptive_kalmanfilter/#Adaptive-noise-covariance","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Adaptive noise covariance","text":"","category":"section"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"Below, we will implement an adaptive filter, where we keep the dynamics noise covariance low by default, but increase it if the filter prediction error is too large. We will use a Z-score to determine if the prediction error is too large. The Z-score is defined as the number of standard deviations the prediction error is away from the estimated mean. This time around we use separate correct! and predict! calls, so that we can access the prediction error as well as the prior covariance of the prediction error, S. S (or the Cholesky factor Sᵪ) will be used to compute the Z-score.","category":"page"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"σw = 1e-5 # Set the covariance to a low value by default\nR1 = σw*[Ts^3/3 Ts^2/2; Ts^2/2 Ts]\nkf = KalmanFilter(A, B, C, D, R1, R2)\nmeasure = LowLevelParticleFilters.measurement(kf)\n\n# Some arrays to store simulation data\nyh = []\nes = Float64[]\nσs = Float64[]\nfor t = 1:T # Main filter loop\n ll, e, S, Sᵪ = correct!(kf, u[t], y[t], nothing, t) # Manually call the prediction step\n σ = √(e'*(Sᵪ\\e)) # Compute the Z-score\n push!(es, e[]) # Save for plotting\n push!(σs, σ)\n if σ > 3 # If the Z-score is too high\n # we temporarily increase the dynamics noise covariance by 1000x to adapt faster\n predict!(kf, u[t], nothing, t; R1 = 1000kf.R1) \n else\n predict!(kf, u[t], nothing, t)\n end\n xh = state(kf)\n yht = measure(xh, u[t], nothing, t)\n push!(yh, yht)\nend\n\nYh = reduce(hcat, yh)\nplot([Y Yh'], lab=[\"Measurement\" \"Adaptive estimate\"])","category":"page"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"Not too bad! This time the filter estimate is much more responsive during the transition, but exhibits favorable noise properties during the stationary phases. We can also plot the prediction error and the Z-score to see how the filter adapts to the dynamics noise covariance.","category":"page"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"plot([es σs], lab=[\"Prediction error\" \"Z-score\"])","category":"page"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"Notice how the prediction errors, that should ideally be centered around zero, remain predominantly negative for a long time interval after the transition. This can be attributed to an overshoot in the velocity state of the estimator, but the rapid decrease of the covariance after the transition makes the filter slow at correcting its overshoot. If we want, we could mitigate this and make the adaptation even more sophisticated by letting the covariance remain large for a while after a transition in operating mode has been detected. Below, we implement a simple version of this, where we use a multiplier σ_wt that defaults to 1, but is increase to a very large value of 1000 if a transition is detected. When no transition is detected, σ_wt is decreased exponentially back down to 1.","category":"page"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"σw = 1e-5 # Set the covariance to a low value by default\nσwt = 1.0\nR1 = σw*[Ts^3/3 Ts^2/2; Ts^2/2 Ts]\nkf = KalmanFilter(A, B, C, D, R1, R2)\nmeasure = LowLevelParticleFilters.measurement(kf)\n\n# Some arrays to store simulation data\nyh = []\nes = Float64[]\nσs = Float64[]\nσwts = Float64[]\nfor t = 1:T # Main filter loop\n global σwt # Note, do not make use of global variables in performance critical code\n ll, e, S, Sᵪ = correct!(kf, u[t], y[t], nothing, t) # Manually call the prediction step\n σ = √(e'*(Sᵪ\\e)) # Compute the Z-score\n push!(es, e[]) # Save for plotting\n push!(σs, σ)\n if σ > 3 # If the Z-score is too high\n σwt = 1000.0 # Set the R1 multiplier to a very large value\n else\n σwt = max(0.7σwt, 1.0) # Decrease exponentially back to 1\n end\n push!(σwts, σwt)\n predict!(kf, u[t], nothing, t; R1 = σwt*kf.R1) \n xh = state(kf)\n yht = measure(xh, u[t], nothing, t)\n push!(yh, yht)\nend\n\nYh = reduce(hcat, yh)\nplot([Y Yh'], lab=[\"Measurement\" \"Adaptive estimate\"])","category":"page"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"plot([es σs σwts], lab=[\"Prediction error\" \"Z-score\" \"\\$σ_{wt}\\$ multiplier\"], layout=2, sp=[1 1 2])","category":"page"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"This time, the prediction errors look more like white noise centered around zero after the initial transient caused by the transition.","category":"page"},{"location":"adaptive_kalmanfilter/#Summary","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Summary","text":"","category":"section"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"This tutorial demonstrated simple Kalman filtering for a double integrator without control inputs. We saw how the filtering estimate could be improved by playing around with the covariance matrices of the estimator, helping it catch up to fast changes in the behavior of the system without sacrificing steady-state noise properties.","category":"page"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"In this case, we handled the modification of R_1 outside of the filter, implementing our own filtering loop. Some applications get away with instead providing time-varying matrices in the form of a 3-dimension array, where the third dimension corresponds to time, or instead of providing a matrix, providing a function R_1(x u p t) allows the matrix to be a function of state, input, parameters and time. These options apply to all matrices in the filter, including the dynamics matrices, ABCD.","category":"page"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"Lastly, we mention the ability of the KalmanFilter to act like a recursive least-squares estimator, by setting the \"forgetting factor α1 when creating the KalmanFilter. α1 will cause the filter will exhibit exponential forgetting similar to an RLS estimator, in addition to the covariance inflation due to R1. It is thus possible to get a RLS-like algorithm by setting R_1 = 0 R_2 = 1α and α 1.","category":"page"},{"location":"adaptive_kalmanfilter/#Disturbance-modeling-and-noise-tuning","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Disturbance modeling and noise tuning","text":"","category":"section"},{"location":"adaptive_kalmanfilter/","page":"Kalman-filter tutorial with LowLevelParticleFilters","title":"Kalman-filter tutorial with LowLevelParticleFilters","text":"See this notebook for a blog post about disturbance modeling and noise tuning using LowLevelParticleFilter.jl","category":"page"},{"location":"dae/#State-estimation-for-high-index-DAEs","page":"State estimation for DAE systems","title":"State estimation for high-index DAEs","text":"","category":"section"},{"location":"dae/","page":"State estimation for DAE systems","title":"State estimation for DAE systems","text":"This tutorial is hosted as a notebook.","category":"page"},{"location":"measurement_models/#measurement_models","page":"Multiple measurement models","title":"Measurement models","text":"","category":"section"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"The Kalman-type filters","category":"page"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"KalmanFilter\nExtendedKalmanFilter\nUnscentedKalmanFilter","category":"page"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"each come with their own built-in measurement model, e.g., the standard KalmanFilter uses the linear measurement model y = Cx + Du + e, while the ExtendedKalmanFilter and UnscentedKalmanFilter use the nonlinear measurement model y = h(xupt) + e or y = h(xupte). For covariance propagation, the ExtendedKalmanFilter uses linearization to approximate the nonlinear measurement model, while the UnscentedKalmanFilter uses the unscented transform.","category":"page"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"It is sometimes useful to mix and match dynamics and measurement models. For example, using the unscented transform from the UKF for the dynamics update (predict!), but the linear measurement model from the standard KalmanFilter for the measurement update (correct!) if the measurement model is linear.","category":"page"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"This is possible by constructing a filter with an explicitly created measurement model. The available measurement models are","category":"page"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"LinearMeasurementModel performs linear propagation of covariance (as is done in KalmanFilter).\nEKFMeasurementModel uses linearization to propagate covariance (as is done in ExtendedKalmanFilter).\nUKFMeasurementModel uses the unscented transform to propagate covariance (as is done in UnscentedKalmanFilter).\nCompositeMeasurementModel combines multiple measurement models.","category":"page"},{"location":"measurement_models/#Constructing-a-filter-with-a-custom-measurement-model","page":"Multiple measurement models","title":"Constructing a filter with a custom measurement model","text":"","category":"section"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"Constructing a Kalman-type filter automatically creates a measurement model of the corresponding type, given the functions/matrices passed to the filter constructor. To construct a filter with a non-standard measurement model, e.g., and UKF with a KF measurement model, manually create the desired measurement model and pass it as the second argument to the constructor. For example, to construct an UKF with a linear measurement model, we do","category":"page"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"using LowLevelParticleFilters, LinearAlgebra\nnx = 100 # Dimension of state\nnu = 2 # Dimension of input\nny = 90 # Dimension of measurements\n\n# Define linear state-space system\nconst __A = 0.1*randn(nx, nx)\nconst __B = randn(nx, nu)\nconst __C = randn(ny,nx)\nfunction dynamics_ip(dx,x,u,p,t)\n # __A*x .+ __B*u\n mul!(dx, __A, x)\n mul!(dx, __B, u, 1.0, 1.0)\n nothing\nend\nfunction measurement_ip(y,x,u,p,t)\n # __C*x\n mul!(y, __C, x)\n nothing\nend\n\nR1 = I(nx)\nR2 = I(ny)\n\nmm_kf = LinearMeasurementModel(__C, 0, R2; nx, ny)\nukf = UnscentedKalmanFilter(dynamics_ip, mm_kf, R1; ny, nu)","category":"page"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"When we create the filter with the custom measurement model, we do not pass the arguments that are associated with the measurement model to the filter constructor, i.e., we do not pass any measurement function, and not the measurement covariance matrix R_2.","category":"page"},{"location":"measurement_models/#Sensor-fusion:-Using-several-different-measurement-models","page":"Multiple measurement models","title":"Sensor fusion: Using several different measurement models","text":"","category":"section"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"Above we constructed a filter with a custom measurement model, we can also pass a custom measurement model when we call correct!. This may be useful when, e.g., performing sensor fusion with sensors operating at different sample rates, or when parts of the measurement model are linear, and other parts are nonlinear.","category":"page"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"The following example instantiates three different filters and three different measurement models. Each filter is updated with each measurement model, demonstrating that any combination of filter and measurement model can be used together.","category":"page"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"using LowLevelParticleFilters, LinearAlgebra\nnx = 10 # Dimension of state\nnu = 2 # Dimension of input\nny = 9 # Dimension of measurements\n\n# Define linear state-space system\nconst __A = 0.1*randn(nx, nx)\nconst __B = randn(nx, nu)\nconst __C = randn(ny,nx)\nfunction dynamics_ip(dx,x,u,p,t)\n # __A*x .+ __B*u\n mul!(dx, __A, x)\n mul!(dx, __B, u, 1.0, 1.0)\n nothing\nend\nfunction measurement_ip(y,x,u,p,t)\n # __C*x\n mul!(y, __C, x)\n nothing\nend\n\nR1 = I(nx) # Covariance matrices\nR2 = I(ny)\n\n# Construct three different filters\nkf = KalmanFilter(__A, __B, __C, 0, R1, R2)\nukf = UnscentedKalmanFilter(dynamics_ip, measurement_ip, R1, R2; ny, nu)\nekf = ExtendedKalmanFilter(dynamics_ip, measurement_ip, R1, R2; nu)\n\n# Simulate some data\nT = 200 # Number of time steps\nU = [randn(nu) for _ in 1:T]\nx,u,y = LowLevelParticleFilters.simulate(kf, U) # Simulate trajectory using the model in the filter\n\n# Construct three different measurement models\nmm_kf = LinearMeasurementModel(__C, 0, R2; nx, ny)\nmm_ekf = EKFMeasurementModel{Float64, true}(measurement_ip, R2; nx, ny)\nmm_ukf = UKFMeasurementModel{Float64, true, false}(measurement_ip, R2; nx, ny)\n\n\nmms = [mm_kf, mm_ekf, mm_ukf]\nfilters = [kf, ekf, ukf]\n\nfor mm in mms, filter in filters\n @info \"Updating $(nameof(typeof(filter))) with measurement model $(nameof(typeof(mm)))\"\n correct!(filter, mm, u[1], y[1]) # Pass the measurement model as the second argument to the correct! function if not using the measurement model built into the filter\nend\nnothing # hide","category":"page"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"Since the dynamics in this particular example is in fact linear, we should get identical results for all three filters.","category":"page"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"using Test\n@test kf.x ≈ ekf.x ≈ ukf.x\n@test kf.R ≈ ekf.R ≈ ukf.R","category":"page"},{"location":"measurement_models/#Video-tutorial","page":"Multiple measurement models","title":"Video tutorial","text":"","category":"section"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"A video demonstrating the use of multiple measurement models in a sensor-fusion context is available on YouTube:","category":"page"},{"location":"measurement_models/","page":"Multiple measurement models","title":"Multiple measurement models","text":"","category":"page"},{"location":"api/#Exported-functions-and-types","page":"API","title":"Exported functions and types","text":"","category":"section"},{"location":"api/#Index","page":"API","title":"Index","text":"","category":"section"},{"location":"api/","page":"API","title":"API","text":"","category":"page"},{"location":"api/","page":"API","title":"API","text":"Modules = [LowLevelParticleFilters]\nPrivate = false","category":"page"},{"location":"api/#LowLevelParticleFilters.AdvancedParticleFilter-Tuple{Integer, Function, Function, Any, Any, Any}","page":"API","title":"LowLevelParticleFilters.AdvancedParticleFilter","text":"AdvancedParticleFilter(N::Integer, dynamics::Function, measurement::Function, measurement_likelihood, dynamics_density, initial_density; p = NullParameters(), threads = false, kwargs...)\n\nThis type represents a standard particle filter but affords extra flexibility compared to the ParticleFilter type, e.g., non-additive noise in the dynamics and measurement functions.\n\nSee the docs for more information: https://baggepinnen.github.io/LowLevelParticleFilters.jl/stable/#AdvancedParticleFilter-1\n\nArguments:\n\nN: Number of particles\ndynamics: A discrete-time dynamics function (x, u, p, t, noise=false) -> x⁺. It's important that the noise argument defaults to false.\nmeasurement: A measurement function (x, u, p, t, noise=false) -> y. It's important that the noise argument defaults to false.\nmeasurement_likelihood: A function (x, u, y, p, t)->logl to evaluate the log-likelihood of a measurement.\ndynamics_density: This field is not used by the advanced filter and can be set to nothing.\ninitial_density: The distribution of the initial state.\nthreads: use threads to propagate particles in parallel. Only activate this if your dynamics is thread-safe. SeeToDee.SimpleColloc is not thread-safe by default due to the use of internal caches, but SeeToDee.Rk4 is.\n\nExtended help\n\nMultiple measurement models\n\nThe measurement_likelihood function is used to evaluate the likelihood of a measurement. If you have multiple sensors and want to perform individual correct! steps for each, call correct!(..., g = custom_likelihood_function).\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.AuxiliaryParticleFilter-Tuple","page":"API","title":"LowLevelParticleFilters.AuxiliaryParticleFilter","text":"AuxiliaryParticleFilter(args...; kwargs...)\n\nTakes exactly the same arguments as ParticleFilter, or an instance of ParticleFilter.\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.CompositeMeasurementModel-Tuple{Any, Vararg{Any}}","page":"API","title":"LowLevelParticleFilters.CompositeMeasurementModel","text":"CompositeMeasurementModel(model1, model2, ...)\n\nA composite measurement model that combines multiple measurement models. This model acts as all component models concatenated. The tuple returned from correct! will be\n\nll: The sum of the log-likelihood of all component models\ne: The concatenated innovation vector\nS: A vector of the innovation covariance matrices of the component models\nSᵪ: A vector of the Cholesky factorizations of the innovation covariance matrices of the component models\nK: A vector of the Kalman gains of the component models\n\nIf all sensors operate on at the same rate, and all measurement models are of the same type, it's more efficient to use a single measurement model with a vector-valued measurement function.\n\nFields:\n\nmodels: A tuple of measurement models\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.EKFMeasurementModel-Union{Tuple{IPM}, NTuple{4, Any}, NTuple{5, Any}} where IPM","page":"API","title":"LowLevelParticleFilters.EKFMeasurementModel","text":"EKFMeasurementModel{IPM}(measurement, R2, ny, Cjac, cache = nothing)\n\nA measurement model for the Extended Kalman Filter.\n\nArguments:\n\nIPM: A boolean indicating if the measurement function is inplace\nmeasurement: The measurement function y = h(x, u, p, t)\nR2: The measurement noise covariance matrix\nny: The number of measurement variables\nCjac: The Jacobian of the measurement function Cjac(x, u, p, t). If none is provided, ForwardDiff will be used.\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.EKFMeasurementModel-Union{Tuple{M}, Tuple{IPM}, Tuple{T}, Tuple{M, Any}} where {T, IPM, M}","page":"API","title":"LowLevelParticleFilters.EKFMeasurementModel","text":"EKFMeasurementModel{T,IPM}(measurement::M, R2; nx, ny, Cjac = nothing)\n\nT is the element type used for arrays\nIPM is a boolean indicating if the measurement function is inplace\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.ExtendedKalmanFilter","page":"API","title":"LowLevelParticleFilters.ExtendedKalmanFilter","text":"ExtendedKalmanFilter(kf, dynamics, measurement; Ajac, Cjac)\nExtendedKalmanFilter(dynamics, measurement, R1,R2,d0=MvNormal(Matrix(R1)); nu::Int, p = NullParameters(), α = 1.0, check = true)\n\nA nonlinear state estimator propagating uncertainty using linearization.\n\nThe constructor to the extended Kalman filter takes dynamics and measurement functions, and either covariance matrices, or a KalmanFilter. If the former constructor is used, the number of inputs to the system dynamics, nu, must be explicitly provided with a keyword argument.\n\nBy default, the filter will internally linearize the dynamics using ForwardDiff. User provided Jacobian functions can be provided as keyword arguments Ajac and Cjac. These functions should have the signature (x,u,p,t)::AbstractMatrix where x is the state, u is the input, p is the parameters, and t is the time.\n\nThe dynamics and measurement function are on the following form\n\nx(t+1) = dynamics(x, u, p, t) + w\ny = measurement(x, u, p, t) + e\n\nwhere w ~ N(0, R1), e ~ N(0, R2) and x(0) ~ d0\n\nSee also UnscentedKalmanFilter which is typically more accurate than ExtendedKalmanFilter. See KalmanFilter for detailed instructions on how to set up a Kalman filter kf.\n\n\n\n\n\n","category":"type"},{"location":"api/#LowLevelParticleFilters.IMM-Tuple{Any, AbstractMatrix, AbstractVector}","page":"API","title":"LowLevelParticleFilters.IMM","text":"IMM(models, P, μ; check = true, p = NullParameters(), interact = true)\n\nInteracting Multiple Model (IMM) filter. This filter is a combination of multiple Kalman-type filters, each with its own state and covariance. The IMM filter is a probabilistically weighted average of the states and covariances of the individual filters. The weights are determined by the probability matrix P and the mixing probabilities μ.\n\nwarning: Experimental\nThis filter is currently considered experimental and the user interface may change in the future without respecting semantic versioning.\n\nIn addition to the predict! and correct! steps, the IMM filter has an interact! method that updates the states and covariances of the individual filters based on the mixing probabilities. The combine! method combines the states and covariances of the individual filters into a single state and covariance. These four functions are typically called in either of the orders\n\ncorrect!, combine!, interact!, predict! (as is done in update!)\ninteract!, predict!, correct!, combine! (as is done in the reference cited below)\n\nThese two orders are cyclic permutations of each other, and the order used in update! is chosen to align with the order used in the other filters, where the initial condition is corrected using the first measurement, i.e., we assume the first measurement updates x(0-1) to x(00).\n\nThe initial (combined) state and covariance of the IMM filter is made up of the weighted average of the states and covariances of the individual filters. The weights are the initial mixing probabilities μ.\n\nRef: \"Interacting multiple model methods in target tracking: a survey\", E. Mazor; A. Averbuch; Y. Bar-Shalom; J. Dayan\n\nArguments:\n\nmodels: An array of Kalman-type filters, such as KalmanFilter, ExtendedKalmanFilter, UnscentedKalmanFilter, etc. The state of each model must have the same meaning, such that forming a weighted average makes sense.\nP: The mode-transition probability matrix. P[i,j] is the probability of transitioning from mode i to mode j (each row must sum to one).\nμ: The initial mixing probabilities. μ[i] is the probability of being in mode i at the initial contidion (must sum to one).\ncheck: If true, check that the inputs are valid. If false, skip the checks.\np: Parameters for the filter. NOTE: this p is shared among all internal filters. The internal p of each filter will be overridden by this one.\ninteract: If true, the filter will run the interaction as part of update! and forward_trajectory. If false, the filter will not run the interaction step. This choice can be overridden by passing the keyword argument interact to the respective functions.\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.KalmanFilter","page":"API","title":"LowLevelParticleFilters.KalmanFilter","text":"KalmanFilter(A,B,C,D,R1,R2,d0=MvNormal(R1); p = NullParameters(), α=1, check=true)\n\nThe matrices A,B,C,D define the dynamics\n\nx' = Ax + Bu + w\ny = Cx + Du + e\n\nwhere w ~ N(0, R1), e ~ N(0, R2) and x(0) ~ d0\n\nThe matrices can be time varying such that, e.g., A[:, :, t] contains the A matrix at time index t. They can also be given as functions on the form\n\nAfun(x, u, p, t) -> A\n\nFor maximum performance, provide statically sized matrices from StaticArrays.jl\n\nα is an optional \"forgetting factor\", if this is set to a value > 1, such as 1.01-1.2, the filter will, in addition to the covariance inflation due to R_1, exhibit \"exponential forgetting\" similar to a Recursive Least-Squares (RLS) estimator. It is thus possible to get a RLS-like algorithm by setting R_1=0 R_2 = 1α and α 1 (α is the inverse of the traditional RLS parameter α = 1λ). The exact form of the covariance update is\n\nR(t+1t) = α AR(t)A^T + R_1\n\nIf check = true (default) the function will check that the eigenvalues of A are less than 2 in absolute value. Large eigenvalues may be an indication that the system matrices are representing a continuous-time system and the user has forgotten to discretize it. Turn off this check by setting check = false.\n\nTutorials on Kalman filtering\n\nThe tutorial \"How to tune a Kalman filter\" details how to figure out appropriate covariance matrices for the Kalman filter, as well as how to add disturbance models to the system model. See also the tutorial in the documentation\n\n\n\n\n\n","category":"type"},{"location":"api/#LowLevelParticleFilters.KalmanFilteringSolution","page":"API","title":"LowLevelParticleFilters.KalmanFilteringSolution","text":"KalmanFilteringSolution{Tx,Txt,TR,TRt,Tll} <: AbstractFilteringSolution\n\nFields\n\nx: predictions x(t+1t) (plotted if plotx=true)\nxt: filtered estimates x(tt) (plotted if plotxt=true)\nR: predicted covariance matrices R(t+1t) (plotted if plotR=true)\nRt: filter covariances R(tt) (plotted if plotRt=true)\nll: loglikelihood\ne: prediction errors e(tt-1) = y - y(tt-1) (plotted if plote=true)\n\nPlot\n\nThe solution object can be plotted\n\nplot(sol, plotx=true, plotxt=true, plotR=true, plotRt=true, plote=true, plotu=true, ploty=true, plotyh=true, plotyht=true, name=\"\")\n\nwhere\n\nplotx: Plot the predictions x(t|t-1)\nplotxt: Plot the filtered estimates x(t|t)\nplotR: Plot the predicted covariances R(t|t-1) as ribbons at ±2σ (1.96 σ to be precise)\nplotRt: Plot the filter covariances R(t|t) as ribbons at ±2σ (1.96 σ to be precise)\nplote: Plot the prediction errors e(t|t-1) = y - ŷ(t|t-1)\nplotu: Plot the input\nploty: Plot the measurements\nplotyh: Plot the predicted measurements ŷ(t|t-1)\nplotyht: Plot the filtered measurements ŷ(t|t)\nname: a string that is prepended to the labels of the plots, which is useful when plotting multiple solutions in the same plot.\n\n\n\n\n\n","category":"type"},{"location":"api/#LowLevelParticleFilters.LinearMeasurementModel","page":"API","title":"LowLevelParticleFilters.LinearMeasurementModel","text":"LinearMeasurementModel{CT, DT, RT, CAT}\n\nA linear measurement model y = C*x + D*u + e.\n\nFields:\n\nC \nD\nR2: The measurement noise covariance matrix\nny: The number of measurement variables\n\n\n\n\n\n","category":"type"},{"location":"api/#LowLevelParticleFilters.ParticleFilter-Tuple{Integer, Function, Function, Any, Any, Any}","page":"API","title":"LowLevelParticleFilters.ParticleFilter","text":"ParticleFilter(N::Integer, dynamics, measurement, dynamics_density, measurement_density, initial_density; threads = false, p = NullParameters(), kwargs...)\n\nSee the docs for more information: https://baggepinnen.github.io/LowLevelParticleFilters.jl/stable/#Particle-filter-1\n\nArguments:\n\nN: Number of particles\ndynamics: A discrete-time dynamics function (x, u, p, t) -> x⁺\nmeasurement: A measurement function (x, u, p, t) -> y\ndynamics_density: A probability-density function for additive noise in the dynamics. Use AdvancedParticleFilter for non-additive noise.\nmeasurement_density: A probability-density function for additive measurement noise. Use AdvancedParticleFilter for non-additive noise.\ninitial_density: Distribution of the initial state.\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.ParticleFilteringSolution","page":"API","title":"LowLevelParticleFilters.ParticleFilteringSolution","text":"ParticleFilteringSolution{F, Tu, Ty, Tx, Tw, Twe, Tll} <: AbstractFilteringSolution\n\nFields:\n\nf: The filter used to produce the solution.\nu: Input\ny: Output / measurements\nx: Particles, the size of this array is (N,T), where N is the number of particles and T is the number of time steps. Each element represents a weighted state hypothesis with weight given by we.\nw: Weights (log space). These are used for internal computations.\nwe: Weights (exponentiated / original space). These are the ones to use to compute weighted means etc., they sum to one for each time step.\nll: Log likelihood\n\nPlot\n\nThe solution object can be plotted\n\nplot(sol; nbinsy=30, xreal=nothing, dim=nothing, ploty=true)\n\n\n\n\n\n","category":"type"},{"location":"api/#LowLevelParticleFilters.SqKalmanFilter","page":"API","title":"LowLevelParticleFilters.SqKalmanFilter","text":"SqKalmanFilter(A,B,C,D,R1,R2,d0=MvNormal(R1); p = NullParameters(), α=1)\n\nA standard Kalman filter on square-root form. This filter may have better numerical performance when the covariance matrices are ill-conditioned.\n\nThe matrices A,B,C,D define the dynamics\n\nx' = Ax + Bu + w\ny = Cx + Du + e\n\nwhere w ~ N(0, R1), e ~ N(0, R2) and x(0) ~ d0\n\nThe matrices can be time varying such that, e.g., A[:, :, t] contains the A matrix at time index t. They can also be given as functions on the form\n\nAfun(x, u, p, t) -> A\n\nThe internal fields storing covariance matrices are for this filter storing the upper-triangular Cholesky factor.\n\nα is an optional \"forgetting factor\", if this is set to a value > 1, such as 1.01-1.2, the filter will, in addition to the covariance inflation due to R_1, exhibit \"exponential forgetting\" similar to a Recursive Least-Squares (RLS) estimator. It is thus possible to get a RLS-like algorithm by setting R_1=0 R_2 = 1α and α 1 (α is the inverse of the traditional RLS parameter α = 1λ). The form of the covariance update is\n\nR(t+1t) = α AR(t)A^T + R_1\n\nRef: \"A Square-Root Kalman Filter Using Only QR Decompositions\", Kevin Tracy https://arxiv.org/abs/2208.06452\n\n\n\n\n\n","category":"type"},{"location":"api/#LowLevelParticleFilters.UKFMeasurementModel-Union{Tuple{AUGM}, Tuple{IPM}, NTuple{8, Any}, NTuple{9, Any}} where {IPM, AUGM}","page":"API","title":"LowLevelParticleFilters.UKFMeasurementModel","text":"UKFMeasurementModel{inplace_measurement,augmented_measurement}(measurement, R2, ny, ne, innovation, mean, cov, cross_cov, cache = nothing)\n\nA measurement model for the Unscented Kalman Filter.\n\nArguments:\n\nmeasurement: The measurement function y = h(x, u, p, t)\nR2: The measurement noise covariance matrix\nny: The number of measurement variables\nne: If augmented_measurement is true, the number of measurement noise variables\ninnovation(y::AbstractVector, yh::AbstractVector) where the arguments represent (measured output, predicted output)\nmean(ys::AbstractVector{<:AbstractVector}): computes the mean of the vector of vectors of output sigma points.\ncov(ys::AbstractVector{<:AbstractVector}, y::AbstractVector): computes the covariance matrix of the output sigma points.\ncross_cov(xs::AbstractVector{<:AbstractVector}, x::AbstractVector, ys::AbstractVector{<:AbstractVector}, y::AbstractVector) where the arguments represents (state sigma points, mean state, output sigma points, mean output). The function should return the cross-covariance matrix between the state and output sigma points.\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.UKFMeasurementModel-Union{Tuple{AUGM}, Tuple{IPM}, Tuple{T}, Tuple{Any, Any}} where {T, IPM, AUGM}","page":"API","title":"LowLevelParticleFilters.UKFMeasurementModel","text":"UKFMeasurementModel{T,IPM,AUGM}(measurement, R2; nx, ny, ne = nothing, innovation = -, mean = safe_mean, cov = safe_cov, cross_cov = cross_cov, static = nothing)\n\nT is the element type used for arrays\nIPM is a boolean indicating if the measurement function is inplace\nAUGM is a boolean indicating if the measurement model is augmented\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.UnscentedKalmanFilter-Union{Tuple{AUGM}, Tuple{AUGD}, Tuple{IPM}, Tuple{IPD}, Tuple{Any, LowLevelParticleFilters.AbstractMeasurementModel, Any}, Tuple{Any, LowLevelParticleFilters.AbstractMeasurementModel, Any, Any}} where {IPD, IPM, AUGD, AUGM}","page":"API","title":"LowLevelParticleFilters.UnscentedKalmanFilter","text":"UnscentedKalmanFilter(dynamics, measurement, R1, R2, d0=MvNormal(Matrix(R1)); p = NullParameters(), ny, nu)\nUnscentedKalmanFilter{IPD,IPM,AUGD,AUGM}(dynamics, measurement_model::AbstractMeasurementModel, R1, d0=SimpleMvNormal(R1); p=NullParameters(), nu)\n\nA nonlinear state estimator propagating uncertainty using the unscented transform.\n\nThe dynamics and measurement function are on either of the following forms\n\nx' = dynamics(x, u, p, t) + w\ny = measurement(x, u, p, t) + e\n\nx' = dynamics(x, u, p, t, w)\ny = measurement(x, u, p, t, e)\n\nwhere w ~ N(0, R1), e ~ N(0, R2) and x(0) ~ d0. The former (default) assums that the noise is additive and added after the dynamics and measurement updates, while the latter assumes that the dynamics functions take an additional argument corresponding to the noise term. The latter form (sometimes refered to as the \"augmented\" form) is useful when the noise is multiplicative or when the noise is added before the dynamics and measurement updates. See \"Augmented UKF\" below for more details on how to use this form.\n\nThe matrices R1, R2 can be time varying such that, e.g., R1[:, :, t] contains the R_1 matrix at time index t. They can also be given as functions on the form\n\nRfun(x, u, p, t) -> R\n\nFor maximum performance, provide statically sized matrices from StaticArrays.jl\n\nny, nu indicate the number of outputs and inputs.\n\nCustom type of u\n\nThe input u may be of any type, e.g., a named tuple or a custom struct. The u provided in the input data is passed directly to the dynamics and measurement functions, so as long as the type is compatible with the dynamics it will work out. The one exception where this will not work is when calling simulate, which assumes that u is an array.\n\nAugmented UKF\n\nIf the noise is not additive, one may use the augmented form of the UKF. In this form, the dynamics functions take additional input arguments that correspond to the noise terms. To enable this form, the typed constructor\n\nUnscentedKalmanFilter{inplace_dynamics,inplace_measurement,augmented_dynamics,augmented_measurement}(...)\n\nis used, where the Boolean type parameters have the following meaning\n\ninplace_dynamics: If true, the dynamics function operates in-place, i.e., it modifies the first argument in dynamics(dx, x, u, p, t). Default is false.\ninplace_measurement: If true, the measurement function operates in-place, i.e., it modifies the first argument in measurement(y, x, u, p, t). Default is false.\naugmented_dynamics: If true the dynamics function is augmented with an additional noise input w, i.e., dynamics(x, u, p, t, w). Default is false.\naugmented_measurement: If true the measurement function is agumented with an additional noise input e, i.e., measurement(x, u, p, t, e). Default is false. (If the measurement noise has fewer degrees of freedom than the number of measurements, you may failure in Cholesky factorizations, see \"Custom Cholesky factorization\" below).\n\nUse of augmented dynamics incurs extra computational cost. The number of sigma points used is 2L+1 where L is the length of the augmented state vector. Without augmentation, L = nx, with augmentation L = nx + nw and L = nx + ne for dynamics and measurement, respectively.\n\nSigma-point rejection\n\nFor problems with challenging dynamics, a mechanism for rejection of sigma points after the dynamics update is provided. A function reject(x) -> Bool can be provided through the keyword argument reject that returns true if a sigma point for x(t+1) should be rejected, e.g., if an instability or non-finite number is detected. A rejected point is replaced by the propagated mean point (the mean point cannot be rejected). This function may be provided either to the constructor of the UKF or passed to the predict! function.\n\nCustom measurement models\n\nBy default, standard arithmetic mean and e(y, yh) = y - yh are used as mean and innovation functions.\n\nBy passing and explicitly created UKFMeasurementModel, one may provide custom functions that compute the mean, the covariance and the innovation. This is useful in situations where the state or a measurement lives on a manifold. One may further override the mean and covariance functions for the state sigma points by passing the keyword arguments state_mean and state_cov to the constructor.\n\nstate_mean(xs::AbstractVector{<:AbstractVector}) computes the mean of the vector of vectors of state sigma points.\nstate_cov(xs::AbstractVector{<:AbstractVector}, m = mean(xs)) where the first argument represent state sigma points and the second argument, which must be optional, represents the mean of those points. The function should return the covariance matrix of the state sigma points.\n\nSee UKFMeasurementModel for more details on how to set up a custom measurement model. Pass the custom measurement model as the second argument to the UKF constructor.\n\nCustom Cholesky factorization\n\nThe UnscentedKalmanFilter supports providing a custom function to compute the Cholesky factorization of the covariance matrices for use in sigma-point generation.\n\nIf either of the following conditions are met, you may experience failure in internal Cholesky factorizations:\n\nThe dynamics noise or measurement noise covariance matrices (R_1 R_2) are singular\nThe measurement is augmented and the measurement noise has fewer degrees of freedom than the number of measurements\n(Under specific technical conditions) The dynamics is augmented and the dynamics noise has fewer degrees of freedom than the number of state variables. The technical conditions are easiest to understand in the linear-systems case, where it corresponds to the Riccati equation associated with the Kalman gain not having a solution. This may happen when the pair (A R1) has uncontrollable modes on the unit circle, for example, when there are integrating modes that are not affected through the noise.\n\nThe error message may look like\n\nERROR: PosDefException: matrix is not positive definite; Factorization failed.\n\nIn such situations, it is advicable to reconsider the noise model and covariance matrices, alternatively, you may provide a custom Cholesky factorization function to the UKF constructor through the keyword argument cholesky!. The function should have the signature cholesky!(A::AbstractMatrix)::Cholesky. A useful alternative factorizaiton when covariance matrices are expected to be singular is cholesky! = R->cholesky!(Positive, Matrix(R)) where the \"positive\" Cholesky factorization is provided by the package PositiveFactorizations.jl, which must be manually installed and loaded by the user.\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.combine!-Tuple{IMM}","page":"API","title":"LowLevelParticleFilters.combine!","text":"combine!(imm::IMM)\n\nCombine the models of the IMM filter into a single state imm.x and covariance imm.R. This is done by taking a weighted average of the states and covariances of the individual models, where the weights are the mixing probabilities μ.\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.commandplot","page":"API","title":"LowLevelParticleFilters.commandplot","text":"commandplot(pf, u, y, p=parameters(pf); kwargs...)\n\nProduce a helpful plot. For customization options (kwargs...), see ?pplot. After each time step, a command from the user is requested.\n\nq: quit\ns n: step n steps\n\nnote: Note\nThis function requires using Plots to be called before it is used.\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.correct!","page":"API","title":"LowLevelParticleFilters.correct!","text":"(; ll, e, S, Sᵪ, K) = correct!(kf::AbstractKalmanFilter, u, y, p = parameters(kf), t::Integer = index(kf), R2)\n\nThe correct step for a Kalman filter returns not only the log likelihood ll and the prediction error e, but also the covariance of the output S, its Cholesky factor Sᵪ and the Kalman gain K.\n\nIf R2 stored in kf is a function R2(x, u, p, t), this function is evaluated at the state before the correction is performed. The measurement noise covariance matrix R2 stored in the filter object can optionally be overridden by passing the argument R2, in this case R2 must be a matrix.\n\nExtended help\n\nTo perform separate measurement updates for different sensors, see the \"Measurement models\" in the documentation.\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.correct!-2","page":"API","title":"LowLevelParticleFilters.correct!","text":"ll, e = correct!(pf, u, y, p = parameters(f), t = index(f))\n\nUpdate state/weights based on measurement y, returns log-likelihood and prediction error (the error is always 0 for particle filters).\n\nExtended help\n\nTo perform separate measurement updates for different sensors, see the \"Measurement models\" in the documentation. For AdvancedParticleFilter, this can be realized by passing a custom measurement_likelihood function as the keyword argument g to correct!, or by calling the lower-level function measurement_equation! with a custom measurement_likelihood.\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.correct!-3","page":"API","title":"LowLevelParticleFilters.correct!","text":"correct!(kf::SqKalmanFilter, u, y, p = parameters(kf), t::Real = index(kf); R2 = get_mat(kf.R2, kf.x, u, p, t))\n\nFor the square-root Kalman filter, a custom provided R2 must be the upper triangular Cholesky factor of the covariance matrix of the measurement noise.\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.correct!-Tuple{IMM, Any, Any, Vararg{Any}}","page":"API","title":"LowLevelParticleFilters.correct!","text":"ll, lls, rest = correct!(imm::IMM, u, y, args; kwargs)\n\nThe correct step of the IMM filter corrects each model with the measurements y and control input u. The mixing probabilities imm.μ are updated based on the likelihood of each model given the measurements and the transition probability matrix P.\n\nThe returned tuple consists of the sum of the log-likelihood of all models, the vector of individual log-likelihoods and an array of the rest of the return values from the correct step of each model.\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.correct!-Tuple{UnscentedKalmanFilter, Any, Any, Any, Real}","page":"API","title":"LowLevelParticleFilters.correct!","text":"correct!(ukf::UnscentedKalmanFilter{IPD, IPM, AUGD, AUGM}, u, y, p = parameters(ukf), t::Real = index(ukf) * ukf.Ts; R2 = get_mat(ukf.R2, ukf.x, u, p, t), mean, cross_cov, innovation)\n\nThe correction step for an UnscentedKalmanFilter allows the user to override, R2, mean, cross_cov, innovation.\n\nArguments:\n\nu: The input\ny: The measurement\np: The parameters\nt: The current time\nR2: The measurement noise covariance matrix, or a function that returns the covariance matrix (x,u,p,t)->R2.\nmean: The function that computes the mean of the output sigma points.\ncross_cov: The function that computes the cross-covariance of the state and output sigma points.\ninnovation: The function that computes the innovation between the measured output and the predicted output.\n\nExtended help\n\nTo perform separate measurement updates for different sensors, see the \"Measurement models\" in the documentation\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.debugplot","page":"API","title":"LowLevelParticleFilters.debugplot","text":"debugplot(pf, u, y, p=parameters(pf); runall=false, kwargs...)\n\nProduce a helpful plot. For customization options (kwargs...), see ?pplot.\n\nrunall=false: if true, runs all time steps befor displaying (faster), if false, displays the plot after each time step.\n\nThe generated plot becomes quite heavy. Initially, try limiting your input to 100 time steps to verify that it doesn't crash.\n\nnote: Note\nThis function requires using Plots to be called before it is used.\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.densityplot","page":"API","title":"LowLevelParticleFilters.densityplot","text":"densityplot(x,[w])\n\nPlot (weighted) particles densities\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.forward_trajectory","page":"API","title":"LowLevelParticleFilters.forward_trajectory","text":"sol = forward_trajectory(kf::AbstractKalmanFilter, u::Vector, y::Vector, p=parameters(kf))\n\nRun a Kalman filter forward to perform (offline / batch) filtering along an entire trajectory u, y.\n\nReturns a KalmanFilteringSolution: with the following\n\nx: predictions x(tt-1)\nxt: filtered estimates x(tt)\nR: predicted covariance matrices R(tt-1)\nRt: filter covariances R(tt)\nll: loglik\n\nsol can be plotted\n\nplot(sol::KalmanFilteringSolution; plotx = true, plotxt=true, plotu=true, ploty=true)\n\nSee KalmanFilteringSolution for more details.\n\nExtended help\n\nVery large systems\n\nIf your system is very large, i.e., the dimension of the state is very large, and the arrays u,y are long, this function may use a lot of memory to store all covariance matrices R, Rt. If you do not need all the information retained by this function, you may opt to call one of the functions\n\nloglik\nLowLevelParticleFilters.sse\nLowLevelParticleFilters.prediction_errors!\n\nThat store significantly less information. The amount of computation performed by all of these functions is identical, the only difference lies in what is stored and returned.\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.forward_trajectory-2","page":"API","title":"LowLevelParticleFilters.forward_trajectory","text":"sol = forward_trajectory(pf, u::AbstractVector, y::AbstractVector, p=parameters(pf))\n\nRun the particle filter for a sequence of inputs and measurements (offline / batch filtering). Return a solution with x,w,we,ll = particles, weights, expweights and loglikelihood\n\nIf MonteCarloMeasurements.jl is loaded, you may transform the output particles to Matrix{MonteCarloMeasurements.Particles} using Particles(x,we). Internally, the particles are then resampled such that they all have unit weight. This is conventient for making use of the plotting facilities of MonteCarloMeasurements.jl.\n\nsol can be plotted\n\nplot(sol::ParticleFilteringSolution; nbinsy=30, xreal=nothing, dim=nothing)\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.forward_trajectory-3","page":"API","title":"LowLevelParticleFilters.forward_trajectory","text":"forward_trajectory(imm::IMM, u, y, p = parameters(imm); interact = true)\n\nWhen performing batch filtering using an IMM filter, one may\n\nOverride the interact parameter of the filter\nAccess the mode probabilities along the trajectory as the sol.extra field. This is a matrix of size (n_modes, T) where T is the length of the trajectory (length of u and y).\n\nThe returned solution object is of type KalmanFilteringSolution and has the following fields:\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.interact!-Tuple{IMM}","page":"API","title":"LowLevelParticleFilters.interact!","text":"interact!(imm::IMM)\n\nThe interaction step of the IMM filter updates the state and covariance of each internal model based on the mixing probabilities imm.μ and the transition probability matrix imm.P.\n\nModels with small mixing probabilities will have their states and covariances updated more towards the states and covariances of models with higher mixing probabilities, and vice versa.\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.log_likelihood_fun-Tuple{Any, AbstractVector, Any, Any, Any}","page":"API","title":"LowLevelParticleFilters.log_likelihood_fun","text":"ll(θ) = log_likelihood_fun(filter_from_parameters(θ::Vector)::Function, priors::Vector{Distribution}, u, y, p)\n\nreturns function θ -> p(y|θ)p(θ)\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.loglik","page":"API","title":"LowLevelParticleFilters.loglik","text":"ll = loglik(filter, u, y, p=parameters(filter))\n\nCalculate log-likelihood for entire sequences u,y\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.logsumexp!","page":"API","title":"LowLevelParticleFilters.logsumexp!","text":"ll = logsumexp!(w, we [, maxw])\n\nNormalizes the weight vector w and returns the weighted log-likelihood\n\nhttps://arxiv.org/pdf/1412.8695.pdf eq 3.8 for p(y) https://discourse.julialang.org/t/fast-logsumexp/22827/7?u=baggepinnen for stable logsumexp\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.mean_trajectory-Tuple{Any, Vector, Vector}","page":"API","title":"LowLevelParticleFilters.mean_trajectory","text":"x,ll = mean_trajectory(pf, u::Vector{Vector}, y::Vector{Vector}, p=parameters(pf))\n\nThis method resets the particle filter to the initial state distribution upon start\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.mean_trajectory-Tuple{ParticleFilteringSolution}","page":"API","title":"LowLevelParticleFilters.mean_trajectory","text":"mean_trajectory(sol::ParticleFilteringSolution)\nmean_trajectory(x::AbstractMatrix, we::AbstractMatrix)\n\nCompute the weighted mean along the trajectory of a particle-filter solution. Returns a matrix of size T × nx. If x and we are supplied, the weights are expected to be in the original space (not log space).\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.metropolis","page":"API","title":"LowLevelParticleFilters.metropolis","text":"metropolis(ll::Function(θ), R::Int, θ₀::Vector, draw::Function(θ) = naive_sampler(θ₀))\n\nPerforms MCMC sampling using the marginal Metropolis (-Hastings) algorithm draw = θ -> θ' samples a new parameter vector given an old parameter vector. The distribution must be symmetric, e.g., a Gaussian. R is the number of iterations. See log_likelihood_fun\n\nExample:\n\nfilter_from_parameters(θ) = ParticleFilter(N, dynamics, measurement, MvNormal(n,exp(θ[1])), MvNormal(p,exp(θ[2])), d0)\npriors = [Normal(0,0.1),Normal(0,0.1)]\nll = log_likelihood_fun(filter_from_parameters,priors,u,y,1)\nθ₀ = log.([1.,1.]) # Initial point\ndraw = θ -> θ .+ rand(MvNormal(0.1ones(2))) # Function that proposes new parameters (has to be symmetric)\nburnin = 200 # If using threaded call, provide number of burnin iterations\n# @time theta, lls = metropolis(ll, 2000, θ₀, draw) # Run single threaded\n# thetam = reduce(hcat, theta)'\n@time thetalls = LowLevelParticleFilters.metropolis_threaded(burnin, ll, 5000, θ₀, draw) # run on all threads, will provide (2000-burnin)*nthreads() samples\nhistogram(exp.(thetalls[:,1:2]), layout=3)\nplot!(thetalls[:,3], subplot=3) # if threaded call, log likelihoods are in the last column\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.reset!-Tuple{LowLevelParticleFilters.AbstractKalmanFilter}","page":"API","title":"LowLevelParticleFilters.reset!","text":"reset!(kf::AbstractKalmanFilter; x0)\n\nReset the initial distribution of the state. Optionally, a new mean vector x0 can be provided.\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.reset!-Tuple{LowLevelParticleFilters.AbstractParticleFilter}","page":"API","title":"LowLevelParticleFilters.reset!","text":"Reset the filter to initial state and covariance/distribution\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.reset!-Tuple{SqKalmanFilter}","page":"API","title":"LowLevelParticleFilters.reset!","text":"reset!(kf::SqKalmanFilter; x0)\n\nReset the initial distribution of the state. Optionally, a new mean vector x0 can be provided.\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.simulate","page":"API","title":"LowLevelParticleFilters.simulate","text":"x,u,y = simulate(f::AbstractFilter, T::Int, du::Distribution, p=parameters(f), [N]; dynamics_noise=true, measurement_noise=true)\nx,u,y = simulate(f::AbstractFilter, u, p=parameters(f); dynamics_noise=true, measurement_noise=true)\n\nSimulate dynamical system forward in time T steps, or for the duration of u. Returns state sequence, inputs and measurements.\n\nu is an input-signal trajectory, alternatively, du is a distribution of random inputs.\n\nA simulation can be considered a draw from the prior distribution over the evolution of the system implied by the selected noise models. Such a simulation is useful in order to evaluate whether or not the noise models are reasonable.\n\nIf MonteCarloMeasurements.jl is loaded, the argument N::Int can be supplied, in which case N simulations are done and the result is returned in the form of Vector{MonteCarloMeasurements.Particles}.\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.smooth","page":"API","title":"LowLevelParticleFilters.smooth","text":"xT,RT,ll = smooth(sol, kf)\nxT,RT,ll = smooth(kf::KalmanFilter, u::Vector, y::Vector, p=parameters(kf))\nxT,RT,ll = smooth(kf::ExtendedKalmanFilter, u::Vector, y::Vector, p=parameters(kf))\n\nReturns smoothed estimates of state x and covariance R given all input output data u,y or an existing solution sol obtained from forward_trajectory.\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.smooth-2","page":"API","title":"LowLevelParticleFilters.smooth","text":"xb,ll = smooth(pf, M, u, y, p=parameters(pf))\nxb,ll = smooth(pf, xf, wf, wef, ll, M, u, y, p=parameters(pf))\n\nPerform particle smoothing using forward-filtering, backward simulation. Return smoothed particles and loglikelihood. See also smoothed_trajs, smoothed_mean, smoothed_cov\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.smoothed_cov-Tuple{Any}","page":"API","title":"LowLevelParticleFilters.smoothed_cov","text":"smoothed_cov(xb)\n\nHelper function to calculate the covariance of smoothed particle trajectories\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.smoothed_mean-Tuple{Any}","page":"API","title":"LowLevelParticleFilters.smoothed_mean","text":"smoothed_mean(xb)\n\nHelper function to calculate the mean of smoothed particle trajectories\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.smoothed_trajs-Tuple{Any}","page":"API","title":"LowLevelParticleFilters.smoothed_trajs","text":"smoothed_trajs(xb)\n\nHelper function to get particle trajectories as a 3-dimensions array (N,M,T) instead of matrix of vectors.\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.update!","page":"API","title":"LowLevelParticleFilters.update!","text":"ll, e = update!(f::AbstractFilter, u, y, p = parameters(f), t = index(f))\n\nPerform one step of predict! and correct!, returns log-likelihood and prediction error\n\n\n\n\n\n","category":"function"},{"location":"api/#LowLevelParticleFilters.update!-Tuple{IMM, Any, Any, Vararg{Any}}","page":"API","title":"LowLevelParticleFilters.update!","text":"update!(imm::IMM, u, y, p, t; correct_kwargs = (;), predict_kwargs = (;), interact = true)\n\nThe combined udpate for an IMM filter performs the following steps:\n\nCorrect each model with the measurements y and control input u.\nCombine the models into a single state and covariance.\nInteract the models to update their respective state and covariance.\nPredict each model to the next time step.\n\nThis differs slightly from the udpate step of other filters, where at the end of an update the state of the filter is the one-step ahead predicted value, whereas here each individual filter has a predicted state, but the combine! step of the IMM filter hasn't been performed on the predictions yet. The state of the IMM filter is thus x(tt) and not x(t+1t) like it is for other filters, and each filter internal to the IMM.\n\nArguments:\n\ncorrect_kwargs: An optional named tuple of keyword arguments that are sent to correct!.\npredict_kwargs: An optional named tuple of keyword arguments that are sent to predict!.\ninteract: Whether or not to run the interaction step.\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.weighted_cov-Tuple{Any, Any}","page":"API","title":"LowLevelParticleFilters.weighted_cov","text":"weighted_cov(x,we)\n\nSimilar to weighted_mean, but returns covariances\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.weighted_mean-Tuple{Any, AbstractVector}","page":"API","title":"LowLevelParticleFilters.weighted_mean","text":"x̂ = weighted_mean(x,we)\n\nCalculated weighted mean of particle trajectories. we are expweights.\n\n\n\n\n\n","category":"method"},{"location":"api/#LowLevelParticleFilters.weighted_mean-Tuple{Any}","page":"API","title":"LowLevelParticleFilters.weighted_mean","text":"x̂ = weighted_mean(pf)\nx̂ = weighted_mean(s::PFstate)\n\n\n\n\n\n","category":"method"},{"location":"api/#StatsAPI.predict!","page":"API","title":"StatsAPI.predict!","text":"predict!(f, u, p = parameters(f), t = index(f))\n\nMove filter state forward in time using dynamics equation and input vector u.\n\n\n\n\n\n","category":"function"},{"location":"api/#StatsAPI.predict!-2","page":"API","title":"StatsAPI.predict!","text":"predict!(kf::SqKalmanFilter, u, p = parameters(kf), t::Real = index(kf); R1 = get_mat(kf.R1, kf.x, u, p, t), α = kf.α)\n\nFor the square-root Kalman filter, a custom provided R1 must be the upper triangular Cholesky factor of the covariance matrix of the process noise.\n\n\n\n\n\n","category":"function"},{"location":"api/#StatsAPI.predict!-3","page":"API","title":"StatsAPI.predict!","text":"predict!(kf::AbstractKalmanFilter, u, p = parameters(kf), t::Integer = index(kf); R1, α = kf.α)\n\nPerform the prediction step (updating the state estimate to x(t+1t)). If R1 stored in kf is a function R1(x, u, p, t), this function is evaluated at the state before the prediction is performed. The dynamics noise covariance matrix R1 stored in kf can optionally be overridden by passing the argument R1, in this case R1 must be a matrix.\n\n\n\n\n\n","category":"function"},{"location":"api/#StatsAPI.predict!-Union{Tuple{AUGM}, Tuple{AUGD}, Tuple{IPM}, Tuple{IPD}, Tuple{UnscentedKalmanFilter{IPD, IPM, AUGD, AUGM}, Any}, Tuple{UnscentedKalmanFilter{IPD, IPM, AUGD, AUGM}, Any, Any}, Tuple{UnscentedKalmanFilter{IPD, IPM, AUGD, AUGM}, Any, Any, Real}} where {IPD, IPM, AUGD, AUGM}","page":"API","title":"StatsAPI.predict!","text":"predict!(ukf::UnscentedKalmanFilter, u, p = parameters(ukf), t::Real = index(ukf) * ukf.Ts; R1 = get_mat(ukf.R1, ukf.x, u, p, t), reject, mean, cov, dynamics)\n\nThe prediction step for an UnscentedKalmanFilter allows the user to override, R1 and any of the functions, reject, mean, cov, dynamics`.\n\nArguments:\n\nu: The input\np: The parameters\nt: The current time\nR1: The dynamics noise covariance matrix, or a function that returns the covariance matrix.\nreject: A function that takes a sigma point and returns true if it should be rejected.\nmean: The function that computes the mean of the state sigma points.\ncov: The function that computes the covariance of the state sigma points.\n\n\n\n\n\n","category":"method"},{"location":"api/","page":"API","title":"API","text":"LowLevelParticleFilters.prediction_errors!","category":"page"},{"location":"api/#LowLevelParticleFilters.prediction_errors!","page":"API","title":"LowLevelParticleFilters.prediction_errors!","text":"prediction_errors!(res, f::AbstractFilter, u, y, p = parameters(f), λ = 1)\n\nCalculate the prediction errors and store the result in res. Similar to sse, this function is useful for sum-of-squares optimization. In contrast to sse, this function returns the residuals themselves rather than their sum of squares. This is useful for Gauss-Newton style optimizers, such as LeastSquaresOptim.LevenbergMarquardt.\n\nArguments:\n\nres: A vector of length ny*length(y). Note, for each datapoint in u and u, there are ny outputs, and thus ny residuals.\nf: Any filter\nλ: A weighting factor to minimize dot(e, λ, e). A commonly used metric is λ = Diagonal(1 ./ (mag.^2)), where mag is a vector of the \"typical magnitude\" of each output. Internally, the square root of W = sqrt(λ) is calculated so that the residuals stored in res are W*e.\n\nSee example in Solving using Gauss-Newton optimization.\n\n\n\n\n\n","category":"function"},{"location":"beetle_example/#Smoothing-the-track-of-a-moving-beetle","page":"Particle-filter tutorial","title":"Smoothing the track of a moving beetle","text":"","category":"section"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"This is an example of smoothing the 2-dimensional trajectory of a moving dung beetle. The example spurred off of this Discourse topic. For more information about the research behind this example, see Artificial light disrupts dung beetles’ sense of direction and A dung beetle that path integrates without the use of landmarks. Special thanks to Yakir Gagnon for providing this example.","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"In this example we will describe the position coordinates, x and y, of the beetle as functions of its velocity, v_t, and direction, θ_t:","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"beginaligned\nx_t+1 = x_t + cos(θ_t)v_t \ny_t+1 = y_t + sin(θ_t)v_t \nv_t+1 = v_t + e_t \nθ_t+1 = θ_t + w_t\nendaligned","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"where e_t N(0σ_e) w_t N(0σ_w) The beetle further has two \"modes\", one where it's moving towards a goal, and one where it's searching in a more erratic manner. Figuring out when this mode switch occurs is the goal of the filtering. The mode will be encoded as a state variable, and used to determine the amount of dynamic noise affecting the angle of the beetle, i.e., in the searching mode, the beetle has more angle noise. The mode switching is modeled as a stochastic process with a binomial distribution (coin flip) describing the likelihood of a switch from mode 0 (moving to goal) and mode 1 (searching). Once the beetle has started searching, it stays in that mode, i.e., the searching mode is \"sticky\" or \"terminal\".","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"The image below shows an example video from which the data is obtained (Image: Bettle)","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"We load a single experiment from file for the purpose of this example (in practice, there may be hundreds of experiments)","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"using LowLevelParticleFilters, LinearAlgebra, StaticArrays, Distributions, Plots, Random\nusing DisplayAs # hide\nusing DelimitedFiles\npath = \"../track.csv\"\nxyt = readdlm(path)\ntosvec(y) = reinterpret(SVector{length(y[1]),Float64}, reduce(hcat,y))[:] |> copy # helper function\ny = tosvec(collect(eachrow(xyt[:,1:2])))\nnothing # hide","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"We then define some properties of the dynamics and the filter. We will use an AdvancedParticleFilter since we want to have fine-grained control over the noise sampling for the mode switch.","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"N = 2000 # Number of particles in the particle filter\nn = 4 # Dimension of state: we have position (2D), speed and angle\np = 2 # Dimension of measurements, we can measure the x and the y\n@inline pos(s) = s[SVector(1,2)]\n@inline vel(s) = s[3]\n@inline ϕ(s) = s[4]\n@inline mode(s) = s[5]\nnothing # hide","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"We then define the probability distributions we need.","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"dgσ = 1 # the deviation of the measurement noise distribution\ndvσ = 0.3 # the deviation of the dynamics noise distribution\nϕσ = 0.5\nconst switch_prob = 0.03 # Probability of mode switch\nconst dg = MvNormal(@SVector(zeros(p)), dgσ^2) # Measurement noise Distribution\nconst df = LowLevelParticleFilters.TupleProduct((Normal.(0,[1e-1, 1e-1, dvσ, ϕσ])...,Binomial(1,switch_prob)))\nd0 = MvNormal(SVector(y[1]..., 0.5, atan((y[2]-y[1])...), 0), [3.,3,2,2,0])\nconst noisevec = zeros(5) # cache vector\nnothing # hide","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"We now define the dynamics, since we use the advanced filter, we include the noise=false argument. The dynamics is directly defined in discrete time.","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"@inline function dynamics(s,u,p,t,noise=false)\n # current state\n m = mode(s)\n v = vel(s)\n a = ϕ(s)\n p = pos(s)\n # get noise\n if noise\n y_noise, x_noise, v_noise, ϕ_noise,_ = rand!(df, noisevec)\n else\n y_noise, x_noise, v_noise, ϕ_noise = 0.,0.,0.,0.\n end\n # next state\n v⁺ = max(0.999v + v_noise, 0.0)\n m⁺ = Float64(m == 0 ? rand() < switch_prob : true)\n a⁺ = a + (ϕ_noise*(1 + m*10))/(1 + v⁺) # next state velocity is used here\n p⁺ = p + SVector(y_noise, x_noise) + SVector(sincos(a))*v⁺ # current angle but next velocity\n SVector{5,Float64}(p⁺[1], p⁺[2], v⁺, a⁺, m⁺) # all next state\nend\nfunction measurement_likelihood(s,u,y,p,t)\n logpdf(dg, pos(s)-y) # A simple linear measurement model with normal additive noise\nend\n@inline measurement(s,u,p,t,noise=false) = s[SVector(1,2)] + noise*rand(dg) # We observe the position coordinates with the measurement\nnothing # hide","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"In this example, we have no control inputs, we thus define a vector of only zeros. We then solve the forward filtering problem and plot the results.","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"u = zeros(length(y))\npf = AuxiliaryParticleFilter(AdvancedParticleFilter(N, dynamics, measurement, measurement_likelihood, df, d0))\nT = length(y)\nsol = forward_trajectory(pf,u[1:T],y[1:T])\n(; x,w,we,ll) = sol\nplot(sol, markerstrokecolor=:auto, m=(2,0.5))\nDisplayAs.PNG(Plots.current()) # hide","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"We can clearly see when the beetle switched mode (state variable 5). This corresponds well to annotations provided by a biologist and is the fundamental question we want to answer with the filtering procedure.","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"We can plot the mean of the filtered trajectory as well","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"xh = mean_trajectory(x,we)\n\n\"plotting helper function\"\nfunction to1series(x::AbstractVector, y)\n r,c = size(y)\n y2 = vec([y; fill(Inf, 1, c)])\n x2 = repeat([x; Inf], c)\n x2,y2\nend\nto1series(y) = to1series(1:size(y,1),y)\n\nfig1 = plot(xh[:,1],xh[:,2], c=:blue, lab=\"estimate\", legend=:bottomleft)\nplot!(xyt[:,1],xyt[:,2], c=:red, lab=\"measurement\")","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"as well as the angle state variable (we subsample the particles to not get sluggish plots)","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"fig2 = scatter(to1series(ϕ.(x)'[:,1:2:end])..., m=(:black, 0.03, 2), lab=\"\", size=(500,300))\nplot!(identity.(xh[:,4]), lab=\"Filtered angle\", legend=:topleft, ylims=(-30, 70))\nDisplayAs.PNG(fig2) # hide","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"The particle plot above indicate that the posterior is multimodal. This phenomenon arises due to the simple model that uses an angle that is allowed to leave the interval 0-2π rad. In this example, we are not interested in the angle, but rather when the beetle switches mode. The filtering distribution above gives a hint at when this happens, but we will not plot the mode trajectory until we have explored smoothing as well.","category":"page"},{"location":"beetle_example/#Smoothing","page":"Particle-filter tutorial","title":"Smoothing","text":"","category":"section"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"The filtering results above does not use all the available information when trying to figure out the state trajectory. To do this, we may call a smoother. We use a particle smoother and compute 10 smoothing trajectories.","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"M = 10 # Number of smoothing trajectories, NOTE: if this is set higher, the result will be better at the expense of linear scaling of the computational cost.\nsb,ll = smooth(pf, M, u, y) # Sample smoothing particles (b for backward-trajectory)\nsbm = smoothed_mean(sb) # Calculate the mean of smoothing trajectories\nsbt = smoothed_trajs(sb) # Get smoothing trajectories\nplot!(fig1, sbm[1,:],sbm[2,:], lab=\"xs\")","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"plot!(fig2, identity.(sbm'[:,4]), lab=\"smoothed\")\nDisplayAs.PNG(fig2) # hide","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"We see that the smoothed trajectory may look very different from the filter trajectory. This is an indication that it's hard to tell what state the beetle is currently in, but easier to look back and tell what state the beetle must have been in at a historical point. ","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"We can also visualize the mode state","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"plot(xh[:,5], lab=\"Filtering\")\nplot!(to1series(sbt[5,:,:]')..., lab=\"Smoothing\", title=\"Mode trajectories\", l=(:black,0.2))","category":"page"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"also this state variable indicates that it's hard to tell what state the beetle is in during filtering, but obvious with hindsight (smoothing). The mode switch occurs when the filtering distribution of the angle becomes drastically wider, indicating that increased dynamics noise is required in order to describe the motion of the beetle.","category":"page"},{"location":"beetle_example/#Summary","page":"Particle-filter tutorial","title":"Summary","text":"","category":"section"},{"location":"beetle_example/","page":"Particle-filter tutorial","title":"Particle-filter tutorial","text":"This example has demonstrated filtering and smoothing in an advanced application that includes manual control over noise, mixed continuous and discrete state.","category":"page"},{"location":"beetle_example_imm/#Filtering-the-track-of-a-moving-beetle-using-IMM","page":"IMM-filter tutorial","title":"Filtering the track of a moving beetle using IMM","text":"","category":"section"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"This tutorial is very similar to Smoothing the track of a moving beetle, but uses an Interacting Multiple Models (IMM) filter to model the mode switching of the beetle. The IMM filter is a mixture model, in this case with internal Unscented Kalman filters, where each Kalman filter represents a different mode of the system. The IMM filter is able to switch between these modes based on the likelihood of the mode given the data.","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"This is an example of smoothing the 2-dimensional trajectory of a moving dung beetle. The example spurred off of this Discourse topic. For more information about the research behind this example, see Artificial light disrupts dung beetles’ sense of direction and A dung beetle that path integrates without the use of landmarks. Special thanks to Yakir Gagnon for providing this example.","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"In this example we will describe the position coordinates, x and y, of the beetle as functions of its velocity, v_t, and direction, θ_t:","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"beginaligned\nx_t+1 = x_t + cos(θ_t)v_t \ny_t+1 = y_t + sin(θ_t)v_t \nv_t+1 = v_t + e_t \nθ_t+1 = θ_t + w_t\nendaligned","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"where e_t N(0σ_e) w_t N(0σ_w) The beetle further has two \"modes\", one where it's moving towards a goal, and one where it's searching in a more erratic manner. Figuring out when this mode switch occurs is the goal of the filtering. The mode will be encoded as two different models, where the difference between the models lies in the amount of dynamic noise affecting the angle of the beetle, i.e., in the searching mode, the beetle has more angle noise. The mode switching is modeled as a stochastic process with a binomial distribution (coin flip) describing the likelihood of a switch from mode 0 (moving to goal) and mode 1 (searching). Once the beetle has started searching, it stays in that mode, i.e., the searching mode is \"sticky\" or \"terminal\".","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"The image below shows an example video from which the data is obtained (Image: Bettle)","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"We load a single experiment from file for the purpose of this example (in practice, there may be hundreds of experiments)","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"using LowLevelParticleFilters, LinearAlgebra, StaticArrays, Distributions, Plots, Random\nusing DisplayAs # hide\nusing DelimitedFiles\ncd(@__DIR__)\npath = \"../track.csv\"\nxyt = readdlm(path)\ntosvec(y) = reinterpret(SVector{length(y[1]),Float64}, reduce(hcat,y))[:] |> copy # helper function\ny = tosvec(collect(eachrow(xyt[:,1:2])))\nnothing # hide","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"We then define some properties of the dynamics and the filter. We will use an AdvancedParticleFilter since we want to have fine-grained control over the noise sampling for the mode switch.","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"nx = 4 # Dimension of state: we have position (2d), speed and angle\nny = 2 # Dimension of measurements, we can measure the x and the y\n@inline pos(s) = s[SVector(1,2)]\n@inline vel(s) = s[3]\n@inline ϕ(s) = s[4]\nnothing # hide","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"We then define the probability distributions we need. The IMM filter takes a transition-probability matrix, P, and an initial mixing probability, μ. P is a Markov (stochastic) matrix, where each row sums to one, and P[i, j] is the probability of switching from mode i to mode j. μ is a vector of probabilities, where μ[i] is the probability of starting in mode i. We also define the noise distributions for the dynamics and the measurements. The dynamics noise is modeled as a Gaussian distribution with a standard deviation of dvσ for the velocity and ϕσ for the angle. The measurement noise is modeled as a Gaussian distribution with a standard deviation of dgσ. The initial state is modeled as a Gaussian distribution with a mean at the first measurement and a standard deviation of d0.","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"dgσ = 1.0 # the deviation of the measurement noise distribution\ndvσ = 0.3 # the deviation of the dynamics noise distribution\nϕσ = 0.5\nP = [0.995 0.005; 0.0 1] # Transition probability matrix, we model the search mode as \"terminal\"\nμ = [1.0, 0.0] # Initial mixing probabilities\nR1 = Diagonal([1e-1, 1e-1, dvσ, ϕσ].^2)\nR2 = dgσ^2*I(ny) # Measurement noise covariance matrix\nd0 = MvNormal(SVector(y[1]..., 0.5, atan((y[2]-y[1])...)), [3.,3,2,2])\nnothing # hide","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"We now define the dynamics, which is directly defined in discrete time. The third argument is a parameter we call modegain, which is used to scale the amount of noise in the angle of the beetle depending on the mode in which it is in. The last argument is a boolean that tells the dynamics function which mode it is in, we will close over this argument when defining the dynamics for the individual Kalman filters that are part of the IMM, one will use m = false and one will use m = true.","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"@inline function dynamics(s,_,modegain,t,w,m)\n # current state\n v = vel(s)\n a = ϕ(s)\n p = pos(s)\n\n y_noise, x_noise, v_noise, ϕ_noise = w\n\n # next state\n v⁺ = max(0.999v + v_noise, 0.0)\n a⁺ = a + (ϕ_noise*(1 + m*modegain))/(1 + v⁺) # next state velocity is used here\n p⁺ = p + SVector(y_noise, x_noise) + SVector(sincos(a))*v⁺ # current angle but next velocity\n SVector(p⁺[1], p⁺[2], v⁺, a⁺) # all next state\nend\n@inline measurement(s,u,p,t) = s[SVector(1,2)] # We observe the position coordinates with the measurement\nnothing # hide","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"In this example, we have no control inputs, we thus define a vector of only zeros. We then solve the forward filtering problem and plot the results.","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"u = zeros(length(y)) # no control inputs\nkffalse = UnscentedKalmanFilter{false,false,true,false}((x,u,p,t,w)->dynamics(x,u,p,t,w,false), measurement, R1, R2, d0; ny, nu=0, p=10)\nkftrue = UnscentedKalmanFilter{false,false,true,false}((x,u,p,t,w)->dynamics(x,u,p,t,w,true), measurement, R1, R2, d0; ny, nu=0, p=10)\n\nimm = IMM([kffalse, kftrue], P, μ; p = 10)\n\nT = length(y)\nsol = forward_trajectory(imm, u, y, interact=true)\nfigx = plot(sol, plotu=false, plotRt=true)\nfigmode = plot(sol.extra', title=\"Mode\")\nplot(figx, figmode)\nDisplayAs.PNG(Plots.current()) # hide","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"If you have followed the particle filter tutorial Smoothing the track of a moving beetle, you will notice that the result here is much worse. We used noise parameters similar to in the particle-gilter example, but those were tuned fo the particle filter. Below, we will attempt to optimize the performance of the IMM filter.","category":"page"},{"location":"beetle_example_imm/#Tuning-by-optimization","page":"IMM-filter tutorial","title":"Tuning by optimization","text":"","category":"section"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"We will attempt to optimize the dynamics and measurement noise covariance matrices and the modegain parameter. We code this up in two functions, one that takes the parameter vector and returns an IMM filter, and one that calculates the loss given the filter. We will optimize the log-likelihood of the data given the filter.","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"params = [log10.(diag(R1)); log10(1); log10(10)]\n\nfunction get_opt_kf(p)\n T = eltype(p)\n R1i = Diagonal(SVector{4}(exp10.(p[1:4])))\n R2i = SMatrix{2,2}(exp10(p[5])*R2)\n d0i = MvNormal(SVector{4, T}(T.(d0.μ)), SMatrix{4,4}(T.(d0.Σ)))\n modegain = 2+exp10(p[6])\n Pi = SMatrix{2,2, Float64,4}(P)\n # sigmoid(x) = 1/(1+exp(-x))\n # switch_prob = sigmoid(p[7])\n # Pi = [1-switch_prob switch_prob; 0 1]\n kffalse = UnscentedKalmanFilter{false,false,true,false}((x,u,p,t,w)->dynamics(x,u,p,t,w,false), measurement, R1i, R2i, d0i; ny, nu=0)\n kftrue = UnscentedKalmanFilter{false,false,true,false}((x,u,p,t,w)->dynamics(x,u,p,t,w,true), measurement, R1i, R2i, d0i; ny, nu=0)\n\n IMM([kffalse, kftrue], Pi, T.(μ), p=modegain)\nend\nfunction cost(pars)\n try\n imm = get_opt_kf(pars)\n T = length(y)\n ll = loglik(imm, u[1:T], y[1:T], interact=true) - 1/2*logdet(imm.models[1].R1)\n return -ll\n catch e\n # rethrow() # If you only get Inf, you can uncomment this line to see the error message\n return eltype(pars)(Inf)\n\tend\nend\n\nusing Optim\nRandom.seed!(0)\nres = Optim.optimize(\n cost,\n params,\n ParticleSwarm(), # Use a gradient-free optimizer. ForwardDiff works, but the algorithm is numerically difficult to compute gradients through and may suffer from overflows in the gradient computation\n Optim.Options(\n show_trace = true,\n show_every = 5,\n iterations = 100,\n time_limit = 30,\n ),\n)\n\nimm = get_opt_kf(res.minimizer)\nimm = get_opt_kf([-0.1981314138910982, -0.18626406669394405, -2.7342979645906547, 0.17994244691004058, -11.706419070755908, -54.16703441089562]) #make sure it goes well # hide\n\nsol = forward_trajectory(imm, u, y)\nplot(sol.extra', title=\"Mode (optimized filter)\")","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"If it went well, the filter should be in mode 1 (the false mode) from the start until around 200 time steps, at which point it should switch to model 2 (true). This method of detecting the mode switch of the beetle appears to be somewhat less robust than the particle filter, but is significantly cheaper computationally. ","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"The IMM filter does not stick in mode 2 perpetually after having reached it since it never actually becomes fully confident that mode 2 has been reached, but detecting the first switch is sufficient to know that the switch has occurred. ","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"The log-likelihood of the solution","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"sol.ll","category":"page"},{"location":"beetle_example_imm/","page":"IMM-filter tutorial","title":"IMM-filter tutorial","text":"should be similar to that of the particle-filter in the tutorial Smoothing the track of a moving beetle, which was around -1660.","category":"page"},{"location":"noisetuning/#How-to-tune-a-Kalman-filter","page":"Noise tuning and disturbance modeling for Kalman filtering","title":"How to tune a Kalman filter","text":"","category":"section"},{"location":"noisetuning/","page":"Noise tuning and disturbance modeling for Kalman filtering","title":"Noise tuning and disturbance modeling for Kalman filtering","text":"This tutorial is hosted as a notebook.","category":"page"},{"location":"fault_detection/#Fault-detection","page":"Fault detection","title":"Fault detection","text":"","category":"section"},{"location":"fault_detection/","page":"Fault detection","title":"Fault detection","text":"This is a video tutorial, available below:","category":"page"},{"location":"fault_detection/","page":"Fault detection","title":"Fault detection","text":"","category":"page"},{"location":"fault_detection/","page":"Fault detection","title":"Fault detection","text":"The notebook used in the tutorial is available here:","category":"page"},{"location":"fault_detection/","page":"Fault detection","title":"Fault detection","text":"identification_12_fault_detection.jl on JuliaHub\nidentification_12_fault_detection.jl on GitHub","category":"page"},{"location":"discretization/#Discretization","page":"Discretization","title":"Discretization","text":"","category":"section"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"This package operates exclusively on discrete-time dynamics, and dynamics describing, e.g., ODE systems must thus be discretized. This page describes the details around discretization for nonlinear and linear systems, as well as how to discretize continuous-time noise processes. ","category":"page"},{"location":"discretization/#Nonlinear-ODEs","page":"Discretization","title":"Nonlinear ODEs","text":"","category":"section"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"Continuous-time dynamics functions on the form (x,u,p,t) -> ẋ can be discretized (integrated) using the function SeeToDee.Rk4, e.g.,","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"using SeeToDee\ndiscrete_dynamics = SeeToDee.Rk4(continuous_dynamics, sampletime; supersample=1)","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"where the integer supersample determines the number of RK4 steps that is taken internally for each change of the control signal (1 is often sufficient and is the default). The returned function discrete_dynamics is on the form (x,u,p,t) -> x⁺.","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"note: Note\nWhen solving state-estimation problems, accurate integration is often less important than during simulation. The motivations for this are severalThe dynamics model is often inaccurate, and solving an inaccurate model to high accuracy can be a waste of effort.\nThe performance is often dictated by the disturbances acting on the system.\nState-estimation enjoys feedback from measurements that corrects for slight errors due to integration.","category":"page"},{"location":"discretization/#Linear-systems","page":"Discretization","title":"Linear systems","text":"","category":"section"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"A linear system on the form ","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"beginaligned\ndotx(t) = Ax(t) + Bu(t)\ny(t) = Cx(t) + Du(t)\nendaligned","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"can be discretized using ControlSystems.c2d, which defaults to a zero-order hold discretization. See the example below for more info.","category":"page"},{"location":"discretization/#Covariance-matrices","page":"Discretization","title":"Covariance matrices","text":"","category":"section"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"Covariance matrices for continuous-time noise processes can also be discretized using ControlSystems.c2d","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"using ControlSystemIdentification\nR1d = c2d(sys::StateSpace{<:Discrete}, R1::Matrix)\nR1d, R2d = c2d(sys::StateSpace{<:Discrete}, R1::Matrix, R2::Matrix)","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"This samples a continuous-time covariance matrix to fit the provided discrete-time system sys.","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"The method used comes from theorem 5 in the reference below.","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"Ref: \"Discrete-time Solutions to the Continuous-time Differential Lyapunov Equation With Applications to Kalman Filtering\", Patrik Axelsson and Fredrik Gustafsson","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"On singular covariance matrices: The traditional double integrator with covariance matrix Q = diagm([0,σ²]) can not be sampled with this method. Instead, the input matrix (\"Cholesky factor\") of Q must be manually kept track of, e.g., the noise of variance σ² enters like N = [0, 1] which is sampled using ZoH and becomes Nd = [Ts^2 / 2; Ts] which results in the covariance matrix σ² * Nd * Nd' (see example below).","category":"page"},{"location":"discretization/#Example","page":"Discretization","title":"Example","text":"","category":"section"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"The following example will discretize a linear double integrator system. Double integrators arise when the position of an object is controlled by a force, i.e., when Newtons second law f = ma governs the dynamics. The system can be written on the form","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"beginaligned\ndot x(t) = Ax(t) + Bu(t) + Nw(t)\ny(t) = Cx(t) + e(t)\nendaligned","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"where N = B are both equal to [0, 1], indicating that the noise w(t) enters like a force (this could be for instance due to air resistance or friction).","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"We start by defining the system that takes u as an input and discretize that with a sample time of T_s = 01.","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"using ControlSystemsBase\nA = [0 1; 0 0]\nB = [0; 1;;]\nC = [1 0]\nD = 0\nTs = 0.1 # Sample time\n\nsys = ss(A,B,C,D)\nsysd = c2d(sys, Ts) # Discretize the dynamics","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"We then form another system, this time with w(t) as the input, and thus N as the input matrix instead of B. We assume that the noise has a standard deviation of sigma_1 = 05","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"σ1 = 0.5\nN = σ1*[0; 1;;]\nsys_w = ss(A,N,C,D)\nsys_wd = c2d(sys_w, Ts) # Discretize the noise system\nNd = sys_wd.B # The discretized noise input matrix\nR1d = Nd*Nd' # The final discrete-time covariance matrix","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"We can verify that the matrix we computed corresponds to the theoretical covariance matrix for a discrete-time double integrator:","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"R1d ≈ σ1^2*[Ts^2 / 2; Ts]*[Ts^2 / 2; Ts]'","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"For a nonlinear system, we could adopt a similar strategy by first linearizing the system around a suitable operating point. Alternatively, we could make use of the fact that some of the state estimators in this package allows the covariance matrices to be functions of the state, and thus compute a new discretized covariance matrix using a linearization around the current state.","category":"page"},{"location":"discretization/#Non-uniform-sample-rates","page":"Discretization","title":"Non-uniform sample rates","text":"","category":"section"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"Special care is needed if the sample rate is not constant, i.e., the time interval between measurements varies. ","category":"page"},{"location":"discretization/#Dropped-samples","page":"Discretization","title":"Dropped samples","text":"","category":"section"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"A common case is that the sample rate is constant, but some measurements are lost. This case is very easy to handle; the filter loop iterates between two steps","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"Prediction using predict!(filter, x, u, p, t)\nCorrection using\ncorrect!(f, u, y, p, t) if using the standard measurement model of the filter\ncorrect!(f, mm, u, y, p, t, mm) to use a custom measurement model mm","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"If a measurement y is lacking, one simply skips the corresponding call to correct! where y is missing. Repeated calls to predict! corresponds to simulating the system without any feedback from measurements, like if an ODE was solved. Internally, the filter will keep track of the covariance of the estimate, which is likely to grow if no measurements are used to inform the filter about the state of the system.","category":"page"},{"location":"discretization/#Sensors-with-different-sample-rates","page":"Discretization","title":"Sensors with different sample rates","text":"","category":"section"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"For Kalman-type filters, it is possible to construct custom measurement models, and pass an instance of a measurement model as the second argument to correct!. This allows for sensor fusion with sensors operating at different rates, or when parts of the measurement model are linear, and other parts are nonlinear. See examples in Measurement models for how to construct explicit measurement models.","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"A video demonstrating the use of multiple measurement models running at different rates is available on YouTube:","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"","category":"page"},{"location":"discretization/#Stochastic-sample-rate","page":"Discretization","title":"Stochastic sample rate","text":"","category":"section"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"In some situations, such as in event-based systems, the sample rate is truly stochastic. There is no single correct way of handling this, and we instead outline some alternative approaches.","category":"page"},{"location":"discretization/","page":"Discretization","title":"Discretization","text":"If the filtering is performed offline on a batch of data, time-varying dynamics can be used, for instance by supplying matrices to a KalmanFilter on the form A[:, :, t]. Each A is then computed as the discretization with the sample time given as the time between measurement t and measurement t+1.\nA conceptually simple approach is to choose a very small sample interval T_s which is smaller than the smallest occuring sample interval in the data, and approximate each sample interval by rounding it to the nearest integer multiple of T_s. This transforms the problem to an instance of the \"dropped samples\" problem described above.\nMake use of an adaptive integrator instead of the fixed-step rk4 supplied in this package, and manually keep track of the step length that needs to be taken.","category":"page"},{"location":"benchmark/#Benchmark-test","page":"Benchmark","title":"Benchmark test","text":"","category":"section"},{"location":"benchmark/","page":"Benchmark","title":"Benchmark","text":"To see how the performance varies with the number of particles, we simulate several times. The following code simulates the system and performs filtering using the simulated measurements. We do this for varying number of time steps and varying number of particles.","category":"page"},{"location":"benchmark/","page":"Benchmark","title":"Benchmark","text":"note: Note\nTo run this code, see the bottom of src/example_lineargaussian.jl.","category":"page"},{"location":"benchmark/","page":"Benchmark","title":"Benchmark","text":"function run_test()\n particle_count = [10, 20, 50, 100, 200, 500, 1000]\n time_steps = [20, 100, 200]\n RMSE = zeros(length(particle_count),length(time_steps)) # Store the RMS errors\n propagated_particles = 0\n t = @elapsed for (Ti,T) = enumerate(time_steps)\n for (Ni,N) = enumerate(particle_count)\n montecarlo_runs = 2*maximum(particle_count)*maximum(time_steps) ÷ T ÷ N\n E = sum(1:montecarlo_runs) do mc_run\n pf = ParticleFilter(N, dynamics, measurement, df, dg, d0) # Create filter\n u = @SVector randn(2)\n x = SVector{2,Float64}(rand(rng, d0))\n y = SVector{2,Float64}(sample_measurement(pf,x,u,0,1))\n error = 0.0\n @inbounds for t = 1:T-1\n pf(u, y) # Update the particle filter\n x = dynamics(x,u,t) + SVector{2,Float64}(rand(rng, df)) # Simulate the true dynamics and add some noise\n y = SVector{2,Float64}(sample_measurement(pf,x,u,0,t)) # Simulate a measuerment\n u = @SVector randn(2) # draw a random control input\n error += sum(abs2,x-weighted_mean(pf))\n end # t\n √(error/T)\n end # MC\n RMSE[Ni,Ti] = E/montecarlo_runs\n propagated_particles += montecarlo_runs*N*T\n @show N\n end # N\n @show T\n end # T\n println(\"Propagated $propagated_particles particles in $t seconds for an average of $(propagated_particles/t/1000) particles per millisecond\")\n return RMSE\nend\n\n@time RMSE = run_test()","category":"page"},{"location":"benchmark/","page":"Benchmark","title":"Benchmark","text":"Propagated 8400000 particles in 1.140468043 seconds for an average of 7365.397085484139 particles per millisecond","category":"page"},{"location":"benchmark/","page":"Benchmark","title":"Benchmark","text":"We then plot the results","category":"page"},{"location":"benchmark/","page":"Benchmark","title":"Benchmark","text":"time_steps = [20, 100, 200]\nparticle_count = [10, 20, 50, 100, 200, 500, 1000]\nnT = length(time_steps)\nleg = reshape([\"$(time_steps[i]) time steps\" for i = 1:nT], 1,:)\nplot(particle_count,RMSE,xscale=:log10, ylabel=\"RMS errors\", xlabel=\" Number of particles\", lab=leg)","category":"page"},{"location":"benchmark/","page":"Benchmark","title":"Benchmark","text":"(Image: window)","category":"page"},{"location":"#LowLevelParticleFilters","page":"Home","title":"LowLevelParticleFilters","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"(Image: CI) (Image: codecov)","category":"page"},{"location":"","page":"Home","title":"Home","text":"This is a library for state estimation, that is, given measurements y(t) from a dynamical system, estimate the state vector x(t). Throughout, we assume dynamics on the form","category":"page"},{"location":"","page":"Home","title":"Home","text":"beginaligned\nx(t+1) = f(x(t) u(t) p t w(t))\ny(t) = g(x(t) u(t) p t e(t))\nendaligned","category":"page"},{"location":"","page":"Home","title":"Home","text":"or the linear version","category":"page"},{"location":"","page":"Home","title":"Home","text":"beginaligned\nx(t+1) = Ax(t) + Bu(t) + w(t)\ny(t) = Cx(t) + Du(t) + e(t)\nendaligned","category":"page"},{"location":"","page":"Home","title":"Home","text":"where x is the state vector, u an input, p some form of parameters, t is the time and we are disturbances (noise). Throughout the documentation, we often call the function f dynamics and the function g measurement.","category":"page"},{"location":"","page":"Home","title":"Home","text":"The dynamics above describe a discrete-time system, i.e., the function f takes the current state and produces the next state. This is in contrast to a continuous-time system, where f takes the current state but produces the time derivative of the state. A continuous-time system can be discretized, described in detail in Discretization.","category":"page"},{"location":"","page":"Home","title":"Home","text":"The parameters p can be anything, or left out. You may write the dynamics functions such that they depend on p and include parameters when you create a filter object. You may also override the parameters stored in the filter object when you call any function on the filter object. This behavior is modeled after the SciML ecosystem.","category":"page"},{"location":"","page":"Home","title":"Home","text":"Depending on the nature of f and g, the best method of estimating the state may vary. If fg are linear and the disturbances are additive and Gaussian, the KalmanFilter is an optimal state estimator. If any of the above assumptions fail to hold, we may need to resort to more advanced estimators. This package provides several filter types, outlined below.","category":"page"},{"location":"#Estimator-types","page":"Home","title":"Estimator types","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"We provide a number of filter types","category":"page"},{"location":"","page":"Home","title":"Home","text":"KalmanFilter. A standard Kalman filter. Is restricted to linear dynamics (possibly time varying) and Gaussian noise.\nSqKalmanFilter. A standard Kalman filter on square-root form (slightly slower but more numerically stable with ill-conditioned covariance).\nExtendedKalmanFilter: For nonlinear systems, the EKF runs a regular Kalman filter on linearized dynamics. Uses ForwardDiff.jl for linearization (or user provided). The noise model must still be Gaussian and additive.\nUnscentedKalmanFilter: The Unscented Kalman filter often performs slightly better than the Extended Kalman filter but may be slightly more computationally expensive. The UKF handles nonlinear dynamics and measurement models, but still requires a Gaussian noise model (may be non additive) and still assumes that all posterior distributions are Gaussian, i.e., can not handle multi-modal posteriors.\nParticleFilter: The particle filter is a nonlinear estimator. This version of the particle filter is simple to use and assumes that both dynamics noise and measurement noise are additive. Particle filters handle multi-modal posteriors.\nAdvancedParticleFilter: This filter gives you more flexibility, at the expense of having to define a few more functions. This filter does not require the noise to be additive and is thus the most flexible filter type.\nAuxiliaryParticleFilter: This filter is identical to ParticleFilter, but uses a slightly different proposal mechanism for new particles.\nIMM: (Currently considered experimental) The Interacting Multiple Models filter switches between multiple internal filters based on a hidden Markov model. This filter is useful when the system dynamics change over time and the change can be modeled as a discrete Markov chain, i.e., the system may switch between a small number of discrete \"modes\".","category":"page"},{"location":"#Functionality","page":"Home","title":"Functionality","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"This package provides ","category":"page"},{"location":"","page":"Home","title":"Home","text":"Filtering, estimating x(t) given measurements up to and including time t. We call the filtered estimate x(tt) (read as x at t given t).\nSmoothing, estimating x(t) given data up to T t, i.e., x(tT).\nParameter estimation.","category":"page"},{"location":"","page":"Home","title":"Home","text":"All filters work in two distinct steps.","category":"page"},{"location":"","page":"Home","title":"Home","text":"The prediction step (predict!). During prediction, we use the dynamics model to form x(tt-1) = f(x(t-1) )\nThe correction step (correct!). In this step, we adjust the predicted state x(tt-1) using the measurement y(t) to form x(tt).","category":"page"},{"location":"","page":"Home","title":"Home","text":"(The IMM filter is an exception to the above and has two additional steps, combine! and interact!)","category":"page"},{"location":"","page":"Home","title":"Home","text":"In general, all filters represent not only a point estimate of x(t), but a representation of the complete posterior probability distribution over x given all the data available up to time t. One major difference between different filter types is how they represent these probability distributions.","category":"page"},{"location":"#Particle-filter","page":"Home","title":"Particle filter","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"A particle filter represents the probability distribution over the state as a collection of samples, each sample is propagated through the dynamics function f individually. When a measurement becomes available, the samples, called particles, are given a weight based on how likely the particle is given the measurement. Each particle can thus be seen as representing a hypothesis about the current state of the system. After a few time steps, most weights are inevitably going to be extremely small, a manifestation of the curse of dimensionality, and a resampling step is incorporated to refresh the particle distribution and focus the particles on areas of the state space with high posterior probability.","category":"page"},{"location":"","page":"Home","title":"Home","text":"Defining a particle filter (ParticleFilter) is straightforward, one must define the distribution of the noise df in the dynamics function, dynamics(x,u,p,t) and the noise distribution dg in the measurement function measurement(x,u,p,t). Both of these noise sources are assumed to be additive, but can have any distribution (see AdvancedParticleFilter for non-additive noise). The distribution of the initial state estimate d0 must also be provided. In the example below, we use linear Gaussian dynamics so that we can easily compare both particle and Kalman filters. (If we have something close to linear Gaussian dynamics in practice, we should of course use a Kalman filter and not a particle filter.)","category":"page"},{"location":"","page":"Home","title":"Home","text":"using LowLevelParticleFilters, LinearAlgebra, StaticArrays, Distributions, Plots\nusing DisplayAs # hide","category":"page"},{"location":"","page":"Home","title":"Home","text":"Define problem","category":"page"},{"location":"","page":"Home","title":"Home","text":"nx = 2 # Dimension of state\nnu = 1 # Dimension of input\nny = 1 # Dimension of measurements\nN = 500 # Number of particles\n\nconst dg = MvNormal(ny,0.2) # Measurement noise Distribution\nconst df = MvNormal(nx,0.1) # Dynamics noise Distribution\nconst d0 = MvNormal(randn(nx),2.0) # Initial state Distribution\nnothing # hide","category":"page"},{"location":"","page":"Home","title":"Home","text":"Define linear state-space system (using StaticArrays for maximum performance)","category":"page"},{"location":"","page":"Home","title":"Home","text":"const A = SA[0.97043 -0.097368\n 0.09736 0.970437]\nconst B = SA[0.1; 0;;]\nconst C = SA[0 1.0]\nnothing # hide","category":"page"},{"location":"","page":"Home","title":"Home","text":"Next, we define the dynamics and measurement equations, they both take the signature (x,u,p,t) = (state, input, parameters, time) ","category":"page"},{"location":"","page":"Home","title":"Home","text":"dynamics(x,u,p,t) = A*x .+ B*u\nmeasurement(x,u,p,t) = C*x\nvecvec_to_mat(x) = copy(reduce(hcat, x)') # Helper function\nnothing # hide","category":"page"},{"location":"","page":"Home","title":"Home","text":"the parameter p can be anything, and is often optional. If p is not provided when performing operations on filters, any p stored in the filter objects (if supported) is used. The default if none is provided and none is stored in the filter is p = LowLevelParticleFilters.NullParameters().","category":"page"},{"location":"","page":"Home","title":"Home","text":"We are now ready to define and use a filter","category":"page"},{"location":"","page":"Home","title":"Home","text":"pf = ParticleFilter(N, dynamics, measurement, df, dg, d0)","category":"page"},{"location":"","page":"Home","title":"Home","text":"With the filter in hand, we can simulate from its dynamics and query some properties","category":"page"},{"location":"","page":"Home","title":"Home","text":"du = MvNormal(nu,1.0) # Random input distribution for simulation\nxs,u,y = simulate(pf,200,du) # We can simulate the model that the pf represents\npf(u[1], y[1]) # Perform one filtering step using input u and measurement y\nparticles(pf) # Query the filter for particles, try weights(pf) or expweights(pf) as well\nx̂ = weighted_mean(pf) # using the current state","category":"page"},{"location":"","page":"Home","title":"Home","text":"If you want to perform batch filtering using an existing trajectory consisting of vectors of inputs and measurements, try any of the functions forward_trajectory, mean_trajectory:","category":"page"},{"location":"","page":"Home","title":"Home","text":"sol = forward_trajectory(pf, u, y) # Filter whole trajectories at once\nx̂,ll = mean_trajectory(pf, u, y)\nplot(sol, xreal=xs, markersize=2)\nDisplayAs.PNG(Plots.current()) # hide","category":"page"},{"location":"","page":"Home","title":"Home","text":"u ad y are then assumed to be vectors of vectors. StaticArrays is recommended for maximum performance.","category":"page"},{"location":"","page":"Home","title":"Home","text":"If MonteCarloMeasurements.jl is loaded, you may transform the output particles to Matrix{MonteCarloMeasurements.Particles} with the layout T × n_state using Particles(x,we). Internally, the particles are then resampled such that they all have unit weight. This is conventient for making use of the plotting facilities of MonteCarloMeasurements.jl.","category":"page"},{"location":"","page":"Home","title":"Home","text":"For a full usage example, see the benchmark section below or example_lineargaussian.jl","category":"page"},{"location":"#Resampling","page":"Home","title":"Resampling","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"The particle filter will perform a resampling step whenever the distribution of the weights has become degenerate. The resampling is triggered when the effective number of samples is smaller than pf.resample_threshold in 0 1, this value can be set when constructing the filter. How the resampling is done is governed by pf.resampling_strategy, we currently provide ResampleSystematic <: ResamplingStrategy as the only implemented strategy. See https://en.wikipedia.org/wiki/Particle_filter for more info.","category":"page"},{"location":"#Particle-Smoothing","page":"Home","title":"Particle Smoothing","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"Smoothing is the process of finding the best state estimate given both past and future data. Smoothing is thus only possible in an offline setting. This package provides a particle smoother, based on forward filtering, backward simulation (FFBS), example usage follows:","category":"page"},{"location":"","page":"Home","title":"Home","text":"N = 2000 # Number of particles\nT = 80 # Number of time steps\nM = 100 # Number of smoothed backwards trajectories\npf = ParticleFilter(N, dynamics, measurement, df, dg, d0)\ndu = MvNormal(nu,1) # Control input distribution\nx,u,y = simulate(pf,T,du) # Simulate trajectory using the model in the filter\ntosvec(y) = reinterpret(SVector{length(y[1]),Float64}, reduce(hcat,y))[:] |> copy\nx,u,y = tosvec.((x,u,y)) # It's good for performance to use StaticArrays to the extent possible\n\nxb,ll = smooth(pf, M, u, y) # Sample smoothing particles\nxbm = smoothed_mean(xb) # Calculate the mean of smoothing trajectories\nxbc = smoothed_cov(xb) # And covariance\nxbt = smoothed_trajs(xb) # Get smoothing trajectories\nxbs = [diag(xbc) for xbc in xbc] |> vecvec_to_mat .|> sqrt\nplot(xbm', ribbon=2xbs, lab=\"PF smooth\")\nplot!(vecvec_to_mat(x), l=:dash, lab=\"True\")","category":"page"},{"location":"","page":"Home","title":"Home","text":"We can plot the particles themselves as well","category":"page"},{"location":"","page":"Home","title":"Home","text":"downsample = 5\nplot(vecvec_to_mat(x), l=(4,), layout=(2,1), show=false)\nscatter!(xbt[1, 1:downsample:end, :]', subplot=1, show=false, m=(1,:black, 0.5), lab=\"\")\nscatter!(xbt[2, 1:downsample:end, :]', subplot=2, m=(1,:black, 0.5), lab=\"\")\nDisplayAs.PNG(Plots.current()) # hide","category":"page"},{"location":"#Kalman-filter","page":"Home","title":"Kalman filter","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"The KalmanFilter (wiki) assumes that f and g are linear functions, i.e., that they can be written on the form","category":"page"},{"location":"","page":"Home","title":"Home","text":"beginaligned\nx(t+1) = Ax(t) + Bu(t) + w(t)\ny(t) = Cx(t) + Du(t) + e(t)\nendaligned","category":"page"},{"location":"","page":"Home","title":"Home","text":"for some matrices ABCD where w sim N(0 R_1) and e sim N(0 R_2) are zero mean and Gaussian. The Kalman filter represents the posterior distributions over x by the mean and a covariance matrix. The magic behind the Kalman filter is that linear transformations of Gaussian distributions remain Gaussian, and we thus have a very efficient way of representing them.","category":"page"},{"location":"","page":"Home","title":"Home","text":"A Kalman filter is easily created using the constructor KalmanFilter. Many of the functions defined for particle filters, are defined also for Kalman filters, e.g.:","category":"page"},{"location":"","page":"Home","title":"Home","text":"R1 = cov(df)\nR2 = cov(dg)\nkf = KalmanFilter(A, B, C, 0, R1, R2, d0)\nsol = forward_trajectory(kf, u, y) # sol contains filtered state, predictions, pred cov, filter cov, loglik\nnothing # hide","category":"page"},{"location":"","page":"Home","title":"Home","text":"It can also be called in a loop like the pf above","category":"page"},{"location":"","page":"Home","title":"Home","text":"for t = 1:T\n kf(u,y) # Performs both correct! and predict!\n # alternatively\n ll, e = correct!(kf, y, nothing, t) # Returns loglikelihood and prediction error (plus other things if you want)\n x = state(kf) # Access the state estimate\n R = covariance(kf) # Access the covariance of the estimate\n predict!(kf, u, nothing, t)\nend","category":"page"},{"location":"","page":"Home","title":"Home","text":"The matrices in the Kalman filter may be time varying, such that A[:, :, t] is A(t). They may also be provided as functions on the form A(t) = A(x u p t). This works for both dynamics and covariance matrices.","category":"page"},{"location":"","page":"Home","title":"Home","text":"The numeric type used in the Kalman filter is determined from the mean of the initial state distribution, so make sure that this has the correct type if you intend to use, e.g., Float32 or ForwardDiff.Dual for automatic differentiation.","category":"page"},{"location":"#Smoothing-using-KF","page":"Home","title":"Smoothing using KF","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"Kalman filters can also be used for smoothing ","category":"page"},{"location":"","page":"Home","title":"Home","text":"kf = KalmanFilter(A, B, C, 0, cov(df), cov(dg), d0)\nxT,R,lls = smooth(kf, u, y) # Returns smoothed state, smoothed cov, loglik\nnothing # hide","category":"page"},{"location":"","page":"Home","title":"Home","text":"Plot and compare PF and KF","category":"page"},{"location":"","page":"Home","title":"Home","text":"plot(vecvec_to_mat(xT), lab=\"Kalman smooth\", layout=2)\nplot!(xbm', lab=\"pf smooth\")\nplot!(vecvec_to_mat(x), lab=\"true\")","category":"page"},{"location":"#Kalman-filter-tuning-tutorial","page":"Home","title":"Kalman filter tuning tutorial","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"The tutorial \"How to tune a Kalman filter\" details how to figure out appropriate covariance matrices for the Kalman filter, as well as how to add disturbance models to the system model.","category":"page"},{"location":"#Unscented-Kalman-Filter","page":"Home","title":"Unscented Kalman Filter","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"The UnscentedKalmanFilter represents posterior distributions over x as Gaussian distributions just like the KalmanFilter, but propagates them through a nonlinear function f by a deterministic sampling of a small number of particles called sigma points (this is referred to as the unscented transform). This UKF thus handles nonlinear functions fg, but only Gaussian disturbances and unimodal posteriors. The UKF will by default treat the noise as additive, but by using the augmented UKF form, non-additive noise may be handled as well. See the docstring of UnscentedKalmanFilter for more details.","category":"page"},{"location":"","page":"Home","title":"Home","text":"The UKF takes the same arguments as a regular KalmanFilter, but the matrices defining the dynamics are replaced by two functions, dynamics and measurement, working in the same way as for the ParticleFilter above (unless the augmented form is used).","category":"page"},{"location":"","page":"Home","title":"Home","text":"ukf = UnscentedKalmanFilter(dynamics, measurement, cov(df), cov(dg), MvNormal(SA[1.,1.]); nu=nu, ny=ny)","category":"page"},{"location":"","page":"Home","title":"Home","text":"info: Info\nIf your function dynamics describes a continuous-time ODE, do not forget to discretize it before passing it to the UKF. See Discretization for more information.","category":"page"},{"location":"","page":"Home","title":"Home","text":"The UnscentedKalmanFilter has many customization options, see the docstring for more details. In particular, the UKF may be created with a linear measurement model as an optimization.","category":"page"},{"location":"#Extended-Kalman-Filter","page":"Home","title":"Extended Kalman Filter","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"The ExtendedKalmanFilter (EKF) is similar to the UKF, but propagates Gaussian distributions by linearizing the dynamics and using the formulas for linear systems similar to the standard Kalman filter. This can be slightly faster than the UKF (not always), but also less accurate for strongly nonlinear systems. The linearization is performed automatically using ForwardDiff.jl unless the user provides Jacobian functions that compute A and C. In general, the UKF is recommended over the EKF unless the EKF is faster and computational performance is the top priority.","category":"page"},{"location":"","page":"Home","title":"Home","text":"The EKF constructor has the following two signatures","category":"page"},{"location":"","page":"Home","title":"Home","text":"ExtendedKalmanFilter(dynamics, measurement, R1, R2, d0=MvNormal(R1); nu::Int, p = LowLevelParticleFilters.NullParameters(), α = 1.0, check = true, Ajac = nothing, Cjac = nothing)\nExtendedKalmanFilter(kf, dynamics, measurement; Ajac = nothing, Cjac = nothing)","category":"page"},{"location":"","page":"Home","title":"Home","text":"The first constructor takes all the arguments required to initialize the extended Kalman filter, while the second one takes an already defined standard Kalman filter. using the first constructor, the user must provide the number of inputs to the system, nu.","category":"page"},{"location":"","page":"Home","title":"Home","text":"where kf is a standard KalmanFilter from which the covariance properties are taken.","category":"page"},{"location":"","page":"Home","title":"Home","text":"info: Info\nIf your function dynamics describes a continuous-time ODE, do not forget to discretize it before passing it to the UKF. See Discretization for more information.","category":"page"},{"location":"#AdvancedParticleFilter","page":"Home","title":"AdvancedParticleFilter","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"The AdvancedParticleFilter works very much like the ParticleFilter, but admits more flexibility in its noise models.","category":"page"},{"location":"","page":"Home","title":"Home","text":"The AdvancedParticleFilter type requires you to implement the same functions as the regular ParticleFilter, but in this case you also need to handle sampling from the noise distributions yourself. The function dynamics must have a method signature like below. It must provide one method that accepts state vector, control vector, parameter, time and noise::Bool that indicates whether or not to add noise to the state. If noise should be added, this should be done inside dynamics An example is given below","category":"page"},{"location":"","page":"Home","title":"Home","text":"using Random\nconst rng = Random.Xoshiro()\nfunction dynamics(x, u, p, t, noise=false) # It's important that `noise` defaults to false\n x = A*x .+ B*u # A simple linear dynamics model in discrete time\n if noise\n x += rand(rng, df) # it's faster to supply your own rng\n end\n x\nend\nnothing # hide","category":"page"},{"location":"","page":"Home","title":"Home","text":"The measurement_likelihood function must have a method accepting state, input, measurement, parameter and time, and returning the log-likelihood of the measurement given the state, a simple example below:","category":"page"},{"location":"","page":"Home","title":"Home","text":"function measurement_likelihood(x, u, y, p, t)\n logpdf(dg, C*x-y) # An example of a simple linear measurement model with normal additive noise\nend\nnothing # hide","category":"page"},{"location":"","page":"Home","title":"Home","text":"This gives you very high flexibility. The noise model in either function can, for instance, be a function of the state, something that is not possible for the simple ParticleFilter. To be able to simulate the AdvancedParticleFilter like we did with the simple filter above, the measurement method with the signature measurement(x,u,p,t,noise=false) must be available and return a sample measurement given state (and possibly time). For our example measurement model above, this would look like this","category":"page"},{"location":"","page":"Home","title":"Home","text":"# This function is only required for simulation\nmeasurement(x, u, p, t, noise=false) = C*x + noise*rand(rng, dg)\nnothing # hide","category":"page"},{"location":"","page":"Home","title":"Home","text":"We now create the AdvancedParticleFilter and use it in the same way as the other filters:","category":"page"},{"location":"","page":"Home","title":"Home","text":"apf = AdvancedParticleFilter(N, dynamics, measurement, measurement_likelihood, df, d0)\nsol = forward_trajectory(apf, u, y, ny) # Perform batch filtering","category":"page"},{"location":"","page":"Home","title":"Home","text":"plot(sol, xreal=x)\nDisplayAs.PNG(Plots.current()) # hide","category":"page"},{"location":"","page":"Home","title":"Home","text":"We can even use this type as an AuxiliaryParticleFilter","category":"page"},{"location":"","page":"Home","title":"Home","text":"apfa = AuxiliaryParticleFilter(apf)\nsol = forward_trajectory(apfa, u, y, ny)\nplot(sol, dim=1, xreal=x) # Same as above, but only plots a single dimension\nDisplayAs.PNG(Plots.current()) # hide","category":"page"},{"location":"","page":"Home","title":"Home","text":"See the tutorials section for more advanced examples, including state estimation for DAE (Differential-Algebraic Equation) systems.","category":"page"},{"location":"#Troubleshooting-and-tuning","page":"Home","title":"Troubleshooting and tuning","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"Tuning a particle filter can be quite the challenge. To assist with this, we provide som visualization tools","category":"page"},{"location":"","page":"Home","title":"Home","text":"debugplot(pf,u[1:20],y[1:20], runall=true, xreal=x[1:20])","category":"page"},{"location":"","page":"Home","title":"Home","text":"The plot displays all state variables and all measurements. The heatmap in the background represents the weighted particle distributions per time step. For the measurement sequences, the heatmap represent the distributions of predicted measurements. The blue dots corresponds to measured values. In this case, we simulated the data and we had access to the state as well, if we do not have that, just omit xreal. You can also manually step through the time-series using","category":"page"},{"location":"","page":"Home","title":"Home","text":"commandplot(pf,u,y; kwargs...)","category":"page"},{"location":"","page":"Home","title":"Home","text":"For options to the debug plots, see ?pplot.","category":"page"},{"location":"#Tuning-noise-parameters-through-optimization","page":"Home","title":"Tuning noise parameters through optimization","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"See examples in Parameter Estimation.","category":"page"},{"location":"#Tuning-through-simulation","page":"Home","title":"Tuning through simulation","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"It is possible to sample from the Bayesian model implied by a filter and its parameters by calling the function simulate. A simple tuning strategy is to adjust the noise parameters such that a simulation looks \"similar\" to the data, i.e., the data must not be too unlikely under the model.","category":"page"},{"location":"#Videos","page":"Home","title":"Videos","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"Several video tutorials using this package are available in the playlists","category":"page"},{"location":"","page":"Home","title":"Home","text":"System identification in Julia\nControl systems in Julia","category":"page"},{"location":"","page":"Home","title":"Home","text":"Some examples featuring this package in particular are","category":"page"},{"location":"","page":"Home","title":"Home","text":"","category":"page"},{"location":"","page":"Home","title":"Home","text":"Using an optimizer to optimize the likelihood of an UnscentedKalmanFilter:","category":"page"},{"location":"","page":"Home","title":"Home","text":"","category":"page"},{"location":"","page":"Home","title":"Home","text":"","category":"page"},{"location":"","page":"Home","title":"Home","text":"Estimation of time-varying parameters:","category":"page"},{"location":"","page":"Home","title":"Home","text":"","category":"page"},{"location":"","page":"Home","title":"Home","text":"","category":"page"},{"location":"","page":"Home","title":"Home","text":"Adaptive control by means of estimation of time-varying parameters:","category":"page"},{"location":"","page":"Home","title":"Home","text":"","category":"page"}] }