use std::sync::Arc;
use antecedent_core::CausalRng;
use crate::error::ProbError;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum GraphIdentFlag {
Identified,
Unidentified,
}
#[derive(Clone, Debug)]
pub struct GraphEnvelopeSubsample {
pub graphs: WeightedGraphSamples,
pub leftover_identified_mass: f64,
pub approximate: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub struct WeightedGraphSamples {
pub n_samples: usize,
pub weights: Arc<[f64]>,
pub identified: Arc<[GraphIdentFlag]>,
pub graph_keys: Arc<[u64]>,
pub edge_marginals: Option<Arc<[f64]>>,
pub orientation_marginals: Option<Arc<[f64]>>,
}
impl WeightedGraphSamples {
pub fn new(
weights: impl Into<Arc<[f64]>>,
identified: impl Into<Arc<[GraphIdentFlag]>>,
graph_keys: impl Into<Arc<[u64]>>,
) -> Result<Self, ProbError> {
let weights = weights.into();
let identified = identified.into();
let graph_keys = graph_keys.into();
let n = weights.len();
if n == 0 {
return Err(ProbError::Shape { message: "empty graph ensemble" });
}
if identified.len() != n || graph_keys.len() != n {
return Err(ProbError::Shape { message: "weights/identified/keys length mismatch" });
}
Ok(Self {
n_samples: n,
weights,
identified,
graph_keys,
edge_marginals: None,
orientation_marginals: None,
})
}
#[must_use]
pub fn total_weight(&self) -> f64 {
self.weights.iter().sum()
}
#[must_use]
pub fn unidentified_mass(&self) -> f64 {
self.weights
.iter()
.zip(self.identified.iter())
.filter(|(_, f)| **f == GraphIdentFlag::Unidentified)
.map(|(w, _)| *w)
.sum()
}
#[must_use]
pub fn identified_mass(&self) -> f64 {
self.total_weight() - self.unidentified_mass()
}
pub fn normalized(&self) -> Result<Self, ProbError> {
let total = self.total_weight();
if !(total > 0.0) {
return Err(ProbError::Shape { message: "non-positive total weight" });
}
let weights: Arc<[f64]> =
Arc::from(self.weights.iter().map(|w| w / total).collect::<Vec<_>>());
Ok(Self {
n_samples: self.n_samples,
weights,
identified: Arc::clone(&self.identified),
graph_keys: Arc::clone(&self.graph_keys),
edge_marginals: self.edge_marginals.clone(),
orientation_marginals: self.orientation_marginals.clone(),
})
}
pub fn stratified_interactive_subsample(
&self,
max_identified: usize,
rng: &mut CausalRng,
) -> Result<GraphEnvelopeSubsample, ProbError> {
if self.n_samples == 0 {
return Err(ProbError::Shape { message: "empty graph ensemble" });
}
let mut identified_idx: Vec<usize> = (0..self.n_samples)
.filter(|&i| self.identified[i] == GraphIdentFlag::Identified)
.collect();
if identified_idx.len() <= max_identified {
return Ok(GraphEnvelopeSubsample {
graphs: self.clone(),
leftover_identified_mass: 0.0,
approximate: false,
});
}
for i in 0..max_identified {
let j = i + (rng.next_u64() as usize % (identified_idx.len() - i));
identified_idx.swap(i, j);
}
let mut flags = self.identified.to_vec();
let mut leftover = 0.0;
for &i in &identified_idx[max_identified..] {
leftover += self.weights[i];
flags[i] = GraphIdentFlag::Unidentified;
}
let graphs = Self {
n_samples: self.n_samples,
weights: Arc::clone(&self.weights),
identified: Arc::from(flags),
graph_keys: Arc::clone(&self.graph_keys),
edge_marginals: self.edge_marginals.clone(),
orientation_marginals: self.orientation_marginals.clone(),
};
Ok(GraphEnvelopeSubsample {
graphs,
leftover_identified_mass: leftover,
approximate: leftover > 0.0,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unidentified_mass_preserved() {
let g = WeightedGraphSamples::new(
vec![0.5, 0.3, 0.2],
vec![
GraphIdentFlag::Identified,
GraphIdentFlag::Unidentified,
GraphIdentFlag::Identified,
],
vec![1, 2, 3],
)
.unwrap();
assert!((g.unidentified_mass() - 0.3).abs() < 1e-12);
assert!((g.identified_mass() - 0.7).abs() < 1e-12);
}
#[test]
fn stratified_subsample_moves_leftover_to_unidentified() {
let g = WeightedGraphSamples::new(
vec![0.25, 0.25, 0.25, 0.25],
vec![
GraphIdentFlag::Identified,
GraphIdentFlag::Identified,
GraphIdentFlag::Identified,
GraphIdentFlag::Unidentified,
],
vec![10, 11, 12, 13],
)
.unwrap();
let mut rng = CausalRng::from_seed(7);
let sub = g.stratified_interactive_subsample(1, &mut rng).unwrap();
assert!(sub.approximate);
assert!(sub.leftover_identified_mass > 0.0);
assert!((sub.graphs.total_weight() - g.total_weight()).abs() < 1e-12);
let expected_uid = g.unidentified_mass() + sub.leftover_identified_mass;
assert!((sub.graphs.unidentified_mass() - expected_uid).abs() < 1e-12);
let n_id =
sub.graphs.identified.iter().filter(|f| **f == GraphIdentFlag::Identified).count();
assert_eq!(n_id, 1);
}
}