Identification using LowLevelParticleFilters.jl

langestefan
Mon, 15 Sep 2025 10:35:23 +0000
Modelling & Simulations

I am looking for an example (or some guidance) on how to fit a Kalman smoother to data using log-likelihood maximization using LowLevelParticleFilters.jl.

I already have a model structure and have collected a dataset. It is a fairly simple 3-state LTI system, of which I can measure 2. It models the (very slow) thermal dynamics of a building in the presence of several large disturbances. I have decently accurate data on a few of these, like the ambient temperature and the solar irradiance. But others I am still missing, like people and heat gains from electronics. (it is actually very similar to the content described in the tutorials by Baggepinnen)

Now my goals are as follows:

Calibrate the model parameters for 24 hour simulation performance. I will use this in an MPC context.

Calibrate the process and measurement noise covariance matrices.

I augmented the state-space model with an integrating disturbance, I tried to follow: Disturbance gallery · LowLevelParticleFilters Documentation

I already followed the tutorials on youtube by Baggepinnen. I also read through the documentation, and followed the tutorials on JuliaHub.

First, I understand I need to disretize the linear model. As described in How to Tune a Kalman Filter: Step-by-Step Guide | JuliaHub, we can use the matrix exponential for this. I implemented the discrete dynamics as follows:

function setup_filter(p::Vector{T}) where {T}
    # unpack parameters
    C_i, C_e, C_h, R_ia, R_ie, R_ea, R_ih, A_e, A_w = p[1:9]
    σi, σe, σh, σq = p[10:13] # process noise std dev
    σmi, σmh = p[14:15] # measurement noise std dev (interior thermostat, supply temp)
    Ti0, Te0, Th0 = p[16:18] # initial state

    # continuous-time state-space matrices (3x3 system)
    A_cont = @SMatrix [
        (-1/R_ia-1/R_ie-1/R_ih)/C_i 1/(C_i*R_ie) 1/(C_i*R_ih);
        1/(C_e*R_ie) (-1/R_ea-1/R_ie)/C_e 0;
        1/(C_h*R_ih) 0 -1/(C_h*R_ih)
    ]
    B_cont = @SMatrix [
        1/(C_i*R_ia) 0 A_w/C_i;
        1/(C_e*R_ea) 0 A_e/C_e;
        0 1/C_h 0
    ]
    G = @SMatrix [1 / C_i; 0; 0]

    # x_aug = [Ti, Te, Th, q]' where q is the integrating disturbance
    A_cont_aug = @SMatrix [
        A_cont[1, 1] A_cont[1, 2] A_cont[1, 3] G[1];
        A_cont[2, 1] A_cont[2, 2] A_cont[2, 3] G[2];
        A_cont[3, 1] A_cont[3, 2] A_cont[3, 3] G[3];
        0.0 0.0 0.0 0.0
    ]

    B_cont_aug = @SMatrix [
        B_cont[1, 1] B_cont[1, 2] B_cont[1, 3];
        B_cont[2, 1] B_cont[2, 2] B_cont[2, 3];
        B_cont[3, 1] B_cont[3, 2] B_cont[3, 3];
        0.0 0.0 0.0
    ]

    # augmented AB matrix for discretization
    AB_cont = zeros(7, 7)
    AB_cont[1:4, 1:4] = Matrix(A_cont_aug)
    AB_cont[1:4, 5:7] = Matrix(B_cont_aug)

    # discretize
    AB_disc = exp(AB_cont * Ts)
    A_disc = SMatrix{4,4}(AB_disc[1:4, 1:4])
    B_disc = SMatrix{4,3}(AB_disc[1:4, 5:7])

    # y = C * x + 0 * u --> output is [Ti, Th]
    C = @SMatrix [1.0 0.0 0.0 0.0; 0.0 0.0 1.0 0.0]

    # process noise covariance R1
    R1 = Diagonal([σi^2 / C_i^2, σe^2 / C_e^2, σh^2 / C_h^2, σq^2 / C_i^2])

    # measurement noise covariance R2
    R2 = Diagonal([σmi^2, σmh^2])

    # initial state distribution d0
    d0 = MvNormal(T.([Ti0, Te0, Th0, 0.0]), Diagonal([1.0, 5.0, 5.0, 1.0]))

    return KalmanFilter(
        A_disc, B_disc, C, zeros(ny, nu),
        R1, R2, d0; Ts, p
    )
end

# Re-initialize with corrected filter
p = [p_init[Param] for Param in
     ["C_i", "C_e", "C_h", "R_ia", "R_ie", "R_ea", "R_ih", "A_e", "A_w",
    "σi", "σe", "σh", "σq", "σmi", "σmh", "Ti0", "Te0", "Th0"]]

kf = setup_filter(p);
measure = LowLevelParticleFilters.measurement(kf)

The problem here is that it doesn’t seem to be possible to autodiff through this, because of the matrix exponential. When I replace the matrix exponential by a simple forward euler step (not ideal for accuracy), I still get autodiff complaints that KalmanFilter constructor in LowLevelParticleFilters.jl is internally calling eigvals() which then fails.

Should I use the c2d(ss) as described here instead? Will that work with autodiff?

Discretization · LowLevelParticleFilters Documentation

I would appreciate some guidance on how to approach this problem.

20 posts - 3 participants