Fusion of measurements into the model simulation is useful for dynamically adapting the numeric simulation to e.g. real-world observations. FhSim facilitates state and parameter estimation using an Unscented Kalman Filter (UKF). Please read this documentation if you intend to use the built-in Kalman filtering in FhSim and/or are not familiar with Kalman filtering. To jump right to the core, see Implementing the KalmanObject.
Overview: Kalman filtering
This section will give a quick and dirty overview of the equations in a Kalman filter and UKF in particular. The reader is encouraged to consult resources elsewhere if a more in-depth introduction is sought.
- Model description and measurements
Suppose we have a state vector \(x(t) \in \mathbb{R}^n\) with an ordinary differential equation on the form
\[\dot x = f(x,t) + w(t),\]
where \(w(t)\) is process noise. The system output \( y(t) \in \mathbb{R}^m\) is obtained at regular time points with period \(\Delta t\). The \(k\)th measurement occurs at \(t_k = t_0 + k\Delta t\) and is on the form
\[y_k := y(t_k) = h(x,t_k) + v(t_k),\]
where \(v(t)\) is measurement noise.
- Covariance matrices
The objective of the Kalman filter is to provide an estimate of the state vector based on the available information. Information of relevance include initial conditions and the measurement sequence. Moreover, the Kalman filter exploits the statistical information about the process and measurement noise vectors together with the uncertainty of the state vector in determining the updated state vector. Let \( z \sim N(\mu,\Sigma) \) indicate that \(z \in
\mathbb{R}^p\) is a multivariate vector with mean \(\mu\) and covariance matrix \(\Sigma \in \mathbb{R}^{p \times p}\), which is positive definite. Now, let
\begin{align*} w &\sim N(0,Q_c),\\ v &\sim N(0,R), \end{align*}
such that \(Q_c\) and \(R\) are the process and measurement covariance matrices, respectively. The diagonal terms of these matrices are the variance of the corresponding state derivative or measurement. The uncertainty of the state vector is taken as the covariance of the estimation error:
\[ P := E[(x -
E[x]])(x - E[x])^T, \]
where \( E[\cdot]\) is the expectation operator. The estimation error covariance matrix \(P(t)\) (and the diagonal entries in particular) is a measure of how uncertain the state estimate is at a particular time point.
- Note
- The covariance \(Q_c\) above is given for the continuous-time case. In the discrete-time case the covariance matrix is given by the difference equation \(x_{k+1} = F(t_k,x_k) + w_k,\, w_k \sim N(0,Q)\). These to matrices are related by \(Q = \Delta t Q_c\). We will proceed by only mentioning the discrete variant, since this is used in the implementation.
- The phases of a Kalman filter
The Kalman filter consists of three phases: initialization, time update, and measurement update. The last two phases are repeated for each new measurement, see diagram illustration.
The three phases of Kalman filtering.
The initialization is done before the estimation itself begins. This includes setting the initial conditions of the state vector and other parameters, just as in a regular simulation in FhSim. The initial condition of the state vector \(\hat x(t_0) = \hat x_0\) is the best guess of the real state. With Kalman filtering the user also has to provide covariance matrices \(P(t_0),\,Q,\,R\).
The time update is an open-loop simulation from one time point \(t_{k-1}\) to \(t_k\), where a new measurement becomes available. For the Extended Kalman filter this is the solution to the initial value problem \( t \in [t_{k-1},t_k]\)
\[ \dot{\hat x}^-(t) = f(\hat x,t),\, \hat x(t_{k-1}) = \hat x_{k-1},\]
where \(\hat x_k^- =\hat x^-(t_k)\) is the so-called a priori estimate. This estimate is has not yet incorporated the measurement \(y_k\).
The estimation error covariance matrix also has to be updated. For the Extended Kalman filter this involves linearizing the system dynamics and solving a matrix differential equation, which includes of the previous time step's covariances \( P_{k-1}\) and \(Q\). Details will not be covered here.
- Note
- As we will see later, the a priori estimate is obtained in a different manner when using the UKF.
In the measurement phase the a priori estimates are updated by incorporating the information that the measurement \(y_k\) provides. The equations for this update are
\begin{align*}
\hat x_k &= \hat x_k^- + K_k(y_k - \hat y_k)\\
P_k &= P_k^- - K_k P_y K_k^T,
\end{align*}
where \(K_k\) is the Kalman gain matrix, which is based on the system uncertainties, and \( \hat y_k \) is the estimated measurement. \( \hat x_k \) is often called the a posteriori state estimate.
Unscented Kalman filter
The Extended Kalman filter (EKF) relies on first-order approximations of the mean and third-order approximations of the covariances when calculating the state estimates. These calculations require Jacobian information, which may be difficult to calculate/approximate accurately. The Unscented Kalman filter circumvent the Jacobian requirement by using so-called unscented transformations. This approach is based on two ideas:
- A nonlinear transformation of a set of vectors are easier to compute than a nonlinear transformation of a whole probability density function (pdf).
- It is possible to find a deterministic set of vectors that approximates a given pdf.
When the unscented transformations are used to propagate the means and covariances instead of its linearized counterparts of the EKF, it is called the Unscented Kalman filter. The method enjoys the benefit of tackling highly nonlinear systems with discontinuities better than for instance the EKF. This approach was first proposed in Julier, S. J., Uhlmann, J. K., and Durrant-Whyte, H. F. (1995). "A new approach for filtering nonlinear systems". In American Control Conference, pages 1628-1632, Seattle, Washington, USA.
- The unscented transformation
An unscented transformation is basically the function evaluation of a set of input vectors through a nonlinear function. The outputs of these function evaluations are used to approximate the statistical properties, such as mean and covariance, of the nonlinear transformation. By appropriately choosing the input vectors and the weighting when combining the outputs, we obtain an approximation of the probability density function of the transformation at hand.
Suppose that we have a nonlinear function \( y: R^n \to R^n\), whose domain has known mean and covariance:
\begin{align*}
y &= h(x)\\
x &\sim N(\hat x, P_x).
\end{align*}
Let \(\{x^{(i)}\}_0^{2n}\) be a sequence of carefully chosen sigma points. We perform a nonlinear transformation \(h(\cdot)\) of all these points to get a mapped sequence \(\{y^{(i)}\}_0^{2n}\). Weighted sums and sample covariance of these outputs provide us with an approximated mean and covariance of the mapping \( h(\cdot)\), namely \( \hat y\) and \(\hat P_y\). This procedure is illustrated for a two-dimensional mapping in the figure below.
The mapping of five sigma points for a two-dimensional mapping. The ellipsoids illustrate level sets of the corresponding covariance matrices.
- Note
- There are in general two nonlinear mappings in the system to be estimated, namely \(f(\cdot)\) and \(h(\cdot)\) in the right-hand-side of the differential equation and measurement function, respectively. The unscented Kalman filter then has to solve an initial value problem (IVP) for each perturbation (sigma point) of the state vector. The resulting sequence of a priori state vectors is used to approximate the mean and covariance. For each of these propagated sigma points the measurement mapping is also calculated. Since \(2n+1\) IVPs have to be solved for a state vector of size \(n\), the calculation time may be high if \(n\) is large. There are also steps related to carefully choosing sigma points that are costly, so the UKF algorithm does not scale particularly well with state dimension.
Implementation in FhSim
In FhSim the UKF is implemented based on the publication http://www.dx.doi.org/10.1109/ICASSP.2001.940586. Please consult this publication for details on the equations involved. We have implemented both the normal and the square root variant, but due to numerical issues (or implementation errors) with the latter, we currently use the normal one.
The Kalman filter implementation make use of three central classes in FhSim: CKalmanIntegrator, estimator::KalmanFilter, and KalmanObject. In addition, some supporting data structures are used.
CKalmanIntegrator utilizes a user-specified fixed-step COdeSolver to step the state forward in time. It also assembles covariance matrices and measurement vector contributions from each KalmanObject. This information is used by the estimator::KalmanFilter (and its child estimator::UKF) to solve the Kalman filter equations and update the state estimate vector and other relevant data structures as described in the above mentioned paper. The two most costly operations in the filter are to simulate the ensemble of sigma points forward in time and the solution to a matrix square root. The ensemble update involves the solution to \(2n+1\) IVPs. In contrast, a pure simulation only needs to solve one IVP at any given time. Large-scale systems are thus expected to struggle for real-time performance (Note: Profiling of the Kalman filter may reveal sources for improvement).
The user usually only has to deal with the KalmanObject interface to use the Kalman filter functionality in their own simulations.
How to use Kalman filtering in FhSim
Suppose you want to estimate some process using a Kalman filter. If you already have implemented a SimObject, you are in luck, because then you only have to inherit from this object in addition to the KalmanObject interface. In broad strokes, once a SimObject has been extended with the KalmanObject interface, the input file for a KalmanObject is not very different from a regular SimObject. Only some new connections and/or input parameters may need to be defined.
The implementation defines a KalmanObject interface class. This class declares several virtual functions that need to be defined in order to enable Kalman filtering. We provide two examples to get you started.
- Example: Simple pendulum
- Case study: A sphere attached to a cable submerged in water
The pendulum example gives you a cut-to-the-bone introduction of what you need to do to get started, whereas the case study is a bit more involved, providing some more guidelines and details on how to use Kalman filtering in your project, faced with some additional challenges.
Case study: A sphere attached to a cable submerged in water
Suppose we have a cable connected to a fixed point in one end and a sphere mass in the other end, both points submerged in water. The setup is under the influence of gravity, buoyancy, and some unknown, slowly-varying current. Every \(\Delta t\) seconds a noisy measurement of the sphere position is obtained. Our objective is to estimate the cable's state vector and the water current. We proceed by modeling the problem as a simple hydrodynamic cable and ball, each component modeled in their own SimObject.
Let the inertial reference frame be fixed to north, east, and down directions with coordinates ordered as \((N,E,D)\). Define \(v_c(t) \in \mathbb{R}^3\) as the slowly-varying uniform current.
Case study: Hydrodynamic ball model
Define \(p(t) \in \mathbb{R}^3\) and \(v(t) \in \mathbb{R}^3\) as position and velocity of the ball, respectively. The ordinary differential equations for the hydrodynamic ball are
\begin{align*}
\dot p &= v,\\
\dot v &= \frac{F(p,v,t)}{m},
\end{align*}
where \(F\) is the force acting on the ball and \(m\) is the ball's mass. The forces acting on the ball are gravity, buoyancy \(B\), drag \(D\), and other external forces: \(F(p,v,t) = mg - B + D + f_{ext}(p,v,t)\), where \(g = (0,0,9.81)\). We use that the ball volume is \(\frac{4}{3}\pi r^3\) and assume that the ball is completely submerged, so \( B = \frac{\rho_w}{\rho_b}mg\), with \(\rho_w,\,\rho_b\) being the water and ball density, respectively. The drag on the ball is modelled as Morison drag \( D = -0.5 \rho_w \pi r^2 C_d (v-v_c)\|v-v_c\|_2\), where \(r\) is the ball radius and \(C_d\) is the drag coefficient (the latter is set to \(0.5\)). After simplifying the equations we arrive at the following force function
\[
F = mg\left(1 - \frac{\rho_w}{\rho_b}\right) - m\frac{3C_d}{8r}\frac{\rho_w}{\rho_b}v_r \|v_r\|_2 + f_{ext}(p,v,t),
\]
where \(v_r := v - v_c\) is the relative current.
Define \( x_b(t) := [p^T\, v^T]^T \in \mathbb R^6\) as the state vector for the ball. The compact description for the ball dynamics is
\begin{align*}
\dot x_b &= f_b(x_b,t),\\
y_b(t) &= p(t) + v(t),
\end{align*}
where \(y_b(t)\) is the noisy measurement of the ball's position.
Case study: Hydrodynamic cable model
We model a simple mass-lumped cable that consists of point masses connected by spring and damper forces. Like the ball, the cable is under the influence of gravity, buoyancy, and Morison drag. The characteristics of the cable are given in the table below.
Description | Symbol | Numeric value (if const.) |
Length | \(L\) | - |
Diameter | \(d\) | - |
Density | \(\rho_c\) | - |
Mass | \(M\) | \(\rho_c \pi \left(\frac{d}{2}\right)^2 L\) |
Elastic modulus | \(E\) | - |
Cross-section drag coef. | \(C_d\) | 1.1 |
Streamlined drag coef. | \(C_t\) | 0.1 |
We discretize the cable into \(n+1\) uniform line segments, so that there are \(n\) point masses excluding the end points. Each point has mass \(m_i = \frac{M}{n}\). Let \( i \in \{1,2,\dots,n\} =: \mathbb{I}_n\) be the indices of the points. The indices \(i = 0\) and \(i = n+1\) are the end points. For each \(i \in \mathbb{I}_n\) let \(p_i(t),v_i(t) \in \mathbb R^3\) be the position and velocity with dynamics
\begin{align*}
\dot p_i &= v_i,\\
\dot v_i &= \frac{F_i(p_{i-1},p_i,p_{i+1},v_i,t)}{m},
\end{align*}
where \(F_i\) is the forces acting on the \(i\)th point mass. The forces are
\[
F_i = m_ig\left(1 - \frac{\rho_w}{\rho_c}\right) + D_i + \Lambda_i + \Delta_i,
\]
where \(\rho_w\) is water density, \(D_i\) is drag, \(\Lambda_i\) is spring forces, and \(\Delta_i\) is damper forces. In the following we will define the drag, spring, and damper forces.
- Drag forces
Each point mass has two line segments connected to it. From each of these two line segments there will be a contribution to the drag that acts on the point. We take half a line segment's length and calculate the drag on this cylinder. Let us continue by defining some expressions that make the exposition simpler. Let \(k,l \in \mathbb I_n \cap \{0,n+1\}, k\neq l\).
\begin{align*}
\bar p (k,l) &:= \frac{p_l - p_k}{\|p_l - p_k\|_2},\\
v(k,v_c) &:= v_k - v_c,\\
v_{k,l}^\| &:= \left(v(k,v_c)\cdot \bar p(k,l)\right)\bar p(k,l),\\
v_{k,l}^\perp &:= v(k,v_c) - v_{k,l}^\|,
\end{align*}
where \(v_c(t) \in \mathbb R^3\) is the water current and \(\cdot\) is the dot product. \(\bar p\) is the unit vector pointing from \( p_k\) to \(p_l\). \( v(k,v_c)\) is the velocity of \(p_k\) relative to the current, whereas \(v_{k,l}^\|\) and \(v_{k,l}^\perp\) are the decomposition into a velocity parallel to and perpendicular to the corresponding line segment, respectively.
The drag on line segment \((k,l)\) acting on point \(k\) is
\[
D(k,l) = -\frac{1}{2}\rho_w \underbrace{d 0.5\|p_l - p_k\|_2}_{A} \|v(k,v_c)\|_2\left(C_d v_{k,l}^\perp + C_t v_{k,l}^\| \right).
\]
The total drag on point \(i \in \mathbb I_n\) is
\[
D_i = D(i,i+1) + D(i,i-1).
\]
- Spring forces
The spring forces acting on a point mass come from the line segments connected to the neighboring points. The relaxed length of line segment \((k,l)\) is \( L_0(k,l) = \frac{L}{n+1} := L_0\). We assume linear spring forces under extension only (no compression forces). The spring force acting on point \(k\) due to line segment \((k,l)\) is
\[
\Lambda(k,l) = \begin{cases}
\overbrace{E\pi \frac{d^2}{4} \frac{1}{L_0}}^{\lambda}\left(\|p_l - p_k\|_2 - L_0 \right)\bar p(k,l) &\mbox{if } \|p_l - p_k\|_2 \geq L_0\\
0 & \mbox{otherwise }
\end{cases}
\]
The spring forces acting on point \(i \in \mathbb I_n\) are
\[
\Lambda_i = \Lambda(i,i+1) + \Lambda(i,i-1).
\]
- Damper forces
The damping forces on a point in the cable also depend on the neighboring points. We denote the damping on point \(k\) from point \(l\) as
\[
\Delta(k,l) = 2\sqrt{\lambda m_k}\left((v_l - v_k) \cdot \bar p(k,l) \right)\bar p(k,l),
\]
where \( k \) was defined when discussing spring forces.
The damping forces acting on point \(i\in \mathbb I_n\) are
\[
\Delta_i= \Delta(i,i+1) + \Delta(i,i-1).
\]
- Compact cable description
We have now described all forces acting on the cable. Putting it all together and detailing the compact description are outside the scope of this case study. We simply state that there exist such an equation. Define \( x_c(t) = [p_1^T\, v_1^T\, \cdots\, p_n^T\, v_n^T]^T \in \mathbb R^{6n}\) as the cable state vector with compact description
\begin{align*}
\dot x_c &= f_c(x_c,t),
\end{align*}
where the end points and water current are considered time-varying expressions.
Case study: Input files
- Simulator setup
Both the hydrodynamic ball and cable have been implemented as SimObjects. Their documentation and can be found in the fhsim_base under Example/HydroBall and Example/HydroCable. We assume that the setup is under the influence of a current, which consists of sine-wave with bias in north direction, and constant current in east and down directions.
\[
v_c(t) = \begin{bmatrix}a\sin(\omega t) + b\\c\\d\end{bmatrix},
\]
where \(a,\, \omega,\, b,\, c,\, d\) are sine-wave amplitude, angular velocity, bias, and constants, respectively.
We use the following input files for simulation of a ball connected to a cable.
Simulation.xml:
<Contents>
<OBJECTS>
<Lib
LibName = "fhsim_base"
SimObject = "Src/Sine"
Name = "NorthCurrent"
PortWidth = "1"
PhaseRad = "0"
PeriodS = "200"
StartTime = "0"
StopTime = "-1"
Bias = "0.7"
Amplitude = "0.3"
/>
<Lib
LibName = "fhsim_base"
SimObject = "Signal/Mux"
Name = "Current"
NumInput = "3"
PortWidth = "3"
InPortWidth1 = "1"
InPortWidth2 = "1"
InPortWidth3 = "1"
/>
<Lib
LibName = "fhsim_base"
SimObject = "Example/HydroBall"
Name = "Ball"
Radius = "1.0"
Density = "1100"
/>
<Lib
LibName = "fhsim_base"
SimObject = "Example/HydroCable"
Name = "Cable"
Diameter = "0.02"
Density = "2000"
Modulus = "20"
Length = "20"
InternalPoints = "7"
/>
</OBJECTS>
<INTERCONNECTIONS>
<Connection
Ball.ExternForce = "Cable.ForceB"
Ball.Current = "Current.Out"
Current.In1 = "NorthCurrent.Out"
Current.In2 = "0.5"
Current.In3 = "0.1"
/>
<Connection
Cable.PositionA = "0,0,0"
Cable.PositionB = "Ball.Position"
Cable.VelocityA = "0,0,0"
Cable.VelocityB = "Ball.Velocity"
Cable.Current = "Current.Out"
/>
</INTERCONNECTIONS>
<INITIALIZATION>
<InitialCondition
Ball.Position = "0,6.15,18.9"
Ball.Velocity = "0,0,0"
/>
</INITIALIZATION>
<INTEGRATION>
<Engine
IntegratorMethod="0"
NumCores="1"
TOutput="0, 0:1e-1:Inf, 1000"
stepsize ="2e-4"
FileOutput="objects:ports"
/>
</INTEGRATION>
</Contents>
Now, suppose the simulation described in A sphere attached to a cable submerged in water in the section above is "a real-world process" and we do not know neither the current, nor the states of the ball or cable. We do, however, get a noisy measurement of the ball's position every \(\Delta t \) seconds. Our objective is to estimate the cable's state vector and the water current.
The simulator setup is as follows. We start a FhSim process and denote this the real-world simulation. Usually, information about the real-world process is collected through various sensors and peripheral devices with various sampling rates and noise. To mimic this behavior we:
- use the Signal/Samplifier SimObject to do a sample and hold with artificially added noise to the ball position.
- employ FhSim's Data Distribution Service (DDS) interface to collect and send information periodically.
We add the following code snippet at appropriate locations in Simulation.xml above (consult documentation for respective SimObjects for details)
<Lib
LibName = "fhsim_base"
SimObject = "Signal/Samplifier"
Name = "Sample"
PortWidth = "1"
NoiseSeed = "42"
Tags = "NorthEastDown"
SamplingPeriod1 = ".1, .1, .1"
NoiseBias1 = "0, 0, 0"
NoiseStdDev1 = "0.15, 0.15, 0.05"
/>
<Lib LibName = "fhsim_extras"
SimObject = "Connection/DdsWriter"
Name = "Send"
Partition = "FhSimExternal"
Topic = "CableMeasure"
DdsRoot = "../lib"
DdsConfigFile = "../resources/ospl.xml"
ForceDdsEnv = "0"
PortWidth = "1"
ArrayIDs = "0"
PortSizes = "3"
SamplingPeriods = "0.1"
/>
<Connection
Sample.In1 = "Ball.Position"
Send.In1 = "Sample.NorthEastDown"
/>
Simulation.xml now simulates the real world process, obtains measurements at regular intervals, and sends them via DDS to be accessed by the estimator. The estimator, which is another FhSim process, employs KalmanObjects and incorporates the received measurements of the simulated real world. We continue by first describing the estimator setup and then detailing the necessary KalmanObject implementations.
- Estimator setup
When estimating the process at hand we need mathematical descriptions of the objects involved. Luckily, we already have implemented SimObjects that capture most of the phenomena at hand. What we do not know, though, is the time-varying current. A common assumption when estimating parameters is to assume that they are constant or slowly-varying. In such a case, one can describe the process with "zero-dynamics" as follows. Suppose \( p(t) \in \mathbb{R}^m\) is an parameter vector with dynamics
\[
\dot p = 0 + w_p(t),
\]
where \(w_p(t)\) is process noise. The initial conditions for this differential equation are the best guesses of the parameters. We use this description for the current in the estimator.
The involved SimObjects have been extended to KalmanObjects and the file below serves as the input file for the estimator.
Estimation.xml:
<Contents>
<OBJECTS>
<Lib
LibName = "fhsim_base"
SimObject = "Kalman/ZeroDynamics"
Name = "Current"
Dimension = "3"
P0 = "5e-2,5e-2,1e-4"
Q = "1e-4,1e-4,1e-5"
/>
<Lib
LibName = "fhsim_base"
SimObject = "Kalman/Example/HydroBall"
Name = "Ball"
Radius = "1.0"
Density = "1100"
MeasurementNoise = "1"
PositionUncertainty = "5e-4"
VelocityUncertainty = "1e-6"
InitialDiagP = "1e-3,1e-3,1e-3,1e-3,1e-3,1e-3"
/>
<Lib
LibName = "fhsim_base"
SimObject = "Kalman/Example/HydroCable"
Name = "Cable"
Diameter = "0.02"
Density = "2000"
Modulus = "20"
Length = "20"
InternalPoints = "2"
ElementPositionUncertainty = "5e-4"
ElementVelocityUncertainty = "1e-6"
ElementInitialDiagP = "1e-3,1e-3,1e-3,1e-3,1e-3,1e-3"
/>
<Lib LibName = "fhsim_extras"
SimObject = "Connection/DdsReader"
Name = "Receive"
Partition = "FhSimExternal"
Topic = "CableMeasure"
DdsRoot = "../lib"
DdsConfigFile = "../resources/ospl.xml"
ForceDdsEnv = "1"
InitializationTimeout = "2000"
PortWidth = "1"
Tags = "Out"
DefaultOut = "0,0,0"
ArrayIDs = "0"
PortSizes = "3"
/>
</OBJECTS>
<INTERCONNECTIONS>
<Connection
Ball.ExternForce = "Cable.ForceB"
Ball.Current = "Current.State"
/>
<Connection
Cable.PositionA = "0,0,0"
Cable.PositionB = "Ball.Position"
Cable.VelocityA = "0,0,0"
Cable.VelocityB = "Ball.Velocity"
Cable.Current = "Current.State"
/>
<Connection
Ball.MeasuredPosition = "Receive.Out"
/>
</INTERCONNECTIONS>
<INITIALIZATION>
<InitialCondition
Ball.Position = "0,19,0"
Ball.Velocity = "0,0,0"
Current.State = "0,0,0"
/>
</INITIALIZATION>
<INTEGRATION>
<Engine
IntegratorMethod="0"
NumCores="1"
TOutput="0, 0:1e-1:Inf, 150"
stepsize ="2e-4"
HMin="0.5"
FileOutput="objects:ports"
/>
</INTEGRATION>
</Contents>
The contents of Estimator.xml is quite similar to Simulator.xml, with some extensions.
- We use instead a Connection/DDSReader to receive the information that the Connection/DDSWriter sends.
- The cable has fewer internal elements.
- Each KalmanObject has additional parameters that must be defined.
- Kalman/Example/HydroBall has a new input port, namely a measurement of its position.
- Note
- Measurements are incorporated at major time steps \(\Delta t > 0\) only. \(\Delta t\) is usually much larger than the step size of the integrator. It is specified in the XML-element INTEGRATION::Engine as the parameter HMin in seconds. In Estimator.xml above, the major time step is 0.5 s, so there are 2 500 integrator steps per measurement update. The major time step \( \Delta t\) must be a integer multiple of the step size (if not, this will be enforces).
- Warning
- Only fixed-step integrators are supported when using KalmanObjects.
- HydroBall with KalmanObject extension
Let's proceed with the Kalman extension of the Example/HydroBall. Consider below the declaration of KalmanHydroBall that has the KalmanObject extension.
#include "KalmanObject.h"
public:
KalmanHydroBall(
const std::string& simObjectName, ISimObjectCreator*
const creator);
estimator::Matrix GetCovarianceP0();
estimator::Matrix GetProcessNoiseQ();
estimator::Matrix GetMeasurementNoiseR();
estimator::Vector ExpectedMeasurementOutput(const double T, const double* const X);
estimator::Vector ActualMeasurementOutput(const double T,, const double* const X);
unsigned int GetNumMeasurements();
protected:
ISignalPort* m_PositionMeasurement;
double m_MeasurementNoise;
double m_PositionUncertainty;
double m_VelocityUncertainty;
double m_InitialP[6];
};
Definition: HydroBall.h:80
Definition: KalmanHydroBall.h:90
Notice that the class inherits from both HydroBall and KalmanObject, so we need to define some virtual fuctions.
- Note
- We make heavily use of the Eigen library for vector and matrix operations. The type definitions estimator::Vector and estimator::Matrix are, respectively, dynamically sized double vectors and matrices from the Eigen library. Readers unfamiliar with this library should consult its documentation Eigen::GettingStarted and Eigen::QuickReferenceGuide.
Below we have included the definition for each function of KalmanHydroBall. We will not explain every line of code, since it is (by some) deemed self-explanatory.
Implementation of Kalman/Example/HydroBall:
KalmanHydroBall::KalmanHydroBall(const std::string& simObjectName, ISimObjectCreator* const creator)
{
creator->AddInport("MeasuredPosition", 3, &m_PositionMeasurement);
creator->GetDoubleParam("MeasurementNoise", &m_MeasurementNoise);
creator->GetDoubleParam("PositionUncertainty", &m_PositionUncertainty);
creator->GetDoubleParam("VelocityUncertainty", &m_VelocityUncertainty);
creator->GetDoubleParamArray("InitialDiagP", m_InitialP, 6);
}
estimator::Matrix KalmanHydroBall::GetCovarianceP0()
{
estimator::Vector diagP0(6);
diagP0 <<
m_InitialP[0], m_InitialP[1], m_InitialP[2],
m_InitialP[3], m_InitialP[4], m_InitialP[5];
return Eigen::DiagonalMatrix<double,Eigen::Dynamic>(diagP0);
}
estimator::Matrix KalmanHydroBall::GetProcessNoiseQ()
{
using namespace estimator;
Vector diagQ_pos = m_PositionUncertainty*Vector::Ones(3);
Vector diagQ_vel = m_VelocityUncertainty*Vector::Ones(3);
Vector diagQ(6);
diagQ << diagQ_pos, diagQ_vel;
return Eigen::DiagonalMatrix<double,Eigen::Dynamic>(diagQ);
}
estimator::Matrix KalmanHydroBall::GetMeasurementNoiseR()
{
estimator::Vector diagR = m_MeasurementNoise*estimator::Vector::Ones(3);
return Eigen::DiagonalMatrix<double,Eigen::Dynamic>(diagR);
}
estimator::Vector KalmanHydroBall::ExpectedMeasurementOutput(const double T, const double* const X)
{
estimator::Vector ret(3);
ret << X[m_PositionIndex], X[m_PositionIndex+1], X[m_PositionIndex+2];
return ret;
}
estimator::Vector KalmanHydroBall::ActualMeasurementOutput(const double T, const double* const X)
{
const double* y = m_PositionMeasurement->GetPortValue(T,X);
estimator::Vector ret(3);
ret << y[0], y[1], y[2];
return ret;
}
unsigned int KalmanHydroBall::GetNumMeasurements()
{
return 3;
}
- Warning
- The implementer must ensure that the columns/rows in the covariance matrices are arranged in the same order as the state or output variable vector. Above, the position state variables were created before velocity state variables (order of call to AddState(.) in the constructor of HydroBall), hence elements for position uncertainty comes before velocity uncertainty.
After following Create a new SimObject, KalmanHydroBall is accessible for use with FhSim.
- HydroCable with KalmanObject extension
The KalmanHydroCable is implemented in a similar fashion as the KalmanHydroBall, so the code will not be presented here. Some code will be given to highlight important points.
Excerpt of Kalman/Example/HydroCable:
#include <unsupported/Eigen/KroneckerProduct>
estimator::Matrix KalmanHydroCable::GetCovarianceP0()
{
using namespace estimator;
Vector diagP0(6);
diagP0 <<
m_InitialP[0], m_InitialP[1], m_InitialP[2],
m_InitialP[3], m_InitialP[4], m_InitialP[5];
Matrix P0i = Eigen::DiagonalMatrix<double,Eigen::Dynamic>(diagP0);
int m = m_NumPoints;
Matrix I = Matrix::Identity(m,m);
Matrix P0 = Eigen::kroneckerProduct(P0i,I);
return P0;
}
estimator::Vector KalmanHydroCable::ExpectedMeasurementOutput(const double T, const double* const X)
{
return estimator::Vector();
}
When the number of internal states depends on input parameters, the implementer must take care so that the dimensions of belonging matrices in KalmanObject scales accordingly.
- ZeroDynamics
One can of course implement a SimObject with KalmanObject interface directly, where both the SimObject and KalmanObject interface are implemented simultaneously:
public:
ZeroDynamics(
const std::string& simObjectName, ISimObjectCreator*
const creator);
void OdeFcn(const double T, const double* const X, double* const XDot, const bool IsMajorTimeStep);
void InitialConditionSetup(const double T, const double* const currentIC, double* const updatedIC, ISimObjectCreator* const creator);
#ifdef FH_VISUALIZATION
void RenderInit(Ogre::Root* const ogreRoot, ISimObjectCreator* const creator){};
void RenderUpdate(const double T, const double* const X){};
#endif
estimator::Matrix GetCovarianceP0();
estimator::Matrix GetProcessNoiseQ();
estimator::Matrix GetMeasurementNoiseR(){ return estimator::Matrix(); }
estimator::Vector ExpectedMeasurementOutput(const double T, const double* const X){ return estimator::Vector(); }
estimator::Vector ActualMeasurementOutput(const double T, const double* const X){ return estimator::Vector(); }
unsigned int GetNumMeasurements(){ return 0; }
protected:
void SetOutputPortValues(const double T, const double* const X);
const double * Output( const double T, const double * const X);
ICommonComputation* m_SetOutputPortValues;
int m_StateIndex, m_NoStates;
std::unique_ptr<double[]> m_State;
std::unique_ptr<double[]> m_P0, m_Q;
};
Definition: ZeroDynamics.h:70
See Running the simulator and estimator example(s) on how to run the code.
Case Study: Results
The above example has been run and below you can se the measured and estimated position of the ball. You can also see the true and estimated current in the three lower subplots.
Using position measurements of the ball, we are able to approximate the current that influences the displacement of the ball. The estimates are not very good, and there may be several reasons for this:
- We have artificially added white noise to the position measurement, which influence the quality of the estimates.
- Tuning the Kalman parameters (Q, R) for each KalmanObject is a tedious and difficult task. Better tuning may give better performance, and vice versa.
- The information from the measurement may not be rich enough to observe the real state. This may be particularly true for the z-component of the current, since we see a bias in the estimate.
It should be pointed out that we use identical numerical model descriptions for the simulator and estimator. This is not the case when filtering real-world measurements, and may be reflected in the estimator performance.
Measured and Estimated ball position and current.
Example: Simple pendulum
Consider a simple pendulum with angle \(\theta\) with respect to the z-axis of a north, east, and down reference frame, and angular velocity \(\omega\)
\begin{align*}
\dot \theta &= \omega\\
\dot \omega &= -\frac{g}{l}\sin \theta,
\end{align*}
where \(g\) is standard gravity and \(l\) is pendulum arm length.
Example: KalmanObject declaration
Suppose you want to estimate its movement, given an angle measurement every \(\Delta t \) seconds. The pendulum is implemented in FhSim as the class Pendulum (Example/Pendulum). To implement the KalmanObject interface on this class, consider the following class declaration, which inherits from both Pendulum and KalmanObject:
#include "KalmanObject.h"
public:
Pendulum(
const std::string& simObjectName, ISimObjectCreator*
const creator);
estimator::Matrix GetCovarianceP0();
estimator::Matrix GetProcessNoiseQ();
estimator::Matrix GetMeasurementNoiseR();
estimator::Vector ExpectedMeasurementOutput(const double T, const double* const X);
estimator::Vector ActualMeasurementOutput(const double T,, const double* const X);
unsigned int GetNumMeasurements();
protected:
ISignalPort* m_AngleMeasurement;
double m_MeasurementNoise;
double m_AngleUncertainty;
double m_AngularVelocityUncertainty;
double m_InitialP[2];
};
Definition: KalmanPendulum.h:82
Definition: Pendulum.h:66
This code shows the declarations of the virtual function from the KalmanObject interface that must be implemented.
Example: KalmanObject definitions
The corresponding definitions for the above code is
KalmanPendulum::KalmanPendulum(const std::string& simObjectName, ISimObjectCreator* const creator)
{
creator->GetDoubleParam("AngleNoise", &m_AngleUncertainty);
creator->GetDoubleParam("AngularVelocityNoise", &m_AngularVelocityUncertainty);
creator->GetDoubleParam("MeasurementNoise", &m_MeasurementNoise);
creator->GetDoubleParamArray("InitialP",m_InitialP, 2);
creator->AddInport("Angle", 1, &m_AngleMeasurement);
}
estimator::Matrix KalmanPendulum::GetCovarianceP0()
{
estimator::Matrix ret(2,2);
ret <<
m_InitialP[0],0,
0,m_InitialP[1];
return ret;
}
estimator::Matrix KalmanPendulum::GetProcessNoiseQ()
{
estimator::Matrix ret(2,2);
ret <<
m_AngleUncertainty,0,
0,m_AngularVelocityUncertainty;
return ret;
}
estimator::Matrix KalmanPendulum::GetMeasurementNoiseR()
{
estimator::Matrix ret(1,1);
ret(0,0) = m_MeasurementNoise;
return ret;
}
estimator::Vector KalmanPendulum::ActualMeasurementOutput(const double T, const double* const X)
{
const double* y = m_AngleMeasurement->GetPortValue(T,X);
estimator::Vector ret(1);
ret << m_AngleMeasurement->GetPortValue(T,X)[0];
return ret;
}
estimator::Vector KalmanPendulum::ExpectedMeasurementOutput(const double T, const double* const X)
{
estimator::Vector ret(1);
ret << X[m_AngleIndex];
return ret;
}
unsigned int KalmanPendulum::GetNumMeasurements()
{
return 1;
}
Example: Input files
To run a simulation using this code, we define two input files: one for a simulator, which emulates the real world, and an estimator, which tries to mimic the real world, given angle measurements on a regular interval. The measurement is transmitted using a Data Distributive Service (DDS) SimObject.
The input file for Simulator.xml is
<Contents>
<OBJECTS>
<Lib
LibName = "fhsim_base"
SimObject = "Example/Pendulum"
Name = "Pen"
Length = "3"
/>
<Lib LibName = "fhsim_extras"
SimObject = "Connection/DdsWriter"
Name = "Send"
Partition = "FhSimExternal"
Topic = "PhysicalEntity"
DdsRoot = "../lib"
DdsConfigFile = "../resources/ospl.xml"
ForceDdsEnv = "0"
PortWidth = "1"
ArrayIDs = "0"
PortSizes = "1"
SamplingPeriods = "0.1"
/>
</OBJECTS>
<INTERCONNECTIONS>
<Connection
Send.In1 = "Pen.Angle"
/>
</INTERCONNECTIONS>
<INITIALIZATION>
<InitialCondition
Pen.Angle = "1.0"
Pen.AngularVelocity = "0"
/>
</INITIALIZATION>
<INTEGRATION>
<Engine
IntegratorMethod="2"
NumCores="1"
TOutput="0, 0:1e-1:Inf, 100"
stepsize ="1e-2"
FileOutput="objects:ports"
/>
</INTEGRATION>
</Contents>
For Estimator.xml we have
<Contents>
<OBJECTS>
<Lib
LibName = "fhsim_base"
SimObject = "Kalman/Example/Pendulum"
Name = "Pen"
Length = "3"
AngleNoise = "1e-1"
AngularVelocityNoise = "1e-3"
MeasurementNoise = "1e-0"
InitialP = "1e-0,1e-0"
/>
<Lib LibName = "fhsim_extras"
SimObject = "Connection/DdsReader"
Name = "Measure"
Partition = "FhSimExternal"
Topic = "PhysicalEntity"
DdsRoot = "../lib"
DdsConfigFile = "../resources/ospl.xml"
ForceDdsEnv = "1"
InitializationTimeout = "2000"
PortWidth = "1"
Tags = "Angle"
DefaultAngle = "0"
ArrayIDs = "0"
PortSizes = "1"
/>
</OBJECTS>
<INTERCONNECTIONS>
<Connection
Pen.Angle = "Measure.Angle"
/>
</INTERCONNECTIONS>
<INITIALIZATION>
<InitialCondition
Pen.Angle = "0.0"
Pen.AngularVelocity = "0"
/>
</INITIALIZATION>
<INTEGRATION>
<Engine
IntegratorMethod="2"
NumCores="1"
TOutput="0, 0:1e-1:Inf, 100"
stepsize ="1e-2"
HMin="0.5"
FileOutput="objects:ports"
/>
</INTEGRATION>
</Contents>
- Note
- The estimator incorporates measurements every \(\Delta t = 0.5\) seconds, specified with HMin=0.5 in Estimator.xml.
- Warning
- Only fixed-step integrators are supported when using KalmanObjects.
Running the simulator and estimator example(s)
Two separate command windows are needed to run the simulator and estimator setup. We make use of FhSim's real-time executable to ensure that the simulation speed are the same for both the processes. In the snippets below we set them to run twice real time.
Simulator:
FhRtVis --input-file Simulator.xml --output-file Simulation.csv --simulation-speed 2
Estimator:
FhRtVis --input-file Estimator.xml --output-file Estimation.csv --simulation-speed 2
We can make a batch script that starts the two processes simultaneously. This is useful if one wants to visualize the states etc. using some visualization software.
- Note
- In the fhsim_extras library there is a simobject named Plot/Qt, which can visualize state variables.
RunBoth.bat (windows only):
@start /D C:\FhSimPlayPen\bin "Simulator" FhRtVis --input-file Simulator.xml --output-file Simulation.csv --simulation-speed 2
@start /D C:\FhSimPlayPen\bin "Estimator" FhRtVis --input-file Estimator.xml --output-file Estimation.csv --simulation-speed 2
Each line in RunBoth.bat is equivalent of performing the following: open a new command window, change directory into the one specified, and then run FhRtVis with the input parameters.
Notes and comments that may interest the user
- Tips and Tricks
- Currently, setting NumCores to 1 is the most efficient approach.
- Setting HMin to negative toggles a verbose Kalman filter where the trace of the estimation error covariance is printed before and after the measurement update \(\text{tr}(P)\). This may be useful to get an indication of the tuning of \(Q\) and \(R\). Very roughly speaking, \(Q\) dictates the rate at which \(\text{tr}(P)\) increases, and \(R\) dictates the decrement of \(\text{tr}(P)\). If, for instance, \(\text{tr}(P)\) increases very rapidly, some or all of the SimObjects' \(Q\) are too large.
- It is possible to mix SimObjects that does not implement the KalmanObject interface with KalmanObjects in the same simulation. The the Kalman filter will then handle non-KalmanObject SimObjects as perfect models, so there will be no perturbation of these states. Currently, though, 2n+1 IVPs will still be solved, where n is the total state dimension.
- It is often very difficult to judge the performance of the filter by looking at a 3D visualization in FhVis. One advise is to use some visualization software for 2D graphs to inspect chosen values. There are a utility library in FhSim that facilitates 2D visualization through a SimObject, Plot/Qt. One can send interesting values using the DDS SimObject from one instances and then use Plot/Qt to visualize everything together. See Connection/DdsWriter, Connection/DdsReader, and Plot/Scope in the
fhsim_extras
.
- Tuning and behavior of the UKF
- A few comments on the various covariance matrices and the behavior of the Kalman filter.
- Usually, only the diagonal entries of the matrix are set to non-zero (positive) values.
- If little is known about the real state vector (i.e. the state estimate is deemed highly uncertain), \(P(t_0)\) is initialized to large values. Consequently, the Kalman filter relies heavily on the measurements when updating the state estimation vector; a steep transient of the state estimate may be observed. Conversely, a \(P(t_0)\) with entries of small magnitude will produce a conservative transient, requiring more time to react to the measurements.
- The relation between \(Q\) and \(R\) plays an important part in the behavior of the Kalman filter. These matrices are often initially defined based on the implementer's considerations of the dynamics' and measurements' uncertainties. If, for instance, the process is described with high fidelity and precision, but the measurements contain a lot of noise, \(Q\) should be small and \(R\) large. The Kalman filter will under these circumstances rely more on the model description than the measurements. These matrices are tuning variables of the Kalman filter.
- What magnitude should entries in P, Q, R have?
- Tuning the KalmanFilter is time consuming, tedious and at times, frustrating.
- The tuning is a case-to-case procedure, but there are some rules-of-thumb that may apply.
- Starting with too large \(P(t_0),\, Q,\, R\) is not advisable, because the perturbation of the states (sigma points) depends on the magnitude of the square root of \(P\). If the system is stiff this may lead to unstable simulations. For example a cable's internal positions will give huge spring forces if the positional perturbations are even modest for a steel cable.
- Start with small \(P(t_0),\, Q,\, R\) and increase them iteratively.
- Let \(P(t_0)\) be small if you know that the state estimates are close to the true values.
- If you know that a measurement has very little noise, set \(R\) accordingly small. E.g. if a scalar position measurement has standard deviation 0.2 meters, the corresponding covariance is 0.04 m^2, so trying to set R slightly larger than this value is a good starting point.
- Scalability and real time
- As mentioned, this filter requires the solution to 2n+1 initial value problems. Normally only 1 IVP is solved, so for resource-demanding simulations, real-time capabilities are very hard to accomplish.
- If real time is not a big issue, syncronizing at a rate slower than real time is the easiest approach: use FhRtSim or FhRtVis and option –simulation-speed \(k\), with \(0 < k \), where \(k=1\) is execution in real time.
- Decouple the simulation (if possible), for instance: simulate perfect simobjects in an own FhSim instance and send port values to a FhSim estimator instance using the DDS interface. Unfortunately this often not possible.
- Multi-rate measurements
- It is assumed that measurements are arriving at a regular interval with predetermined period.
- Measurements with different sampling rates are supported through the use of Not-a-Number measurements.
- The Kalman filter comes with Not-a-Number detection of input vectors. Measurements that contain NaN-values are removed from the stacked measurement vector (and the corresponding R-matrix). For instance, suppose you have fast measurements every 0.1 seconds and slow measurements every 1 second. Setting the major time step to 0.1 seconds will update the state estimate with the fast measurement every 0.1 seconds. Every 1 second the measurement vector consists of both the slow and the fast measurement and will update the state estimate accordingly. This property requires that the ports responsible for communicating measurement vectors should output NaNs when there are no measurement (or it is outdated). To achieve this behavior is left as a task to the implementer.