Figure 4.5:

Evolution of the state (solid line) and EKF state estimate (dashed line).

Code for Figure 4.5

Text of the GNU GPL.

main.m


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
% This file simulates the Batch Reactor Example (3.2.1) from Haseltine
% & Rawlings (2005)
% EKF is implemented to this example and it is shown that it
% fails when a poor prior is used.
data = struct();

% States: x = [Ca Cb Cc]'
% Total pressure is measured (C = RT*[1 1 1])

% Simulate.
fprintf('Simulating without clipping.\n');
data.noclip = doekf(120, false());
fprintf('Simulating with clipping.\n');
data.yesclip = doekf(600, true());

batchreactor.m


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
function pars = batchreactor(Nsim)
% pars = batchreactor(Nsim)
%
% Returns a struct of batch reactor parameters to avoid duplication across
% MHE, EKF, and UKF scripts.
narginchk(1, 1);
mpc = import_mpctools();

% Sizes.
Delta = 0.25;
Nx = 3;
Ny = 1;
Nw = Nx;
Nv = Ny;

% Random variable standard deviations.
sig_v = 0.25; % Measurement noise.
sig_w = 0.001; % State noise.
sig_p = 0.5; % Prior.

P = sig_p.^2*eye(Nx);
Q = sig_w.^2*eye(Nw);
R = sig_v.^2*eye(Ny);

% Get model functions.
plant = mpc.getCasadiIntegrator(@batch_model, Delta, [Nx], {'x'});
frk4 = mpc.getCasadiFunc(@(x) batch_model(x), [Nx], {'x'}, {'frk4'}, ...
                         'rk4', true(), 'Delta', Delta, 'M', 4);
f = mpc.getCasadiFunc(@(x, w) frk4(x) + w, [Nx, Nw], {'x', 'w'}, {'f'});
h = mpc.getCasadiFunc(@measurement, [Nx], {'x'}, {'h'});

% Choose prior.
xhat0 = [1; 0; 4];
x0 = [0.5; 0.05; 0.0];

% Simulate the system to get data.
rng(0);
w = sig_w*randn(Nw, Nsim);
v = sig_v*randn(Nv, Nsim + 1);

xsim = NaN(Nx, Nsim + 1);
xsim(:,1) = x0;

ysim = NaN(Ny, Nsim + 1);
yclean = NaN(Ny, Nsim + 1);

for t = 1:(Nsim + 1)
    xsim(:,t) = max(xsim(:,t), 0); % Make sure concentration is nonnegative.
    yclean(:,t) = full(h(xsim(:,t)));
    ysim(:,t) = yclean(:,t) + v(:,t);

    if t <= Nsim
        xsim(:,t + 1) = full(plant(xsim(:,t))) + w(:,t);
    end
end
t = (0:Nsim)*Delta;

% Get plotting function for convenience.
reactorplot = @reactorplot;

% Package everybody up. Note that this is a quick and dirty approach.
varnames = who();
pars = struct();
for i = 1:length(varnames);
    v = varnames{i};
    pars.(v) = eval(v);
end

end%function

function dxdt = batch_model(x)
    % Nonlinear ODE function.
    k1 = 0.5;
    km1 = 0.05;
    k2 = 0.2;
    km2 = 0.01;

    cA = x(1);
    cB = x(2);
    cC = x(3);

    rate1 = k1*cA - km1*cB*cC;
    rate2 = k2*cB.^2 - km2*cC;
    dxdt = [
        -rate1;
        rate1 - 2*rate2;
        rate1 + rate2;
    ];
end%function

function y = measurement(x)
    % Linear measurement function.
    RT = 0.0821*400;
    y = RT*sum(x);
end%function

function fig = reactorplot(x, xhat, y, yhat, yclean, Delta)
    % Make a plot of actual and estimated states.
    narginchk(6, 6);
    fig = figure();
    Nx = 3;
    Nt = size(x, 2) - 1;
    t = (0:Nt)*Delta;

    ax = subplot(2, 1, 2);
    hold('on');
    plot(t, yclean, '-k', 'DisplayName', 'Actual');
    plot(t, yclean, '--c', 'DisplayName', 'Measured');
    plot(t, yhat, 'ok', 'DisplayName', 'Estimated');
    ylabel('P');
    xlabel('t');
    legend('Location', 'EastOutside');
    set(ax, 'ylim', [10, 35]);

    ax = subplot(2, 1, 1);
    hold('on');
    colors = {'r', 'b', 'g'};
    names = {'A', 'B', 'C'};
    for i = 1:Nx
        plot(t, x(i,:) + 1e-10, ['-', colors{i}], 'DisplayName', ...
             sprintf('Actual C_%s', names{i}));
        plot(t, xhat(i,:) + 1e-10, ['o', colors{i}], 'DisplayName', ...
             sprintf('Estimated C_%s', names{i}));
    end
    xlabel('t');
    ylabel('c_i');
    legend('Location', 'EastOutside');
    set(ax, 'ylim', [-1, 1.5]);
end%function

doekf.m


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
function data = doekf(Nsim, clipping)
    % data = doekf(Nsim, clipping)
    %
    % Simulate EKF with or without clipping.
    pars = batchreactor(Nsim);
    Nx = pars.Nx;
    Ny = pars.Ny;
    Nw = pars.Nw;

    % Get linearization functions.
    f = pars.f;
    Afunc = f.factory('Afunc', f.name_in(), 'jac:f:x');
    Gfunc = f.factory('Gfunc', f.name_in(), 'jac:f:w');
    h = pars.h;
    Cfunc = h.factory('Cfunc', h.name_in(), 'jac:h:x');

    % Choose simulation parameters.
    Nsim = pars.Nsim;
    t = pars.t;
    x = pars.xsim;
    y = pars.ysim;
    Qw = pars.Q;
    Rv = pars.R;

    % Try ekf.
    xhat = NaN(Nx, Nsim + 1);
    yhat = NaN(Ny, Nsim + 1);
    e = NaN(Ny, Nsim + 1);

    xhat(:,1) = pars.xhat0; % Poor initial guess
    L = cell(Nsim + 1, 1);
    P = cell(Nsim + 1, 1);
    P{1} = pars.P;

    % Loop for EKF State Estimation
    for t = 1:(Nsim + 1)
        % Measurement.
        yhat(:,t) = full(h(xhat(:,t)));
        e(:,t) = y(:,t) - yhat(:,t);

        % Correction
        C = full(Cfunc(xhat(:,t)));
        L{t} = (P{t}*C')/(C*P{t}*C' + Rv); % EKF gain
        P{t} = P{t} - L{t}*C*P{t};

        xhat(:,t) = xhat(:,t) + L{t}*e(:,t);

        % Implementing clipping
        if clipping
            xhat(:,t) = max(xhat(:,t), 0);
        end

        % Advance forecast.
        if t <= Nsim
            w = zeros(Nw, 1);

            A = full(Afunc(xhat(:,t), w));
            G = full(Gfunc(xhat(:,t), w));

            xhat(:,t + 1) = full(f(xhat(:,t), w));
            P{t + 1} = A*P{t}*A' + G*Qw*G';
        end
    end

    % Make a plot.
    pars.reactorplot(x, xhat, y, yhat, pars.yclean, pars.Delta);
    if clipping
        set(gca(), 'yscale', 'log', 'ylim', [1e-4, 1e3]);
    end

    % Form data table.
    data = [pars.t', x', xhat', y', yhat'];