Skip to content
Draft
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ac1afb8
feat: Add support for graph6 format
hsunwenfang Sep 1, 2025
2a09e57
git commit -m "chore: trigger self-hosted CI"
hsunwenfang Sep 1, 2025
c6e5e7c
remove gh action yaml
hsunwenfang Sep 2, 2025
826a8e4
tidy up tests/test_graph6_py.py
hsunwenfang Sep 4, 2025
efab5f1
Remove noxfile.py before PR as requested
hsunwenfang Sep 4, 2025
d6ed94f
restore: revert graph6.rs to pre-refactor monolithic implementation (…
hsunwenfang Sep 4, 2025
f47a0ff
pytests passed
hsunwenfang Sep 5, 2025
1f00fd1
remove excessive test files
hsunwenfang Sep 5, 2025
d9a2c6b
release note
hsunwenfang Sep 6, 2025
42b158b
clean up tests
hsunwenfang Sep 6, 2025
f8241fe
delete unused github action yaml
hsunwenfang Sep 6, 2025
d6be2f2
tests: remove duplicate graph6 and sparse6 size/format test files aft…
hsunwenfang Sep 6, 2025
ffd809d
removed unwrap and panic
hsunwenfang Sep 22, 2025
a5c6a90
update naming
hsunwenfang Sep 22, 2025
ed32f8a
update naming
hsunwenfang Sep 22, 2025
5bd2a65
tidyup python
hsunwenfang Sep 23, 2025
8a2ae09
Remove let wrapped = std::panic::catch_unwind(|| {
hsunwenfang Sep 23, 2025
84f202c
fix the namings
hsunwenfang Sep 23, 2025
cd3d88f
use tempfile.NamedTemporaryFile()
hsunwenfang Sep 23, 2025
6ecd347
Use generator to help tests
hsunwenfang Sep 23, 2025
886b893
centralize byte string checking in graph6.rs/ SizeCodec
hsunwenfang Sep 24, 2025
aca2137
i/o naming align to @_rustworkx_dispatch
hsunwenfang Sep 24, 2025
d8b13d1
cargo fmt, nox test, nox -e docs
hsunwenfang Sep 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions releasenotes/notes/add-graph6-support.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
features:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please read https://docs.openstack.org/reno/latest/user/usage.html#editing-a-release-note for the correct format of release notes

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

used reno to generate one with nox -e docs compatible doc

- |
This note documents the graph6 family of ASCII formats and the helpers
added to the codebase. The summary below is a concise description of the
formats (based on the canonical formats document) and a few developer
notes to help maintainers and tests.

references:
- https://users.cecs.anu.edu.au/~bdm/data/formats.txt
issue:
- https://github.com/Qiskit/rustworkx/issues/1496

graph6
- A compact ASCII-based encoding for simple undirected graphs.
- Encodes the graph order (n) using a variable-length size field and packs
the upper-triangular adjacency bits into 6-bit chunks. Each 6-bit value
is mapped into a printable ASCII character by adding an offset (so the
encoded bytes are printable ASCII).
- Typical uses: small-to-moderately-dense graphs where the adjacency
matrix can be packed efficiently.

digraph6
- A variant of the graph6 scheme adapted for directed graphs. It encodes
a representation of the full adjacency matrix (row-major) using the
same 6-bit packing and ASCII mapping as graph6.
- Useful for representing directed graphs where directionality matters.

sparse6
- An alternate encoding designed for very sparse graphs. Instead of
packing the full adjacency matrix, sparse6 records adjacency in a more
compact variable-length integer form (adjacency lists / runs), which
yields much smaller files for low edge-density graphs.

Developer notes
- Parsers must correctly handle variable-length size fields, 6-bit packing
and padding to 6-bit boundaries. Edge cases around very small and very
large n should be handled robustly.
- Tests should prefer roundtrip and structural checks (parse -> graph ->
serialize -> parse) and verify canonical encodings where the format
features:
- title: Add graph6/digraph6/sparse6 support and format summary
release: unreleased
notes: |
This note documents the graph6 family of ASCII formats and the helpers
added to the codebase. The summary below is a concise description of the
formats (based on the canonical formats document) and a few developer
notes to help maintainers and tests.

30 changes: 30 additions & 0 deletions rustworkx/digraph6.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""digraph6 format helpers.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now, this wrapper is pointless:

  • For writing, see
    def write_graphml(graph, path, /, keys=None, compression=None):
    for the correct pattern
  • For reading, if the file format contains the hint: use the same pattern from
    pub fn read_graphml<'py>(
    . If it doesn't, then just have two functions digraph_read_graph6 and graph_read_graph6

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed graph6.py, digraph6.py, sparse6.py and use a write_graph6 function in init with _rustworkx_dispatch


Directed variant of graph6 per the documented format. The core dispatch
routine already auto-detects directed strings (leading '&') or header form
(>>digraph6<<:). This namespace provides clarity and future room for
specialized helpers without breaking existing API.
"""
from __future__ import annotations

from . import read_graph6_str as _read_graph6_str
from . import write_graph6_from_pydigraph as _write_graph6_from_pydigraph

__all__ = [
"read_graph6_str",
"write_graph6_from_pydigraph",
"read",
"write",
]


def read_graph6_str(repr: str): # noqa: D401 - thin wrapper
return _read_graph6_str(repr)

read = read_graph6_str


def write_graph6_from_pydigraph(digraph): # noqa: D401 - thin wrapper
return _write_graph6_from_pydigraph(digraph)

write = write_graph6_from_pydigraph
43 changes: 43 additions & 0 deletions rustworkx/graph6.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""graph6 format helpers.

This module provides a namespace for working with undirected graph6 strings
as described in: https://users.cecs.anu.edu.au/~bdm/data/formats.txt

It wraps the low-level functions exported from the compiled extension
(`read_graph6_str`, `write_graph6_from_pygraph`) and offers convenience
helpers. Backwards compatibility: existing top-level functions in
`rustworkx` remain valid; this is a thin façade only.
"""
from __future__ import annotations

from . import read_graph6_str as _read_graph6_str
from . import write_graph6_from_pygraph as _write_graph6_from_pygraph

__all__ = [
"read_graph6_str",
"write_graph6_from_pygraph",
"read",
"write",
]


def read_graph6_str(repr: str):
"""Parse a graph6 representation into a PyGraph.

Accepts either raw graph6, header form (>>graph6<<:), or directed strings.
For clarity, use digraph6.read_graph6_str for directed graphs. This wrapper
leaves behavior unchanged (delegates to the core function) but documents
intent that this namespace targets undirected graphs.
"""
g = _read_graph6_str(repr)
return g

# Short aliases
read = read_graph6_str


def write_graph6_from_pygraph(graph):
"""Serialize a PyGraph to a graph6 string."""
return _write_graph6_from_pygraph(graph)

write = write_graph6_from_pygraph
24 changes: 24 additions & 0 deletions rustworkx/sparse6.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""sparse6 format helpers (placeholder).

The sparse6 format is related to graph6/digraph6 but optimized for sparse
graphs. Parsing is currently not implemented in the Rust core; the Rust
layer returns an UnsupportedFormat error when an explicit sparse6 header is
encountered.

This module centralizes the placeholder so future implementation can add
real parsing while giving users a discoverable namespace today.
"""
from __future__ import annotations

from . import read_sparse6_str as _read_sparse6_str
from . import write_sparse6_from_pygraph as _write_sparse6_from_pygraph

__all__ = ["read_sparse6_str", "write_sparse6_from_pygraph"]


def read_sparse6_str(repr: str):
return _read_sparse6_str(repr)


def write_sparse6_from_pygraph(pygraph, header: bool = True):
return _write_sparse6_from_pygraph(pygraph, header)
122 changes: 122 additions & 0 deletions src/digraph6.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
use crate::{get_edge_iter_with_weights, StablePyGraph};
use crate::graph6::{utils, IOError, GraphConversion};
use pyo3::prelude::*;
use pyo3::types::PyAny;
use petgraph::graph::NodeIndex;
use petgraph::algo;

/// Directed graph implementation (extracted from graph6.rs)
#[derive(Debug)]
pub struct DiGraph {
pub bit_vec: Vec<usize>,
pub n: usize,
}
impl DiGraph {
/// Creates a new DiGraph from a graph6 representation string
pub fn from_d6(repr: &str) -> Result<Self, IOError> {
let bytes = repr.as_bytes();
Self::valid_digraph(bytes)?;
let (n, n_len) = utils::parse_size(bytes, 1)?;
let Some(bit_vec) = Self::build_bitvector(bytes, n, 1 + n_len) else {
return Err(IOError::NonCanonicalEncoding);
};
Ok(Self { bit_vec, n })
}

/// Creates a new DiGraph from a flattened adjacency matrix
#[cfg(test)]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't run tests for rustworkx with cargo test. Please add an equivalent test that covers this in .py

pub fn from_adj(adj: &[usize]) -> Result<Self, IOError> {
let n2 = adj.len();
let n = (n2 as f64).sqrt() as usize;
if n * n != n2 {
return Err(IOError::InvalidAdjacencyMatrix);
}
let bit_vec = adj.to_vec();
Ok(Self { bit_vec, n })
}

/// Validates graph6 directed representation
pub(crate) fn valid_digraph(repr: &[u8]) -> Result<bool, IOError> {
if repr[0] == b'&' {
Ok(true)
} else {
Err(IOError::InvalidDigraphHeader)
}
}

/// Iteratores through the bytes and builds a bitvector
/// representing the adjaceny matrix of the graph
fn build_bitvector(bytes: &[u8], n: usize, offset: usize) -> Option<Vec<usize>> {
let bv_len = n * n;
utils::fill_bitvector(bytes, bv_len, offset)
}
}

impl GraphConversion for DiGraph {
fn bit_vec(&self) -> &[usize] {
&self.bit_vec
}

fn size(&self) -> usize {
self.n
}

fn is_directed(&self) -> bool {
true
}
}

/// Convert internal DiGraph to PyDiGraph
pub fn digraph_to_pydigraph<'py>(py: Python<'py>, g: &DiGraph) -> PyResult<Bound<'py, PyAny>> {
use crate::graph6::GraphConversion as _;
let mut graph = StablePyGraph::<petgraph::Directed>::with_capacity(g.size(), 0);
for _ in 0..g.size() {
graph.add_node(py.None());
}
for i in 0..g.size() {
for j in 0..g.size() {
if g.bit_vec[i * g.size() + j] == 1 {
let u = NodeIndex::new(i);
let v = NodeIndex::new(j);
graph.add_edge(u, v, py.None());
}
}
}
let out = crate::digraph::PyDiGraph {
graph,
cycle_state: algo::DfsSpace::default(),
check_cycle: false,
node_removed: false,
multigraph: true,
attrs: py.None(),
};
Ok(out.into_pyobject(py)?.into_any())
}

#[pyfunction]
#[pyo3(signature=(pydigraph))]
pub fn write_graph6_from_pydigraph(pydigraph: Py<crate::digraph::PyDiGraph>) -> PyResult<String> {
Python::with_gil(|py| {
let g = pydigraph.borrow(py);
let n = g.graph.node_count();
let mut bit_vec = vec![0usize; n * n];
for (i, j, _w) in get_edge_iter_with_weights(&g.graph) {
bit_vec[i * n + j] = 1;
}
let graph6 = crate::graph6::write::write_graph6(bit_vec, n, true);
Ok(graph6)
})
}

#[pyfunction]
#[pyo3(signature=(digraph, path))]
pub fn digraph_write_graph6_file(digraph: Py<crate::digraph::PyDiGraph>, path: &str) -> PyResult<()> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just call the methods that write to file *_write_graph6, *_write_sparse6, etc. For methods that write to string, use to_str or from_str.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The naming is now graph_write_graph6 digraph_write_graph6

let s = write_graph6_from_pydigraph(digraph)?;
crate::graph6::to_file(path, &s)
.map_err(|e| pyo3::exceptions::PyIOError::new_err(format!("IO error: {}", e)))?;
Ok(())
}

// Enable write_graph() in tests for DiGraph via the WriteGraph trait
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't run tests for rustworkx with cargo test. Please remove this

#[cfg(test)]
impl crate::graph6::write::WriteGraph for DiGraph {}
Loading
Loading