Add biadjacency matrix helpers#1589
Conversation
Add constructors and matrix export functions for dense NumPy biadjacency matrices on PyGraph and PyDiGraph. The constructors create row nodes before column nodes, and the export functions use explicit row and column node-index orders. Also add type stubs, API documentation, a release note, and tests for construction, null values, validation, directed edge direction, and parallel-edge reductions. Fixes Qiskit#1412
Coverage Report for CI Build 26497143214Warning Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes. Coverage increased (+0.06%) to 94.784%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
IvanIsCoding
left a comment
There was a problem hiding this comment.
So there are many comments. On the most immediate part, look at https://github.com/Qiskit/rustworkx/blob/main/rustworkx/__init__.py for a reference on the universal functions. And I left a couple Rust comments.
With that being said, the biggest flaw in the PR is that we don't return or accept any types from https://docs.scipy.org/doc/scipy/reference/sparse.html. This is what users expect when dealing with sparse arrays.
You'll need to rewrite your whole code to not use NumPy and tweak tests such that scipy does not become a required dependency. It should be an optional dependency.
One way of decoupling our code from scipy is to leverage #1515. We can serialize to matrix market and then deserialize from it with scipy for rustworkx.biadjacency_matrix. And the other way around for from_biadjacency_matrix, ask scipy to export a format we can read and then load it.
| self.lowpt.insert(ei, self.height[&w]); | ||
| self.lowpt_2.insert(ei, self.height[&v]); | ||
| } | ||
| // do *not* consider ``(v, w)`` as a back edge if ``(w, v)`` is a tree edge. |
There was a problem hiding this comment.
Remove this diff it's unrelated to the PR
There was a problem hiding this comment.
Thank you for comments @IvanIsCoding , looking to address them.
This particular change was due to cargo clippy --workspace --all-targets --all-features -- -D warnings failing for cargo 1.95; Rust 1.96 released recently.
I understand CI might pin lower version, but maybe useful for the future, would you find useful if I create new PR for this little change to fix future clippy ?
There was a problem hiding this comment.
Sure, feel free to bump the Rust versions for Clippy and the formatter. Just try to keep it separate from this PR
| :func:`~rustworkx.graph_biadjacency_matrix`, and | ||
| :func:`~rustworkx.digraph_biadjacency_matrix` for dense NumPy |
There was a problem hiding this comment.
So document the individual methods for the PyDiGraph and PyGraph methods (e.g. from_biadjacency_matrix have different documentation entries). But for biadjacency_matrix, you should only note it once in the release with the universal function you are missing.
| from .rustworkx import weakly_connected_components as weakly_connected_components | ||
| from .rustworkx import digraph_adjacency_matrix as digraph_adjacency_matrix | ||
| from .rustworkx import graph_adjacency_matrix as graph_adjacency_matrix | ||
| from .rustworkx import digraph_biadjacency_matrix as digraph_biadjacency_matrix |
There was a problem hiding this comment.
A reminder to:
- Add a universal function with
@_rustworkx_dispatchthat works with both graph types - Document that function stub here
| parallel_edge_fn: ParallelEdgeFn, | ||
| ) { | ||
| let key = [row, column]; | ||
| let count = parallel_edge_count.entry(key).or_insert(0); |
There was a problem hiding this comment.
Use a match with the entry() method:
Line 266 in 9fb91e8
It will save one hashmap access. Those add up. The code is also cleaner.
| Ok(matrix.into_pyarray(py)) | ||
| } | ||
|
|
||
| type ParallelEdgeFn = fn(f64, f64, usize) -> f64; |
There was a problem hiding this comment.
For code organization:
- create a
biadjacency.rsin this module and import it - move the duplicate code from
digraph.rsandgraph.rshere with generics as well
| text_signature = "(graph, row_order, column_order, /, weight_fn=None, default_weight=1.0, null_value=0.0, parallel_edge=\"sum\")" | ||
| )] | ||
| #[allow(clippy::too_many_arguments)] | ||
| pub fn digraph_biadjacency_matrix<'py>( |
There was a problem hiding this comment.
After you create the other file, keep the main function here still
| let parallel_edge_fn = parallel_edge_fn(parallel_edge)?; | ||
| let row_map = biadjacency_node_map(&row_order); | ||
| let column_map = biadjacency_node_map(&column_order); | ||
| let mut matrix = Array2::<f64>::from_elem((row_order.len(), column_order.len()), null_value); |
There was a problem hiding this comment.
So this is the fatal flaw in the PR: this shouldn't be a NumPy array. This should be a scipy object, one of their sparse arrays.
You'll need to follow https://docs.rs/pyo3/0.26.0/pyo3/#example-using-python-from-rust to interact with Python objects from SciPy. Import the module, throw an error if it is not there, and then proceed.
|
|
||
| fn _from_biadjacency_matrix<'p, T>( | ||
| py: Python<'p>, | ||
| matrix: PyReadonlyArray2<'p, T>, |
There was a problem hiding this comment.
Same flaw, deal with scipy objects. You can check how NetworkX handles it for a reference.
| ) | ||
|
|
||
|
|
||
| class TestDiGraphBiadjacencyMatrix(unittest.TestCase): |
There was a problem hiding this comment.
As a reminder, tests will look different. #1221 has the old code we deleted but the summary is: tests are optional, if scipy is not present tests should not run. We should update pyproject.toml to only install scipy for a range of python versions and preferably skip Windows as well.
|
|
||
|
|
||
| class TestGraphBiadjacencyMatrix(unittest.TestCase): | ||
| def test_from_biadjacency_matrix(self): |
There was a problem hiding this comment.
Same comment about test setup
Fixes #1412
Summary
This adds dense NumPy biadjacency matrix support for both graph types:
PyGraph.from_biadjacency_matrix()PyDiGraph.from_biadjacency_matrix()rustworkx.graph_biadjacency_matrix()rustworkx.digraph_biadjacency_matrix()For an input matrix with shape
(m, n), the constructors createm + nnodes, with row nodes first and column nodes second. Non-null entries create edges between the corresponding row and column nodes; for
PyDiGraph, those edges are directed from row nodes to column nodes.The output functions take explicit row and column node-index orders. These orders must contain existing node indices, must not contain duplicates, and must be disjoint. Parallel edges supp
ort the same
sum,min,max, andavgreductions as the existing adjacency matrix helpers.Why
Issue #1412 requested NetworkX-like biadjacency matrix functionality for non-square bipartite matrix workflows. This fills that API gap while keeping the behavior explicit for rustworkx no
de-index based graphs.
Notes for reviewers
NaNnull values, dtype validation, empty dimensions, directed edge directionality, undirected reverse-edge handling, parallel-edge reductions, invalid
parallel_edge, missing/removed node indices, duplicate orders, overlapping orders, and empty output orders.rustworkx-core/src/planar/lr_planar.rsso currentcargo clippy -D warningspasses.Checks run
git diff --checkcargo fmt --all -- --checkcargo check --workspace --all-featurescargo clippy --workspace --all-targets --all-features -- -D warningsnox -e lintnox -e stubsnox -e test -- adjacency_matrix— 85 passednox -e test— 2410 run, 2399 passed, 11 skipped