Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@ rustworkx-core/Cargo.lock
**/.DS_Store
venv/
.python-version
.venv-graph6/
graph6-doc/
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
prelude: >
Definition : https://users.cecs.anu.edu.au/~bdm/data/formats.txt
Added native read/write support for the ASCII graph encoding formats
graph6, digraph6, and sparse6. These enable compact text (and optionally
gzip-compressed) serialization of simple graphs and directed graphs with
canonical size-field validation and clearer error reporting.

features:
- |
Introduced built‑in helpers for three related ASCII graph formats:

graph6
- Undirected simple graph encoding using a variable-length size field and
6-bit packing of the upper triangular adjacency matrix.

digraph6
- Directed extension of graph6 (string begins with '&'), encoding the full
n × n adjacency (row‑major) with the same 6‑bit packing.

sparse6
- Space‑efficient encoding for very sparse undirected graphs using runs of
variable-length integers instead of a dense upper triangle.

Unified size codec
- A shared GraphNumberCodec enforces canonical size (N(n)) encoding across
graph6, digraph6, and sparse6, preventing divergence in edge cases.
Exposed indirectly via parse_graph6_size() for testing and tooling.

Error handling
- Added/used Python exceptions: Graph6ParseError, Graph6OverflowError,
Graph6PanicError (panic guard for sparse6 parser).
- Non‑canonical encodings, invalid characters, and oversize graphs fail
fast with deterministic error types instead of panics.

Gzip support
- graph_write_graph6 / digraph_write_graph6 transparently gzip output
when the destination filename ends with ".gz".

Testing & validation
- Round‑trip tests for undirected, directed, and sparse variants.
- Boundary tests for size field forms (short, medium, long) and overflow.
- Sparse6 round‑trip on small disconnected components.

Developer notes
- Only simple (0/1) adjacency is serialized; parallel edges and weights
are collapsed.
- parse_graph6_size() enforces minimal encoding; use offset=1 for
directed strings that start with '&'.

issues:
- |
Original feature request / discussion: https://github.com/Qiskit/rustworkx/issues/1496
Implemented in PR: https://github.com/Qiskit/rustworkx/pull/1500
upgrade:
- |
Parsing now rejects non‑canonical size encodings and any n >= 2^36 with
explicit typed errors. If prior ad‑hoc tooling accepted such inputs, they
may now raise Graph6ParseError or Graph6OverflowError.
5 changes: 5 additions & 0 deletions rustworkx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from .rustworkx import *


# flake8: noqa
import rustworkx.visit

Expand Down Expand Up @@ -2311,3 +2312,7 @@ def write_graphml(graph, path, /, keys=None, compression=None):
:raises RuntimeError: when an error is encountered while writing the GraphML file.
"""
raise TypeError(f"Invalid Input Type {type(graph)} for graph")

@_rustworkx_dispatch
def write_graph6(graph, path):
raise TypeError(f"Invalid Input Type {type(graph)} for graph")
134 changes: 134 additions & 0 deletions src/digraph6.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
use crate::graph6::{utils, GraphConversion, IOError};
use crate::{get_edge_iter_with_weights, StablePyGraph};
use petgraph::algo;
use petgraph::graph::NodeIndex;
use pyo3::prelude::*;
use pyo3::types::PyAny;

/// Directed graph implementation (extracted from graph6.rs)
#[derive(Debug)]
pub struct DiGraph6 {
pub bit_vec: Vec<usize>,
pub n: usize,
}
impl DiGraph6 {
/// 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
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.is_empty() {
return Err(IOError::InvalidDigraphHeader);
}
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 DiGraph6 {
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 digraph6_to_pydigraph<'py>(py: Python<'py>, g: &DiGraph6) -> 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))]
/// Encode a directed `PyDiGraph` into the graph6 digraph extension form.
/// The returned string starts with '&' followed by the size field and data.
/// Multi edges are collapsed; only 0/1 adjacency is represented. Fails if
/// n >= 2^36 or encoding would overflow.
pub fn digraph_write_graph6_to_str<'py>(
py: Python<'py>,
pydigraph: Py<crate::digraph::PyDiGraph>,
) -> PyResult<String> {
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::to_file(bit_vec, n, true)?;
Ok(graph6)
}

#[pyfunction]
#[pyo3(signature=(digraph, path))]
/// Write a `PyDiGraph` to a file in digraph6 (graph6 with '&' prefix) format.
/// Supports gzip when the filename ends with `.gz`. Overwrites existing file.
/// Returns IOError for filesystem failures.
pub fn digraph_write_graph6(
py: Python<'_>,
digraph: Py<crate::digraph::PyDiGraph>,
path: &str,
) -> PyResult<()> {
let s = digraph_write_graph6_to_str(py, digraph)?;
crate::graph6::to_file(path, &s)
.map_err(|e| pyo3::exceptions::PyIOError::new_err(format!("IO error: {}", e)))?;
Ok(())
}

impl crate::graph6::write::WriteGraph for DiGraph6 {}
Loading