Skip to content

Commit

Permalink
fix the behavior for immutable graphs in methods related to isomorphisms
Browse files Browse the repository at this point in the history
  • Loading branch information
dcoudert committed Jan 7, 2025
1 parent 1be0a58 commit 58109a4
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 21 deletions.
36 changes: 30 additions & 6 deletions src/sage/graphs/bliss.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,9 @@ cdef canonical_form_from_edge_list(int Vnr, list Vout, list Vin, int Lnr=1, list
return new_edges


cpdef canonical_form(G, partition=None, return_graph=False, use_edge_labels=True, certificate=False) noexcept:
cpdef canonical_form(G, partition=None, return_graph=False,
use_edge_labels=True, certificate=False,
immutable=None) noexcept:
r"""
Return a canonical label for the given (di)graph.
Expand All @@ -404,6 +406,12 @@ cpdef canonical_form(G, partition=None, return_graph=False, use_edge_labels=True
- ``certificate`` -- boolean (default: ``False``); when set to ``True``,
returns the labeling of G into a canonical graph
- ``immutable`` -- boolean (default: ``None``); whether to create a
mutable/immutable (di)graph. ``immutable=None`` (default) means that
the (di)graph and its canonical (di)graph will behave the same way.
This parameter is ignored when ``return_graph`` is ``False``.
TESTS::
sage: from sage.graphs.bliss import canonical_form # optional - bliss
Expand Down Expand Up @@ -503,6 +511,19 @@ cpdef canonical_form(G, partition=None, return_graph=False, use_edge_labels=True
Traceback (most recent call last):
...
ValueError: some vertices of the graph are not in the partition
Check the behavior of parameter ``immutable``::
sage: g = Graph({1: {2: 'a'}})
sage: canonical_form(g, return_graph=True).is_immutable() # optional - bliss
False
sage: canonical_form(g, return_graph=True, immutable=True).is_immutable() # optional - bliss
True
sage: g = Graph({1: {2: 'a'}}, immutable=True)
sage: canonical_form(g, return_graph=True).is_immutable() # optional - bliss
True
sage: canonical_form(g, return_graph=True, immutable=False).is_immutable() # optional - bliss
False
"""
# We need this to convert the numbers from <unsigned int> to <long>.
# This assertion should be true simply for memory reasons.
Expand Down Expand Up @@ -579,14 +600,17 @@ cpdef canonical_form(G, partition=None, return_graph=False, use_edge_labels=True
relabel = {int2vert[i]: j for i, j in relabel.items()}

if return_graph:
if immutable is None:
immutable = G.is_immutable()
if directed:
from sage.graphs.digraph import DiGraph
H = DiGraph(new_edges, loops=G.allows_loops(), multiedges=G.allows_multiple_edges())
from sage.graphs.digraph import DiGraph as GT
else:
from sage.graphs.graph import Graph
H = Graph(new_edges, loops=G.allows_loops(), multiedges=G.allows_multiple_edges())
from sage.graphs.graph import Graph as GT

H = GT([range(G.order()), new_edges], format='vertices_and_edges',
loops=G.allows_loops(), multiedges=G.allows_multiple_edges(),
immutable=immutable)

H.add_vertices(range(G.order()))
return (H, relabel) if certificate else H

# Warning: this may break badly in Python 3 if the graph is not simple
Expand Down
137 changes: 122 additions & 15 deletions src/sage/graphs/generic_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -23711,10 +23711,9 @@ def coarsest_equitable_refinement(self, partition, sparse=True):
raise TypeError("partition (%s) is not valid for this graph: there is a cell of length 0" % partition)
if self.has_multiple_edges():
raise TypeError("refinement function does not support multiple edges")
G = copy(self)
perm_from = list(G)
perm_from = list(self)
perm_to = {v: i for i, v in enumerate(perm_from)}
G.relabel(perm=perm_to)
G = self.relabel(perm=perm_to, inplace=False, immutable=True)
partition = [[perm_to[b] for b in cell] for cell in partition]
n = G.order()
if sparse:
Expand Down Expand Up @@ -23962,6 +23961,18 @@ def automorphism_group(self, partition=None, verbosity=0,
....: partition=[V])
sage: str(a2) == str(b2) # optional - bliss
True

Check the behavior with immutable graphs::

sage: G = Graph(graphs.PetersenGraph(), immutable=True)
sage: G.automorphism_group(return_group=False, orbits=True, algorithm='sage')
[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]]
sage: G = graphs.PetersenGraph()
sage: G.allow_multiple_edges(True)
sage: G.add_edges(G.edges())
sage: G = Graph(G, immutable=True)
sage: G.automorphism_group(return_group=False, orbits=True, algorithm='sage')
[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]]
"""
from sage.features.bliss import Bliss
have_bliss = Bliss().is_present()
Expand Down Expand Up @@ -24012,7 +24023,7 @@ def automorphism_group(self, partition=None, verbosity=0,
partition = [list(self)]

if edge_labels or self.has_multiple_edges():
ret = graph_isom_equivalent_non_edge_labeled_graph(self, partition,
ret = graph_isom_equivalent_non_edge_labeled_graph(self, partition=partition,
return_relabeling=True,
ignore_edge_labels=(not edge_labels))
G, partition, relabeling = ret
Expand Down Expand Up @@ -24470,6 +24481,15 @@ def is_isomorphic(self, other, certificate=False, verbosity=0, edge_labels=False
(True, {6: 'x', 7: 'y'})
sage: A.is_isomorphic(B, certificate=True, edge_labels=True)
(False, None)

Check the behavior with immutable graphs::

sage: A = DiGraph([(6,7,'a'), (6,7,'b')], multiedges=True, immutable=True)
sage: B = DiGraph([('x','y','u'), ('x','y','v')], multiedges=True)
sage: A.is_isomorphic(B, certificate=True)
(True, {6: 'x', 7: 'y'})
sage: B.is_isomorphic(A, certificate=True)
(True, {'x': 6, 'y': 7})
"""
if not self.order() and not other.order():
return (True, None) if certificate else True
Expand Down Expand Up @@ -24563,7 +24583,8 @@ def multilabel(e):
return True, isom_trans

def canonical_label(self, partition=None, certificate=False,
edge_labels=False, algorithm=None, return_graph=True):
edge_labels=False, algorithm=None, return_graph=True,
immutable=None):
r"""
Return the canonical graph.

Expand Down Expand Up @@ -24604,6 +24625,10 @@ class by some canonization function `c`. If `G` and `H` are graphs,
instead of the canonical graph; only available when ``'bliss'``
is explicitly set as algorithm.

- ``immutable`` -- boolean (default: ``None``); whether to create a
mutable/immutable (di)graph. ``immutable=None`` (default) means that
the (di)graph and its canonical (di)graph will behave the same way.

EXAMPLES:

Canonization changes isomorphism to equality::
Expand Down Expand Up @@ -24690,6 +24715,23 @@ class by some canonization function `c`. If `G` and `H` are graphs,
Graph on 3 vertices
sage: C.vertices(sort=True)
[0, 1, 2]
sage: G.canonical_label(algorithm='bliss').is_immutable() # optional - bliss
True
sage: G.canonical_label(algorithm='sage').is_immutable()
True
sage: G.canonical_label(algorithm='bliss', immutable=False).is_immutable() # optional - bliss
False
sage: G.canonical_label(algorithm='sage', immutable=False).is_immutable()
False
sage: G = Graph([[1, 2], [2, 3]])
sage: G.canonical_label(algorithm='bliss').is_immutable() # optional - bliss
False
sage: G.canonical_label(algorithm='sage').is_immutable()
False
sage: G.canonical_label(algorithm='bliss', immutable=True).is_immutable() # optional - bliss
True
sage: G.canonical_label(algorithm='sage', immutable=True).is_immutable()
True

Corner cases::

Expand Down Expand Up @@ -24767,11 +24809,13 @@ class by some canonization function `c`. If `G` and `H` are graphs,

if algorithm == 'bliss':
if return_graph:
vert_dict = canonical_form(self, partition, False, edge_labels, True)[1]
vert_dict = canonical_form(self, partition=partition, return_graph=False,
use_edge_labels=edge_labels, certificate=True)[1]
if not certificate:
return self.relabel(vert_dict, inplace=False)
return (self.relabel(vert_dict, inplace=False), vert_dict)
return canonical_form(self, partition, return_graph, edge_labels, certificate)
return self.relabel(vert_dict, inplace=False, immutable=immutable)
return (self.relabel(vert_dict, inplace=False, immutable=immutable), vert_dict)
return canonical_form(self, partition=partition, return_graph=False,

Check warning on line 24817 in src/sage/graphs/generic_graph.py

View check run for this annotation

Codecov / codecov/patch

src/sage/graphs/generic_graph.py#L24817

Added line #L24817 was not covered by tests
use_edge_labels=edge_labels, certificate=certificate)

# algorithm == 'sage':
from sage.groups.perm_gps.partn_ref.refinement_graphs import search_tree
Expand All @@ -24783,7 +24827,8 @@ class by some canonization function `c`. If `G` and `H` are graphs,
if partition is None:
partition = [list(self)]
if edge_labels or self.has_multiple_edges():
G, partition, relabeling = graph_isom_equivalent_non_edge_labeled_graph(self, partition, return_relabeling=True)
G, partition, relabeling = graph_isom_equivalent_non_edge_labeled_graph(self, partition=partition,
return_relabeling=True)
G_vertices = list(chain(*partition))
G_to = {u: i for i, u in enumerate(G_vertices)}
DoDG = DiGraph if self._directed else Graph
Expand All @@ -24795,7 +24840,6 @@ class by some canonization function `c`. If `G` and `H` are graphs,
partition = [[G_to[vv] for vv in cell] for cell in partition]
a, b, c = search_tree(GC, partition, certificate=True, dig=dig)
# c is a permutation to the canonical label of G, which depends only on isomorphism class of self.
H = copy(self)
c_new = {v: c[G_to[relabeling[v]]] for v in self}
else:
G_vertices = list(chain(*partition))
Expand All @@ -24808,9 +24852,12 @@ class by some canonization function `c`. If `G` and `H` are graphs,
GC = HB.c_graph()[0]
partition = [[G_to[vv] for vv in cell] for cell in partition]
a, b, c = search_tree(GC, partition, certificate=True, dig=dig)
H = copy(self)
c_new = {v: c[G_to[v]] for v in G_to}
H.relabel(c_new)

if immutable is None:
immutable = self.is_immutable()
H = self.relabel(perm=c_new, inplace=False, immutable=immutable)

if certificate:
return H, c_new
return H
Expand Down Expand Up @@ -24917,6 +24964,14 @@ def is_cayley(self, return_group=False, mapping=False,
sage: graphs.CompleteBipartiteGraph(50, 50).is_cayley() # needs sage.groups
True

Check the behavior with immutable graphs::

sage: C7 = groups.permutation.Cyclic(7) # needs sage.groups
sage: S = [(1,2,3,4,5,6,7), (1,3,5,7,2,4,6), (1,5,2,6,3,7,4)]
sage: d = C7.cayley_graph(generators=S) # needs sage.groups
sage: d.copy(immutable=True).is_cayley() # needs sage.groups
True

TESTS::

sage: graphs.EmptyGraph().is_cayley()
Expand Down Expand Up @@ -25641,7 +25696,8 @@ def tachyon_vertex_plot(g, bgcolor=(1, 1, 1),

def graph_isom_equivalent_non_edge_labeled_graph(g, partition=None, standard_label=None,
return_relabeling=False, return_edge_labels=False,
inplace=False, ignore_edge_labels=False):
inplace=False, ignore_edge_labels=False,
immutable=None):
r"""
Helper function for canonical labeling of edge labeled (di)graphs.

Expand Down Expand Up @@ -25692,6 +25748,9 @@ def graph_isom_equivalent_non_edge_labeled_graph(g, partition=None, standard_lab
that attributes of ``g`` are *not* copied for speed issues, only
edges and vertices.

This parameter cannot be set to ``True`` if the input graph
``g`` is immutable.

- ``ignore_edge_labels`` -- boolean (default: ``False``); if
``True``, ignore edge labels, so when constructing the new
graph, only multiple edges are replaced with vertices. Labels on
Expand All @@ -25700,6 +25759,12 @@ def graph_isom_equivalent_non_edge_labeled_graph(g, partition=None, standard_lab
graph correspond to right vertices in the same partition in the
new graph.

- ``immutable`` -- boolean (default: ``None``); whether to create a
mutable/immutable (di)graph. ``immutable=None`` (default) means that the
(di)graph and the returned (di)graph will behave the same way.

This parameter is ignored when ``inplace`` is ``True``.

OUTPUT:

- if ``inplace`` is ``False``: the unlabeled graph without
Expand Down Expand Up @@ -25789,7 +25854,47 @@ def graph_isom_equivalent_non_edge_labeled_graph(g, partition=None, standard_lab
sage: g = graph_isom_equivalent_non_edge_labeled_graph(G)
sage: g[0].is_bipartite()
False

Check the behavior with immutable graphs::

sage: Him = Graph([(0, 1, 1), (1, 2), (2, 123), ('a', 123)], immutable=True)
sage: graph_isom_equivalent_non_edge_labeled_graph(Him, inplace=True)
Traceback (most recent call last):
...
ValueError: parameter 'inplace' cannot be True for immutable graphs
sage: g, _ = graph_isom_equivalent_non_edge_labeled_graph(Him)
sage: g.is_immutable()
True
sage: g, _ = graph_isom_equivalent_non_edge_labeled_graph(Him, immutable=False)
sage: g.is_immutable()
False
sage: H = Graph([(0, 1, 1), (1, 2), (2, 123), ('a', 123)], immutable=False)
sage: g, _ = graph_isom_equivalent_non_edge_labeled_graph(H, immutable=True)
sage: g.is_immutable()
True
sage: graph_isom_equivalent_non_edge_labeled_graph(H, inplace=True, immutable=True)
[[[0, 1, 2, 3, 4], [5]]]
sage: H.is_immutable()
False
sage: G = Graph(multiedges=True, sparse=True)
sage: G.add_edges((0, 1, i) for i in range(10))
sage: G.add_edge(1, 2, 'string')
sage: G.add_edge(2, 123)
sage: G.add_edge('a', 123)
sage: g = graph_isom_equivalent_non_edge_labeled_graph(G)
sage: g[0].is_immutable()
False
sage: Gim = G.copy(immutable=True)
sage: g = graph_isom_equivalent_non_edge_labeled_graph(Gim, standard_label='string',
....: return_edge_labels=True)
sage: g[0].is_immutable()
True
"""
if inplace and g.is_immutable():
raise ValueError("parameter 'inplace' cannot be True for immutable graphs")
if immutable is None:
immutable = g.is_immutable()

from sage.graphs.graph import Graph
from sage.graphs.digraph import DiGraph
from itertools import chain
Expand Down Expand Up @@ -25837,7 +25942,7 @@ def graph_isom_equivalent_non_edge_labeled_graph(g, partition=None, standard_lab
if inplace:
g._backend = G._backend
elif not inplace:
G = copy(g)
G = g.copy(immutable=False)
else:
G = g

Expand Down Expand Up @@ -25937,6 +26042,8 @@ def graph_isom_equivalent_non_edge_labeled_graph(g, partition=None, standard_lab

return_data = []
if not inplace:
if immutable:
G = G.copy(immutable=True)
return_data.append(G)
return_data.append(new_partition)
if return_relabeling:
Expand Down

0 comments on commit 58109a4

Please sign in to comment.