Skip to main content

whitaker_common/test_support/
decomposition_label_propagation.rs

1//! Observable label-propagation seams for decomposition advice tests.
2
3use crate::decomposition_advice::{
4    MethodProfileBuilder, build_feature_vector,
5    community::{
6        LabelPropagationReport as RuntimeLabelPropagationReport,
7        propagate_labels_report as runtime_propagate_labels_report,
8    },
9};
10
11use super::adjacency::{AdjacencyError, EdgeInput, validate_edges};
12
13/// Observable label-propagation results for declarative graph input.
14///
15/// # Examples
16///
17/// ```rust
18/// use whitaker_common::test_support::decomposition::{
19///     EdgeInput, label_propagation_report,
20/// };
21///
22/// let report = label_propagation_report(
23///     &["gamma", "alpha", "beta"],
24///     &[EdgeInput {
25///         left: 0,
26///         right: 1,
27///         weight: 5,
28///     }],
29///     1,
30/// )
31/// .expect("valid input");
32///
33/// assert_eq!(report.labels().len(), 3);
34/// ```
35#[derive(Clone, Debug, Eq, PartialEq)]
36pub struct LabelPropagationReport {
37    runtime: RuntimeLabelPropagationReport,
38    has_active_nodes: bool,
39}
40
41impl LabelPropagationReport {
42    /// Returns the final label vector.
43    #[must_use]
44    pub fn labels(&self) -> &[usize] {
45        &self.runtime.labels
46    }
47
48    /// Returns the propagated label for `node`, or `None` if it is out of
49    /// range.
50    #[must_use]
51    pub fn label_of(&self, node: usize) -> Option<usize> {
52        self.labels().get(node).copied()
53    }
54
55    /// Returns the number of propagation passes performed.
56    #[must_use]
57    pub fn iteration_count(&self) -> usize {
58        self.runtime.iteration_count
59    }
60
61    /// Returns `true` when the graph contains at least one active node.
62    #[must_use]
63    pub fn has_active_nodes(&self) -> bool {
64        self.has_active_nodes
65    }
66
67    /// Returns `true` when every label is a valid node index.
68    #[must_use]
69    pub fn all_labels_in_bounds(&self) -> bool {
70        self.runtime
71            .labels
72            .iter()
73            .all(|&label| label < self.runtime.labels.len())
74    }
75}
76
77/// Runs label propagation over declarative graph input.
78///
79/// # Errors
80///
81/// Returns a typed validation error when any edge violates the production
82/// adjacency contract.
83pub fn label_propagation_report(
84    method_names: &[&str],
85    edges: &[EdgeInput],
86    max_iterations: usize,
87) -> Result<LabelPropagationReport, AdjacencyError> {
88    let similarity_edges = validate_edges(method_names.len(), edges)?;
89    let adjacency = crate::decomposition_advice::community::build_adjacency(
90        method_names.len(),
91        &similarity_edges,
92    );
93    let vectors = method_names
94        .iter()
95        .map(|method_name| build_feature_vector(&MethodProfileBuilder::new(*method_name).build()))
96        .collect::<Vec<_>>();
97    let has_active_nodes = adjacency.iter().any(|neighbours| !neighbours.is_empty());
98    let runtime = runtime_propagate_labels_report(&vectors, &adjacency, max_iterations);
99
100    Ok(LabelPropagationReport {
101        runtime,
102        has_active_nodes,
103    })
104}