#[derive(Clone, Debug, PartialEq, serde::Serialize)]
pub struct ElectreResult {
pub concordance: Vec<Vec<f64>>,
pub discordance: Vec<Vec<f64>>,
pub dominance: Vec<Vec<bool>>,
pub kernel: Vec<usize>,
pub dominated: Vec<usize>,
}
pub fn electre_i(
dataset: &[Vec<f64>],
weights: &[f64],
c_hat: f64,
d_hat: f64,
) -> Result<ElectreResult, String> {
let m = dataset.len();
if m == 0 {
return Err("ELECTRE I: empty dataset".into());
}
let n = weights.len();
if n == 0 {
return Err("ELECTRE I: no criteria".into());
}
for (i, row) in dataset.iter().enumerate() {
if row.len() != n {
return Err(format!(
"ELECTRE I: alternative {i} has {} values but there are {n} criteria",
row.len()
));
}
if row.iter().any(|x| !x.is_finite()) {
return Err(format!("ELECTRE I: alternative {i} has a non-finite value"));
}
}
let wsum: f64 = weights.iter().sum();
let mut concordance = vec![vec![0.0f64; m]; m];
for a in 0..m {
for b in 0..m {
let mut acc = 0.0;
for k in 0..n {
if dataset[a][k] >= dataset[b][k] {
acc += weights[k];
}
}
concordance[a][b] = if wsum != 0.0 { acc / wsum } else { 0.0 };
}
}
let mut delta = 0.0f64;
for k in 0..n {
let mut lo = f64::INFINITY;
let mut hi = f64::NEG_INFINITY;
for row in dataset {
lo = lo.min(row[k]);
hi = hi.max(row[k]);
}
delta = delta.max(hi - lo);
}
let mut discordance = vec![vec![0.0f64; m]; m];
for a in 0..m {
for b in 0..m {
let worst = dataset[b]
.iter()
.zip(dataset[a].iter())
.map(|(bk, ak)| bk - ak)
.fold(f64::NEG_INFINITY, f64::max);
let d = if delta != 0.0 { worst / delta } else { 0.0 };
discordance[a][b] = if d < 0.0 { 0.0 } else { d };
}
}
let mut dominance = vec![vec![false; m]; m];
for a in 0..m {
for b in 0..m {
if a != b && concordance[a][b] >= c_hat && discordance[a][b] <= d_hat {
dominance[a][b] = true;
}
}
}
let mut kernel: Vec<usize> = (0..m)
.filter(|&j| (0..m).all(|i| !dominance[i][j]))
.collect();
let dominated: Vec<usize> = (0..m)
.filter(|&j| kernel.iter().any(|&ki| dominance[ki][j]))
.collect();
let additions: Vec<usize> = (0..m)
.filter(|&j| {
!kernel.contains(&j)
&& !dominated.contains(&j)
&& (0..m).any(|i| dominance[i][j])
&& kernel.iter().any(|&ki| !dominance[ki][j])
})
.collect();
kernel.extend(additions);
kernel.sort_unstable();
Ok(ElectreResult {
concordance,
discordance,
dominance,
kernel,
dominated,
})
}
impl super::wsm::DecisionMatrix {
pub fn electre_i(&self, c_hat: f64, d_hat: f64) -> Result<ElectreResult, String> {
self.validate()?;
let weights = self.normalized_weights();
let dataset: Vec<Vec<f64>> = self
.alternatives
.iter()
.map(|a| {
a.values
.iter()
.zip(&self.criteria)
.map(|(&x, c)| match c.direction {
super::wsm::Direction::Benefit => x,
super::wsm::Direction::Cost => -x,
})
.collect()
})
.collect();
electre_i(&dataset, &weights, c_hat, d_hat)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ref_dataset() -> Vec<Vec<f64>> {
vec![
vec![0.80, 0.60, 0.90],
vec![0.70, 0.90, 0.50],
vec![0.50, 0.80, 0.70],
vec![0.90, 0.40, 0.60],
]
}
#[test]
fn diagonal_and_ranges() {
let w = [0.40, 0.35, 0.25];
let r = electre_i(&ref_dataset(), &w, 0.65, 0.40).unwrap();
for i in 0..4 {
assert!((r.concordance[i][i] - 1.0).abs() < 1e-12);
assert!(r.discordance[i][i].abs() < 1e-12);
}
for a in 0..4 {
for b in 0..4 {
assert!((0.0..=1.0).contains(&r.concordance[a][b]));
assert!((0.0..=1.0).contains(&r.discordance[a][b]));
}
}
}
#[test]
fn kernel_is_a3_dropped() {
let w = [0.40, 0.35, 0.25];
let r = electre_i(&ref_dataset(), &w, 0.65, 0.40).unwrap();
assert_eq!(r.kernel, vec![0, 1, 3]);
assert_eq!(r.dominated, vec![2]);
}
#[test]
fn shape_mismatch_is_an_error() {
let w = [0.5, 0.5];
assert!(electre_i(&[vec![1.0, 2.0], vec![3.0]], &w, 0.6, 0.4).is_err());
}
}