It crashes because it tries to convert 10**400 to a float and that fails:
10**400 / 1e200 Traceback (most recent call last): File "<pyshell#10>", line 1, in <module> 10**400 / 1e200 OverflowError: int too large to convert to float
But with two ints it succeeds:
10**400 / 10**200 1e+200
Note that 1e200 is an integer:
1e200.is_integer() True
So that could losslessly be converted to int, and then the division would succeed:
10**400 / int(1e200) 1e+200
So could/should 10**400 / 1e200 be implemented to do that instead of raising the error? Or is it a too rare use case and not worth the effort, or does something else speak against it?
That sounds like a lot of extra checks to put on "/" when the error message is clear and the user could implement their own checks if they are running into this niche use case and do 10**400 / int(1e200). Damian (he/him) On Sat, Feb 19, 2022 at 8:36 AM Stefan Pochmann <smpochmann@gmail.com> wrote:
It crashes because it tries to convert 10**400 to a float and that fails:
10**400 / 1e200 Traceback (most recent call last): File "<pyshell#10>", line 1, in <module> 10**400 / 1e200 OverflowError: int too large to convert to float
But with two ints it succeeds:
10**400 / 10**200 1e+200
Note that 1e200 is an integer:
1e200.is_integer() True
So that could losslessly be converted to int, and then the division would succeed:
10**400 / int(1e200) 1e+200
So could/should 10**400 / 1e200 be implemented to do that instead of raising the error? Or is it a too rare use case and not worth the effort, or does something else speak against it? _______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/O7FE5A... Code of Conduct: http://python.org/psf/codeofconduct/
On Sat, Feb 19, 2022 at 6:31 AM Damian Shaw <damian.peter.shaw@gmail.com> wrote:
That sounds like a lot of extra checks to put on "/"
This isn’t about the division — it’s about the literal. The “e” notation makes a float. And floats have fixed precision. Python could try to be smart and create an integer for e notation that happens to be an integer value, but I really don’t see the utility in that. The other “Solution” would be for Python to adopt unlimited precision floats. A major change. -CHB when the error message is clear and the user could implement their own
checks if they are running into this niche use case and do 10**400 / int(1e200).
Damian (he/him)
On Sat, Feb 19, 2022 at 8:36 AM Stefan Pochmann <smpochmann@gmail.com> wrote:
It crashes because it tries to convert 10**400 to a float and that fails:
10**400 / 1e200 Traceback (most recent call last): File "<pyshell#10>", line 1, in <module> 10**400 / 1e200 OverflowError: int too large to convert to float
But with two ints it succeeds:
10**400 / 10**200 1e+200
Note that 1e200 is an integer:
1e200.is_integer() True
So that could losslessly be converted to int, and then the division would succeed:
10**400 / int(1e200) 1e+200
So could/should 10**400 / 1e200 be implemented to do that instead of raising the error? Or is it a too rare use case and not worth the effort, or does something else speak against it? _______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/O7FE5A... Code of Conduct: http://python.org/psf/codeofconduct/
_______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/LBNH5I... Code of Conduct: http://python.org/psf/codeofconduct/
-- Christopher Barker, PhD (Chris) Python Language Consulting - Teaching - Scientific Software Development - Desktop GUI and Web Development - wxPython, numpy, scipy, Cython
On Sun, 20 Feb 2022 at 06:50, Christopher Barker <pythonchb@gmail.com> wrote:
On Sat, Feb 19, 2022 at 6:31 AM Damian Shaw <damian.peter.shaw@gmail.com> wrote:
That sounds like a lot of extra checks to put on "/"
This isn’t about the division — it’s about the literal. The “e” notation makes a float. And floats have fixed precision.
Python could try to be smart and create an integer for e notation that happens to be an integer value, but I really don’t see the utility in that.
The other “Solution” would be for Python to adopt unlimited precision floats. A major change.
Currently, ints will automatically upcast to floats as needed. In theory, any float greater than 1<<52 (on IEEE 64-bit floats) could be losslessly upcast to integer the same way. I don't think the value would justify the uncertainty of type conversions, though - float(x) + int(y) could be either int or float depending on the values given. IMO the only way to justify this kind of flip-flop casting would be for the Python data type to simply be "number", and then it's internally implemented in whichever way makes sense - and that would be an even bigger change than unlimited-precision floats. ChrisA
Christopher Barker wrote:
On Sat, Feb 19, 2022 at 6:31 AM Damian Shaw damian.peter.shaw@gmail.com wrote:
That sounds like a lot of extra checks to put on "/" This isn’t about the division — it’s about the literal.
No, it's about the division. Not about the literal. Sorry I was unclear. I don't want 1e200 to become an int, I meant the division could be as "semi-heroic" (as Tim put it) as the int/int division. That is, when it notices the overflow, instead of just raising the error it could check whether the float is an integer and provide the result that way.
[Stefan Pochmann <smpochmann@gmail.com>]
It crashes because it tries to convert 10**400 to a float and that fails:
10**400 / 1e200 Traceback (most recent call last): File "<pyshell#10>", line 1, in <module> 10**400 / 1e200 OverflowError: int too large to convert to float
But with two ints it succeeds:
10**400 / 10**200 1e+200
Simlalary for 1e200 / 10**400. That is, it doesn't matter to this whether int.__truediv__ or float.__truediv__ is invoked. If the types are mixed, the operands are coerced to float first.
Note that 1e200 is an integer:
1e200.is_integer() True
So that could losslessly be converted to int, and then the division would succeed:
10**400 / int(1e200) 1e+200
So could/should 10**400 / 1e200 be implemented to do that instead of raising the error?
It could. But I don't think it "should".
Or is it a too rare use case and not worth the effort, or does something else speak against it?
Too rare, and expensive. int.__truediv__ makes semi-heroic efforts to get 10**400 / int(1e200) "right" (well, as right as can be), and that's expensive, and probably more surprising than not.for most users. For example,
int(1e200) 99999999999999996973312221251036165947450327545502362648241750950346848435554075534196338404706251868027512415973882408182135734368278484639385041047239877871023591066789981811181813306167128854888448
is, all on its own, probably more surprising than not for most users. It's not to me (or presumably to you), but I have no use for Python magically converting a float to int. It doesn't in other contexts either: [1, 2, 3][2.0] Traceback (most recent call last): ... TypeError: list indices must be integers or slices, not float In purely computational contexts, "when an inf and a float are mixed, the int is converted to a float first" is a simple, predictable,and reliable rule:
pow(10**400, 0.5) Traceback (most recent call last): ,,, OverflowError: int too large to convert to float
In practice, over decades I've seen that kind of exception only a handful of times, and it always pointed to a logical error in my code.
Tim Peters wrote:
int.__truediv__ makes semi-heroic efforts to get 10**400 / int(1e200) "right" (well, as right as can be)
Yes, those semi-heroic efforts actually are what inspired my question. A few days ago I noticed that and was delighted it works. Use case was the exact calculation of value around 8, for which I used fractions.Fraction. Numerator and denominator were large, but the division numerator/denominator still successfully provided a readable and right-as-can-be float. So yes, even I who brought this up only noticed *that* semi-heroism after years of using Python, and don't have an actual use case for the int-and-float division. The int/int one just gave me the impression that Python is trying hard to give me results when possible, so I wondered about this case.
[Tim]
int.__truediv__ makes semi-heroic efforts to get 10**400 / int(1e200) "right" (well, as right as can be)
[Stefan Pochmann <smpochmann@gmail.com>]
Yes, those semi-heroic efforts actually are what inspired my question. A few days ago I noticed that and was delighted it works.
Use case was the exact calculation of value around 8, for which I used fractions.Fraction. Numerator and denominator were large, but the division numerator/denominator still successfully provided a readable and right-as-can-be float.
So yes, even I who brought this up only noticed *that* semi-heroism after years of using Python,
I was similarly surprised that a Fraction instance with giant numerator and denominator converted to a float beautifully. That wasn't always so in Python .> and don't have an actual use case for the int-and-float division. The int/int
one just gave me the impression that Python is trying hard to give me results when possible, so I wondered about this case.
I believe there's something deeper at work with int / int: Python has long said that numeric values that compare equal have the same hash code, and can be used interchangeably as, e.g., dict keys. But if we're going to say that some Fraction f "is equal" to some float `x`,then surely Fraction(x) == f and float(f) == x should be true too. That's of real practical value, because it simplifies reasoning about code mixing floats and Fractions (and Decimals). I believe that's what drove int.__truediv__'s heroism: a desire to support that high-level promise about Python's numerics. BTW, the promise that numeric values that compare equal have the same hash code, regardless of type, is a tricky thing to support. Mark Dickinson nailed it :-)
On Saturday, February 19, 2022, Stefan Pochmann <smpochmann@gmail.com> wrote:
1e200.is_integer() True
So that could losslessly be converted to int, and then the division would succeed
If the float argument isn't an integer, you can multiply both sides by a power of 2 that makes it one (if it's finite, obviously), so this would still work. Currently, int/int and float/float are always properly rounded. It would be nice if you could just say that a/b is properly rounded whenever a and b are int or float, and forget about conversion issues. So I'm +1 on this. It would make certain divisions more expensive than they currently are, but only those where one argument is float and the other is an int that isn't exactly representable as a float (meaning, in particular, its absolute value is greater than 2**53). It seems unlikely to me that anyone is doing a lot of divisions of that kind and needs the speed, but if they are and do, casting to float beforehand is an easy workaround. Also, 10**400/float('inf') should return +0.0 instead of raising OverflowError, and 10**400/float('nan') should be NaN. The division should behave as though both arguments are promoted to arbitrary-precision floating point (but the result is rounded to 64-bit float). I wouldn't suggest that if it was hard to implement, but I think it's pretty easy when long_true_divide already exists.
On Sat, Feb 19, 2022 at 1:37 PM Stefan Pochmann <smpochmann@gmail.com> wrote:
So could/should 10**400 / 1e200 be implemented to do that instead of raising the error? Or is it a too rare use case and not worth the effort, or does something else speak against it?
For me, it's not so much that it's not worth the effort, but more that it's not worth disrupting the clean and easily-digestible mixed-arithmetic model that we currently have. Unrelated question: under this proposal, what would you want `Fraction(10**400) / 1e200` to do? Should it also produce a `float` approximation to 1e200, or is it okay for it to raise `OverflowError` as it currently does? -- Mark
Mark Dickinson wrote:
Unrelated question: under this proposal, what would you want `Fraction(10**400) / 1e200` to do? Should it also produce a `float` approximation to 1e200, or is it okay for it to raise `OverflowError` as it currently does?
Somehow that sounds like a trick question :-). I'd say 1e200, yes, although didn't think much about it. Btw the full error message I get for that is "OverflowError: integer division result too large for a float". Even for `Fraction(10**400, 3) / 3.14`. Saying "integer division" when neither operand is an integer and I'm also not using `//` feels suboptimal.
On Sun, Feb 20, 2022 at 3:59 PM Stefan Pochmann <smpochmann@gmail.com> wrote:
Mark Dickinson wrote:
Unrelated question: under this proposal, what would you want `Fraction(10**400) / 1e200` to do?
Somehow that sounds like a trick question :-). I'd say 1e200, yes, although didn't think much about it.
Well, maybe a little. One of the benefits of the current design is that we only need homogeneous per-type arithmetic operations: the implementation only needs to be able to multiply a float by another float, or an int by another int, or a complex number by another complex number. And then adding in the rules for upcasting gives us the benefit of mixed-type arithmetic with little expense in terms of additional complexity in the implementation. But if we start adding inhomogeneous mixed-type arithmetic that's a whole lot more cases that have to be covered - we now potentially need one addition rule per *pair* of types, instead of just one addition rule per type. So we'd likely end up with dedicated complex * float, float * int, Fraction * float, Fraction * complex, ... operations. And we have to do this for a whole bunch of operations, not just division. For example, it would be oddly inconsistent to special-case 10**400 / 1e200 but not 1e-200 * 10**400. And I suspect that the more complex rules would also make life much more difficult for 3rd-party numeric types that want to play nice with the builtin types. There's definitely potential benefit in some of this - e.g., It Would Be Nice If `-1 * complex(inf, 0.0)` gave `complex(-inf, -0.0)` instead of the current result of `complex(-inf, nan)`. But the price in added complexity - both conceptual complexity and implementation complexity - seems too high. Btw the full error message I get for that is "OverflowError: integer
division result too large for a float". Even for `Fraction(10**400, 3) / 3.14`. Saying "integer division" when neither operand is an integer and I'm also not using `//` feels suboptimal.
Agreed; I'm sure there's room for improvement in the error messages. -- Mark
On Mon, Feb 21, 2022 at 12:24 PM Mark Dickinson <dickinsm@gmail.com> wrote:
e.g., It Would Be Nice If `-1 * complex(inf, 0.0)` gave `complex(-inf, -0.0)` instead of the current result of `complex(-inf, nan)`. But the price in added complexity - both conceptual complexity and implementation complexity - seems too high.
There are some heterogeneous binary operations implemented already. For example 10**400 < float('inf') correctly returns True even though there's no type in Python to which both values could be losslessly coerced to perform that comparison. I don't think adding a few more cases like that implies an obligation to fix every case. It's a "perfect is the enemy of the good" situation. I disagree with your notion of conceptual complexity. The conceptually simplest thing to do is to perform the requested operation exactly and then round if necessary. Only practical difficulties prevent that from happening in all cases. It's not conceptually simple to convert int to float and float to complex blindly regardless of the consequences. C99 gets complex * real multiplication right, despite C being the poster child for numeric promotions, and so does C++. It isn't hard to get it right, and programmers would be happier if Python got it right. Programmers don't expect or want consistent promotion to complex even when it breaks things.
[Mark Dickinson <dickinsm@gmail.com>]
.... There's definitely potential benefit in some of this - e.g., It Would Be Nice If `-1 * complex(inf, 0.0)` gave `complex(-inf, -0.0)` instead of the current result of `complex(-inf, nan)`.
Except replacing -1 with "-1.0" or "complex(-1)" would presumably _still_ return complex(-inf, nan), despite that
-1 == -1.0 == complex(-1) True
That would be mondo surprising too. If that's wanted, better for complex.__mul__ to detect on its own whether component parts are 0, and use a simpler multiplication implementation if so. For example, this similar surprise has nothing to do with type promotion:
1j * complex(math.inf, -0.0) (nan+infj)
But the price in added complexity - both conceptual complexity and implementation complexity - seems too high.
That, plus I'm\ still\ waiting for a plausible use case ;-)
On Mon, Feb 21, 2022 at 1:39 PM Tim Peters <tim.peters@gmail.com> wrote:
[Mark Dickinson <dickinsm@gmail.com>]
It Would Be Nice If `-1 * complex(inf, 0.0)` gave `complex(-inf, -0.0)` instead of the current result of `complex(-inf, nan)`.
Except replacing -1 with "-1.0" or "complex(-1)" would presumably _still_ return complex(-inf, nan), despite that
-1 == -1.0 == complex(-1) True
I think Python should do what C99 and C++ do, which is define complex(a, b) * c to mean complex(a * c, b * c). Python's complex constructor accepts two complex arguments, so that definition works even in the complex * complex case, though in practice you'd want to optimize for common argument types and defer to __rmul__ if the second argument is of unknown type. It does have the consequence that values that compare equal can have different arithmetic behavior. That happens in IEEE arithmetic with ±0, and it's a good thing in certain cases. IEEE arithmetic deliberately preserves the sign of 0 where it can. It's not an accident of the representation. There is a difference between a float and a complex with ±0 imaginary part that justifies the different answers in this case: the former is real by construction, while the latter may have underflowed. It's unfortunate that underflowed values compare equal to true zero, but there's a reason they say you shouldn't compare floating-point numbers for equality. If that's wanted, better for complex.__mul__
to detect on its own whether component parts are 0, and use a simpler multiplication implementation if so.
I think it's better not to do that for the reason in the previous paragraph. For example, this similar surprise has nothing to do with type promotion:
1j * complex(math.inf, -0.0) (nan+infj)
1j is, in effect, being prematurely promoted to complex because Python lacks an imaginary type. C99 has _Imaginary for this reason. Another consequence of the missing imaginary type is that you can't write negative zeros in complex literals consistently. C++ doesn't have std::imaginary, possibly because it has no imaginary literals. That, plus I'm still waiting for a plausible use case ;-)
Why are complex numbers in core Python in the first place? I'm not sure, but I think it's for the same reason as Ellipsis and binary @ and the third argument to slice: to support numpy, since numpy can't define its own syntax. The core devs wouldn't normally add syntax just for some third-party library, but numpy is so important that they bend the rules. People use numpy to do heavy-duty real-world number crunching. The weird IEEE corner cases actually affect the stability of these calculations; that's why the IEEE standard tried to pin down their behavior. I think that improving Python's built-in numerics would have benefits for numpy users in the form of fewer failed computations (mysteriously failed for the many that don't have the numeric-analysis chops to work out what went wrong). I think it would have strategic value. It's less noticeable than adding syntax, but also easier.
I have to say that signed zeroes and signed infinities don't work well in 754 for complex numbers. I know Kahan _wanted_ signed zeroes to help sort out branch cuts for complex functions, but he appears to be alone in the world in finding them useful for that ;-) Complex multiplication for values with signed zero components isn't even associative. At least not under any implementation I've tried. I pointed that out on David Hough's "numeric-interest" mailing list while 754 was still being hammered out, but it was "already too late" to do anything about it. Here's a short driver: from itertools import product pz = 0.0 cs = [complex(*t) for t in product((pz, -pz), repeat=2)] for x, y, z in product(cs, repeat=3): t1 = (x*y)*z t2 = x*(y*z) if repr(t1) != repr(t2): print(x, y, z, t1, t2) On my Windows 3.10.1, associativity fails in 24 (of the 64) cases: 0j 0j (-0-0j) -0j 0j 0j -0j (-0+0j) (-0+0j) 0j 0j -0j (-0-0j) -0j (-0+0j) 0j (-0+0j) (-0+0j) -0j 0j 0j (-0-0j) -0j -0j (-0+0j) 0j (-0-0j) (-0-0j) (-0+0j) 0j -0j 0j (-0+0j) (-0+0j) 0j -0j -0j (-0-0j) (-0+0j) 0j -0j (-0+0j) (-0+0j) (-0+0j) -0j -0j (-0+0j) (-0-0j) -0j 0j -0j (-0-0j) 0j (-0+0j) -0j -0j (-0-0j) (-0+0j) -0j 0j (-0+0j) 0j -0j 0j (-0+0j) (-0+0j) -0j 0j 0j (-0+0j) (-0+0j) (-0+0j) 0j 0j -0j (-0+0j) (-0+0j) -0j -0j (-0+0j) (-0+0j) (-0-0j) -0j 0j -0j (-0+0j) (-0-0j) (-0-0j) -0j (-0+0j) (-0-0j) 0j 0j 0j -0j (-0-0j) -0j 0j (-0+0j) -0j (-0-0j) -0j -0j 0j (-0+0j) (-0-0j) (-0+0j) -0j 0j -0j (-0-0j) (-0-0j) 0j 0j (-0+0j) (-0-0j) (-0-0j) (-0+0j) (-0+0j) -0j That isn't just academic. My employer's FORTRAN compiler failed to pass the US govt's validation suite at the time, because of a bizarre test failure: the accidental signs of these zeroes can have very visible consequences (e.g., as arguments to atan2(), which the validation suite called on the real and imag components after raising a complex zero to an integer power - and _which_ of the four complex zeroes you got depended on the grouping of the multiplies). Don't try to sell me the idea that any of that is logical ;-) BTW, exactly which cases in the above fail can also depend on the rounding mode (because x + -x is +0 for any finite x, _except_ under to-minus-infinity rounding, where the result changes to -0). Signed infinities are even sillier here. You want a single projective infinity in the complex plane, not a pair of signed infinities on each axis. And early drafts of 754 had a control bit to determine which flavor of infinity you got. But it's one of the few bits of esoterica that got dropped near the end. Partly because signed zeroes seemingly demand signed infinities too, like lutefisk demands haggis ;-) There was much to applaud in 754, but to my eyes signed zeroes, and NaN != NaN, caused far more trouble than they helped - and the exception model is such a pain it's almost never been implemented as intended (Python's `decimal` module being an exception, and Apple's long-dead SANE environment). BTW, complex was added as a full-blown built-in type in 1.4b1, long before Python had a major user base (mid-1990s). I think Guido added it because he was trained as a mathematician, so felt obligated ;-) Seriously, various Python versions of it (and rationals) were long in use as test cases for hammering out rules and APIs for coercion (and other native language features).
Tim, Does 754 say anything about complex numbers? It strikes me that what is needed is a defined behavior for complex numbers, rather than expecting them to magically work for all the edge cases simply by applying the algebraic rules with two floats. Your example of a complex infinity that should be one thing is a good one. Maybe the same for complex zero :-) Anyway, Python is probably not the place to define a standard like this, but if there's a good one out there, then using it in Python could be nice. -CHB On Tue, Feb 22, 2022 at 6:41 PM Tim Peters <tim.peters@gmail.com> wrote:
I have to say that signed zeroes and signed infinities don't work well in 754 for complex numbers. I know Kahan _wanted_ signed zeroes to help sort out branch cuts for complex functions, but he appears to be alone in the world in finding them useful for that ;-)
Complex multiplication for values with signed zero components isn't even associative. At least not under any implementation I've tried. I pointed that out on David Hough's "numeric-interest" mailing list while 754 was still being hammered out, but it was "already too late" to do anything about it.
Here's a short driver:
from itertools import product pz = 0.0 cs = [complex(*t) for t in product((pz, -pz), repeat=2)] for x, y, z in product(cs, repeat=3): t1 = (x*y)*z t2 = x*(y*z) if repr(t1) != repr(t2): print(x, y, z, t1, t2)
On my Windows 3.10.1, associativity fails in 24 (of the 64) cases:
0j 0j (-0-0j) -0j 0j 0j -0j (-0+0j) (-0+0j) 0j 0j -0j (-0-0j) -0j (-0+0j) 0j (-0+0j) (-0+0j) -0j 0j 0j (-0-0j) -0j -0j (-0+0j) 0j (-0-0j) (-0-0j) (-0+0j) 0j -0j 0j (-0+0j) (-0+0j) 0j -0j -0j (-0-0j) (-0+0j) 0j -0j (-0+0j) (-0+0j) (-0+0j) -0j -0j (-0+0j) (-0-0j) -0j 0j -0j (-0-0j) 0j (-0+0j) -0j -0j (-0-0j) (-0+0j) -0j 0j (-0+0j) 0j -0j 0j (-0+0j) (-0+0j) -0j 0j 0j (-0+0j) (-0+0j) (-0+0j) 0j 0j -0j (-0+0j) (-0+0j) -0j -0j (-0+0j) (-0+0j) (-0-0j) -0j 0j -0j (-0+0j) (-0-0j) (-0-0j) -0j (-0+0j) (-0-0j) 0j 0j 0j -0j (-0-0j) -0j 0j (-0+0j) -0j (-0-0j) -0j -0j 0j (-0+0j) (-0-0j) (-0+0j) -0j 0j -0j (-0-0j) (-0-0j) 0j 0j (-0+0j) (-0-0j) (-0-0j) (-0+0j) (-0+0j) -0j
That isn't just academic. My employer's FORTRAN compiler failed to pass the US govt's validation suite at the time, because of a bizarre test failure: the accidental signs of these zeroes can have very visible consequences (e.g., as arguments to atan2(), which the validation suite called on the real and imag components after raising a complex zero to an integer power - and _which_ of the four complex zeroes you got depended on the grouping of the multiplies).
Don't try to sell me the idea that any of that is logical ;-) BTW, exactly which cases in the above fail can also depend on the rounding mode (because x + -x is +0 for any finite x, _except_ under to-minus-infinity rounding, where the result changes to -0).
Signed infinities are even sillier here. You want a single projective infinity in the complex plane, not a pair of signed infinities on each axis. And early drafts of 754 had a control bit to determine which flavor of infinity you got. But it's one of the few bits of esoterica that got dropped near the end. Partly because signed zeroes seemingly demand signed infinities too, like lutefisk demands haggis ;-)
There was much to applaud in 754, but to my eyes signed zeroes, and NaN != NaN, caused far more trouble than they helped - and the exception model is such a pain it's almost never been implemented as intended (Python's `decimal` module being an exception, and Apple's long-dead SANE environment).
BTW, complex was added as a full-blown built-in type in 1.4b1, long before Python had a major user base (mid-1990s). I think Guido added it because he was trained as a mathematician, so felt obligated ;-) Seriously, various Python versions of it (and rationals) were long in use as test cases for hammering out rules and APIs for coercion (and other native language features). _______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/3IY3NT... Code of Conduct: http://python.org/psf/codeofconduct/
I stumbled across this: https://math.stackexchange.com/questions/4172817/complex-floating-point-type..., which discusses implementing complex floating point in polar form (r: unsigned double, theta: fixed point float), rather than rectangular form (x: signed double, y: signed double). Of course, you lose out on easy addition if you implement C in polar form (the accepted answer mentions this). But it smooths some of the other issues mentioned above, as you get complex infinities `(+inf,theta)` and "signed" zeroes `(epsilon,theta)`. One could even let theta represent the angle scaled by a factor of 1/pi, to increase the precision of theta, so `i = (1,0.5)` and `-1 = (1,1)`. One could also keep track of a complex number in both rectangular and polar form (x, y, r, theta). So `i = (0,1,1,0,5)` and `-1 = (-1,0,1,1)`. Then just use use rectangular coordinates for +- operations and polar coordinates for */. And then after each +- operation, recalculate the polar coordinates, and vice versa. Though now that I think about it more, that might be far more expensive, and there might also be some issues with recalculating x,y for a complex infinity. Thoughts? Om ---- On Wed, 23 Feb 2022 21:40:58 -0600 Christopher Barker <pythonchb@gmail.com> wrote ----
Tim,
Does 754 say anything about complex numbers?
It strikes me that what is needed is a defined behavior for complex numbers, rather than expecting them to magically work for all the edge cases simply by applying the algebraic rules with two floats.
Your example of a complex infinity that should be one thing is a good one. Maybe the same for complex zero :-)
Anyway, Python is probably not the place to define a standard like this, but if there's a good one out there, then using it in Python could be nice.
-CHB
On Tue, Feb 22, 2022 at 6:41 PM Tim Peters <tim.peters@gmail.com> wrote: I have to say that signed zeroes and signed infinities don't work well in 754 for complex numbers. I know Kahan _wanted_ signed zeroes to help sort out branch cuts for complex functions, but he appears to be alone in the world in finding them useful for that ;-)
Complex multiplication for values with signed zero components isn't even associative. At least not under any implementation I've tried. I pointed that out on David Hough's "numeric-interest" mailing list while 754 was still being hammered out, but it was "already too late" to do anything about it.
Here's a short driver:
from itertools import product pz = 0.0 cs = [complex(*t) for t in product((pz, -pz), repeat=2)] for x, y, z in product(cs, repeat=3): t1 = (x*y)*z t2 = x*(y*z) if repr(t1) != repr(t2): print(x, y, z, t1, t2)
On my Windows 3.10.1, associativity fails in 24 (of the 64) cases:
0j 0j (-0-0j) -0j 0j 0j -0j (-0+0j) (-0+0j) 0j 0j -0j (-0-0j) -0j (-0+0j) 0j (-0+0j) (-0+0j) -0j 0j 0j (-0-0j) -0j -0j (-0+0j) 0j (-0-0j) (-0-0j) (-0+0j) 0j -0j 0j (-0+0j) (-0+0j) 0j -0j -0j (-0-0j) (-0+0j) 0j -0j (-0+0j) (-0+0j) (-0+0j) -0j -0j (-0+0j) (-0-0j) -0j 0j -0j (-0-0j) 0j (-0+0j) -0j -0j (-0-0j) (-0+0j) -0j 0j (-0+0j) 0j -0j 0j (-0+0j) (-0+0j) -0j 0j 0j (-0+0j) (-0+0j) (-0+0j) 0j 0j -0j (-0+0j) (-0+0j) -0j -0j (-0+0j) (-0+0j) (-0-0j) -0j 0j -0j (-0+0j) (-0-0j) (-0-0j) -0j (-0+0j) (-0-0j) 0j 0j 0j -0j (-0-0j) -0j 0j (-0+0j) -0j (-0-0j) -0j -0j 0j (-0+0j) (-0-0j) (-0+0j) -0j 0j -0j (-0-0j) (-0-0j) 0j 0j (-0+0j) (-0-0j) (-0-0j) (-0+0j) (-0+0j) -0j
That isn't just academic. My employer's FORTRAN compiler failed to pass the US govt's validation suite at the time, because of a bizarre test failure: the accidental signs of these zeroes can have very visible consequences (e.g., as arguments to atan2(), which the validation suite called on the real and imag components after raising a complex zero to an integer power - and _which_ of the four complex zeroes you got depended on the grouping of the multiplies).
Don't try to sell me the idea that any of that is logical ;-) BTW, exactly which cases in the above fail can also depend on the rounding mode (because x + -x is +0 for any finite x, _except_ under to-minus-infinity rounding, where the result changes to -0).
Signed infinities are even sillier here. You want a single projective infinity in the complex plane, not a pair of signed infinities on each axis. And early drafts of 754 had a control bit to determine which flavor of infinity you got. But it's one of the few bits of esoterica that got dropped near the end. Partly because signed zeroes seemingly demand signed infinities too, like lutefisk demands haggis ;-)
There was much to applaud in 754, but to my eyes signed zeroes, and NaN != NaN, caused far more trouble than they helped - and the exception model is such a pain it's almost never been implemented as intended (Python's `decimal` module being an exception, and Apple's long-dead SANE environment).
BTW, complex was added as a full-blown built-in type in 1.4b1, long before Python had a major user base (mid-1990s). I think Guido added it because he was trained as a mathematician, so felt obligated ;-) Seriously, various Python versions of it (and rationals) were long in use as test cases for hammering out rules and APIs for coercion (and other native language features). _______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/3IY3NT... Code of Conduct: http://python.org/psf/codeofconduct/ _______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/ZXPQ5C... Code of Conduct: http://python.org/psf/codeofconduct/
I spent a couple hours putting this together: https://github.com/omajoshi/complex_math It does lazy updating, staying in polar form until an addition, then staying in rectangular form until the next multiplication. I implemented Tim Peters' tests (from two emails back): https://github.com/omajoshi/complex_math/blob/main/test.py Here are my results vs his: https://github.com/omajoshi/complex_math/blob/main/test.txt The library may be useless, but as far as I can it is associative (at least on Tim's tests). Om ---- On Wed, 23 Feb 2022 22:36:52 -0600 om <om+python@omajoshi.com> wrote ----
I stumbled across this: https://math.stackexchange.com/questions/4172817/complex-floating-point-type..., which discusses implementing complex floating point in polar form (r: unsigned double, theta: fixed point float), rather than rectangular form (x: signed double, y: signed double).
Of course, you lose out on easy addition if you implement C in polar form (the accepted answer mentions this). But it smooths some of the other issues mentioned above, as you get complex infinities `(+inf,theta)` and "signed" zeroes `(epsilon,theta)`. One could even let theta represent the angle scaled by a factor of 1/pi, to increase the precision of theta, so `i = (1,0.5)` and `-1 = (1,1)`.
One could also keep track of a complex number in both rectangular and polar form (x, y, r, theta). So `i = (0,1,1,0,5)` and `-1 = (-1,0,1,1)`. Then just use use rectangular coordinates for +- operations and polar coordinates for */. And then after each +- operation, recalculate the polar coordinates, and vice versa.
Though now that I think about it more, that might be far more expensive, and there might also be some issues with recalculating x,y for a complex infinity. Thoughts?
Om
---- On Wed, 23 Feb 2022 21:40:58 -0600 Christopher Barker <pythonchb@gmail.com> wrote ----
Tim,
Does 754 say anything about complex numbers?
It strikes me that what is needed is a defined behavior for complex numbers, rather than expecting them to magically work for all the edge cases simply by applying the algebraic rules with two floats.
Your example of a complex infinity that should be one thing is a good one. Maybe the same for complex zero :-)
Anyway, Python is probably not the place to define a standard like this, but if there's a good one out there, then using it in Python could be nice.
-CHB
On Tue, Feb 22, 2022 at 6:41 PM Tim Peters <tim.peters@gmail.com> wrote: I have to say that signed zeroes and signed infinities don't work well in 754 for complex numbers. I know Kahan _wanted_ signed zeroes to help sort out branch cuts for complex functions, but he appears to be alone in the world in finding them useful for that ;-)
Complex multiplication for values with signed zero components isn't even associative. At least not under any implementation I've tried. I pointed that out on David Hough's "numeric-interest" mailing list while 754 was still being hammered out, but it was "already too late" to do anything about it.
Here's a short driver:
from itertools import product pz = 0.0 cs = [complex(*t) for t in product((pz, -pz), repeat=2)] for x, y, z in product(cs, repeat=3): t1 = (x*y)*z t2 = x*(y*z) if repr(t1) != repr(t2): print(x, y, z, t1, t2)
On my Windows 3.10.1, associativity fails in 24 (of the 64) cases:
0j 0j (-0-0j) -0j 0j 0j -0j (-0+0j) (-0+0j) 0j 0j -0j (-0-0j) -0j (-0+0j) 0j (-0+0j) (-0+0j) -0j 0j 0j (-0-0j) -0j -0j (-0+0j) 0j (-0-0j) (-0-0j) (-0+0j) 0j -0j 0j (-0+0j) (-0+0j) 0j -0j -0j (-0-0j) (-0+0j) 0j -0j (-0+0j) (-0+0j) (-0+0j) -0j -0j (-0+0j) (-0-0j) -0j 0j -0j (-0-0j) 0j (-0+0j) -0j -0j (-0-0j) (-0+0j) -0j 0j (-0+0j) 0j -0j 0j (-0+0j) (-0+0j) -0j 0j 0j (-0+0j) (-0+0j) (-0+0j) 0j 0j -0j (-0+0j) (-0+0j) -0j -0j (-0+0j) (-0+0j) (-0-0j) -0j 0j -0j (-0+0j) (-0-0j) (-0-0j) -0j (-0+0j) (-0-0j) 0j 0j 0j -0j (-0-0j) -0j 0j (-0+0j) -0j (-0-0j) -0j -0j 0j (-0+0j) (-0-0j) (-0+0j) -0j 0j -0j (-0-0j) (-0-0j) 0j 0j (-0+0j) (-0-0j) (-0-0j) (-0+0j) (-0+0j) -0j
That isn't just academic. My employer's FORTRAN compiler failed to pass the US govt's validation suite at the time, because of a bizarre test failure: the accidental signs of these zeroes can have very visible consequences (e.g., as arguments to atan2(), which the validation suite called on the real and imag components after raising a complex zero to an integer power - and _which_ of the four complex zeroes you got depended on the grouping of the multiplies).
Don't try to sell me the idea that any of that is logical ;-) BTW, exactly which cases in the above fail can also depend on the rounding mode (because x + -x is +0 for any finite x, _except_ under to-minus-infinity rounding, where the result changes to -0).
Signed infinities are even sillier here. You want a single projective infinity in the complex plane, not a pair of signed infinities on each axis. And early drafts of 754 had a control bit to determine which flavor of infinity you got. But it's one of the few bits of esoterica that got dropped near the end. Partly because signed zeroes seemingly demand signed infinities too, like lutefisk demands haggis ;-)
There was much to applaud in 754, but to my eyes signed zeroes, and NaN != NaN, caused far more trouble than they helped - and the exception model is such a pain it's almost never been implemented as intended (Python's `decimal` module being an exception, and Apple's long-dead SANE environment).
BTW, complex was added as a full-blown built-in type in 1.4b1, long before Python had a major user base (mid-1990s). I think Guido added it because he was trained as a mathematician, so felt obligated ;-) Seriously, various Python versions of it (and rationals) were long in use as test cases for hammering out rules and APIs for coercion (and other native language features). _______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/3IY3NT... Code of Conduct: http://python.org/psf/codeofconduct/ _______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/ZXPQ5C... Code of Conduct: http://python.org/psf/codeofconduct/
The non-associativity isn't just signed zeros: regular floating point addition and multiplication of positive numbers is not associative from math import nextafter x = nextafter(1.0, 2.0) print((x+x)+1.0, x+(x+1.0)) x2 = nextafter(1.0, 0.0) print((x*x2)*x2, x*(x2*x2))
[Dennis Sweeney <sweeney.dennis650@gmail.com>]
The non-associativity isn't just signed zeros: regular floating point addition and multiplication of positive numbers is not associative
Yes, but that's shallow, having to do with rounding errors. In the example I gave, the "inexact" flag never gets set. As far as 754 is concerned, every infinitely precise result was exactly representable.Replace, e.g., "pz = 0.0" with "pz = 12345.0" in my examp\le, and associativity is not violated. Indeed, it's routine (in some circles) to use the `decimal` module for exact bigint arithmetic, setting the Inexact trap to verify that you've specified enough precision for that "to work" So long as Inexact doesn't trigger, you can be confident that your final results are exactly right. This isn't "abuse". It's one of the _intended_ uises for the inexact flag/trap; Except in the presence of signed zeroes, where, if Z is a zero, Z + (-Z) -> +0;0 is a made-up (arbitrary, unprincipled) rule (except under floor rounding, where it's replaced by an equally arbitrary rule). That's the source for why using the usual Cartesian rules for computing the product of complex numbers with signed zero components yields incoherent results. The failure h\as nothing to do with losing information, apart from some fuzzy conception of that "well, +0..0 means it underflowed from the positive direction, and despite that we haven't the slightest idea of what the true value is apart from the sign, we're going to pretend it's an exact input - and that its true value is larger than the absolute value of the equally unknown true value -0.0 represents - except under floor rounding, where we'll pretend the opposite". IOW, garbage in, garbage out ;-)
[Om Joshi <om+python@omajoshi.com>]
I spent a couple hours putting this together: https://github.com/omajoshi/complex_math
It does lazy updating, staying in polar form until an addition, then staying in rectangular form until the next multiplication.
I implemented Tim Peters' tests (from two emails back): https://github.com/omajoshi/complex_math/blob/main/test.py
Here are my results vs his: https://github.com/omajoshi/complex_math/blob/main/test.txt
The library may be useless, but as far as I can it is associative (at least on Tim's tests).
Ya, polar is much more natural for expressing complex multiplication. The Cartesian form _can_ be made associative too, but I'm not aware of any implementation that actually does so. Proof: there are, up to isomorphism, two 4-element groups, both of which happen to be commutative. Pick either one, and name each of the 4 elements with one of the 4 complex zeroes. Then let the group's "multiplication" table _define_ the result. That's major hackery, though, and doesn't seem to follow from any "natural" way of doing 754 arithmetic on the components. Confounding it: some other results for signed zeroes are defined by various semi-authoritative sources, like Annex G of the most recent C standards. In particular, "the angle" ("phase", or "argument") of a complex number is computed by carg(z), where special cases are supposed to do the same as atan2(cimag(z), creal(z)),. Which is more arbitrary gibberish for signed zeroes: ... print(y, x, atan2(y, x)) ... 0.0 0.0 0.0 0.0 -0.0 3.141592653589793 -0.0 0.0 -0.0 -0.0 -0.0 -3.141592653589793 Which Python's cmath.phase() satisfies on my box:
cmath.phase(complex(0.0, 0.0)) 0.0 cmath.phase(complex(-0.0, 0.0)) 3.141592653589793 cmath.phase(complex(0.0, -0.0)) -0.0 cmath.phase(complex(-0.0, -0.0)) -3.141592653589793
But I don't suggest anyone spend time on this. It's a bottomless pit, and nobody cares :-)
Stefan Pochmann writes:
Mark Dickinson wrote:
Unrelated question: under this proposal, what would you want `Fraction(10**400) / 1e200` to do?
Btw the full error message I get for that is "OverflowError: integer division result too large for a float". Even for `Fraction(10**400, 3) / 3.14`. Saying "integer division" when neither operand is an integer and I'm also not using `//` feels suboptimal.
In converting Fraction(m, n) to float, presumably you just do m / n, no?
float(Fraction(10**400, 10**399)) 10.0 float(Fraction(10**400, 3)) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/opt/local/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/numbers.py", line 291, in __float__ return self.numerator / self.denominator OverflowError: integer division result too large for a float
Seems reasonable to me.
[Mark Dickinson <dickinsm@gmail.com>]
Unrelated question: under this proposal, what would you want `Fraction(10**400) / 1e200` to do? Should it also produce a `float` approximation to 1e200, or is it okay for it to raise `OverflowError` as it currently does?
The principled alternative to the current "in mixed-type arithmetic involving a float, operands are converted to float first" is "all operations are computed internally as if with infinite precision, then if the result is within the range of representable floats, the result is rounded to the closest such". But you already knew that ;-) It's a slippery slope, of scant discernible benefit, and still hackish. For example, 10**600 / 1e200 / 1e200. If we just do the first division "as if with infinite ...", it still blows up, despite that the computation as a whole would not if done "as if with infinite ;...:". Floats are not the computable reals ;-)
On Sun, Feb 20, 2022 at 9:41 AM Tim Peters <tim.peters@gmail.com> wrote:
It's a slippery slope, of scant discernible benefit, and still hackish. For example, 10**600 / 1e200 / 1e200.
That's how IEEE arithmetic works: a/b is computed to infinite precision then properly rounded, but (a/b)/c isn't. Yes, it's not ideal, but it was doable, and was better than not even having consistent behavior for a/b, which was the state of things before the standard. Python follows the IEEE rounding model for float/float and int/int. Following it for float/int and int/float would be pretty easy since the hard work has already been done to support int/int. Let's just do it, and not worry about whether it's hypocritical to fix this without fixing the bigger problem. Once you start using slippery-slope arguments, pretty soon you're using them for everything, and progress grinds to a halt...
[Ben Rudiak-Gould <benrudiak@gmail.com>]
... Python follows the IEEE rounding model for float/float and int/int. Following it for float/int and int/float would be pretty easy since the hard work has already been done to support int/int.
It doesn't end there. Mark was asking about analogous behavior for mixing Fraction and float, and after that's answered "yes", the same for Decimal.
Let's just do it,
Who is "us"? I await your comprehensive patch ;-)
and not worry about whether it's hypocritical to fix this without fixing the bigger problem.
I'm not concerned with hypocrisy. I'm concerned that we'd be giving up a simple-to-understand rule about how mixing floats with other types works ("convert the other type to float, and if it's not representable as a float raise an exception"), and adding piles of delicate new code to cater to a "use case" that DOESN"T EXIST. In 3 decades. nobody ever asked for this.Eveni in this thread, the OP was just idly curious - they have no actual use for it either.
Once you start using slippery-slope arguments, pretty soon you're using them for everything, and progress grinds to a halt...
Curiously, that itself is a slippery-slope argument ;-)
On Mon, 21 Feb 2022 at 09:53, Tim Peters <tim.peters@gmail.com> wrote:
Once you start using slippery-slope arguments, pretty soon you're using them for everything, and progress grinds to a halt...
Curiously, that itself is a slippery-slope argument ;-)
Be careful. Once you start calling people out for using slippery-slope arguments, pretty soon you're busy all over the internet, contributing nothing else to a discussion... ChrisA ... oh.
participants (11)
-
Ben Rudiak-Gould
-
Chris Angelico
-
Christopher Barker
-
Damian Shaw
-
Dennis Sweeney
-
Mark Dickinson
-
om
-
Om Joshi
-
Stefan Pochmann
-
Stephen J. Turnbull
-
Tim Peters