use super::Objective;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum PreferenceFunction {
Usual,
UShape { q: f64 },
VShape { p: f64 },
Level { q: f64, p: f64 },
Linear { q: f64, p: f64 },
Gaussian { sigma: f64 },
}
impl PreferenceFunction {
pub fn degree(self, d: f64) -> f64 {
if d <= 0.0 {
return 0.0;
}
match self {
PreferenceFunction::Usual => 1.0,
PreferenceFunction::UShape { q } => {
if d > q {
1.0
} else {
0.0
}
}
PreferenceFunction::VShape { p } => {
if p <= 0.0 || d > p {
1.0
} else {
d / p
}
}
PreferenceFunction::Level { q, p } => {
if d <= q {
0.0
} else if d <= p {
0.5
} else {
1.0
}
}
PreferenceFunction::Linear { q, p } => {
if d <= q {
0.0
} else if d <= p {
(d - q) / (p - q)
} else {
1.0
}
}
PreferenceFunction::Gaussian { sigma } => {
if sigma <= 0.0 {
1.0
} else {
1.0 - (-(d * d) / (2.0 * sigma * sigma)).exp()
}
}
}
}
}
#[derive(Clone, Debug, PartialEq, serde::Serialize)]
pub struct PrometheeResult {
pub phi_plus: Vec<f64>,
pub phi_minus: Vec<f64>,
pub net_flow: Vec<f64>,
pub ranking: Vec<usize>,
}
impl PrometheeResult {
pub fn winner(&self) -> Option<usize> {
self.ranking.first().copied()
}
}
pub fn promethee_ii(
matrix: &[Vec<f64>],
weights: &[f64],
types: &[Objective],
prefs: &[PreferenceFunction],
) -> Result<PrometheeResult, String> {
let m = matrix.len();
if m == 0 {
return Err("PROMETHEE II: empty decision matrix".into());
}
let n = weights.len();
if n == 0 {
return Err("PROMETHEE II: no criteria".into());
}
if types.len() != n || prefs.len() != n {
return Err(format!(
"PROMETHEE II: {n} weights but {} types / {} preference functions",
types.len(),
prefs.len()
));
}
for (i, row) in matrix.iter().enumerate() {
if row.len() != n {
return Err(format!(
"PROMETHEE II: alternative {i} has {} values but there are {n} criteria",
row.len()
));
}
if row.iter().any(|x| !x.is_finite()) {
return Err(format!(
"PROMETHEE II: alternative {i} has a non-finite value"
));
}
}
if m == 1 {
return Ok(PrometheeResult {
phi_plus: vec![0.0],
phi_minus: vec![0.0],
net_flow: vec![0.0],
ranking: vec![0],
});
}
let pi = |a: usize, b: usize| -> f64 {
(0..n)
.map(|j| {
let raw = matrix[a][j] - matrix[b][j];
let d = match types[j] {
Objective::Max => raw,
Objective::Min => -raw,
};
weights[j] * prefs[j].degree(d)
})
.sum()
};
let denom = (m - 1) as f64;
let mut phi_plus = vec![0.0; m];
let mut phi_minus = vec![0.0; m];
for a in 0..m {
let mut plus = 0.0;
let mut minus = 0.0;
for b in 0..m {
if a == b {
continue;
}
plus += pi(a, b);
minus += pi(b, a);
}
phi_plus[a] = plus / denom;
phi_minus[a] = minus / denom;
}
let net_flow: Vec<f64> = (0..m).map(|i| phi_plus[i] - phi_minus[i]).collect();
let ranking = super::topsis::rank_desc(&net_flow);
Ok(PrometheeResult {
phi_plus,
phi_minus,
net_flow,
ranking,
})
}
impl super::wsm::DecisionMatrix {
pub fn promethee_ii_usual(&self) -> Result<PrometheeResult, 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 prefs = vec![PreferenceFunction::Usual; self.criteria.len()];
let matrix: Vec<Vec<f64>> = self.alternatives.iter().map(|a| a.values.clone()).collect();
promethee_ii(&matrix, &weights, &types, &prefs)
}
}
#[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 net_flows_sum_to_zero_and_rank() {
let w = [0.40, 0.35, 0.25];
let t = [Objective::Min, Objective::Max, Objective::Max];
let p = [PreferenceFunction::Usual; 3];
let r = promethee_ii(&ref_matrix(), &w, &t, &p).unwrap();
let sum: f64 = r.net_flow.iter().sum();
assert!(sum.abs() < 1e-12, "net flows sum to {sum}");
assert_eq!(r.winner(), Some(2));
assert_eq!(r.ranking, vec![2, 0, 1, 3]);
}
#[test]
fn preference_function_shapes() {
assert_eq!(PreferenceFunction::Usual.degree(0.0), 0.0);
assert_eq!(PreferenceFunction::Usual.degree(3.0), 1.0);
assert_eq!(PreferenceFunction::VShape { p: 4.0 }.degree(2.0), 0.5);
assert_eq!(
PreferenceFunction::Level { q: 1.0, p: 3.0 }.degree(2.0),
0.5
);
assert_eq!(
PreferenceFunction::Linear { q: 1.0, p: 3.0 }.degree(2.0),
0.5
);
let g = PreferenceFunction::Gaussian { sigma: 1.0 };
assert!(g.degree(0.5) < g.degree(2.0));
}
#[test]
fn shape_mismatch_is_an_error() {
let w = [0.5, 0.5];
let t = [Objective::Max, Objective::Max];
let p = [PreferenceFunction::Usual; 2];
assert!(promethee_ii(&[vec![1.0, 2.0], vec![3.0]], &w, &t, &p).is_err());
}
}