use crate::descriptor::{SpaceDescriptor, from_nat, to_nat};
use crate::error::RankAdapterError;
use crate::limits::DiscreteRankLimits;
use num_bigint::BigUint;
use sim_lib_rank::Nat;
use std::collections::BTreeSet;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SimpleGraphSpace {
pub n: usize,
pairs: Vec<(usize, usize)>,
}
impl SimpleGraphSpace {
pub fn new(n: usize) -> Self {
Self::try_new(n).expect("simple-graph node count should fit default rank limits")
}
pub fn try_new(n: usize) -> Result<Self, RankAdapterError> {
DiscreteRankLimits::DEFAULT.check_simple_graph_nodes(n)?;
let mut pairs = Vec::new();
for i in 0..n {
for j in (i + 1)..n {
pairs.push((i, j));
}
}
Ok(SimpleGraphSpace { n, pairs })
}
pub fn descriptor(&self) -> SpaceDescriptor {
SpaceDescriptor {
id: "rank/discrete/simple-graph",
version: 1,
params: vec![("n", self.n.to_string())],
order: "adjacency-upper-triangle",
metric: "edge-symmetric-difference",
}
}
pub fn edge_slots(&self) -> usize {
self.pairs.len()
}
pub fn cardinality(&self) -> Nat {
to_nat(BigUint::from(1u32) << self.pairs.len())
}
pub fn rank(&self, edges: &[(usize, usize)]) -> Result<Nat, RankAdapterError> {
let mut rank = BigUint::from(0u32);
let mut seen = BTreeSet::new();
for &(i, j) in edges {
let (lo, hi) = (i.min(j), i.max(j));
if i == j || hi >= self.n {
return Err(RankAdapterError::Invalid(format!(
"edge ({i},{j}) invalid for n={}",
self.n
)));
}
if !seen.insert((lo, hi)) {
return Err(RankAdapterError::Invalid(format!(
"duplicate edge ({lo},{hi}) for n={}",
self.n
)));
}
let pos = self
.pairs
.iter()
.position(|&p| p == (lo, hi))
.expect("pair in range");
rank.set_bit(pos as u64, true);
}
Ok(to_nat(rank))
}
pub fn unrank(&self, ordinal: &Nat) -> Result<Vec<(usize, usize)>, RankAdapterError> {
let bits = from_nat(ordinal);
let bound = BigUint::from(1u32) << self.pairs.len();
if bits >= bound {
return Err(RankAdapterError::Invalid(format!(
"simple graph ordinal {bits} >= cardinality {bound}"
)));
}
let mut edges = Vec::new();
for (pos, &pair) in self.pairs.iter().enumerate() {
if bits.bit(pos as u64) {
edges.push(pair);
}
}
Ok(edges)
}
pub fn distance(&self, a: &Nat, b: &Nat) -> Result<Nat, RankAdapterError> {
let ea: BTreeSet<_> = self.unrank(a)?.into_iter().collect();
let eb: BTreeSet<_> = self.unrank(b)?.into_iter().collect();
Ok(to_nat(BigUint::from(ea.symmetric_difference(&eb).count())))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn nat(i: u32) -> Nat {
to_nat(BigUint::from(i))
}
#[test]
fn round_trips_all_graphs_on_3_nodes() {
let space = SimpleGraphSpace::new(3); assert_eq!(space.edge_slots(), 3);
assert_eq!(space.cardinality(), nat(8));
for i in 0..8u32 {
let edges = space.unrank(&nat(i)).unwrap();
assert_eq!(space.rank(&edges).unwrap(), nat(i));
}
}
#[test]
fn edge_symmetric_difference() {
let space = SimpleGraphSpace::new(4);
let a = space.rank(&[(0, 1), (1, 2)]).unwrap();
let b = space.rank(&[(1, 2), (2, 3)]).unwrap();
assert_eq!(space.distance(&a, &b).unwrap(), nat(2));
}
#[test]
fn invalid_edge_rejected() {
let space = SimpleGraphSpace::new(3);
assert!(matches!(
space.rank(&[(0, 9)]),
Err(RankAdapterError::Invalid(_))
));
}
#[test]
fn duplicate_edge_rejected() {
let space = SimpleGraphSpace::new(3);
assert!(matches!(
space.rank(&[(0, 1), (1, 0)]),
Err(RankAdapterError::Invalid(_))
));
}
#[test]
fn unrank_rejects_cardinality() {
let space = SimpleGraphSpace::new(3);
assert!(matches!(
space.unrank(&space.cardinality()),
Err(RankAdapterError::Invalid(_))
));
}
#[test]
fn checked_constructor_rejects_first_out_of_range_node_count() {
assert_eq!(SimpleGraphSpace::try_new(16).unwrap().edge_slots(), 120);
assert!(matches!(
SimpleGraphSpace::try_new(17),
Err(RankAdapterError::LimitExceeded(_))
));
}
}