Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix the behavior for immutable graphs in methods related to isomorphisms in sage/graphs/generic_graph.py #39296

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 24 additions & 3 deletions src/sage/graphs/bipartite_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -2549,7 +2549,8 @@ def _subgraph_by_deleting(self, vertices=None, edges=None, inplace=False,
return B

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 @@ -2590,6 +2591,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::

sage: B = BipartiteGraph( [(0, 4), (0, 5), (0, 6), (0, 8), (1, 5),
Expand Down Expand Up @@ -2648,6 +2653,19 @@ class by some canonization function `c`. If `G` and `H` are graphs,
sage: B.canonical_label()
Bipartite multi-graph on 4 vertices

Check the behavior for immutable graphs::

sage: G = BipartiteGraph(graphs.CycleGraph(4))
sage: G.canonical_label().is_immutable()
False
sage: G.canonical_label(immutable=True).is_immutable()
True
sage: G = BipartiteGraph(graphs.CycleGraph(4), immutable=True)
sage: G.canonical_label().is_immutable()
True
sage: G.canonical_label(immutable=False).is_immutable()
False

.. SEEALSO::

:meth:`~sage.graphs.generic_graph.GenericGraph.canonical_label()`
Expand All @@ -2657,7 +2675,8 @@ class by some canonization function `c`. If `G` and `H` are graphs,
certificate=certificate,
edge_labels=edge_labels,
algorithm=algorithm,
return_graph=return_graph)
return_graph=return_graph,
immutable=immutable)

else:
from sage.groups.perm_gps.partn_ref.refinement_graphs import search_tree
Expand Down Expand Up @@ -2696,7 +2715,9 @@ class by some canonization function `c`. If `G` and `H` are graphs,
a, b, c = search_tree(GC, partition, certificate=True, dig=False)
cert = {v: c[G_to[v]] for v in G_to}

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

C.left = {cert[v] for v in self.left}
C.right = {cert[v] for v in self.right}
Expand Down
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 @@ -23766,10 +23766,9 @@
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 @@ -24017,6 +24016,18 @@
....: 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 @@ -24067,7 +24078,7 @@
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 @@ -24525,6 +24536,15 @@
(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 @@ -24618,7 +24638,8 @@
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 @@ -24659,6 +24680,10 @@
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 @@ -24745,6 +24770,23 @@
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 @@ -24822,11 +24864,13 @@

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 24872 in src/sage/graphs/generic_graph.py

View check run for this annotation

Codecov / codecov/patch

src/sage/graphs/generic_graph.py#L24872

Added line #L24872 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 @@ -24838,7 +24882,8 @@
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 @@ -24850,7 +24895,6 @@
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 @@ -24863,9 +24907,12 @@
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 @@ -24972,6 +25019,14 @@
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 @@ -25696,7 +25751,8 @@

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 @@ -25747,6 +25803,9 @@
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 @@ -25755,6 +25814,12 @@
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 @@ -25844,7 +25909,47 @@
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 @@ -25892,7 +25997,7 @@
if inplace:
g._backend = G._backend
elif not inplace:
G = copy(g)
G = g.copy(immutable=False)
else:
G = g

Expand Down Expand Up @@ -25992,6 +26097,8 @@

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
Loading