use nabled_core::scalar::NabledReal;
use nabled_linalg::lu::{self, LuProviderScalar};
use ndarray::{Array1, Array2, ArrayView2};
use crate::ControlError;
fn validate_siso_state_input<T: NabledReal>(
a: &ArrayView2<'_, T>,
b: &ArrayView2<'_, T>,
pole_count: usize,
) -> Result<usize, ControlError> {
if a.is_empty() {
return Err(ControlError::EmptyMatrix);
}
let n = a.nrows();
if a.ncols() != n || b.nrows() != n || b.ncols() != 1 || pole_count != n {
return Err(ControlError::DimensionMismatch);
}
Ok(n)
}
fn controllability_matrix<T: NabledReal + LuProviderScalar>(
a: &ArrayView2<'_, T>,
b: &ArrayView2<'_, T>,
) -> Result<Array2<T>, ControlError> {
let n = validate_siso_state_input(a, b, a.nrows())?;
let mut ctl = Array2::<T>::zeros((n, n));
let mut power = b.to_owned();
for col in 0..n {
for row in 0..n {
ctl[[row, col]] = power[[row, 0]];
}
if col + 1 < n {
power = a.dot(&power);
}
}
Ok(ctl)
}
fn monic_coefficients_from_poles<T: NabledReal>(poles: &[T]) -> Vec<T> {
let mut coeffs =
vec![num_traits::FromPrimitive::from_f64(1.0).unwrap_or_else(num_traits::One::one)];
for &pole in poles {
let mut next = vec![num_traits::Zero::zero(); coeffs.len() + 1];
for (index, &coef) in coeffs.iter().enumerate() {
next[index] += coef;
next[index + 1] -= coef * pole;
}
coeffs = next;
}
coeffs
}
fn matrix_monic_polynomial<T: NabledReal + LuProviderScalar>(
a: &ArrayView2<'_, T>,
coeffs: &[T],
) -> Array2<T> {
let n = a.nrows();
let degree = coeffs.len() - 1;
let mut powers = Vec::with_capacity(degree + 1);
let mut power = Array2::<T>::eye(n);
for _ in 0..=degree {
powers.push(power.clone());
power = a.dot(&power);
}
let mut result = Array2::<T>::zeros((n, n));
for (index, &coef) in coeffs.iter().enumerate() {
let exponent = degree - index;
for i in 0..n {
for j in 0..n {
result[[i, j]] += coef * powers[exponent][[i, j]];
}
}
}
result
}
fn ackermann_gain<T: NabledReal + LuProviderScalar>(
a: &ArrayView2<'_, T>,
b: &ArrayView2<'_, T>,
poles: &[T],
) -> Result<Array2<T>, ControlError> {
let _n = validate_siso_state_input(a, b, poles.len())?;
let n = a.nrows();
let ctl = controllability_matrix(a, b)?;
let det = lu::determinant(&ctl).map_err(|_| ControlError::SingularSystem)?;
if num_traits::Float::abs(det)
<= num_traits::FromPrimitive::from_f64(1e-12).unwrap_or(T::epsilon())
{
return Err(ControlError::InvalidInput("system is not controllable".to_string()));
}
let ctl_inv = lu::inverse(&ctl).map_err(|_| ControlError::SingularSystem)?;
let coeffs = monic_coefficients_from_poles(poles);
let alpha_a = matrix_monic_polynomial(a, &coeffs);
let mut selector = Array1::<T>::zeros(n);
selector[n - 1] = num_traits::FromPrimitive::from_f64(1.0).unwrap_or_else(num_traits::One::one);
let gain_row = selector.dot(&ctl_inv.dot(&alpha_a));
Ok(Array2::from_shape_vec((1, n), gain_row.to_vec()).expect("valid gain shape"))
}
pub fn place_poles<T: NabledReal + LuProviderScalar>(
a: &Array2<T>,
b: &Array2<T>,
poles: &[T],
) -> Result<Array2<T>, ControlError> {
place_poles_view(&a.view(), &b.view(), poles)
}
pub fn place_poles_view<T: NabledReal + LuProviderScalar>(
a: &ArrayView2<'_, T>,
b: &ArrayView2<'_, T>,
poles: &[T],
) -> Result<Array2<T>, ControlError> {
ackermann_gain(a, b, poles)
}
pub fn place_poles_into<T: NabledReal + LuProviderScalar>(
a: &Array2<T>,
b: &Array2<T>,
poles: &[T],
output: &mut Array2<T>,
) -> Result<(), ControlError> {
let gain = place_poles(a, b, poles)?;
if output.dim() != gain.dim() {
return Err(ControlError::DimensionMismatch);
}
output.assign(&gain);
Ok(())
}
#[cfg(test)]
mod tests {
use approx::assert_relative_eq;
use nabled_linalg::eigen;
use ndarray::arr2;
use num_complex::Complex;
use super::*;
fn eigenvalue_near(
requested: f64,
values: impl IntoIterator<Item = Complex<f64>>,
tol: f64,
) -> bool {
values.into_iter().any(|lambda| {
let re_diff = (lambda.re - requested).abs();
let im = lambda.im.abs();
re_diff < tol && im < tol
})
}
#[test]
fn place_poles_double_integrator() {
let a = arr2(&[[0.0, 1.0], [0.0, 0.0]]);
let b = arr2(&[[0.0], [1.0]]);
let poles = [-1.0, -2.0];
let k = place_poles(&a, &b, &poles).expect("gain");
assert_eq!(k.dim(), (1, 2));
let closed = &a - &b.dot(&k);
let eig = eigen::nonsymmetric(&closed).expect("eigen");
for &pole in &poles {
assert!(
eigenvalue_near(pole, eig.eigenvalues.iter().copied(), 1e-6),
"missing pole {pole}"
);
}
}
#[test]
fn place_poles_view_into_matches_allocating() {
let a = arr2(&[[0.0, 1.0], [0.0, 0.0]]);
let b = arr2(&[[0.0], [1.0]]);
let poles = [-3.0, -4.0];
let gain = place_poles_view(&a.view(), &b.view(), &poles).expect("view");
let mut into_buf = arr2(&[[0.0, 0.0]]);
place_poles_into(&a, &b, &poles, &mut into_buf).expect("into");
assert_relative_eq!(gain, into_buf, epsilon = 1e-12);
}
#[test]
fn place_poles_rejects_uncontrollable() {
let a = arr2(&[[1.0, 0.0], [0.0, 2.0]]);
let b = arr2(&[[0.0], [1.0]]);
let poles = [-1.0, -2.0];
let err = place_poles(&a, &b, &poles).unwrap_err();
assert!(matches!(err, ControlError::InvalidInput(_) | ControlError::SingularSystem));
}
#[test]
fn place_poles_rejects_empty_and_mismatched_dimensions() {
let empty = arr2(&[[]]);
let b = arr2(&[[0.0], [1.0]]);
let poles = [-1.0, -2.0];
assert!(matches!(place_poles(&empty, &b, &poles), Err(ControlError::EmptyMatrix)));
let a = arr2(&[[0.0, 1.0], [0.0, 0.0]]);
let bad_b = arr2(&[[0.0, 1.0], [0.0, 0.0]]);
assert!(matches!(place_poles(&a, &bad_b, &poles), Err(ControlError::DimensionMismatch)));
let mut into_buf = arr2(&[[0.0]]);
assert!(matches!(
place_poles_into(&a, &b, &poles, &mut into_buf),
Err(ControlError::DimensionMismatch)
));
}
}