Skip to main content

sc_neurocore_engine/bindings/
ollivier_ricci.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Commercial license available
3// © Concepts 1996–2026 Miroslav Šotek. All rights reserved.
4// © Code 2020–2026 Miroslav Šotek. All rights reserved.
5// ORCID: 0009-0009-3560-0851
6// Contact: www.anulum.li | protoscience@anulum.li
7// SC-NeuroCore — Ollivier-Ricci PyO3 binding
8
9//! Python binding for discrete Ollivier-Ricci graph curvature.
10
11use pyo3::exceptions::PyValueError;
12use pyo3::prelude::*;
13
14use crate::topology::{self, CurvatureError};
15
16/// Register the Ollivier-Ricci curvature function with the extension module.
17pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
18    module.add_function(wrap_pyfunction!(py_ollivier_ricci_curvature, module)?)?;
19    Ok(())
20}
21
22fn curvature_error_message(error: CurvatureError) -> &'static str {
23    match error {
24        CurvatureError::BadShape => "knm must be a square coupling matrix with at least one node",
25        CurvatureError::BadValue => "knm must contain only finite, non-negative values",
26        CurvatureError::BadIndex => "node index out of range for coupling graph",
27        CurvatureError::Infeasible => "transport problem is infeasible",
28    }
29}
30
31fn map_curvature_error(error: CurvatureError) -> PyErr {
32    PyValueError::new_err(curvature_error_message(error))
33}
34
35/// Discrete Ollivier-Ricci curvature between two nodes of a coupling graph.
36///
37/// Parity contract with `sc_neurocore.math.topology.ollivier_ricci_curvature`:
38/// for the same `knm` and `(i, j)`, the Rust value agrees with the Python
39/// value to within float64 round-off.
40///
41/// `knm_flat` is the row-major `n x n` coupling matrix. Raises `ValueError`
42/// on a malformed shape, a non-finite or negative entry, or an out-of-range
43/// index, mirroring the Python validation.
44#[pyfunction]
45#[pyo3(signature = (knm_flat, n, i, j))]
46fn py_ollivier_ricci_curvature(knm_flat: Vec<f64>, n: usize, i: usize, j: usize) -> PyResult<f64> {
47    topology::ollivier_ricci_curvature(&knm_flat, n, i, j).map_err(map_curvature_error)
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn error_messages_preserve_the_public_contract() {
56        assert_eq!(
57            curvature_error_message(CurvatureError::BadShape),
58            "knm must be a square coupling matrix with at least one node"
59        );
60        assert_eq!(
61            curvature_error_message(CurvatureError::BadValue),
62            "knm must contain only finite, non-negative values"
63        );
64        assert_eq!(
65            curvature_error_message(CurvatureError::BadIndex),
66            "node index out of range for coupling graph"
67        );
68        assert_eq!(
69            curvature_error_message(CurvatureError::Infeasible),
70            "transport problem is infeasible"
71        );
72    }
73}