symengine.py
symengine.py copied to clipboard
Floats are equal, but their hash is different
It generally holds that if two hashable objects are equal, their hashes must be the same.
It seems that python builtin float and symengine.Float violate this rule:
from symengine import Float
Float(2.2) == 2.2 # True
hash(Float(2.2)) == hash(2.2) # False
Shouldn't the hash of a symengine.Float just use the python float's hash?
The SymEngine Float is not the same as Python float. Two different objects and Python uses a different hash functions to calculate its hash.
I know, but if two objects equate, they should also yield the same hash, otherwise you get undefined behavior in e.g. hash tables
I think @Darkdragon84 is right, it is not immediately obvious to me how we would achieve this, but poking around with some python standard library classes, this promise seems to hold:
>>> import decimal
>>> decimal.Decimal('0.5') == 0.5
True
>>> hash(decimal.Decimal('0.5')) == hash(0.5)
True
>>> from fractions import Fraction
>>> Fraction(1, 2) == 0.5
True
>>> hash(Fraction(1, 2)) == hash(0.5)
True
If we support float's .as_integer_ratio(), we could use this as the basis for hash computation perhaps? (but it would be potentially be slower, but non-conformance for speed in the python bindings should probably be opt-in with a compile time flag).
EDIT: Here's the relevant section in Python's documentation.
The only required property is that objects which compare equal have the same hash value;
We need to be careful to not create a strong coupling to the Python hash implementation. So if this should be fixed it needs to be in the Python bindings. Other languages we want to bind to most probably does the hashing in a different way.
I feel that the only way would be to first convert to a Python float and then hash. This would avoid the coupling and generate the same hash if objects compare equal. Perhaps we also need to do this for int. I don't think we would need it for Rational since it doesn't compare equal with floatand cannot be integers.
@rikardn I agree, any overhead should solely be carried by the python bindings, and if there is any overhead I think we should make it simple to opt-out of at compile time (or runtime, if that's doable).
Converting to float, followed by hash sounds like a good approach to me.
Regarding Rational, I wonder if this is an oversight by us, I feel like this really should return True:
>>> symengine.Rational(1,2) == 0.5
False
(but that would be a separate issue I guess).
@bjodah The float not comparing equal to a rational is by design. We see 1/2 as an exact number and 0.5 being 0.5 + $\epsilon$, where espilon can be a small irrational number.
I know, and that's fine. But I still would have preferred it comparing equal whenever the floating point representation matches exactly.
On Wed, Apr 9, 2025, 08:17 Rikard Nordgren @.***> wrote:
@bjodah https://github.com/bjodah The float not comparing equal to a rational is by design. We see 1/2 as an exact number and 0.5 being 0.5 + $\epsilon$, where espilon can be a small irrational number.
— Reply to this email directly, view it on GitHub https://github.com/symengine/symengine.py/issues/513#issuecomment-2788389488, or unsubscribe https://github.com/notifications/unsubscribe-auth/AADWUMFKBNPOXWVLWUAYF3L2YS3QBAVCNFSM6AAAAAB2OR3PYSVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDOOBYGM4DSNBYHA . You are receiving this because you were mentioned.Message ID: @.***> rikardn left a comment (symengine/symengine.py#513) https://github.com/symengine/symengine.py/issues/513#issuecomment-2788389488
@bjodah https://github.com/bjodah The float not comparing equal to a rational is by design. We see 1/2 as an exact number and 0.5 being 0.5 + $\epsilon$, where espilon can be a small irrational number.
— Reply to this email directly, view it on GitHub https://github.com/symengine/symengine.py/issues/513#issuecomment-2788389488, or unsubscribe https://github.com/notifications/unsubscribe-auth/AADWUMFKBNPOXWVLWUAYF3L2YS3QBAVCNFSM6AAAAAB2OR3PYSVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDOOBYGM4DSNBYHA . You are receiving this because you were mentioned.Message ID: @.***>