Generate lifted form matrices for discrete state space system

70 views (last 30 days)
I have a discrete recursive state space equation
x_{k+1}=A_dx_k+B_du_k
y_k=Cx_k+Du_k
where x0 is nx x 1, y is M x ny, u is N x nu.
I want to work with a single equation where y =f([g(theta) = A B C D] , x0 , u). explicit input-output form.
The program that I wrote for this already works and leverages a few tricks, but I'm sure that this is a very well studied problem in Control theory. I want to have a function that:
  1. Names everything correctly (like Toeplitz matrix, Markov Parameter, Observability, convolution kernel)
  2. Is as fast as possible
  3. Can work as a function handle to preserve memory (sometimes my input data N is 100k long and that is not kind to my computer when Btheta can just be a matvec)
Ultimately I understand this program very well but not well enough that I think this is a good implementation. There must be some toolbox or app I am missing.
function [B_theta_c, y_theta_c, uc, I, A_theta, Hd, B_theta, yt] = toeplitzBtheta_FOCUSS_PreCalcs(y, theta, u, x0, sys_desc, choice, dat)
% Extract dimensions
[M, ny] = size(y); % M = number of output samples, ny = number of outputs
[N, nu] = size(u); % N = number of input samples, nu = number of inputs
nx = length(x0); % nx = number of states
[Ad, Bd, Cd, Dd, d] = StateSpace(theta, sys_desc, dat, nx, ny, nu);
% Pre-indexing for downsampling
indices = calculateDownsamplingIndices(N, M);
CAk = powers_CAk(Ad, Cd, N+1); % C*A^k powers are common to all three precalculations
CAk_needed = CAk(:, :, indices);
A_theta = reshape(permute(pagemtimes(CAk_needed, x0), [3 1 2]), M, ny);
Hd_step = reshape(permute(pagemtimes(CAk_needed, d), [3 1 2]), M, ny);
Hd = cumsum(Hd_step, 1);
B_theta_col = pagemtimes(CAk, Bd); % ny x nu x N+1
B_theta = zeros(ny*M, nu*N);
% Build B_theta row by row
% Fill in each row at once by dropping in the impulse array slice with the right delay
for i = 1:M
ii = indices(i); % downsampled time for this output row
BT_row_idx = (0:ny-1)*M + i; % rows for all ny outputs at time i
% --- Valid-lag mask (skip zeros): determine which input columns can affect output at time ii
t_vec = ii - (1:N); % 1×N
j_valid = find((t_vec > 0) & (t_vec <= N));
tj = t_vec(j_valid); % 1×K, K can be 0
% Gather the (ny×nu)×K impulse slices for this row; empty is fine
H_sel = B_theta_col(:, :, tj); % ny × nu × K
% --- Drop the entire ny×K block into place for each input channel (vectorized write)
for c = 1:nu
BT_col_idx = (c-1)*N + j_valid;
vals = reshape(H_sel(:, c, :), ny, []); % ny × K (or ny×0)
B_theta(BT_row_idx, BT_col_idx) = vals;
end
% % --- Drop the entire ny×K block into place for each input channel (vectorized write)
% row_block = zeros(ny, nu*N);
% cols = zeros(1, nu*numel(j_valid));
% p = 1;
% for k = 1:numel(j_valid)
% cols(p:p+nu-1) = (o:nu-1)*N + j_valid(k);
% p = p + nu;
% end
% row_block(:, cols) = H_sel(:,:);
% B_theta(BT_row_idx, :) = row_block; % assign the whole row block at once
% end
end
% Get the corresponding B_theta matrix for the chosen component
start_col = (choice-1)*N + 1;
end_col = choice*N;
B_theta_c = B_theta(:, start_col:end_col);
%Calculate the contribution from all OTHER components
uc = u(:, choice); % extract chosen signal
% uc = u(choice, :)';
B_theta_u_others = B_theta * u(:) - B_theta_c * u(:, choice);
yt = y - A_theta - Hd;
y_theta_c = yt(:) - B_theta_u_others;
I = speye(size(B_theta_c, 1));
end
function CAk = powers_CAk(Ad, Cd, N)
% powers_CAk - Precompute C*A^k matrices efficiently
%
% This function precomputes C*A^k for k = 0 to N-1 using efficient algorithms.
% For 2x2 matrices, it uses the Cayley-Hamilton recurrence relation.
% For larger matrices, it uses direct matrix multiplication.
% Get matrix dimensions
[nx, ~] = size(Ad);
ny = size(Cd, 1);
% Initialize output matrix
CAk = zeros(ny, nx, N);
% Set first matrix: C*A^0 = C
CAk(:, :, 1) = Cd;
CAk(:, :, 2) = Cd * Ad;
% For 2x2 matrices, use Cayley-Hamilton recurrence for efficiency
% This avoids computing matrix powers directly
if nx == 2
% Get trace and determinant for recurrence relation
s1 = trace(Ad); % s1 = λ1 + λ2
s2 = det(Ad); % s2 = λ1 * λ2
% Use recurrence: A^k = s1*A^(k-1) - s2*A^(k-2)
for k = 3:N
CAk(:, :, k) = s1 * CAk(:, :, k-1) - s2 * CAk(:, :, k-2);
end
else
% For larger matrices, use direct multiplication
% A^k = A^(k-1) * A
for k = 3:N
CAk(:, :, k) = CAk(:, :, k-1) * Ad;
end
end
end
  10 Comments
Torsten
Torsten on 9 Oct 2025 at 20:25
Edited: Torsten on 9 Oct 2025 at 20:48
Here is the computation of A_theta and B_theta for the example from above:
rng("default")
A = diag([0.7,0.5]); B = rand(2,2); C = rand(2,2); D = zeros(2,2);
x0 = [1;2];
u = rand(2,5); % five output samples
C5 = repmat({C},1,5);
D5 = repmat({D},1,5);
A_theta = blkdiag(C5{:})*[eye(size(A));A;A^2;A^3;A^4];
B_theta = blkdiag(C5{:})*[zeros(2,10);[B,zeros(2,8)];[A^1*B,B,zeros(2,6)];[A^2*B,A^1*B,B,zeros(2,4)];[A^3*B,A^2*B,A^1*B,B,zeros(2,2)]] + ...
blkdiag(D5{:});
y = A_theta*x0 + B_theta*u(:)
y = 10×1
1.1894 1.1913 1.7789 1.6595 1.5379 1.4484 1.8397 1.5497 1.8095 1.3427
<mw-icon class=""></mw-icon>
<mw-icon class=""></mw-icon>
Do you see the pattern and can you transfer it to the general case ?
Can you deduce an efficient code to compute the matrices involved (A,A^2,...,A^4,A*B,A^2*B,A^3*B)?
Moses
Moses on 9 Oct 2025 at 21:47
Yes, that's not the hard part -- Calculating the successive powers involved was a part of my old program (the powers_CAk function plus that last line with pagemtimes)
[nx, ~] = size(Ad);
ny = size(Cd, 1);
% Initialize output matrix
CAk = zeros(ny, nx, N);
% Set first matrix: C*A^0 = C
CAk(:, :, 1) = Cd;
CAk(:, :, 2) = Cd * Ad;
% For 2x2 matrices, use Cayley-Hamilton recurrence for efficiency
% This avoids computing matrix powers directly
if nx == 2
% Get trace and determinant for recurrence relation
s1 = trace(Ad); % s1 = λ1 + λ2
s2 = det(Ad); % s2 = λ1 * λ2
% Use recurrence: A^k = s1*A^(k-1) - s2*A^(k-2)
for k = 3:N
CAk(:, :, k) = s1 * CAk(:, :, k-1) - s2 * CAk(:, :, k-2);
end
else
% For larger matrices, use direct multiplication
% A^k = A^(k-1) * A
for k = 3:N
CAk(:, :, k) = CAk(:, :, k-1) * Ad;
end
end
B_theta_col = pagemtimes(CAk, Bd);
Right now I'm just using the command
h = impulse(sys_d, Kmax);
Where sys-d is the discritized state space and Kmax is a truncated impulse horizon (can be considered to be N)
This gives me All of the C*A^k*B powers I need.
The problem I have is with forming that final matrix. I know the structure: lower triangular Toeplitz (meaning every diagonal is the same and the upper triangle is all zeros), with a certain sampling rate such that rows are removed from the full toeplitz matrix (or never created).
My current implementation to get B_Theta costs a big M long for loop (M could be up to 100k long)
B_theta = zeros(ny*M, nu*N);
% Build B_theta row by row
% Fill in each row at once by dropping in the impulse array slice with the right delay
for i = 1:M
ii = indices(i); % downsampled time for this output row
BT_row_idx = (0:ny-1)*M + i; % rows for all ny outputs at time i
% --- Valid-lag mask (skip zeros): determine which input columns can affect output at time ii
t_vec = ii - (1:N); % 1×N
j_valid = find((t_vec > 0) & (t_vec <= N));
tj = t_vec(j_valid); % 1×K, K can be 0
% Gather the (ny×nu)×K impulse slices for this row; empty is fine
H_sel = h(:, :, tj); % ny × nu × K
% --- Drop the entire ny×K block into place for each input channel (vectorized write)
for c = 1:nu
BT_col_idx = (c-1)*N + j_valid;
vals = reshape(H_sel(:, c, :), ny, []); % ny × K (or ny×0)
B_theta(BT_row_idx, BT_col_idx) = vals;
end
end
SO in summary, I've solved the challenge of computing A_theta*x0 and B_theta_col, but efficiently forming the full B_theta matrix is vexing me.
I'm especially not sure if I want the matrix is a block-toeplitz form (where each ny or nu row/column is stacked to keep the matrix 2D, (M*ny, N*nu)) or in tensor form where there is an M*N matrix that contains elements of dimension ny*nu
I need the B_theta matrix to multiply against other matricies later, so I'm leaning toward keeping block_toeplitz.
Also like I said M can be very large - so I a mtrying to avoid blkdiag and repmat, and instead using the sparse() matrix command.

Sign in to comment.

Answers (0)

Categories

Find more on Sparse Matrices in Help Center and File Exchange

Products


Release

R2025b

Community Treasure Hunt

Find the treasures in MATLAB Central and discover how the community can help you!

Start Hunting!