#17086: GenericGraph's documentation, __eq__, and __hash__ out of sync
----------------------------+----------------------------
   Reporter:  emassop       |            Owner:
       Type:  defect        |           Status:  new
   Priority:  major         |        Milestone:  sage-6.4
  Component:  graph theory  |         Keywords:
  Merged in:                |          Authors:
  Reviewers:                |  Report Upstream:  N/A
Work issues:                |           Branch:
     Commit:                |     Dependencies:
   Stopgaps:                |
----------------------------+----------------------------
 1. The documentation of `GenericGraph.__eq__` contains "For equality, ...,
 output the same vertex list (in order), ...". However, since
 
[http://git.sagemath.org/sage.git/commit/?id=b59285e6f9b91f25ee2c77bfe940a45c51b58f80
 b59285e6] (Trac #14806: Immutable graph backend), the order of the
 vertices is no longer checked. So either that check should be
 reintroduced, or the documentation should be updated.

    The list returned by `.vertices()` is sorted, so that checking the
 order only affects the result in rare cases. Hence I think updating the
 documentation should be okay, even while it is technically backwards
 incompatible.

 2. While `GenericGraph.__eq__` no longer takes the ordering of vertices
 into account, `GenericGraph.__hash__` still does. Consequently graphs that
 compare equal can have different hashes.

 3. For graphs that are not weighted, `GenericGraph.__eq__` ignores the
 edge labels, while `GenericGraph.__hash__` does not.

 Item 3 is most easily shown:
 {{{
 sage: G1 = Graph({0: {1: 'edge label A'}}, immutable=True)
 sage: G2 = Graph({0: {1: 'edge label B'}}, immutable=True)
 sage: G1 == G2
 True
 sage: G1.__hash__() == G2.__hash__()
 False
 }}}

 For items 2 and 3, here is some contrived but deterministic code showing
 that the order is not checked in `.__eq__`, while it is in `.__hash__`:
 {{{
 #!python
 import functools
 @functools.total_ordering
 class C:
     def __init__(self, val, reverse):
         self.val = val
         self.reverse = reverse
     def __repr__(self):
         return 'C(%r, %r)' % (self.val, self.reverse)
     def __hash__(self):
         return hash(self.val)
     def __eq__(self, other):
         return self.val == other.val
     def __lt__(self, other):
         if self.reverse:
             return self.val > other.val
         else:
             return self.val < other.val
 G1 = Graph({C(0, False): [], C(1, False): []}, immutable=True)
 G2 = Graph({C(0, True ): [], C(1, True ): []}, immutable=True)
 }}}
 {{{
 sage: [G1.vertices(), G2.vertices()]
 [[C(0, False), C(1, False)], [C(1, True), C(0, True)]]
 sage: G1 == G2
 True
 sage: hash(G1) == hash(G2)
 False
 }}}

 The trick in the code above is to obtain tuples (x1, y1) and (x2, y2) that
 compare equal but sort differently. Above this is achieved explicitly
 using the argument `reverse`. However, this can also happen by
 coincidence, as the code below shows. Here the inequality comparison falls
 back to comparison of ids, so we just generate objects until the ids are
 in an unlucky order.
 {{{
 #!python
 from random import randint
 from sage.graphs.graph import Graph

 class Foo:
     def __init__(self, b):
         self.b = b
     def __eq__(self, other):
         return self.b == other.b
     def __hash__(self):
         return hash(self.b)
     def __repr__(self):
         return "foo(%r)" % (self.b,)

 def gen(f):
     print "* Trying to obtain res = ((f(0),f(1)), (f(0),f(1))) with\n" \
           "  (not res[0][0] < res[0][1]) and (res[1][0] < res[1][1])."
     candidates = [[],[]]
     res = [None, None]
     while res[False] is None or res[True] is None:
         bit = randint(0,1)
         candidates[bit].append( f(bit) )
         for x in candidates[0]:
             for y in candidates[1]:
                 res[bool(x < y)] = (x,y)
     print "* Succeeded after %i random picks." \
            % (len(candidates[0]) + len(candidates[1]),)
     print ("* res[0] = %r\n  res[1] = %r") % (res[0], res[1])
     return res

 def doit(f):
     print
     print "* f = %r" % (f,)
     res = gen(f)
     print "* For i=0 and i=1 creating graph G[i] with vertices " \
           "res[i] and no edges"
     G = [Graph(dict((x, []) for x in res[bit]), immutable = True)
          for bit in range(2)]
     print "* G[0] = %r\n  G[1] = %r" % (G[0], G[1])
     print "* G[0].vertices() = %r\n  G[1].vertices() = %r" % \
             (G[0].vertices(), G[1].vertices())
     print "*         res[0] == res[1]         : %r" % \
             (res[0] == res[1],)
     print "* sorted(res[0]) == sorted(res[1]) : %r" % \
             (sorted(res[0]) == sorted(res[1]),)
     print "*           G[0] == G[1]           : %r" % (G[0] == G[1],)
     print "*     hash(G[0]) == hash(G[1])     : %r" % \
             (hash(G[0]) == hash(G[1]),)
     print
 }}}
 {{{
 sage: doit(Foo)

 * f = <class __main__.Foo at 0xacb694ac>
 * Trying to obtain res = ((f(0),f(1)), (f(0),f(1))) with
   (not res[0][0] < res[0][1]) and (res[1][0] < res[1][1]).
 * Succeeded after 6 random picks.
 * res[0] = (foo(0), foo(1))
   res[1] = (foo(0), foo(1))
 * For i=0 and i=1 creating graph G[i] with vertices res[i] and no edges
 * G[0] = Graph on 2 vertices
   G[1] = Graph on 2 vertices
 * G[0].vertices() = [foo(1), foo(0)]
   G[1].vertices() = [foo(0), foo(1)]
 *         res[0] == res[1]         : True
 * sorted(res[0]) == sorted(res[1]) : False
 *           G[0] == G[1]           : True
 *     hash(G[0]) == hash(G[1])     : False

 }}}

 The custom class `Foo` above is not necessary:
 {{{
 sage: doit(lambda x: sage.graphs.graph.Graph(x, immutable=True))

 * f = <function <lambda> at 0xac556b1c>
 * Trying to obtain res = ((f(0),f(1)), (f(0),f(1))) with
   (not res[0][0] < res[0][1]) and (res[1][0] < res[1][1]).
 * Succeeded after 4 random picks.
 * res[0] = (Graph on 0 vertices, Graph on 1 vertex)
   res[1] = (Graph on 0 vertices, Graph on 1 vertex)
 * For i=0 and i=1 creating graph G[i] with vertices res[i] and no edges
 * G[0] = Graph on 2 vertices
   G[1] = Graph on 2 vertices
 * G[0].vertices() = [Graph on 1 vertex, Graph on 0 vertices]
   G[1].vertices() = [Graph on 0 vertices, Graph on 1 vertex]
 *         res[0] == res[1]         : True
 * sorted(res[0]) == sorted(res[1]) : False
 *           G[0] == G[1]           : True
 *     hash(G[0]) == hash(G[1])     : False

 }}}

--
Ticket URL: <http://trac.sagemath.org/ticket/17086>
Sage <http://www.sagemath.org>
Sage: Creating a Viable Open Source Alternative to Magma, Maple, Mathematica, 
and MATLAB

-- 
You received this message because you are subscribed to the Google Groups 
"sage-trac" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To post to this group, send email to [email protected].
Visit this group at http://groups.google.com/group/sage-trac.
For more options, visit https://groups.google.com/d/optout.

Reply via email to