Source code for direct_data_driven_mpc.utilities.initial_state_estimation

"""
Functions for estimating the initial state of LTI systems.

This module provides functions related to state estimation for LTI systems,
which include:

- Calculation of Observability and Toeplitz matrices,
- Estimation of initial states from input-output data,
- Calculation of equilibrium input-output pairs for steady-state conditions.
"""

import numpy as np


[docs] def observability_matrix(A: np.ndarray, C: np.ndarray) -> np.ndarray: """ Calculate the observability matrix for a state-space system defined by state matrix `A` and output matrix `C`. The observability matrix is constructed over `n` time steps, where `n` is the number of states in the system, which corresponds to the dimension of the `A` matrix. Args: A (np.ndarray): The state matrix of the system. C (np.ndarray): The output matrix of the system. Returns: np.ndarray: The observability matrix of the system. """ # Get number of states n = A.shape[0] # Number of states Ot = np.vstack([C @ np.linalg.matrix_power(A, i) for i in range(n)]) return Ot
[docs] def toeplitz_input_output_matrix( A: np.ndarray, B: np.ndarray, C: np.ndarray, D: np.ndarray, t: int ) -> np.ndarray: """ Construct a Toeplitz matrix that maps inputs to outputs for a state-space system defined by matrices `A` (state), `B` (input), `C` (output) and `D` (feedforward), over the time interval [0, t-1]. This matrix is used to express the linear response of the system outputs to the system inputs, extended over t time steps. For t = 3, this matrix takes the form:: [D 0 0] Tt = [CB D 0] [CAB CB D] Args: A (np.ndarray): The state matrix of the system. B (np.ndarray): The input matrix of the system. C (np.ndarray): The output matrix of the system. D (np.ndarray): The feedforward matrix of the system. t (int): The number of time steps for the Toeplitz matrix extension. Returns: np.ndarray: The Toeplitz input-output matrix of the system over t steps. Examples: >>> import numpy as np >>> A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) >>> B = np.array([[1], [1], [0]]) >>> C = np.array([[1, 0, 2], [0, 1, 0]]) >>> D = np.array([[0], [1]]) >>> t = 3 >>> print(toeplitz_input_output_matrix(A, B, C, D, t)) [[ 0. 0. 0.] [ 1. 0. 0.] [ 1. 0. 0.] [ 1. 1. 0.] [33. 1. 0.] [ 9. 1. 1.]] """ if t <= 0: raise ValueError("The number of time steps t must be positive.") # Get number of inputs and outputs m = B.shape[1] # Number of inputs p = C.shape[0] # Number of outputs # Precompute powers of A A_pows = [np.linalg.matrix_power(A, i) for i in range(t)] # Construct Toeplitz input-output matrix Tt = np.zeros((p * t, m * t)) for i in range(t): for j in range(t): if i == j: Tt[i * p : (i + 1) * p, j * m : (j + 1) * m] = D elif j < i: Tt[i * p : (i + 1) * p, j * m : (j + 1) * m] = ( C @ A_pows[i - j - 1] @ B ) return Tt
[docs] def estimate_initial_state( Ot: np.ndarray, Tt: np.ndarray, U: np.ndarray, Y: np.ndarray ) -> np.ndarray: """ Estimate the initial state of an observable system based on its input-output history using a least squares observer with the Toeplitz input-output and observability matrices of the system. Args: Ot (np.ndarray): The observability matrix of the system. Tt (np.ndarray): The Toeplitz input-output matrix of the system over `t` steps. U (np.ndarray): The vector of inputs over the past `t` time steps. Y (np.ndarray): The vector of outputs over the past `t` time steps. Returns: np.ndarray: The estimated initial state. Raises: ValueError: If there is a dimension mismatch between the inputs. """ # Check correct matrix dimensions if Ot.shape[0] != Y.shape[0]: raise ValueError( f"Dimension mismatch: `Ot` has {Ot.shape[0]} rows but `Y` has " f"{Y.shape[0]} rows." ) if Tt.shape[0] != Y.shape[0]: raise ValueError( f"Dimension mismatch: `Tt` has {Tt.shape[0]} rows but `Y` has " f"{Y.shape[0]} rows." ) if Tt.shape[1] != U.shape[0]: raise ValueError( f"Dimension mismatch: `Tt` has {Tt.shape[1]} columns but `U` has " f"{U.shape[0]} rows." ) # Estimate initial state based on input-output data initial_x = np.linalg.pinv(Ot) @ (Y - Tt @ U) return initial_x
[docs] def calculate_equilibrium_output_from_input( A: np.ndarray, B: np.ndarray, C: np.ndarray, D: np.ndarray, u_eq: np.ndarray, ) -> np.ndarray: """ Calculate the equilibrium output `y_eq` corresponding to an input `u_eq` so they represent an equilibrium pair of the system defined by matrices `A` (state), `B` (input), `C` (output) and `D` (feedforward). This function assumes a Linear Time-Invariant (LTI) system and that the equilibrium is calculated under zero initial conditions using the final value theorem. Args: A (np.ndarray): The state matrix of the system. B (np.ndarray): The input matrix of the system. C (np.ndarray): The output matrix of the system. D (np.ndarray): The feedforward matrix of the system. u_eq (np.ndarray): An input vector of the system. Returns: np.ndarray: The equilibrium output `y_eq` corresponding to the input `u_eq`. """ n = A.shape[0] # Order of the system # Calculate equilibrium output using the final value theorem, # assuming zero initial conditions M = C @ np.linalg.inv(np.eye(n) - A) @ B + D y_eq = M @ u_eq return y_eq
[docs] def calculate_equilibrium_input_from_output( A: np.ndarray, B: np.ndarray, C: np.ndarray, D: np.ndarray, y_eq: np.ndarray, ) -> np.ndarray: """ Calculate the equilibrium input `u_eq` corresponding to an output `y_eq` so they represent an equilibrium pair of the system defined by matrices `A` (state), `B` (input), `C` (output) and `D` (feedforward). This function assumes a Linear Time-Invariant (LTI) system and that the equilibrium is calculated under zero initial conditions using the final value theorem. Args: A (np.ndarray): The state matrix of the system. B (np.ndarray): The input matrix of the system. C (np.ndarray): The output matrix of the system. D (np.ndarray): The feedforward matrix of the system. y_eq (np.ndarray): An output vector of the system. Returns: np.ndarray: The equilibrium input `u_eq` corresponding to the output `y_eq`. """ n = A.shape[0] # Order of the system # Calculate equilibrium input using the final value theorem, # assuming zero initial conditions M = C @ np.linalg.inv(np.eye(n) - A) @ B + D u_eq = np.linalg.pinv(M) @ y_eq return u_eq