use std::collections::HashMap;
use crate::error::OptimizeError;
pub type Coalition = u64;
pub type CharacteristicFunction = HashMap<Coalition, f64>;
#[derive(Debug, Clone)]
pub struct CooperativeGame {
pub n_players: usize,
pub characteristic: CharacteristicFunction,
}
impl CooperativeGame {
pub fn new(n_players: usize) -> Self {
assert!(
n_players <= 63,
"CooperativeGame supports at most 63 players due to bitmask representation"
);
let mut characteristic = CharacteristicFunction::new();
for mask in 0u64..(1u64 << n_players) {
characteristic.insert(mask, 0.0);
}
Self {
n_players,
characteristic,
}
}
pub fn set_value(&mut self, coalition: &[usize], value: f64) {
let mask = players_to_mask(coalition);
self.characteristic.insert(mask, value);
}
pub fn get_value(&self, coalition: &[usize]) -> f64 {
let mask = players_to_mask(coalition);
*self.characteristic.get(&mask).unwrap_or(&0.0)
}
pub fn grand_coalition_value(&self) -> f64 {
let mask = (1u64 << self.n_players) - 1;
*self.characteristic.get(&mask).unwrap_or(&0.0)
}
pub fn is_superadditive(&self) -> bool {
let n = self.n_players;
let all_masks: Vec<u64> = (0u64..(1u64 << n)).collect();
for &s_mask in &all_masks {
for &t_mask in &all_masks {
if s_mask & t_mask != 0 {
continue; }
let v_s = *self.characteristic.get(&s_mask).unwrap_or(&0.0);
let v_t = *self.characteristic.get(&t_mask).unwrap_or(&0.0);
let v_st = *self
.characteristic
.get(&(s_mask | t_mask))
.unwrap_or(&0.0);
if v_st < v_s + v_t - 1e-9 {
return false;
}
}
}
true
}
pub fn is_convex(&self) -> bool {
let n = self.n_players;
for s_mask in 0u64..(1u64 << n) {
for t_mask in 0u64..(1u64 << n) {
let v_s = *self.characteristic.get(&s_mask).unwrap_or(&0.0);
let v_t = *self.characteristic.get(&t_mask).unwrap_or(&0.0);
let v_st = *self
.characteristic
.get(&(s_mask | t_mask))
.unwrap_or(&0.0);
let v_s_cap_t = *self
.characteristic
.get(&(s_mask & t_mask))
.unwrap_or(&0.0);
if v_st + v_s_cap_t < v_s + v_t - 1e-9 {
return false;
}
}
}
true
}
}
pub fn players_to_mask(players: &[usize]) -> Coalition {
players.iter().fold(0u64, |mask, &p| mask | (1u64 << p))
}
pub fn mask_to_players(mask: Coalition, n_players: usize) -> Vec<usize> {
(0..n_players).filter(|&i| (mask >> i) & 1 == 1).collect()
}
pub fn shapley_value(game: &CooperativeGame) -> Vec<f64> {
let n = game.n_players;
let mut phi = vec![0.0_f64; n];
let mut fact = vec![1u64; n + 1];
for k in 1..=n {
fact[k] = fact[k - 1] * k as u64;
}
let grand_mask = (1u64 << n) - 1;
for i in 0..n {
let player_mask = 1u64 << i;
for s_mask in 0u64..(1u64 << n) {
if s_mask & player_mask != 0 {
continue;
}
let s_size = s_mask.count_ones() as usize;
let s_union_i = s_mask | player_mask;
let v_s = *game.characteristic.get(&s_mask).unwrap_or(&0.0);
let v_s_union_i = *game.characteristic.get(&s_union_i).unwrap_or(&0.0);
let weight = (fact[s_size] * fact[n - s_size - 1]) as f64 / fact[n] as f64;
phi[i] += weight * (v_s_union_i - v_s);
}
let _ = grand_mask; }
phi
}
pub fn banzhaf_index(game: &CooperativeGame) -> Vec<f64> {
let n = game.n_players;
let mut beta = vec![0.0_f64; n];
let denominator = if n > 0 {
(1u64 << (n - 1)) as f64
} else {
1.0
};
for i in 0..n {
let player_mask = 1u64 << i;
for s_mask in 0u64..(1u64 << n) {
if s_mask & player_mask != 0 {
continue;
}
let v_s = *game.characteristic.get(&s_mask).unwrap_or(&0.0);
let v_su = *game
.characteristic
.get(&(s_mask | player_mask))
.unwrap_or(&0.0);
beta[i] += v_su - v_s;
}
beta[i] /= denominator;
}
beta
}
pub fn is_in_core(game: &CooperativeGame, imputation: &[f64]) -> bool {
let n = game.n_players;
if imputation.len() != n {
return false;
}
let tol = 1e-9;
let total: f64 = imputation.iter().sum();
let grand_val = game.grand_coalition_value();
if (total - grand_val).abs() > tol {
return false;
}
for mask in 1u64..(1u64 << n) {
let coalition_payoff: f64 = (0..n)
.filter(|&i| (mask >> i) & 1 == 1)
.map(|i| imputation[i])
.sum();
let coalition_val = *game.characteristic.get(&mask).unwrap_or(&0.0);
if coalition_payoff < coalition_val - tol {
return false;
}
}
true
}
pub fn has_nonempty_core(game: &CooperativeGame) -> bool {
if game.is_convex() {
return true;
}
core_feasibility_check(game)
}
fn core_feasibility_check(game: &CooperativeGame) -> bool {
let phi = shapley_value(game);
is_in_core(game, &phi)
}
pub fn nucleolus(game: &CooperativeGame) -> Result<Vec<f64>, OptimizeError> {
let n = game.n_players;
if n == 0 {
return Ok(Vec::new());
}
nucleolus_lp(game)
}
fn nucleolus_lp(game: &CooperativeGame) -> Result<Vec<f64>, OptimizeError> {
let n = game.n_players;
let grand_val = game.grand_coalition_value();
let mut x = vec![grand_val / n as f64; n];
let all_masks: Vec<u64> = (1u64..((1u64 << n) - 1)).collect();
if all_masks.is_empty() {
return Ok(vec![grand_val]);
}
let mut active_masks = all_masks.clone();
let mut fixed_masks: Vec<(u64, f64)> = Vec::new();
let max_rounds = n + 1;
for _round in 0..max_rounds {
if active_masks.is_empty() {
break;
}
let excesses: Vec<f64> = active_masks
.iter()
.map(|&mask| {
let v_s = *game.characteristic.get(&mask).unwrap_or(&0.0);
let x_s: f64 = (0..n)
.filter(|&i| (mask >> i) & 1 == 1)
.map(|i| x[i])
.sum();
v_s - x_s
})
.collect();
let result = minimize_max_excess(game, &active_masks, &fixed_masks, n, grand_val)?;
x = result.0;
let epsilon_star = result.1;
let tol = 1e-7;
let newly_fixed: Vec<u64> = active_masks
.iter()
.zip(excesses.iter())
.filter_map(|(&mask, &excess)| {
let new_excess = {
let v_s = *game.characteristic.get(&mask).unwrap_or(&0.0);
let x_s: f64 = (0..n)
.filter(|&i| (mask >> i) & 1 == 1)
.map(|i| x[i])
.sum();
v_s - x_s
};
if (new_excess - epsilon_star).abs() < tol {
Some(mask)
} else {
None
}
})
.collect();
for mask in &newly_fixed {
fixed_masks.push((*mask, epsilon_star));
}
active_masks.retain(|m| !newly_fixed.contains(m));
if newly_fixed.is_empty() {
break;
}
}
Ok(x)
}
fn minimize_max_excess(
game: &CooperativeGame,
active_masks: &[u64],
fixed_masks: &[(u64, f64)],
n: usize,
grand_val: f64,
) -> Result<(Vec<f64>, f64), OptimizeError> {
let n_active = active_masks.len();
let n_fixed = fixed_masks.len();
let n_vars = n + 1; let eps_idx = n;
let n_ineq = n_active;
let n_eq = n_fixed + 1; let n_slack = n_ineq;
let n_artif = n_ineq + n_eq; let total_vars = n_vars + n_slack + n_artif;
let n_rows = n_ineq + n_eq + 1; let n_cols = total_vars + 1; let rhs_col = total_vars;
let big_m = 1e8_f64;
let mut tab = vec![0.0_f64; n_rows * n_cols];
for (k, &mask) in active_masks.iter().enumerate() {
let v_s = *game.characteristic.get(&mask).unwrap_or(&0.0);
for i in 0..n {
if (mask >> i) & 1 == 1 {
tab[k * n_cols + i] = 1.0;
}
}
tab[k * n_cols + eps_idx] = 1.0;
tab[k * n_cols + n_vars + k] = -1.0;
let artif_idx = n_vars + n_slack + k;
tab[k * n_cols + artif_idx] = 1.0;
tab[k * n_cols + rhs_col] = v_s;
}
for (k, &(mask, e_s)) in fixed_masks.iter().enumerate() {
let row = n_ineq + k;
let v_s = *game.characteristic.get(&mask).unwrap_or(&0.0);
let rhs = v_s - e_s;
for i in 0..n {
if (mask >> i) & 1 == 1 {
tab[row * n_cols + i] = 1.0;
}
}
let artif_idx = n_vars + n_slack + n_active + k;
tab[row * n_cols + artif_idx] = 1.0;
tab[row * n_cols + rhs_col] = rhs;
}
let eff_row = n_ineq + n_fixed;
for i in 0..n {
tab[eff_row * n_cols + i] = 1.0;
}
let artif_eff_idx = n_vars + n_slack + n_active + n_fixed;
tab[eff_row * n_cols + artif_eff_idx] = 1.0;
tab[eff_row * n_cols + rhs_col] = grand_val;
let obj_row = n_ineq + n_eq;
tab[obj_row * n_cols + eps_idx] = 1.0;
for k in 0..n_artif {
tab[obj_row * n_cols + n_vars + n_slack + k] = big_m;
}
for row in 0..(n_ineq + n_eq) {
let artif_col = n_vars + n_slack + row;
let coeff = tab[obj_row * n_cols + artif_col];
if coeff.abs() > 1e-15 {
for k in 0..n_cols {
let val = tab[row * n_cols + k] * coeff;
tab[obj_row * n_cols + k] -= val;
}
}
}
let mut basis: Vec<usize> = (n_vars + n_slack..n_vars + n_slack + n_artif).collect();
let n_constraint_rows = n_ineq + n_eq;
simplex_min(&mut tab, &mut basis, n_constraint_rows, n_cols, total_vars)?;
let mut x = vec![0.0_f64; n];
let mut epsilon = 0.0_f64;
for (b_idx, &var) in basis.iter().enumerate() {
let val = tab[b_idx * n_cols + rhs_col];
if var < n {
x[var] = val;
} else if var == eps_idx {
epsilon = val;
}
}
Ok((x, epsilon))
}
fn simplex_min(
tab: &mut Vec<f64>,
basis: &mut Vec<usize>,
n_constraints: usize,
n_cols: usize,
n_vars: usize,
) -> Result<(), OptimizeError> {
let obj_row = n_constraints;
let rhs_col = n_cols - 1;
let max_iter = 50_000;
for _iter in 0..max_iter {
let pivot_col = (0..n_vars).find(|&j| tab[obj_row * n_cols + j] < -1e-9);
let pivot_col = match pivot_col {
None => return Ok(()),
Some(c) => c,
};
let mut min_ratio = f64::INFINITY;
let mut pivot_row = None;
for i in 0..n_constraints {
let elem = tab[i * n_cols + pivot_col];
if elem > 1e-9 {
let ratio = tab[i * n_cols + rhs_col] / elem;
if ratio < min_ratio - 1e-12 {
min_ratio = ratio;
pivot_row = Some(i);
} else if (ratio - min_ratio).abs() < 1e-12 {
if let Some(pr) = pivot_row {
if basis[i] < basis[pr] {
pivot_row = Some(i);
}
}
}
}
}
let pivot_row = pivot_row.ok_or_else(|| {
OptimizeError::ComputationError("Nucleolus LP is unbounded".to_string())
})?;
let pivot_val = tab[pivot_row * n_cols + pivot_col];
for k in 0..n_cols {
tab[pivot_row * n_cols + k] /= pivot_val;
}
for i in 0..=n_constraints {
if i == pivot_row {
continue;
}
let factor = tab[i * n_cols + pivot_col];
if factor.abs() < 1e-15 {
continue;
}
for k in 0..n_cols {
let pv = tab[pivot_row * n_cols + k];
tab[i * n_cols + k] -= factor * pv;
}
}
basis[pivot_row] = pivot_col;
}
Err(OptimizeError::ConvergenceError(
"Nucleolus LP did not converge within maximum iterations".to_string(),
))
}
pub fn tau_value(game: &CooperativeGame) -> Result<Vec<f64>, OptimizeError> {
let n = game.n_players;
let grand_val = game.grand_coalition_value();
if n == 0 {
return Ok(Vec::new());
}
let grand_mask = (1u64 << n) - 1;
let utopia: Vec<f64> = (0..n)
.map(|i| {
let complement = grand_mask & !(1u64 << i);
let v_complement = *game.characteristic.get(&complement).unwrap_or(&0.0);
grand_val - v_complement
})
.collect();
let minimum_right: Vec<f64> = (0..n)
.map(|i| {
let player_mask = 1u64 << i;
let mut max_val = 0.0_f64;
let v_solo = *game.characteristic.get(&player_mask).unwrap_or(&0.0);
max_val = max_val.max(v_solo);
for mask in 1u64..(1u64 << n) {
if (mask >> i) & 1 == 0 {
continue;
}
let v_s = *game.characteristic.get(&mask).unwrap_or(&0.0);
let others_utopia: f64 = (0..n)
.filter(|&j| j != i && (mask >> j) & 1 == 1)
.map(|j| utopia[j])
.sum();
let val = v_s - others_utopia;
max_val = max_val.max(val);
}
max_val
})
.collect();
let ranges: Vec<f64> = utopia
.iter()
.zip(minimum_right.iter())
.map(|(m, mi)| m - mi)
.collect();
let sum_ranges: f64 = ranges.iter().sum();
let sum_min: f64 = minimum_right.iter().sum();
if sum_ranges.abs() < 1e-12 {
if (sum_min - grand_val).abs() < 1e-9 {
return Ok(minimum_right);
}
return Err(OptimizeError::ComputationError(
"Tau-value undefined: utopia and minimum right vectors are identical with no scaling possible".to_string()
));
}
let lambda = (grand_val - sum_min) / sum_ranges;
let tau: Vec<f64> = minimum_right
.iter()
.zip(ranges.iter())
.map(|(&mi, &ri)| mi + lambda * ri)
.collect();
Ok(tau)
}
pub fn airport_game(costs: &[f64]) -> CooperativeGame {
let n = costs.len();
let mut game = CooperativeGame::new(n);
for mask in 1u64..(1u64 << n) {
let max_cost = (0..n)
.filter(|&i| (mask >> i) & 1 == 1)
.map(|i| costs[i])
.fold(f64::NEG_INFINITY, f64::max);
game.set_value(&mask_to_players(mask, n), max_cost);
}
game
}
pub fn glove_game(left_players: &[usize], right_players: &[usize]) -> CooperativeGame {
let all_players: Vec<usize> = left_players
.iter()
.chain(right_players.iter())
.cloned()
.collect();
let n = if all_players.is_empty() {
0
} else {
*all_players.iter().max().unwrap_or(&0) + 1
};
let mut game = CooperativeGame::new(n);
let left_mask: u64 = players_to_mask(left_players);
let right_mask: u64 = players_to_mask(right_players);
for mask in 1u64..(1u64 << n) {
let n_left = (mask & left_mask).count_ones() as f64;
let n_right = (mask & right_mask).count_ones() as f64;
let value = n_left.min(n_right);
game.set_value(&mask_to_players(mask, n), value);
}
game
}
pub fn weighted_voting_game(weights: &[f64], quota: f64) -> CooperativeGame {
let n = weights.len();
let mut game = CooperativeGame::new(n);
for mask in 1u64..(1u64 << n) {
let coalition_weight: f64 = (0..n)
.filter(|&i| (mask >> i) & 1 == 1)
.map(|i| weights[i])
.sum();
let value = if coalition_weight >= quota { 1.0 } else { 0.0 };
game.set_value(&mask_to_players(mask, n), value);
}
game
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn make_simple_3_player_game() -> CooperativeGame {
let mut game = CooperativeGame::new(3);
game.set_value(&[0], 0.0);
game.set_value(&[1], 0.0);
game.set_value(&[2], 0.0);
game.set_value(&[0, 1], 0.6);
game.set_value(&[0, 2], 0.6);
game.set_value(&[1, 2], 0.6);
game.set_value(&[0, 1, 2], 1.0);
game
}
#[test]
fn test_cooperative_game_construction() {
let game = CooperativeGame::new(3);
assert_eq!(game.n_players, 3);
assert_eq!(game.grand_coalition_value(), 0.0);
}
#[test]
fn test_set_get_value() {
let mut game = CooperativeGame::new(3);
game.set_value(&[0, 1], 5.0);
assert_relative_eq!(game.get_value(&[0, 1]), 5.0);
assert_relative_eq!(game.get_value(&[1, 0]), 5.0); }
#[test]
fn test_grand_coalition_value() {
let mut game = CooperativeGame::new(3);
game.set_value(&[0, 1, 2], 10.0);
assert_relative_eq!(game.grand_coalition_value(), 10.0);
}
#[test]
fn test_is_superadditive() {
let game = make_simple_3_player_game();
assert!(game.is_superadditive());
}
#[test]
fn test_is_convex() {
let mut game = CooperativeGame::new(2);
game.set_value(&[0], 1.0);
game.set_value(&[1], 1.0);
game.set_value(&[0, 1], 3.0); assert!(game.is_convex());
}
#[test]
fn test_shapley_value_symmetric() {
let game = make_simple_3_player_game();
let phi = shapley_value(&game);
assert_eq!(phi.len(), 3);
assert_relative_eq!(phi[0], phi[1], epsilon = 1e-10);
assert_relative_eq!(phi[1], phi[2], epsilon = 1e-10);
let total: f64 = phi.iter().sum();
assert_relative_eq!(total, 1.0, epsilon = 1e-10);
}
#[test]
fn test_shapley_value_dummy_player() {
let mut game = CooperativeGame::new(3);
game.set_value(&[0], 0.0);
game.set_value(&[1], 0.0);
game.set_value(&[2], 0.0);
game.set_value(&[0, 1], 1.0);
game.set_value(&[0, 2], 0.0);
game.set_value(&[1, 2], 0.0);
game.set_value(&[0, 1, 2], 1.0);
let phi = shapley_value(&game);
assert_relative_eq!(phi[2], 0.0, epsilon = 1e-10);
let total: f64 = phi.iter().sum();
assert_relative_eq!(total, 1.0, epsilon = 1e-10);
}
#[test]
fn test_banzhaf_index() {
let game = weighted_voting_game(&[1.0, 1.0, 1.0, 1.0], 2.0);
let beta = banzhaf_index(&game);
assert_relative_eq!(beta[0], beta[1], epsilon = 1e-10);
assert_relative_eq!(beta[1], beta[2], epsilon = 1e-10);
}
#[test]
fn test_is_in_core_valid() {
let game = make_simple_3_player_game();
let phi = shapley_value(&game);
if game.is_convex() {
assert!(is_in_core(&game, &phi));
}
let equal_split = vec![1.0 / 3.0; 3];
assert!(is_in_core(&game, &equal_split));
}
#[test]
fn test_is_in_core_invalid() {
let game = make_simple_3_player_game();
let imputation = vec![1.0, 0.0, 0.0];
assert!(!is_in_core(&game, &imputation));
}
#[test]
fn test_has_nonempty_core_convex() {
let mut game = CooperativeGame::new(3);
game.set_value(&[0], 1.0);
game.set_value(&[1], 1.0);
game.set_value(&[2], 1.0);
game.set_value(&[0, 1], 3.0);
game.set_value(&[0, 2], 3.0);
game.set_value(&[1, 2], 3.0);
game.set_value(&[0, 1, 2], 7.0);
assert!(game.is_convex());
assert!(has_nonempty_core(&game));
}
#[test]
fn test_nucleolus_returns_imputation() {
let game = make_simple_3_player_game();
let nuc = nucleolus(&game).expect("nucleolus computes");
assert_eq!(nuc.len(), 3);
let total: f64 = nuc.iter().sum();
assert_relative_eq!(total, 1.0, epsilon = 1e-4);
}
#[test]
fn test_nucleolus_symmetric_game_equal() {
let game = make_simple_3_player_game();
let nuc = nucleolus(&game).expect("nucleolus");
assert_relative_eq!(nuc[0], 1.0 / 3.0, epsilon = 1e-3);
assert_relative_eq!(nuc[1], 1.0 / 3.0, epsilon = 1e-3);
assert_relative_eq!(nuc[2], 1.0 / 3.0, epsilon = 1e-3);
}
#[test]
fn test_weighted_voting_game() {
let game = weighted_voting_game(&[2.0, 2.0, 1.0], 3.0);
assert_relative_eq!(game.get_value(&[0, 1]), 1.0);
assert_relative_eq!(game.get_value(&[0, 2]), 1.0);
assert_relative_eq!(game.get_value(&[2]), 0.0);
assert_relative_eq!(game.get_value(&[0, 1, 2]), 1.0);
}
#[test]
fn test_glove_game() {
let game = glove_game(&[0, 1], &[2]);
assert_relative_eq!(game.get_value(&[0, 2]), 1.0); assert_relative_eq!(game.get_value(&[1, 2]), 1.0); assert_relative_eq!(game.get_value(&[0, 1]), 0.0); assert_relative_eq!(game.get_value(&[0, 1, 2]), 1.0); }
#[test]
fn test_airport_game() {
let costs = vec![1.0, 2.0, 3.0];
let game = airport_game(&costs);
assert_relative_eq!(game.get_value(&[0, 1]), 2.0);
assert_relative_eq!(game.get_value(&[0, 1, 2]), 3.0);
assert_relative_eq!(game.get_value(&[0]), 1.0);
}
#[test]
fn test_tau_value_efficiency() {
let game = make_simple_3_player_game();
match tau_value(&game) {
Ok(tau) => {
let total: f64 = tau.iter().sum();
assert_relative_eq!(total, 1.0, epsilon = 1e-6);
}
Err(_) => {
}
}
}
#[test]
fn test_players_to_mask_roundtrip() {
let players = vec![0, 2, 4];
let mask = players_to_mask(&players);
let recovered = mask_to_players(mask, 6);
assert_eq!(recovered, players);
}
}