Skip to content

Commit

Permalink
Add edge canonization
Browse files Browse the repository at this point in the history
- Allow setting new data for edges and nodes
- Only add a self-edge once to the vertex's edge list
  • Loading branch information
benruijl committed Sep 11, 2024
1 parent 558078f commit 30ce931
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 2 deletions.
69 changes: 69 additions & 0 deletions src/api/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9963,6 +9963,8 @@ impl PythonNumericalIntegrator {
}

/// A graph that supported directional edges, parallel edges, self-edges and custom data on the nodes and edges.
///
/// Warning: modifying the graph if it is contained in a `dict` or `set` will invalidate the hash.
#[pyclass(name = "Graph", module = "symbolica")]
#[derive(Clone, PartialEq, Eq, Hash)]
struct PythonGraph {
Expand Down Expand Up @@ -10093,6 +10095,68 @@ impl PythonGraph {
.map_err(|e| exceptions::PyValueError::new_err(e))
}

/// Set the data of the node at index `index`, returning the old data.
pub fn set_node_data(
&mut self,
index: isize,
data: PythonExpression,
) -> PyResult<PythonExpression> {
if index.unsigned_abs() < self.graph.nodes().len() {
let n = if index < 0 {
self.graph.nodes().len() - index.abs() as usize
} else {
index as usize
};
Ok(self.graph.set_node_data(n, data.expr).into())
} else {
Err(PyIndexError::new_err(format!(
"Index {} out of bounds: the graph only has {} nodes.",
index,
self.graph.nodes().len(),
)))
}
}

/// Set the data of the edge at index `index`, returning the old data.
pub fn set_edge_data(
&mut self,
index: isize,
data: PythonExpression,
) -> PyResult<PythonExpression> {
if index.unsigned_abs() < self.graph.edges().len() {
let e = if index < 0 {
self.graph.edges().len() - index.abs() as usize
} else {
index as usize
};
Ok(self.graph.set_edge_data(e, data.expr).into())
} else {
Err(PyIndexError::new_err(format!(
"Index {} out of bounds: the graph only has {} edges.",
index,
self.graph.edges().len(),
)))
}
}

/// Set the directed status of the edge at index `index`, returning the old value.
pub fn set_directed(&mut self, index: isize, directed: bool) -> PyResult<bool> {
if index.unsigned_abs() < self.graph.edges().len() {
let e = if index < 0 {
self.graph.edges().len() - index.abs() as usize
} else {
index as usize
};
Ok(self.graph.set_directed(e, directed).into())
} else {
Err(PyIndexError::new_err(format!(
"Index {} out of bounds: the graph only has {} edges.",
index,
self.graph.edges().len(),
)))
}
}

/// Get the `idx`th node.
fn __getitem__(&self, idx: isize) -> PyResult<(Vec<usize>, PythonExpression)> {
self.node(idx)
Expand Down Expand Up @@ -10168,6 +10232,11 @@ impl PythonGraph {
)
}

/// Sort and relabel the edges of the graph, keeping the vertices fixed.
pub fn canonize_edges(&mut self) {
self.graph.canonize_edges();
}

/// Return true `iff` the graph is isomorphic to `other`.
fn is_isomorphic(&self, other: &PythonGraph) -> bool {
self.graph.is_isomorphic(&other.graph)
Expand Down
53 changes: 52 additions & 1 deletion src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,10 +277,28 @@ impl<N, E> Graph<N, E> {
data,
});
self.nodes[source].edges.push(index);
self.nodes[target].edges.push(index);

if source != target {
self.nodes[target].edges.push(index);
}
Ok(index)
}

/// Set the data of the node at index `index`, returning the old data.
pub fn set_node_data(&mut self, index: usize, data: N) -> N {
std::mem::replace(&mut self.nodes[index].data, data)
}

/// Set the data of the edge at index `index`, returning the old data.
pub fn set_edge_data(&mut self, index: usize, data: E) -> E {
std::mem::replace(&mut self.edges[index].data, data)
}

/// Set the directed status of the edge at index `index`, returning the old value.
pub fn set_directed(&mut self, index: usize, directed: bool) -> bool {
std::mem::replace(&mut self.edges[index].directed, directed)
}

/// Delete the last added edge. This operation is O(1).
pub fn delete_last_edge(&mut self) -> Option<Edge<E>> {
if let Some(edge) = self.edges.pop() {
Expand Down Expand Up @@ -427,6 +445,22 @@ impl<N, E: Eq + Ord + Hash> Graph<N, E> {

count
}

/// Sort and relabel the edges of the graph, keeping the vertices fixed.
pub fn canonize_edges(&mut self) {
for n in &mut self.nodes {
n.edges.clear();
}

self.edges.sort();

for (i, e) in self.edges.iter().enumerate() {
self.nodes[e.vertices.0].edges.push(i);
if e.vertices.0 != e.vertices.1 {
self.nodes[e.vertices.1].edges.push(i);
}
}
}
}

struct GenerationSettings<'a, E> {
Expand Down Expand Up @@ -1387,6 +1421,23 @@ mod test {
assert_eq!(c.graph.edge(0).vertices, (0, 2));
}

#[test]
fn canonize_edges() {
let mut g = Graph::new();
let n0 = g.add_node(0);
let n1 = g.add_node(1);
let n2 = g.add_node(2);

g.add_edge(n2, n1, true, 0).unwrap();
g.add_edge(n0, n0, false, 0).unwrap();
g.add_edge(n0, n1, true, 0).unwrap();
g.add_edge(n1, n0, false, 2).unwrap();

g.canonize_edges();

assert_eq!(g.node(0).edges, [0, 1, 2]);
}

#[test]
fn generate() {
let gs = Graph::<_, &str>::generate(
Expand Down
17 changes: 16 additions & 1 deletion symbolica.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -3319,7 +3319,10 @@ class RandomNumberGenerator:


class Graph:
"""A graph that supported directional edges, parallel edges, self-edges and expression data on the nodes and edges."""
"""A graph that supported directional edges, parallel edges, self-edges and expression data on the nodes and edges.
Warning: modifying the graph if it is contained in a `dict` or `set` will invalidate the hash.
"""

def __new__(_cls):
"""Create a new empty graph."""
Expand Down Expand Up @@ -3420,8 +3423,20 @@ class Graph:
Optionally, the edge can be set as directed. The default data is the number 0.
"""

def set_node_data(self, index: int, data: Expression | int) -> Expression:
"""Set the data of the node at index `index`, returning the old data."""

def set_edge_data(self, index: int, data: Expression | int) -> Expression:
"""Set the data of the edge at index `index`, returning the old data."""

def set_directed(self, index: int, directed: bool) -> bool:
"""Set the directed status of the edge at index `index`, returning the old value."""

def canonize(self) -> Tuple[Graph, Sequence[int], Expression, Sequence[int]]:
"""Write the graph in a canonical form. Returns the canonicalized graph, the vertex map, the automorphism group size, and the orbit."""

def canonize_edges(self) -> None:
"""Sort and relabel the edges of the graph, keeping the vertices fixed."""

def is_isomorphic(self, other: Graph) -> bool:
"""Check if the graph is isomorphic to another graph."""

0 comments on commit 30ce931

Please sign in to comment.