use crate::error::{OptimError, Result};
use scirs2_core::ndarray::Array1;
use scirs2_core::numeric::Float;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::fmt::Debug;
use super::aggregator::ByzantineTolerantAggregator;
use super::helpers::{cosine_similarity, from_scalar, l2_norm, to_scalar, FLAME_NOISE_LAMBDA};
use super::types::SplitMix64;
impl<T: Float + Debug + Send + Sync + 'static + scirs2_core::ndarray::ScalarOperand>
ByzantineTolerantAggregator<T>
{
pub(super) fn fools_gold_aggregation(
&mut self,
gradients: &HashMap<String, Array1<T>>,
) -> Result<Array1<T>> {
Self::validate_cohort(gradients)?;
let ids: Vec<String> = {
let ordered = Self::ordered_cohort(gradients);
ordered.iter().map(|(id, _)| (*id).clone()).collect()
};
for participant_id in &ids {
let gradient = gradients.get(participant_id).ok_or_else(|| {
OptimError::InvalidState(format!("missing gradient for '{participant_id}'"))
})?;
match self.fools_gold_history.get_mut(participant_id) {
Some(history) if history.len() == gradient.len() => {
*history = &*history + gradient;
}
_ => {
self.fools_gold_history
.insert(participant_id.clone(), gradient.clone());
}
}
}
let weights = self.compute_fools_gold_weights(&ids)?;
let dim = gradients
.get(&ids[0])
.map(|g| g.len())
.ok_or_else(|| OptimError::InvalidState("empty FoolsGold cohort".to_string()))?;
let mut result: Array1<T> = Array1::zeros(dim);
let mut total_weight = 0.0f64;
for (participant_id, weight) in ids.iter().zip(weights.iter()) {
let gradient = gradients.get(participant_id).ok_or_else(|| {
OptimError::InvalidState(format!("missing gradient for '{participant_id}'"))
})?;
let scaled: T = to_scalar(*weight)?;
result = result + gradient * scaled;
total_weight += *weight;
}
if total_weight > 0.0 {
let divisor: T = to_scalar(total_weight)?;
Ok(result / divisor)
} else {
let mut mean: Array1<T> = Array1::zeros(dim);
for participant_id in &ids {
let gradient = gradients.get(participant_id).ok_or_else(|| {
OptimError::InvalidState(format!("missing gradient for '{participant_id}'"))
})?;
mean = mean + gradient;
}
let divisor: T = to_scalar(ids.len() as f64)?;
Ok(mean / divisor)
}
}
pub(super) fn compute_fools_gold_weights(&self, ids: &[String]) -> Result<Vec<f64>> {
let n = ids.len();
if n == 0 {
return Ok(Vec::new());
}
if n == 1 {
return Ok(vec![1.0]);
}
let histories: Vec<&Array1<T>> = ids
.iter()
.map(|id| {
self.fools_gold_history.get(id).ok_or_else(|| {
OptimError::InvalidState(format!("missing FoolsGold history for '{id}'"))
})
})
.collect::<Result<Vec<_>>>()?;
let mut cs = vec![vec![0.0f64; n]; n];
for i in 0..n {
for j in (i + 1)..n {
let similarity = if histories[i].len() == histories[j].len() {
from_scalar(cosine_similarity(histories[i], histories[j])?)?
} else {
0.0
};
cs[i][j] = similarity;
cs[j][i] = similarity;
}
}
let row_max = |row: &[f64], skip: usize| -> f64 {
row.iter()
.enumerate()
.filter(|(j, _)| *j != skip)
.map(|(_, v)| *v)
.fold(f64::NEG_INFINITY, f64::max)
};
let v: Vec<f64> = (0..n).map(|i| row_max(&cs[i], i)).collect();
let mut pardoned = cs.clone();
for i in 0..n {
for j in 0..n {
if i != j && v[j] > v[i] && v[j] > 0.0 {
pardoned[i][j] *= v[i] / v[j];
}
}
}
let mut alpha: Vec<f64> = (0..n)
.map(|i| (1.0 - row_max(&pardoned[i], i)).clamp(0.0, 1.0))
.collect();
let max_alpha = alpha.iter().copied().fold(0.0f64, f64::max);
if max_alpha > 0.0 {
for a in alpha.iter_mut() {
*a = (*a / max_alpha).clamp(0.0, 1.0);
}
} else {
return Ok(vec![0.0; n]);
}
Ok(alpha
.into_iter()
.map(|a| {
let clamped = a.clamp(1e-9, 0.99);
let logit = (clamped / (1.0 - clamped)).ln() + 0.5;
if logit.is_finite() {
logit.clamp(0.0, 1.0)
} else {
1.0
}
})
.collect())
}
pub(super) fn flame_aggregation(
&self,
gradients: &HashMap<String, Array1<T>>,
) -> Result<Array1<T>> {
Self::validate_cohort(gradients)?;
let ordered = Self::ordered_cohort(gradients);
let grads: Vec<&Array1<T>> = ordered.iter().map(|(_, g)| *g).collect();
let n = grads.len();
let dim = grads[0].len();
let admitted = Self::flame_admitted_indices(&grads)?;
if admitted.is_empty() {
return Err(OptimError::InvalidState(
"FLAME clustering admitted no gradients".to_string(),
));
}
let mut norms: Vec<T> = grads.iter().map(|g| l2_norm(g)).collect();
norms.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
let two: T = to_scalar(2.0)?;
let median_norm = if norms.len().is_multiple_of(2) {
let mid = norms.len() / 2;
(norms[mid - 1] + norms[mid]) / two
} else {
norms[norms.len() / 2]
};
let mut result: Array1<T> = Array1::zeros(dim);
for &index in &admitted {
let gradient = grads[index];
let norm = l2_norm(gradient);
let gamma = if norm > T::zero() && norm > median_norm {
median_norm / norm
} else {
T::one()
};
result = result + gradient * gamma;
}
let divisor: T = to_scalar(admitted.len() as f64)?;
result = result / divisor;
let sigma = from_scalar(median_norm)? * FLAME_NOISE_LAMBDA;
if sigma > 0.0 {
let seed = self
.round
.wrapping_mul(0x0000_0100_0000_01B3)
.wrapping_add(n as u64)
.wrapping_add(0x0000_0F1A_3E5E_ED00);
let mut rng = SplitMix64::new(seed);
for value in result.iter_mut() {
let noise: T = to_scalar(rng.next_gaussian() * sigma)?;
*value = *value + noise;
}
}
Ok(result)
}
pub(super) fn flame_admitted_indices(grads: &[&Array1<T>]) -> Result<Vec<usize>> {
let n = grads.len();
if n == 0 {
return Ok(Vec::new());
}
let min_size = n / 2 + 1;
let mut distance = vec![vec![0.0f64; n]; n];
for i in 0..n {
for j in (i + 1)..n {
let d = 1.0 - from_scalar(cosine_similarity(grads[i], grads[j])?)?;
distance[i][j] = d;
distance[j][i] = d;
}
}
let mut clusters: Vec<Vec<usize>> = (0..n).map(|i| vec![i]).collect();
if clusters[0].len() >= min_size {
return Ok(clusters[0].clone());
}
while clusters.len() > 1 {
let mut best: Option<(usize, usize, f64)> = None;
for a in 0..clusters.len() {
for b in (a + 1)..clusters.len() {
let mut sum = 0.0;
for &x in &clusters[a] {
for &y in &clusters[b] {
sum += distance[x][y];
}
}
let linkage = sum / (clusters[a].len() * clusters[b].len()) as f64;
if best.is_none_or(|(_, _, current)| linkage < current) {
best = Some((a, b, linkage));
}
}
}
let Some((a, b, _)) = best else { break };
let merged = clusters.remove(b);
clusters[a].extend(merged);
if clusters[a].len() >= min_size {
let mut out = clusters[a].clone();
out.sort_unstable();
return Ok(out);
}
}
let mut out = clusters.into_iter().next().unwrap_or_default();
out.sort_unstable();
Ok(out)
}
pub fn flame_admitted_ids(
&self,
gradients: &HashMap<String, Array1<T>>,
) -> Result<Vec<String>> {
Self::validate_cohort(gradients)?;
let ordered = Self::ordered_cohort(gradients);
let grads: Vec<&Array1<T>> = ordered.iter().map(|(_, g)| *g).collect();
let admitted = Self::flame_admitted_indices(&grads)?;
Ok(admitted
.into_iter()
.map(|index| ordered[index].0.clone())
.collect())
}
}