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

Equal hashes for non-isomorphic bipartite graphs with edge labels #35146

Merged
merged 14 commits into from
Mar 26, 2023
58 changes: 57 additions & 1 deletion src/sage/graphs/bipartite_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
from .graph import Graph
from sage.rings.integer import Integer
from sage.misc.decorators import rename_keyword
from sage.misc.cachefunc import cached_method


class BipartiteGraph(Graph):
Expand Down Expand Up @@ -95,6 +96,11 @@ class BipartiteGraph(Graph):
- ``weighted`` -- boolean (default: ``None``); whether graph thinks of
itself as weighted or not. See ``self.weighted()``

- ``hash_labels`` -- boolean (default: ``None``); whether to include edge
labels during hashing. This parameter defaults to ``True`` if the graph is
weighted. This parameter is ignored if the graph is mutable.
Beware that trying to hash unhashable labels will raise an error.

.. NOTE::

All remaining arguments are passed to the ``Graph`` constructor
Expand Down Expand Up @@ -346,7 +352,7 @@ class BipartiteGraph(Graph):

"""

def __init__(self, data=None, partition=None, check=True, *args, **kwds):
def __init__(self, data=None, partition=None, check=True, hash_labels=None, *args, **kwds):
"""
Create a bipartite graph.

Expand Down Expand Up @@ -396,6 +402,7 @@ def __init__(self, data=None, partition=None, check=True, *args, **kwds):
Graph.__init__(self, **kwds)
self.left = set()
self.right = set()
self._hash_labels = hash_labels
return

# need to turn off partition checking for Graph.__init__() adding
Expand Down Expand Up @@ -543,8 +550,57 @@ def __init__(self, data=None, partition=None, check=True, *args, **kwds):
if alist_file:
self.load_afile(data)

if hash_labels is None and hasattr(data, '_hash_labels'):
hash_labels = data._hash_labels
self._hash_labels = hash_labels

return

@cached_method
def __hash__(self):
"""
Compute a hash for ``self``, if ``self`` is immutable.

EXAMPLES::

sage: A = BipartiteGraph([(1, 2, 1)], immutable=True)
sage: B = BipartiteGraph([(1, 2, 33)], immutable=True)
sage: A.__hash__() == B.__hash__()
True
sage: A = BipartiteGraph([(1, 2, 1)], immutable=True, hash_labels=True)
sage: B = BipartiteGraph([(1, 2, 33)], immutable=True, hash_labels=True)
sage: A.__hash__() == B.__hash__()
False
sage: A = BipartiteGraph([(1, 2, 1)], immutable=True, weighted=True)
sage: B = BipartiteGraph([(1, 2, 33)], immutable=True, weighted=True)
sage: A.__hash__() == B.__hash__()
False

TESTS::

sage: A = BipartiteGraph([(1, 2, 1)], immutable=False)
sage: A.__hash__()
Traceback (most recent call last):
...
TypeError: This graph is mutable, and thus not hashable. Create an immutable copy by `g.copy(immutable=True)`
sage: B = BipartiteGraph([(1, 2, {'length': 3})], immutable=True, hash_labels=True)
sage: B.__hash__()
Traceback (most recent call last):
...
TypeError: unhashable type: 'dict'
"""
if self.is_immutable():
# Determine whether to hash edge labels
use_labels = self._use_labels_for_hash()
edge_items = self.edge_iterator(labels=use_labels)
if self.allows_multiple_edges():
from collections import Counter
edge_items = Counter(edge_items).items()
return hash((frozenset(self.left), frozenset(self.right), frozenset(edge_items)))

raise TypeError("This graph is mutable, and thus not hashable. "
"Create an immutable copy by `g.copy(immutable=True)`")

def _upgrade_from_graph(self):
"""
Set the left and right sets of vertices from the input graph.
Expand Down
12 changes: 11 additions & 1 deletion src/sage/graphs/digraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,11 @@ class DiGraph(GenericGraph):
immutable digraph. Note that ``immutable=True`` is actually a shortcut for
``data_structure='static_sparse'``.

- ``hash_labels`` -- boolean (default: ``None``); whether to include edge
labels during hashing. This parameter defaults to ``True`` if the digraph
is weighted. This parameter is ignored if the digraph is mutable.
Beware that trying to hash unhashable labels will raise an error.

- ``vertex_labels`` -- boolean (default: ``True``); whether to allow any
object as a vertex (slower), or only the integers `0,...,n-1`, where `n`
is the number of vertices.
Expand Down Expand Up @@ -517,7 +522,7 @@ def __init__(self, data=None, pos=None, loops=None, format=None,
weighted=None, data_structure="sparse",
vertex_labels=True, name=None,
multiedges=None, convert_empty_dict_labels_to_None=None,
sparse=True, immutable=False):
sparse=True, immutable=False, hash_labels=None):
"""
TESTS::

Expand Down Expand Up @@ -842,6 +847,10 @@ def __init__(self, data=None, pos=None, loops=None, format=None,
# weighted, multiedges, loops, verts and num_verts should now be set
self._weighted = weighted

if hash_labels is None and hasattr(data, '_hash_labels'):
hash_labels = data._hash_labels
self._hash_labels = hash_labels

self._pos = copy(pos)

if format != 'DiGraph' or name is not None:
Expand All @@ -856,6 +865,7 @@ def __init__(self, data=None, pos=None, loops=None, format=None,
self._immutable = True

# Formats

def dig6_string(self):
r"""
Return the ``dig6`` representation of the digraph as an ASCII string.
Expand Down
106 changes: 102 additions & 4 deletions src/sage/graphs/generic_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,40 @@ def __eq__(self, other):

return self._backend.is_subgraph(other._backend, self, ignore_labels=not self.weighted())


def _use_labels_for_hash(self):
r"""
Helper method for method ``__hash__``.

This method checks whether parameter ``hash_labels`` has been specified
by the user. Otherwise, defaults to the value of parameter ``weigthed``.

TESTS::

sage: G = Graph()
sage: G._use_labels_for_hash()
False
sage: G = Graph(hash_labels=True)
sage: G._use_labels_for_hash()
True
sage: G = Graph(hash_labels=False)
sage: G._use_labels_for_hash()
False
sage: G = Graph(weighted=True)
sage: G._use_labels_for_hash()
True
sage: G = Graph(weighted=False)
sage: G._use_labels_for_hash()
False
sage: G = Graph(hash_labels=False, weighted=True)
sage: G._use_labels_for_hash()
False
"""
if not hasattr(self, "_hash_labels") or self._hash_labels is None:
self._hash_labels = self.weighted()
return self._hash_labels


@cached_method
def __hash__(self):
"""
Expand Down Expand Up @@ -685,9 +719,29 @@ def __hash__(self):
sage: G1.__hash__() == G2.__hash__()
True

Make sure ``hash_labels`` parameter behaves as expected
(:trac:`33255`)::

sage: A = Graph([(1, 2, 1)], immutable=True)
sage: B = Graph([(1, 2, 33)], immutable=True)
sage: A.__hash__() == B.__hash__()
True
sage: A = Graph([(1, 2, 1)], immutable=True, hash_labels=True)
sage: B = Graph([(1, 2, 33)], immutable=True, hash_labels=True)
sage: A.__hash__() == B.__hash__()
False
sage: A = Graph([(1, 2, 1)], immutable=True, weighted=True)
sage: B = Graph([(1, 2, 33)], immutable=True, weighted=True)
sage: A.__hash__() == B.__hash__()
False
sage: A = Graph([(1, 2, 1)], immutable=True, hash_labels=False, weighted=True)
sage: B = Graph([(1, 2, 33)], immutable=True, hash_labels=False, weighted=True)
sage: A.__hash__() == B.__hash__()
True
"""
if self.is_immutable():
edge_items = self.edge_iterator(labels=self._weighted)
use_labels = self._use_labels_for_hash()
edge_items = self.edge_iterator(labels=use_labels)
if self.allows_multiple_edges():
from collections import Counter
edge_items = Counter(edge_items).items()
Expand Down Expand Up @@ -955,7 +1009,7 @@ def is_immutable(self):

# Formats

def copy(self, weighted=None, data_structure=None, sparse=None, immutable=None):
def copy(self, weighted=None, data_structure=None, sparse=None, immutable=None, hash_labels=None):
"""
Change the graph implementation

Expand Down Expand Up @@ -985,6 +1039,12 @@ def copy(self, weighted=None, data_structure=None, sparse=None, immutable=None):
used to copy an immutable graph, the data structure used is
``"sparse"`` unless anything else is specified.

- ``hash_labels`` -- boolean (default: ``None``); whether to include
edge labels during hashing of the copy. This parameter defaults to
``True`` if the graph is weighted. This parameter is ignored when
parameter ``immutable`` is not ``True``.
Beware that trying to hash unhashable labels will raise an error.

.. NOTE::

If the graph uses
Expand Down Expand Up @@ -1141,6 +1201,43 @@ def copy(self, weighted=None, data_structure=None, sparse=None, immutable=None):
sage: G._immutable = True
sage: G.copy()._backend
<sage.graphs.base.sparse_graph.SparseGraphBackend object at ...>

Copying and changing ``hash_labels`` parameter::

sage: G = Graph({0: {1: 'edge label A'}}, immutable=True, hash_labels=False)
sage: hash(G.copy(hash_labels=True, immutable=True)) == hash(G)
False
sage: hash(G.copy(hash_labels=False, immutable=True)) == hash(G)
True
sage: hash(G.copy(hash_labels=None, immutable=True)) == hash(G)
True
sage: G = Graph({0: {1: 'edge label A'}}, immutable=True, hash_labels=True)
sage: hash(G.copy(hash_labels=True, immutable=True)) == hash(G)
True
sage: hash(G.copy(hash_labels=False, immutable=True)) == hash(G)
False
sage: hash(G.copy(hash_labels=None, immutable=True)) == hash(G)
True
sage: G1 = Graph({0: {1: 'edge label A'}}, immutable=True, hash_labels=False)
sage: G2 = Graph({0: {1: 'edge label B'}}, immutable=True, hash_labels=False)
sage: hash(G1) == hash(G2)
True
sage: G1c = G1.copy(hash_labels=True, immutable=True)
sage: G2c = G2.copy(hash_labels=True, immutable=True)
sage: hash(G1c) == hash(G2c)
False
sage: G = Graph({0: {1: 'edge label A'}}, immutable=True, hash_labels=False)
sage: H = G.copy(hash_labels=True)
sage: H.is_immutable()
False
sage: H._hash_labels
True
sage: I = H.copy(immutable=True)
sage: hash(G) == hash(I)
False
sage: G = Graph({0: {1: 'edge label A'}}, immutable=True, hash_labels=True)
sage: hash(G) == hash(I)
True
"""
# Which data structure should be used ?
if data_structure is not None:
Expand Down Expand Up @@ -1169,7 +1266,8 @@ def copy(self, weighted=None, data_structure=None, sparse=None, immutable=None):
# Immutable copy of an immutable graph ? return self !
# (if okay for weightedness)
if (self.is_immutable() and
(weighted is None or self._weighted == weighted)):
(weighted is None or self._weighted == weighted) and
(hash_labels is None or self._hash_labels == hash_labels)):
from sage.graphs.base.static_sparse_backend import StaticSparseBackend
if (isinstance(self._backend, StaticSparseBackend) and
(data_structure == 'static_sparse' or data_structure is None)):
Expand All @@ -1183,7 +1281,7 @@ def copy(self, weighted=None, data_structure=None, sparse=None, immutable=None):
data_structure = "sparse"

G = self.__class__(self, name=self.name(), pos=copy(self._pos),
weighted=weighted,
weighted=weighted, hash_labels=hash_labels,
data_structure=data_structure)

attributes_to_copy = ('_assoc', '_embedding')
Expand Down
11 changes: 10 additions & 1 deletion src/sage/graphs/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,11 @@ class Graph(GenericGraph):
immutable graph. Note that ``immutable=True`` is actually a shortcut for
``data_structure='static_sparse'``. Set to ``False`` by default.

- ``hash_labels`` -- boolean (default: ``None``); whether to include edge
labels during hashing. This parameter defaults to ``True`` if the graph is
weighted. This parameter is ignored if the graph is mutable.
Beware that trying to hash unhashable labels will raise an error.

- ``vertex_labels`` -- boolean (default: ``True``); whether to allow any
object as a vertex (slower), or only the integers `0,...,n-1`, where `n`
is the number of vertices.
Expand Down Expand Up @@ -910,7 +915,7 @@ def __init__(self, data=None, pos=None, loops=None, format=None,
weighted=None, data_structure="sparse",
vertex_labels=True, name=None,
multiedges=None, convert_empty_dict_labels_to_None=None,
sparse=True, immutable=False):
sparse=True, immutable=False, hash_labels=None):
"""
TESTS::

Expand Down Expand Up @@ -1253,6 +1258,10 @@ def __init__(self, data=None, pos=None, loops=None, format=None,
weighted = False
self._weighted = getattr(self, '_weighted', weighted)

if hash_labels is None and hasattr(data, '_hash_labels'):
hash_labels = data._hash_labels
self._hash_labels = hash_labels

self._pos = copy(pos)

if format != 'Graph' or name is not None:
Expand Down