Skip to content

Conversation

dizzyi
Copy link
Contributor

@dizzyi dizzyi commented Aug 7, 2025

Development Workflow

  • make format
  • make test
  • make lint

Features

  • Doc on degree, katz centrality

  • Examples

    • centrality.rs
      cargo run --example centrality
      
    • centrality_eigenvector.rs and centrality_eigenvector.py
      both program should return same result
      cargo run --example centrality_eigenvector
      python ./examples/centrality_eigenvector.py
      
    • centrality_katz.rs andcentrality_katz.py
      both program should return same result
      cargo run --example centrality_katz
      python ./examples/centrality_katz.py
      
  • Katz Centrality improvement

API change

katz_centrality(&graph, alpha, beta, max_iter)
|
v
katz_centrality(
    &graph, alpha, beta, max_iter, 
    weighted /*bool*/, normalized /*bool*/
)

Examples

katz_centrality_impl

fn katz_centrality_impl_example() {
    use graphina::centrality::algorithms::katz_centrality_impl;
    use graphina::core::types::Graph;

    let mut g: Graph<(i32, f64), (f64, f64)> = Graph::new();
    //               ^^^^^^^^^^  ^^^^^^^^^^
    //                        |           L arbitrary type as edge
    //                        L arbitrary type as node
    let nodes = [
        g.add_node((1, 2.0)),
        g.add_node((2, 3.0)),
        g.add_node((3, 2.0)),
    ];
    g.add_edge(nodes[0], nodes[1], (0.0, 1.0));
    g.add_edge(nodes[0], nodes[2], (1.0, 0.0));

    let centrality = katz_centrality_impl(
        &g,
        |_n| 0.01,                        // <-- custom alpha depend on node attribute
        |(i, f): &(i32, f64)| f.powi(*i), // <-- custom beta depend on node attribute
        1_000,
        1e-6_f64,
        true,
        |w| w.0 * 10.0 + w.1, // <-- custom evaluation for edge weight
    );
    println!("{:.5?}", centrality); // [0.23167, 0.71650, 0.65800]
}

katz_centrality

fn katz_centrality_example() {
    use graphina::centrality::algorithms::katz_centrality;
    use graphina::core::types::Graph;

    let mut g = Graph::new();
    let nodes = [g.add_node(1), g.add_node(2), g.add_node(3)];
    g.add_edge(nodes[0], nodes[1], 1.0);
    g.add_edge(nodes[0], nodes[2], 2.0);

    let centrality = katz_centrality(&g, 0.1, 1.0, 1000, false, true);
    println!("{:.5?}", centrality); // [0.61078, 0.55989, 0.55989]
    let centrality = katz_centrality(&g, 0.01, 0.5, 1000, true, true);
    println!("{:.5?}", centrality); // [0.58301, 0.57158, 0.57741]
}

Code Changes

katz_centrality

pub fn katz_centrality<A, Ty>(
    graph: &BaseGraph<A, f64, Ty>,
    alpha: f64,
    beta: f64,
    max_iter: usize,
    weighted: bool, // whether or not the edge are weighted
    normalized: bool, // whether or not the result are normalized
) -> Vec<f64>
where
    Ty: crate::core::types::GraphConstructor<A, f64>,
{
    let alpha = |_a: &A| alpha; // closure returning constant f64
    let beta = |_a: &A| beta;// closure returning constant f64
    let eval_weight = if weighted {
        |f: &f64| *f // closure returning edge weight
    } else {
        |_f: &f64| 1.0 // closure returning constant 1.0
    };
    katz_centrality_impl(
        graph,
        alpha,
        beta,
        max_iter,
        1e-6_f64,
        normalized,
        eval_weight,
    )
}

katz_centrality_impl

pub fn katz_centrality_impl<A, W, Ty>(
    graph: &BaseGraph<A, W, Ty>,
    alpha: impl Fn(&A) -> f64, // take closure to evalue alpha for each node
    beta: impl Fn(&A) -> f64,  // take closure to evalue beta for each node
    max_iter: usize,
    tol: f64,
    normalized: bool,
    eval_weight: impl Fn(&W) -> f64,// take closure to evalue weight for each edge
) -> Vec<f64>
where
    Ty: GraphConstructor<A, W>,
{
    let n = graph.node_count();

    // calculate all alpha and beta first
    let betas = (0..n)
        .map(|i| beta(graph.node_attr(NodeId(NodeIndex::new(i))).unwrap()))
        .collect::<Vec<_>>();
    let alphas = (0..n)
        .map(|i| alpha(graph.node_attr(NodeId(NodeIndex::new(i))).unwrap()))
        .collect::<Vec<_>>();

    // initalize centrality as zeros
    let mut centrality = vec![0.0; n];
    let mut next = vec![0.0; n];

    for _ in 0..max_iter {

        // iterate through edges
        for (src, dst, w) in graph.edges() {
            let w = eval_weight(w);
            next[dst.index()] += w * centrality[src.index()];

            // if graph is undirected, ignore backward edge
            if !<Ty as GraphConstructor<A, W>>::is_directed() {
                next[src.index()] += w * centrality[dst.index()];
            }
        }

        // alpha * A * x + beta
        for (i, n) in next.iter_mut().enumerate() {
            *n = alphas[i] * *n + betas[i];
        }

        let diff: f64 = centrality
            .iter_mut()
            .zip(next.iter_mut())
            .map(|(a, b)| {
                let d = (*a - *b).abs();
                // update centrality from next
                *a = *b;
                // reset next to zero
                *b = 0.0;
                d
            })
            .sum();

        if diff < tol * n as f64 {

            // only normalize at output,
            // instead of every iteration (*)
            if normalized {
                let norm = centrality.iter().map(|x| x * x).sum::<f64>().sqrt();
                if norm > 0.0 {
                    for x in &mut centrality {
                        *x /= norm;
                    }
                }
            }
            break;
        }
    }
    centrality
}

(*) : instead of normalize every iteration, it only normalize before return, this is how [networkx] does it, it granted some numerical instablity if alpha is larger than the recipocal of the maximum eigen value of the network.

Copy link

codecov bot commented Aug 7, 2025

Codecov Report

❌ Patch coverage is 66.66667% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.11%. Comparing base (f3cd4c5) to head (0a8c662).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
src/centrality/algorithms.rs 66.66% 14 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #19      +/-   ##
==========================================
- Coverage   80.57%   80.11%   -0.46%     
==========================================
  Files          12       12              
  Lines        1807     1841      +34     
==========================================
+ Hits         1456     1475      +19     
- Misses        351      366      +15     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@habedi habedi self-assigned this Aug 7, 2025
@habedi habedi added documentation Improvements or additions to documentation enhancement New feature or request development Development-related work labels Aug 7, 2025
@habedi habedi merged commit 385c85b into habedi:main Aug 7, 2025
4 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
development Development-related work documentation Improvements or additions to documentation enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants