Source code for state_space_control.analysis
"""Analysis helpers for plants and closed loops (scipy-only)."""
from typing import Optional, Tuple
import numpy as np
from .base import Plant
[docs]
def damping_report(sys: Plant) -> str:
"""Per-pole natural frequency and damping ratio table."""
lines = ['pole wn [rad/s] zeta']
for p in np.sort_complex(sys.poles()):
wn = abs(p)
zeta = -p.real / wn if wn > 0 else float('nan')
lines.append(f'{p:24.4g} {wn:11.4g} {zeta:6.3f}')
return '\n'.join(lines)
[docs]
def step_response(
sys: Plant,
input_index: int = 0,
t_final: Optional[float] = None,
n_points: int = 500,
) -> Tuple[np.ndarray, np.ndarray]:
"""Step response of one input channel; returns (t, y[t, outputs])."""
from scipy.signal import StateSpace, step
lti = StateSpace(sys.A, sys.B[:, [input_index]], sys.C,
sys.D[:, [input_index]])
if t_final is None:
# ~8 time constants of the slowest stable pole, fallback 10 s.
stable = [p for p in sys.poles() if p.real < -1e-9]
t_final = 8.0 / min(-p.real for p in stable) if stable else 10.0
t = np.linspace(0.0, t_final, n_points)
t, y = step(lti, T=t)
return t, np.atleast_2d(y.T).T
[docs]
def settling_time(
t: np.ndarray, y: np.ndarray, band: float = 0.02
) -> float:
"""Time after which y stays within +/-band of its final value."""
yf = y[-1]
scale = max(abs(yf), 1e-12)
outside = np.abs(y - yf) > band * scale
if not outside.any():
return 0.0
return float(t[np.max(np.nonzero(outside)) + 1]) \
if np.max(np.nonzero(outside)) + 1 < len(t) else float('inf')