10

I know that comparison operators with complex numbers can't be defined in general. That is why python throws a TypeError exception when trying to use out-of-the-box complex comparison. I understand why this is the case (please don't go off topic trying to explain why two complex numbers can't be compared).

That said, in this particular case I would like to implement complex number comparison based on their magnitudes. In other words, for z1 and z2 complex values, then z1 > z2 if-and-only-if abs(z1) > abs(z2), where abs() implements complex number magnitude, as in numpy.abs().

I have come up with a solution (at least I think I have) as follows:

import numpy as np

class CustomComplex(complex):
    def __lt__(self, other):
        return np.abs(self) < np.abs(other)
    def __le__(self, other):
        return np.abs(self) <= np.abs(other)
    def __eq__(self, other):
        return np.abs(self) == np.abs(other)
    def __ne__(self, other):
        return np.abs(self) != np.abs(other)
    def __gt__(self, other):
        return np.abs(self) > np.abs(other)
    def __ge__(self, other):
        return np.abs(self) >= np.abs(other)

complex = CustomComplex

This seems to work, but I have a few questions:

  1. Is this the way to go or is there a better alternative?
  2. I would like my package to transparently work with the built-in complex data type as well as numpy.complex. How can this be done elegantly, without code duplication?
jorgeh
  • 1,727
  • 20
  • 32
  • 1
    you have also overwritten the equality operator. I assume this is behaviour you want? – eickenberg Jun 20 '14 at 19:16
  • 1
    Interesting questions, I don't have an informed answer, but plus 1. Also think this is reasonable. Agree with @eickenberg , though, you don't need to define `__eq__` and `__ne__` – Russia Must Remove Putin Jun 20 '14 at 19:18
  • It actually really depends on what you want to do. If you are doing this for fun, then that seems like something interesting. If you need to do numerics, I'd just always use `np.abs` before comparing (in the case of complex arrays). – eickenberg Jun 20 '14 at 19:47
  • 1
    Some would tell you that you only need to define `__lt__` and `__eq__` explicitly - all the others can be defined in terms of those two... – twalberg Jun 20 '14 at 20:01
  • Note that Numpy arrays have a different behavior: on arrays: ``>,<`` is allowed but only compares the real component; ``==`` compares the whole number, ``<=,>=`` does not seem to be consistent (tested on Python 2.7, Numpy 1.8.1). You can implement your custom data type in Numpy (see ``dtype`` docs), but I have no experience with it. Personally I would go with @eickenberg to be rather explicit than implicit (``import this``). – Dietrich Jun 21 '14 at 12:09
  • @AaronHall Of course one should define it, you *can* compare floats with no problem. The chosen implementation is more debatable and whether this is a good practice is another question, but the operator should be provided. – luk32 Jun 24 '14 at 20:30
  • @luk32 if I recall from when I looked into it, `__eq__` and `__ne__` was already defined for `complex`, but in this context, it probably makes sense. I don't know. I think this is why the behavior is not defined by default to begin with. http://math.stackexchange.com/questions/786908/does-it-make-sense-to-compare-complex-numbers-in-certain-circumstances – Russia Must Remove Putin Jun 24 '14 at 21:18
  • 4
    Beware, on a mathematical point of view what you are defining is not an order relation but only a preorder. The relation is reflexive (a <= a) and transitive (a<=b and b<=c => a <=c) but it is neither symetric nor anti-symetric : you can have a <= b and b <= a with a != b (examples : 1, -1, i and -i) – Serge Ballesta Jun 24 '14 at 21:22

2 Answers2

6

I'm afraid I'm going to be off topic (yes I fully read your post :-) ). Ok, Python do allow you to try to compare complex numbers that way because you can define separately all operators even if I strongly advice you not to redefine __eq__ like you did : you are saying 1 == -1 !

IMHO the problem lies there and will spring at your face at one moment (or at the face of anyone who would use your package) : when using equalities and inequalities, ordinary mortals (and most python code) do simple assumptions like -1 != 1, and (a <= b) && (b <= a) implies a == b. And you simply cannot have those 2 assumptions be true at the same time for pure mathematical reasons.

Another classic assumption is a <= b is equivalent to -b <= -a. But with you pre-order a <= b is equivalent to -a <= -b !

That being said, I'll try to answer to your 2 questions :

  • 1: IMHO it is a harmfull way (as dicussed above), but I have no better alternative ...
  • 2: I think a mixin could be an elegant way to limit code duplication

Code example (based on your own code, but not extensively tested):

import numpy as np

class ComplexOrder(Object):
    def __lt__(self, other):
        return np.absolute(self) < np.absolute(other)
    # ... keep want you want (including or not eq and ne)
    def __ge__(self, other):
        return np.absolute(self) >= np.absolute(other)

class OrderedComplex(ComplexOrder, complex):
    def __init__(self, real, imag = 0):
        complex.__init__(self, real, imag)

class NPOrderedComplex64(ComplexOrder, np.complex64):
    def __init__(self, real = 0):
        np.complex64.__init__(self, real)
Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252
  • 1
    +1. The problem only gets worse when you note that Python's containment is based on equality testing. So if complex number comparisons worked this way, then `5 in [3+4j]` would give `True`. – Mark Dickinson Jun 25 '14 at 07:51
1

I'll forgo all the reasons why this may be a bad idea, as per your request.

Is this the way to go or is there a better alternative?

No need to go with numpy, when the normal abs accepts complex numbers and is much faster*. There's also a convenient total_ordering in functools that works well for such simple comparisons, if you want to reduce code (but this may be slower):

from functools import total_ordering
@total_ordering
class CustomComplex(complex):
    def __eq__(self, other):
        return abs(self) == abs(other)
    def __lt__(self, other):
        return abs(self) < abs(other)

(That's all the code you need.)


I would like my package to transparently work with the built-in complex data type as well as numpy.complex. How can this be done elegantly, without code duplication?

It automatically works when the right argument is a normal complex (or any) number:

>>> CustomComplex(1+7j) < 2+8j
True

But that's the best you can do, if you want to use the operators < etc. and not functions. The complex type doesn't allow you to set __lt__ and the TypeError is hardcoded.

If you want to do such comparisons on normal complex numbers, you must define and use your own comparison functions instead of the normal operators. Or just use abs(a) < abs(b) which is clear and not terribly verbose.


* Timing built-in abs vs. numpy.abs:

>>> timeit.timeit('abs(7+6j)')
0.10257387161254883
>>> timeit.timeit('np.abs(7+6j)', 'import numpy as np')
1.6638610363006592
otus
  • 5,572
  • 1
  • 34
  • 48