BALRED
Balanced truncation reduces a state-space model by preserving the states with the largest Hankel singular values, which represent how strongly each state contributes to input-output dynamics.
For a continuous-time linear system
\dot{x} = Ax + Bu, \quad y = Cx + Du
balanced reduction computes a transformed realization where controllability and observability Gramians are equal and diagonal, then truncates low-energy states to produce a reduced model of selected order.
This wrapper accepts stable continuous-time state-space matrices and returns the reduced realization as deterministic matrix data so the result can be compared reliably in Excel and automated tests.
Excel Usage
=BALRED(A, B, C, D, order)
A(list[list], required): The state dynamics matrix A (NxN).B(list[list], required): The input matrix B (NxM).C(list[list], required): The output matrix C (Ny x N).D(list[list], required): The feedthrough matrix D (Ny x M).order(int, required): Desired order of the reduced model.
Returns (str): Stringified dictionary containing the reduced state-space matrices A, B, C, and D.
Example 1: Reduce stable diagonal system to order two
Inputs:
| A | B | C | D | order | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| -1 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 0 | 2 |
| 0 | -2 | 0 | 0 | 1 | ||||||
| 0 | 0 | -3 | 0 | 1 | ||||||
| 0 | 0 | 0 | -4 | 1 |
Excel formula:
=BALRED({-1,0,0,0;0,-2,0,0;0,0,-3,0;0,0,0,-4}, {1;1;1;1}, {1,1,1,1}, {0}, 2)
Expected output:
"{'A': [[-1.8881943210548617, -1.0094735271597388], [-1.0094735271597395, -2.396610708833071]], 'B': [[1.9213621306674076], [0.5463180989081131]], 'C': [[1.9213621306674076, 0.5463180989081124]], 'D': [[0.0]]}"
Example 2: Reduce coupled second-order system to order one
Inputs:
| A | B | C | D | order | ||
|---|---|---|---|---|---|---|
| -2 | 1 | 1 | 1 | 0 | 0 | 1 |
| -1 | -3 | 0.5 |
Excel formula:
=BALRED({-2,1;-1,-3}, {1;0.5}, {1,0}, {0}, 1)
Expected output:
"{'A': [[-2.03576165455737]], 'B': [[1.0281468837336207]], 'C': [[1.0281468837336207]], 'D': [[0.0]]}"
Example 3: Reduce stable MIMO system to order two
Inputs:
| A | B | C | D | order | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| -3 | 1 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 2 |
| 0 | -2 | 1 | 0 | 1 | 0 | 1 | 1 | 0 | 0 | |
| 0 | 0 | -1 | 1 | 1 |
Excel formula:
=BALRED({-3,1,0;0,-2,1;0,0,-1}, {1,0;0,1;1,1}, {1,0,0;0,1,1}, {0,0;0,0}, 2)
Expected output:
"{'A': [[-0.8992182054067066, 0.30850424263019266], [-0.1383294994280792, -3.6954672181661814]], 'B': [[0.8890469932778241, 1.2443170611921701], [0.8160586847390662, -0.4681882168801548]], 'C': [[0.34033169244847766, 0.8278820677431518], [1.4909405904144586, -0.4469488396593018]], 'D': [[0.0, 0.0], [0.0, 0.0]]}"
Example 4: Request full order from already stable system
Inputs:
| A | B | C | D | order | ||
|---|---|---|---|---|---|---|
| -1 | 0 | 1 | 1 | 1 | 0 | 2 |
| 0 | -4 | 2 |
Excel formula:
=BALRED({-1,0;0,-4}, {1;2}, {1,1}, {0}, 2)
Expected output:
"{'A': [[-1.8936609374091684, -1.3719886811400714], [-1.3719886811400714, -3.106339062590834]], 'B': [[1.6097857971368428], [0.6392102059076513]], 'C': [[1.6097857971368423, 0.6392102059076515]], 'D': [[0.0]]}"
Python Code
Show Code
import control as ct
import numpy as np
from scipy.linalg import cholesky, solve_continuous_lyapunov, svd
def balred(A, B, C, D, order):
"""
Balanced reduced order model of a system.
See: https://python-control.readthedocs.io/en/latest/generated/control.balred.html
This example function is provided as-is without any representation of accuracy.
Args:
A (list[list]): The state dynamics matrix A (NxN).
B (list[list]): The input matrix B (NxM).
C (list[list]): The output matrix C (Ny x N).
D (list[list]): The feedthrough matrix D (Ny x M).
order (int): Desired order of the reduced model.
Returns:
str: Stringified dictionary containing the reduced state-space matrices A, B, C, and D.
"""
try:
def to_2d_array(values, name):
if not isinstance(values, list):
values = [[values]]
elif values and not isinstance(values[0], list):
values = [[item] for item in values]
try:
array = np.asarray(values, dtype=float)
except (TypeError, ValueError):
return None, f"Error: {name} must contain only numeric values"
if array.ndim != 2:
return None, f"Error: {name} must be a 2D matrix"
return array, None
def clean_matrix(values):
matrix = np.real_if_close(values, tol=1000)
matrix = np.where(np.abs(matrix) < 1e-12, 0.0, matrix)
return np.asarray(matrix, dtype=float).tolist()
def canonicalize_state_signs(a_matrix, b_matrix, c_matrix):
a_matrix = np.asarray(a_matrix, dtype=float).copy()
b_matrix = np.asarray(b_matrix, dtype=float).copy()
c_matrix = np.asarray(c_matrix, dtype=float).copy()
for index in range(a_matrix.shape[0]):
signature = np.concatenate((c_matrix[:, index], b_matrix[index, :], a_matrix[:, index], a_matrix[index, :]))
significant = signature[np.abs(signature) > 1e-12]
if significant.size and significant[0] < 0:
a_matrix[:, index] *= -1
a_matrix[index, :] *= -1
b_matrix[index, :] *= -1
c_matrix[:, index] *= -1
return a_matrix, b_matrix, c_matrix
A_mat, error = to_2d_array(A, "A")
if error:
return error
B_mat, error = to_2d_array(B, "B")
if error:
return error
C_mat, error = to_2d_array(C, "C")
if error:
return error
D_mat, error = to_2d_array(D, "D")
if error:
return error
nstates = A_mat.shape[0]
if nstates == 0 or A_mat.shape[1] != nstates:
return "Error: A must be a non-empty square matrix"
if B_mat.shape[0] != nstates:
return "Error: B must have the same number of rows as A"
if C_mat.shape[1] != nstates:
return "Error: C must have the same number of columns as A"
if D_mat.shape != (C_mat.shape[0], B_mat.shape[1]):
return "Error: D must have dimensions compatible with B and C"
desired_order = int(order)
if desired_order < 1 or desired_order > nstates:
return "Error: order must be between 1 and the number of states in A"
eigenvalues = np.linalg.eigvals(A_mat)
if np.any(np.real(eigenvalues) >= 0):
return "Error: balanced reduction requires a stable continuous-time A matrix"
try:
system = ct.ss(A_mat.tolist(), B_mat.tolist(), C_mat.tolist(), D_mat.tolist())
reduced = ct.balred(system, desired_order)
a_red = np.asarray(reduced.A, dtype=float)
b_red = np.asarray(reduced.B, dtype=float)
c_red = np.asarray(reduced.C, dtype=float)
d_red = np.asarray(reduced.D, dtype=float)
except Exception:
gram_controllability = solve_continuous_lyapunov(A_mat, -(B_mat @ B_mat.T))
gram_observability = solve_continuous_lyapunov(A_mat.T, -(C_mat.T @ C_mat))
gram_controllability = 0.5 * (gram_controllability + gram_controllability.T)
gram_observability = 0.5 * (gram_observability + gram_observability.T)
factor_controllability = cholesky(gram_controllability, lower=True)
factor_observability = cholesky(gram_observability, lower=True)
left_vectors, singular_values, right_vectors_h = svd(
factor_observability.T @ factor_controllability
)
if np.any(singular_values[:desired_order] <= 0):
return "Error: system must be controllable and observable for balanced reduction"
sigma_inv_sqrt = np.diag(1.0 / np.sqrt(singular_values))
transform = factor_controllability @ right_vectors_h.T @ sigma_inv_sqrt
inverse_transform = sigma_inv_sqrt @ left_vectors.T @ factor_observability.T
balanced_a = inverse_transform @ A_mat @ transform
balanced_b = inverse_transform @ B_mat
balanced_c = C_mat @ transform
a_red = balanced_a[:desired_order, :desired_order]
b_red = balanced_b[:desired_order, :]
c_red = balanced_c[:, :desired_order]
d_red = D_mat
a_red, b_red, c_red = canonicalize_state_signs(a_red, b_red, c_red)
return str({
"A": clean_matrix(a_red),
"B": clean_matrix(b_red),
"C": clean_matrix(c_red),
"D": clean_matrix(d_red),
})
except Exception as e:
return f"Error: {str(e)}"