julia icon indicating copy to clipboard operation
julia copied to clipboard

Fix pi == one(pi)*pi or update docs for one()

Open petvana opened this issue 5 years ago • 19 comments

Based on the discussion in #37931, I have found that pi == one(pi)*pi is broken. However, it should hold according to the documentation of one() function:

    one(x)
    one(T::type)
Return a multiplicative identity for `x`: a value such that
`one(x)*x == x*one(x) == x`.  Alternatively `one(T)` can
take a type `T`, in which case `one` returns a multiplicative
identity for any `x` of type `T`.

If possible, `one(x)` returns a value of the same type as `x`,
and `one(T)` returns a value of type `T`.  However, this may
not be the case for types representing dimensionful quantities
(e.g. time in days), since the multiplicative
identity must be dimensionless.  In that case, `one(x)`
should return an identity value of the same precision
(and shape, for matrices) as `x`.
...

Don't know if you prefer fixing the implementation or just updating the documentation to make it consistent. Notice that since one(pi) already returns true, it would be possible to make true * pi === pi.

petvana avatar Oct 10 '20 18:10 petvana

FYI: Returning 1.0 (or 1 or true) isn't strictly wrong.

That is the multiplicative identity. The problem comes when you multiply it with pi, or even 2 with pi, you want to get pi or 2pi, but that's only possibly in a CAS (maybe e.g. in http://nemocas.org/ ). It was decided that actualy calculations would convert to floats that make pi rational approximation.

PallHaraldsson avatar Oct 11 '20 00:10 PallHaraldsson

I think in this case the docs is at fault. It should say that identity only holds for T being a concrete type.

edit: I meant to distinguish where a T is "concretely" represented by bits but find it hard to point finger at:

julia> isconcretetype(typeof(pi))
true
julia> isbits(pi)
true
julia> sizeof(Int)
8
julia> sizeof(Irrational{:π})
0

One way to frame the problem is to say: multiplicative identity of Irrational{:pi} doesn't exist. (even before we talk about if the identity is of the same type or not)

Moelf avatar Oct 11 '20 01:10 Moelf

typeof(pi) is a concrete type.

yuyichao avatar Oct 11 '20 03:10 yuyichao

🤷‍♂️ Really starting to regret the Irrational type entirely.

StefanKarpinski avatar Oct 11 '20 17:10 StefanKarpinski

While of course it would be nice, I don't really see why this so badly needs to hold --- many mathematical identities are not true e.g. with floating-point numbers.

JeffBezanson avatar Oct 12 '20 16:10 JeffBezanson

At some point, I think we had the property that true*x was just x and false*x should always be zero(x). If we had that then this identity would hold. It would be a bit weird though since true*x and false*x would not be of the same type when x is an irrational. Perhaps that would be ok since it would be very amenable to constant propagation and type analysis.

StefanKarpinski avatar Oct 14 '20 13:10 StefanKarpinski

Perhaps a case where a 0-bit integer is useful!

JeffBezanson avatar Oct 14 '20 18:10 JeffBezanson

Isn't pi (and friends) already 0-bit integers? (base pi and whatnot)

vtjnash avatar Oct 14 '20 21:10 vtjnash

First, thank you for the responses. I understand this is a minor issue.

I totally agree with @StefanKarpinski that the whole Irrational type is somehow unfortunate. I can demonstrate it by more practical examples:

  1. Problem with negation (not even helps)
julia> x = big(1.0)
1.0
julia> cos(-big(π)-x) == cos(π+x)
true
julia> cos(-π-x) ≈ cos(π+x)
false
  1. may look like an irrational constant (but is already converted to Float64), and the order of operations matters
julia> 2π+x == π+π+x
true
julia> 2π+x ≈ π+(π+x)
false

The current documentation contains only the following text, but nothing about Float64 is a fallback type.

  AbstractIrrational <: Real

  Number type representing an exact irrational value, which is automatically
  rounded to the correct precision in arithmetic operations with other numeric
  quantities.

What I love about julia is that it is easy to use and intuitive. But the behavior in these examples is not intuitive.

There is no easy fix. pi is already threatened as Float64 in many cases. Thus, the least breaking change would probably be to make pi behave like Float64 constant in v2.0 with a single exception of big(pi) or BigFloat(pi), respectively. This would also solve this issue since pi == one(pi)*pi would compare two floats. However, it would be necessary to deprecate current arithmetic operations with {Float16, Float32, BigFloat} like

big(1.0) + pi           # deprecate this
big(1.0) + big(pi)      # in favour of this

I belive it would also make user's code less error-prone.

petvana avatar Oct 16 '20 12:10 petvana

julia> cos(-big(π)-x) == cos(π+x)
true
julia> cos(-π-x) ≈ cos(π+x)
false

The latter says a bit more about how cos is implemented on floats (vs big numbers) rather than about pi.

It is a bit surprising that it's not even approximately true, with cos symmetric and e.g.:

julia> cos(-π-0.1) == cos(π+0.1)
true

Note also that despite:

julia> cos(-big(π)-x) == cos(π+x)
true

Neither side gives the correct value as both are irrational (and the same), there the approximation of is just the same.

You did the right thing with -big(pi), note how careful you would have to be:

julia> -big(pi) ≈ big(-pi)
false

PallHaraldsson avatar Oct 16 '20 13:10 PallHaraldsson

The issue came up again in a discussion, with a suggestion by @nsajko to have a function return irrational numbers instead based on an explicitly specified type, eg

irrational(:π, Float64) = 3.141592653589793

etc.

Yes, this is breaking, but it could be revisited for 2.0.

tpapp avatar Sep 26 '22 13:09 tpapp

A better alternative would perhaps be to remove Irrational and instead just have each irrational be a function that takes a T <: AbstractFloat and returns the value of T closest to the given irrational. It seems like the lesson of irrationals is that if you want to do anything with them, convert them to float first. We might as well just have a function give a float to begin with.

Edit: That's literally what you wrote just above, derp. Disregard me, I must have brainfarted and couldn't read.

jakobnissen avatar Sep 26 '22 18:09 jakobnissen

@jakobnissen Isn't that the same thing though?

nsajko avatar Sep 26 '22 19:09 nsajko

We could actually have π == one(π)*π behave as expected even before Julia 2. I think there are basically two ways to a fix (without sacrificing type stability):

  1. Make one(π) return an instance of a singleton type One <: Integer. Then multiplication of Irrational and One values could return the given Irrational value without type stability issues. A package named Zeros is already registered and seems to implement One as required here, so maybe it could be moved to Base: https://juliahub.com/ui/Packages/Zeros/zPOBQ/0.3.0 ? Singleton <:Number types like Zero or One would be useful in many more places, for example: if Zero <: Integer existed in Base, we could define imag(::Real) as imag(::Real) = Zero(). EDIT: related discussion in #34003

  2. Assuming that it's OK to add some data fields to struct Irrational, we could have one(π) isa typeof(π). For example, if we had a Bool exponent as a data member of Irrational, a zero exponent would indicate that the value of the Irrational value is one. This approach could be taken even further, for example if we had an Int8 scaling factor in addition to the exponent, we could also have -π isa typeof(π) and zero(π) isa typeof(π). In such an implementation the scaling factor and exponent could both be encoded within a single Int8 value.

I guess that the first approach would be both easier to implement and more promising, but the second approach also seems OK. EDIT: actually, the second approach would be more powerful, in that it would prevent any information loss due to negation (-), which could fix some of the issues noted in the comments above. In particular, I think we could have cos(-π-x) == cos(π+x) with the second approach. EDIT2: on the other hand, the first approach could also be extended for achieving lossless -(::Irrational) by defining it using a wrapper type Neg: -(x::Irrational) = Neg(x).

Thoughts?

nsajko avatar Aug 13 '23 00:08 nsajko

I still think that the whole concept of irrationals with precision resolved by context is a design mistake, so I think that fixing that by introducing yet another type is just compounding this.

Irrational of a given precision should be requested by the user, π (as a value) should be deprecated, and possibly replaced by π(::T) or similar.

tpapp avatar Aug 13 '23 07:08 tpapp

I mostly agree, I was just thinking about what can be done before Julia 2.

nsajko avatar Aug 13 '23 10:08 nsajko

A new interface irrational(:π, Float64) etc can be added any time without being breaking. That said, it might as well just live in a package.

In the meantime, Irrational as is now could remain unchanged, but its use could be discouraged.

tpapp avatar Aug 14 '23 07:08 tpapp

It seems that Irrational is basically AbstractFloat with infinity digits. The difference is that, whereas promote usually takes two arguments and makes them the wider type, instead it demotes Irrational to the (float of) the other argument. In this sense, Irrational has its uses (but pi(::Type) might have been the better choice).

So one candidate resolution (which I will dismiss in my next paragraph) is to change == for Irrational to promote the arguments first (although Integer comparisons could virtually always be false), so that pi == x if oftype(x, pi) == x.

The problem is that this would create an inconsistency with inequalities. Because Float64(pi) < pi is true and this is "more true" than them being ==. So the implementation of a promote-based comparison would create a violation of the incredibly useful and widely-assumed property that two reals (except NaN) should be related by precisely one of <, ==, or >.

Maybe this is the original sin of the Irrational concept? We have other issues, like the current "paradox" that pi - Float64(pi) == 0.0 even though pi > Float64(pi) and the difference would be easily approximated as another Float64 (no pair of IEEE-754 floats in compliant arithmetic have the property that they are != yet their difference is 0). In this light, we could change == and </> to all promote Irrationals beforehand and that would resolve the paradox, at the price of losing the true fact that Float64(pi) < pi (but how useful is this?).

All of this is easily reconciled by the trivial fact that Irrational does not propagate through virtually any non-symbolic calculation (and is usually evaluated before the calculation, rather than during). Existential issues with Irrational aside, I'm not really worried about this issue. T(pi) == one(T(pi)) * T(pi) is true for any T<:AbstractFloat and I think that's good enough for me. Where in practice does one check an (uncasted) Irrational in a == with an arithmetic result?

mikmoore avatar Aug 26 '24 15:08 mikmoore

I think that the easiest solution at this point (while remaining in Julia 1.x) could be just documenting the intended use and limitations of Irrational. Specifically, that it resolves into floating point in some situations, and this may not satisfy mathematical identities because, well, floating point.

Where in practice does one check an (uncasted) Irrational in a == with an arithmetic result?

Yes, I agree. We should just document that this is not a symbolic algebra system and using it as such will result in problems. Irrational is a mechanism to access mathematical constants at given precisions conveniently, nothing more.

tpapp avatar Aug 26 '24 16:08 tpapp