Spaces:
Running on Zero
Running on Zero
| """Linear state-space and algebraic system analysis tools.""" | |
| from __future__ import annotations | |
| from typing import Any | |
| import numpy as np | |
| from scipy import linalg, signal | |
| from controlai_agent.registry import registry | |
| from controlai_agent.verifier import verifier | |
| def exact_zoh(A: list[list[float]], B: list[list[float]], Ts: float) -> dict[str, Any]: | |
| A_mat = np.array(A, dtype=float) | |
| B_mat = np.array(B, dtype=float) | |
| n = A_mat.shape[0] | |
| m = B_mat.shape[1] if B_mat.ndim > 1 else 1 | |
| C_dummy = np.eye(n) | |
| D_dummy = np.zeros((n, m)) | |
| Ad, Bd, _, _, _ = signal.cont2discrete((A_mat, B_mat, C_dummy, D_dummy), Ts, method="zoh") | |
| # Run independent verification | |
| v_report = verifier.verify_zoh(A_mat, B_mat, Ts, Ad, Bd) | |
| return { | |
| "Ad": Ad.tolist(), | |
| "Bd": Bd.tolist(), | |
| "sample_time": Ts, | |
| "verification": v_report, | |
| } | |
| def eigen_analysis(A: list[list[float]], discrete: bool = False) -> dict[str, Any]: | |
| A_mat = np.array(A, dtype=float) | |
| eigenvalues = np.linalg.eigvals(A_mat) | |
| if discrete: | |
| stable = bool(np.all(np.abs(eigenvalues) < 1.0)) | |
| criterion = "|lambda_i| < 1" | |
| else: | |
| stable = bool(np.all(np.real(eigenvalues) < 0.0)) | |
| criterion = "Re(lambda_i) < 0" | |
| formatted_eigs = [] | |
| for eig in eigenvalues: | |
| if abs(eig.imag) < 1e-9: | |
| formatted_eigs.append(f"{eig.real:.6g}") | |
| else: | |
| sign = "+" if eig.imag >= 0 else "-" | |
| formatted_eigs.append(f"{eig.real:.6g} {sign} {abs(eig.imag):.6g}j") | |
| return { | |
| "eigenvalues": [[float(e.real), float(e.imag)] for e in eigenvalues], | |
| "eigenvalues_formatted": formatted_eigs, | |
| "is_stable": stable, | |
| "criterion": criterion, | |
| "time_domain": "discrete" if discrete else "continuous", | |
| } | |
| def controllability_analysis(A: list[list[float]], B: list[list[float]]) -> dict[str, Any]: | |
| A_mat = np.array(A, dtype=float) | |
| B_mat = np.array(B, dtype=float) | |
| n = A_mat.shape[0] | |
| blocks = [B_mat] | |
| current = B_mat | |
| for _ in range(1, n): | |
| current = A_mat @ current | |
| blocks.append(current) | |
| C_mat = np.hstack(blocks) | |
| rank = int(np.linalg.matrix_rank(C_mat)) | |
| is_controllable = rank == n | |
| return { | |
| "controllability_matrix": C_mat.tolist(), | |
| "rank": rank, | |
| "state_dimension": n, | |
| "is_controllable": is_controllable, | |
| } | |
| def observability_analysis(A: list[list[float]], C: list[list[float]]) -> dict[str, Any]: | |
| A_mat = np.array(A, dtype=float) | |
| C_mat = np.array(C, dtype=float) | |
| n = A_mat.shape[0] | |
| blocks = [C_mat] | |
| current = C_mat | |
| for _ in range(1, n): | |
| current = current @ A_mat | |
| blocks.append(current) | |
| O_mat = np.vstack(blocks) | |
| rank = int(np.linalg.matrix_rank(O_mat)) | |
| is_observable = rank == n | |
| return { | |
| "observability_matrix": O_mat.tolist(), | |
| "rank": rank, | |
| "state_dimension": n, | |
| "is_observable": is_observable, | |
| } | |
| def state_space_to_transfer_function( | |
| A: list[list[float]], B: list[list[float]], C: list[list[float]], D: list[list[float]] | |
| ) -> dict[str, Any]: | |
| A_mat = np.array(A, dtype=float) | |
| B_mat = np.array(B, dtype=float) | |
| C_mat = np.array(C, dtype=float) | |
| D_mat = np.array(D, dtype=float) | |
| num, den = signal.ss2tf(A_mat, B_mat, C_mat, D_mat) | |
| poles = np.roots(den) | |
| zeros = np.roots(num[0]) if len(num[0]) > 1 else [] | |
| return { | |
| "numerator": num[0].tolist(), | |
| "denominator": den.tolist(), | |
| "poles": [[float(p.real), float(p.imag)] for p in poles], | |
| "zeros": [[float(z.real), float(z.imag)] for z in zeros], | |
| } | |
| def solve_lyapunov(A: list[list[float]], Q: list[list[float]], discrete: bool = False) -> dict[str, Any]: | |
| A_mat = np.array(A, dtype=float) | |
| Q_mat = np.array(Q, dtype=float) | |
| if discrete: | |
| P = linalg.solve_discrete_lyapunov(A_mat.T, Q_mat) | |
| residual = float(np.max(np.abs(A_mat.T @ P @ A_mat - P + Q_mat))) | |
| else: | |
| P = linalg.solve_continuous_lyapunov(A_mat.T, -Q_mat) | |
| residual = float(np.max(np.abs(A_mat.T @ P + P @ A_mat + Q_mat))) | |
| p_eigs = np.linalg.eigvalsh(P) | |
| is_pos_def = bool(np.all(p_eigs > 1e-10)) | |
| return { | |
| "P": P.tolist(), | |
| "P_eigenvalues": p_eigs.tolist(), | |
| "is_positive_definite": is_pos_def, | |
| "residual_max": residual, | |
| "time_domain": "discrete" if discrete else "continuous", | |
| } | |