use super::Objective;
#[derive(Clone, Debug, PartialEq, serde::Serialize)]
pub struct WpmResult {
pub scores: Vec<f64>,
pub ranking: Vec<usize>,
}
impl WpmResult {
pub fn winner(&self) -> Option<usize> {
self.ranking.first().copied()
}
}
pub fn wpm(matrix: &[Vec<f64>], weights: &[f64], types: &[Objective]) -> Result<WpmResult, String> {
let m = matrix.len();
if m == 0 {
return Err("WPM: empty decision matrix".into());
}
let n = weights.len();
if n == 0 {
return Err("WPM: no criteria".into());
}
if types.len() != n {
return Err(format!(
"WPM: {} weights but {} criterion types",
n,
types.len()
));
}
for (i, row) in matrix.iter().enumerate() {
if row.len() != n {
return Err(format!(
"WPM: 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!(
"WPM: 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 sum: f64 = matrix.iter().map(|r| r[j]).sum();
for i in 0..m {
norm[i][j] = matrix[i][j] / sum;
}
}
Objective::Min => {
let sum_recip: f64 = matrix.iter().map(|r| 1.0 / r[j]).sum();
for i in 0..m {
norm[i][j] = (1.0 / matrix[i][j]) / sum_recip;
}
}
}
}
let scores: Vec<f64> = norm
.iter()
.map(|row| (0..n).map(|j| row[j].powf(weights[j])).product::<f64>())
.collect();
let ranking = super::topsis::rank_desc(&scores);
Ok(WpmResult { scores, ranking })
}
impl super::wsm::DecisionMatrix {
pub fn wpm(&self) -> Result<WpmResult, 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();
wpm(&matrix, &weights, &types)
}
}
#[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 scores_positive_and_ranked() {
let w = [0.40, 0.35, 0.25];
let t = [Objective::Min, Objective::Max, Objective::Max];
let r = wpm(&ref_matrix(), &w, &t).unwrap();
assert!(r.scores.iter().all(|s| *s > 0.0));
assert_eq!(r.winner(), Some(2));
assert_eq!(r.ranking, vec![2, 3, 0, 1]);
}
#[test]
fn non_positive_value_is_an_error() {
let w = [0.5, 0.5];
let t = [Objective::Max, Objective::Max];
assert!(wpm(&[vec![1.0, 0.0], vec![2.0, 3.0]], &w, &t).is_err());
}
#[test]
fn shape_mismatch_is_an_error() {
let w = [0.5, 0.5];
let t = [Objective::Max, Objective::Max];
assert!(wpm(&[vec![1.0, 2.0], vec![3.0]], &w, &t).is_err());
}
}