use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AllocationError {
Infeasible,
SingularMatrix,
DimensionMismatch,
InvalidWeight,
InvalidBounds,
}
impl core::fmt::Display for AllocationError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Infeasible => write!(f, "allocation infeasible"),
Self::SingularMatrix => write!(f, "singular Gram matrix"),
Self::DimensionMismatch => write!(f, "dimension mismatch"),
Self::InvalidWeight => write!(f, "non-positive weight"),
Self::InvalidBounds => write!(f, "invalid bounds (u_min > u_max)"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct WeightedPseudoInverse<S, const N: usize, const M: usize> {
b: [[S; M]; N],
w: [S; M],
u_min: [S; M],
u_max: [S; M],
u_pref: [S; M],
singular_tol: S,
}
impl<S: ControlScalar, const N: usize, const M: usize> WeightedPseudoInverse<S, N, M> {
pub fn new(
b: [[S; M]; N],
w: [S; M],
u_min: [S; M],
u_max: [S; M],
) -> Result<Self, AllocationError> {
for i in 0..M {
if w[i] <= S::ZERO {
return Err(AllocationError::InvalidWeight);
}
if u_min[i] > u_max[i] {
return Err(AllocationError::InvalidBounds);
}
}
Ok(Self {
b,
w,
u_min,
u_max,
u_pref: [S::ZERO; M],
singular_tol: S::from_f64(1e-10),
})
}
pub fn set_preference(&mut self, u_pref: [S; M]) {
self.u_pref = u_pref;
}
pub fn set_singular_tol(&mut self, tol: S) {
self.singular_tol = tol;
}
pub fn allocate(&self, v_des: &[S; N]) -> Result<[S; M], AllocationError> {
let mut w_inv = [S::ZERO; M];
for (inv, &wi) in w_inv.iter_mut().zip(self.w.iter()) {
*inv = S::ONE / wi;
}
let mut gram = [[S::ZERO; N]; N];
for (row, gram_row) in gram.iter_mut().enumerate() {
for (col, gram_val) in gram_row.iter_mut().enumerate() {
*gram_val = w_inv
.iter()
.enumerate()
.fold(S::ZERO, |acc, (k, &w_inv_k)| {
acc + self.b[row][k] * w_inv_k * self.b[col][k]
});
}
}
let mut rhs = [S::ZERO; N];
for (row, rhs_val) in rhs.iter_mut().enumerate() {
let bu: S = self.b[row]
.iter()
.zip(self.u_pref.iter())
.fold(S::ZERO, |acc, (&bij, &up)| acc + bij * up);
*rhs_val = v_des[row] - bu;
}
let lambda = gauss_solve_n::<S, N>(&gram, &rhs, self.singular_tol)?;
let mut u = [S::ZERO; M];
for (j, (u_j, (&up_j, &w_inv_j))) in u
.iter_mut()
.zip(self.u_pref.iter().zip(w_inv.iter()))
.enumerate()
{
let bt_lam: S = self
.b
.iter()
.zip(lambda.iter())
.fold(S::ZERO, |acc, (b_row, &lam_i)| acc + b_row[j] * lam_i);
*u_j = up_j + w_inv_j * bt_lam;
}
self.constrained_realloc(u, v_des)
}
fn constrained_realloc(
&self,
mut u: [S; M],
v_des: &[S; N],
) -> Result<[S; M], AllocationError> {
let mut saturated = [false; M];
for j in 0..M {
if u[j] < self.u_min[j] {
u[j] = self.u_min[j];
saturated[j] = true;
} else if u[j] > self.u_max[j] {
u[j] = self.u_max[j];
saturated[j] = true;
}
}
let free_count = saturated.iter().filter(|&&s| !s).count();
if free_count == 0 {
return Ok(u);
}
let mut v_res = [S::ZERO; N];
for (row, v_res_val) in v_res.iter_mut().enumerate() {
let bu: S = self.b[row]
.iter()
.zip(u.iter())
.fold(S::ZERO, |acc, (&bij, &uj)| acc + bij * uj);
*v_res_val = v_des[row] - bu;
}
let res_norm: S = v_res.iter().fold(S::ZERO, |acc, &v| acc + v * v);
if res_norm < S::from_f64(1e-20) {
return Ok(u);
}
let mut free_idx = [0usize; M];
let mut fi = 0usize;
for (j, &sat) in saturated.iter().enumerate() {
if !sat {
free_idx[fi] = j;
fi += 1;
}
}
let mut gram_free = [[S::ZERO; N]; N];
for (row, gram_row) in gram_free.iter_mut().enumerate() {
for (col, gram_val) in gram_row.iter_mut().enumerate() {
*gram_val = free_idx[..free_count].iter().fold(S::ZERO, |acc, &k| {
acc + self.b[row][k] * (S::ONE / self.w[k]) * self.b[col][k]
});
}
}
let lambda_free = match gauss_solve_n::<S, N>(&gram_free, &v_res, self.singular_tol) {
Ok(lam) => lam,
Err(_) => return Ok(u),
};
for &k in free_idx[..free_count].iter() {
let bt_lam: S = self
.b
.iter()
.zip(lambda_free.iter())
.fold(S::ZERO, |acc, (b_row, &lam_i)| acc + b_row[k] * lam_i);
let delta = (S::ONE / self.w[k]) * bt_lam;
u[k] = (u[k] + delta).clamp_val(self.u_min[k], self.u_max[k]);
}
Ok(u)
}
pub fn weighted_cost(&self, u: &[S; M]) -> S {
u.iter().zip(self.u_pref.iter()).zip(self.w.iter()).fold(
S::ZERO,
|acc, ((&uj, &up), &wj)| {
let diff = uj - up;
acc + wj * diff * diff
},
)
}
pub fn virtual_control(&self, u: &[S; M]) -> [S; N] {
let mut v = [S::ZERO; N];
for (row, v_val) in v.iter_mut().enumerate() {
*v_val = self.b[row]
.iter()
.zip(u.iter())
.fold(S::ZERO, |acc, (&bij, &uj)| acc + bij * uj);
}
v
}
pub fn tracking_error(&self, u: &[S; M], v_des: &[S; N]) -> S {
let v = self.virtual_control(u);
let sq: S = v.iter().zip(v_des.iter()).fold(S::ZERO, |acc, (&vi, &di)| {
let d = vi - di;
acc + d * d
});
S::from_f64(libm::sqrt(sq.to_f64()))
}
}
#[allow(clippy::needless_range_loop)]
fn gauss_solve_n<S: ControlScalar, const N: usize>(
a: &[[S; N]; N],
b: &[S; N],
tol: S,
) -> Result<[S; N], AllocationError> {
let mut aug = *a;
let mut rhs = *b;
for col in 0..N {
let mut max_row = col;
let mut max_val = aug[col][col].abs();
for row in (col + 1)..N {
let v = aug[row][col].abs();
if v > max_val {
max_val = v;
max_row = row;
}
}
if max_val < tol {
return Err(AllocationError::SingularMatrix);
}
if max_row != col {
aug.swap(max_row, col);
rhs.swap(max_row, col);
}
let pivot = aug[col][col];
for row in (col + 1)..N {
let factor = aug[row][col] / pivot;
for k in col..N {
let sub = factor * aug[col][k];
aug[row][k] -= sub;
}
let sub_r = factor * rhs[col];
rhs[row] -= sub_r;
}
}
let mut x = [S::ZERO; N];
for i in (0..N).rev() {
let mut acc = rhs[i];
for j in (i + 1)..N {
acc -= aug[i][j] * x[j];
}
x[i] = acc / aug[i][i];
}
Ok(x)
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq<const M: usize>(a: &[f64; M], b: &[f64; M], tol: f64) -> bool {
a.iter()
.zip(b.iter())
.all(|(&ai, &bi)| (ai - bi).abs() <= tol)
}
#[test]
fn test_square_exact_allocation() {
let b = [[1.0f64, 0.0], [0.0, 1.0]];
let w = [1.0f64, 1.0];
let u_min = [-10.0f64, -10.0];
let u_max = [10.0f64, 10.0];
let alloc =
WeightedPseudoInverse::<f64, 2, 2>::new(b, w, u_min, u_max).expect("valid constructor");
let v_des = [1.0f64, 2.0];
let u = alloc.allocate(&v_des).expect("allocation ok");
assert!(
approx_eq(&u, &[1.0, 2.0], 1e-9),
"square identity: got {:?}",
u
);
assert!(alloc.tracking_error(&u, &v_des) < 1e-9);
}
#[test]
fn test_over_actuated_weighted() {
let b: [[f64; 3]; 2] = [[1.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let w = [1.0f64, 1.0, 100.0];
let u_min = [-10.0f64; 3];
let u_max = [10.0f64; 3];
let alloc =
WeightedPseudoInverse::<f64, 2, 3>::new(b, w, u_min, u_max).expect("valid constructor");
let v_des = [1.0f64, 1.0];
let u = alloc.allocate(&v_des).expect("allocation ok");
let v_actual = alloc.virtual_control(&u);
assert!(
(v_actual[0] - 1.0).abs() < 1e-8,
"v[0] mismatch: {}",
v_actual[0]
);
assert!(
(v_actual[1] - 1.0).abs() < 1e-8,
"v[1] mismatch: {}",
v_actual[1]
);
assert!(
(u[0] - u[1]).abs() < 1e-8,
"equal weight actuators should share equally: {:?}",
u
);
assert!((u[2] - 1.0).abs() < 1e-8, "u[2] should be 1.0: {}", u[2]);
}
#[test]
fn test_weight_preference_low_weight_favored() {
let b: [[f64; 2]; 1] = [[1.0, 1.0]];
let w = [0.1f64, 10.0];
let u_min = [-10.0f64; 2];
let u_max = [10.0f64; 2];
let alloc =
WeightedPseudoInverse::<f64, 1, 2>::new(b, w, u_min, u_max).expect("valid constructor");
let v_des = [1.0f64];
let u = alloc.allocate(&v_des).expect("allocation ok");
assert!(
u[0] > u[1] * 5.0,
"low-weight actuator should carry more: u={:?}",
u
);
let v_actual = alloc.virtual_control(&u);
assert!((v_actual[0] - 1.0).abs() < 1e-8);
}
#[test]
fn test_saturation_clamped() {
let b = [[1.0f64, 0.0], [0.0, 1.0]];
let w = [1.0f64, 1.0];
let u_min = [-2.0f64, -2.0];
let u_max = [2.0f64, 2.0];
let alloc =
WeightedPseudoInverse::<f64, 2, 2>::new(b, w, u_min, u_max).expect("valid constructor");
let v_des = [5.0f64, 5.0];
let u = alloc.allocate(&v_des).expect("allocation ok");
assert!(u[0] <= 2.0 + 1e-12 && u[0] >= -2.0 - 1e-12);
assert!(u[1] <= 2.0 + 1e-12 && u[1] >= -2.0 - 1e-12);
}
#[test]
fn test_preferred_state() {
let b: [[f64; 2]; 1] = [[1.0, 1.0]];
let w = [1.0f64, 1.0];
let u_min = [-10.0f64; 2];
let u_max = [10.0f64; 2];
let mut alloc =
WeightedPseudoInverse::<f64, 1, 2>::new(b, w, u_min, u_max).expect("valid constructor");
let v_des = [2.0f64];
let u_no_pref = alloc.allocate(&v_des).expect("ok");
assert!((u_no_pref[0] - 1.0).abs() < 1e-8);
assert!((u_no_pref[1] - 1.0).abs() < 1e-8);
alloc.set_preference([3.0, 0.0]);
let u_pref = alloc.allocate(&v_des).expect("ok");
assert!(
u_pref[0] > 1.0,
"u[0] should be pulled toward preference: {:?}",
u_pref
);
let v_actual = alloc.virtual_control(&u_pref);
assert!((v_actual[0] - 2.0).abs() < 1e-8);
}
#[test]
fn test_invalid_weight_rejected() {
let b = [[1.0f64, 0.0], [0.0, 1.0]];
let w = [0.0f64, 1.0];
let u_min = [-10.0f64; 2];
let u_max = [10.0f64; 2];
assert_eq!(
WeightedPseudoInverse::<f64, 2, 2>::new(b, w, u_min, u_max),
Err(AllocationError::InvalidWeight)
);
}
#[test]
fn test_invalid_bounds_rejected() {
let b = [[1.0f64, 0.0], [0.0, 1.0]];
let w = [1.0f64; 2];
let u_min = [5.0f64, 0.0];
let u_max = [2.0f64, 10.0];
assert_eq!(
WeightedPseudoInverse::<f64, 2, 2>::new(b, w, u_min, u_max),
Err(AllocationError::InvalidBounds)
);
}
#[test]
fn test_weighted_cost() {
let b = [[1.0f64, 0.0], [0.0, 1.0]];
let w = [2.0f64, 3.0];
let u_min = [-10.0f64; 2];
let u_max = [10.0f64; 2];
let alloc =
WeightedPseudoInverse::<f64, 2, 2>::new(b, w, u_min, u_max).expect("valid constructor");
let u = [1.0f64, 2.0];
let cost = alloc.weighted_cost(&u);
assert!((cost - 14.0).abs() < 1e-10, "cost: {}", cost);
}
}