use super::Objective;
#[derive(Clone, Debug, PartialEq, serde::Serialize)]
pub struct WaspasResult {
pub scores: Vec<f64>,
pub q_sum: Vec<f64>,
pub q_prod: Vec<f64>,
pub lambda: f64,
pub ranking: Vec<usize>,
}
impl WaspasResult {
pub fn winner(&self) -> Option<usize> {
self.ranking.first().copied()
}
}
pub fn waspas(
matrix: &[Vec<f64>],
weights: &[f64],
types: &[Objective],
lambda: f64,
) -> Result<WaspasResult, String> {
let m = matrix.len();
if m == 0 {
return Err("WASPAS: empty decision matrix".into());
}
let n = weights.len();
if n == 0 {
return Err("WASPAS: no criteria".into());
}
if types.len() != n {
return Err(format!(
"WASPAS: {} weights but {} criterion types",
n,
types.len()
));
}
if !(0.0..=1.0).contains(&lambda) || !lambda.is_finite() {
return Err(format!("WASPAS: lambda {lambda} must lie in [0, 1]"));
}
for (i, row) in matrix.iter().enumerate() {
if row.len() != n {
return Err(format!(
"WASPAS: alternative {i} has {} values but there are {n} criteria",
row.len()
));
}
for (j, &x) in row.iter().enumerate() {
if !x.is_finite() || x <= 0.0 {
return Err(format!(
"WASPAS: alternative {i} criterion {j} value {x} must be finite and > 0"
));
}
}
}
let mut norm = vec![vec![0.0f64; n]; m];
for j in 0..n {
match types[j] {
Objective::Max => {
let hi = matrix
.iter()
.map(|r| r[j])
.fold(f64::NEG_INFINITY, f64::max);
for i in 0..m {
norm[i][j] = matrix[i][j] / hi;
}
}
Objective::Min => {
let lo = matrix.iter().map(|r| r[j]).fold(f64::INFINITY, f64::min);
for i in 0..m {
norm[i][j] = lo / matrix[i][j];
}
}
}
}
let q_sum: Vec<f64> = norm
.iter()
.map(|row| (0..n).map(|j| row[j] * weights[j]).sum::<f64>())
.collect();
let q_prod: Vec<f64> = norm
.iter()
.map(|row| (0..n).map(|j| row[j].powf(weights[j])).product::<f64>())
.collect();
let scores: Vec<f64> = (0..m)
.map(|i| lambda * q_sum[i] + (1.0 - lambda) * q_prod[i])
.collect();
let ranking = super::topsis::rank_desc(&scores);
Ok(WaspasResult {
scores,
q_sum,
q_prod,
lambda,
ranking,
})
}
impl super::wsm::DecisionMatrix {
pub fn waspas(&self) -> Result<WaspasResult, String> {
self.validate()?;
let weights = self.normalized_weights();
let types: Vec<Objective> = self
.criteria
.iter()
.map(|c| match c.direction {
super::wsm::Direction::Benefit => Objective::Max,
super::wsm::Direction::Cost => Objective::Min,
})
.collect();
let matrix: Vec<Vec<f64>> = self.alternatives.iter().map(|a| a.values.clone()).collect();
waspas(&matrix, &weights, &types, 0.5)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ref_matrix() -> Vec<Vec<f64>> {
vec![
vec![250.0, 16.0, 12.0],
vec![200.0, 16.0, 8.0],
vec![300.0, 32.0, 16.0],
vec![275.0, 24.0, 10.0],
]
}
#[test]
fn blend_lies_between_its_halves_and_ranks() {
let w = [0.40, 0.35, 0.25];
let t = [Objective::Min, Objective::Max, Objective::Max];
let r = waspas(&ref_matrix(), &w, &t, 0.5).unwrap();
for i in 0..4 {
let lo = r.q_sum[i].min(r.q_prod[i]);
let hi = r.q_sum[i].max(r.q_prod[i]);
assert!(
r.scores[i] >= lo - 1e-12 && r.scores[i] <= hi + 1e-12,
"blend {} outside [{lo}, {hi}]",
r.scores[i]
);
}
assert_eq!(r.winner(), Some(2));
assert_eq!(r.ranking, vec![2, 3, 1, 0]);
}
#[test]
fn lambda_zero_is_wpm_lambda_one_is_wsm() {
let w = [0.40, 0.35, 0.25];
let t = [Objective::Min, Objective::Max, Objective::Max];
let r0 = waspas(&ref_matrix(), &w, &t, 0.0).unwrap();
let r1 = waspas(&ref_matrix(), &w, &t, 1.0).unwrap();
for i in 0..4 {
assert!((r0.scores[i] - r0.q_prod[i]).abs() < 1e-12);
assert!((r1.scores[i] - r1.q_sum[i]).abs() < 1e-12);
}
}
#[test]
fn out_of_range_lambda_is_an_error() {
let w = [0.5, 0.5];
let t = [Objective::Max, Objective::Max];
assert!(waspas(&[vec![1.0, 2.0], vec![3.0, 4.0]], &w, &t, 1.5).is_err());
}
#[test]
fn non_positive_value_is_an_error() {
let w = [0.5, 0.5];
let t = [Objective::Max, Objective::Max];
assert!(waspas(&[vec![1.0, 0.0], vec![2.0, 3.0]], &w, &t, 0.5).is_err());
}
}