Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
petvana opened this issue Oct 10, 2020 · 19 comments
Open

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

petvana opened this issue Oct 10, 2020 · 19 comments
Labels
maths Mathematical functions

Comments

@petvana
Copy link
Member

petvana commented Oct 10, 2020

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.

@PallHaraldsson
Copy link
Contributor

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.

@Moelf
Copy link
Contributor

Moelf commented Oct 11, 2020

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)

@yuyichao
Copy link
Contributor

typeof(pi) is a concrete type.

@StefanKarpinski
Copy link
Member

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

@JeffBezanson
Copy link
Member

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.

@StefanKarpinski
Copy link
Member

StefanKarpinski commented Oct 14, 2020

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.

@JeffBezanson
Copy link
Member

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

@vtjnash
Copy link
Member

vtjnash commented Oct 14, 2020

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

@petvana
Copy link
Member Author

petvana commented Oct 16, 2020

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.

@PallHaraldsson
Copy link
Contributor

PallHaraldsson commented Oct 16, 2020

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 added a commit to PallHaraldsson/julia that referenced this issue Oct 16, 2020
Clarifying (fixing?) JuliaLang#37977

[skip ci]
@tpapp
Copy link
Contributor

tpapp commented Sep 26, 2022

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.

@jakobnissen
Copy link
Contributor

jakobnissen commented Sep 26, 2022

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.

@nsajko

This comment was marked as resolved.

@brenhinkeller brenhinkeller added the maths Mathematical functions label Nov 20, 2022
@nsajko
Copy link
Contributor

nsajko commented Aug 13, 2023

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 Defining zero() seriously #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?

@tpapp
Copy link
Contributor

tpapp commented Aug 13, 2023

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.

@nsajko
Copy link
Contributor

nsajko commented Aug 13, 2023

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

@tpapp
Copy link
Contributor

tpapp commented Aug 14, 2023

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.

nsajko added a commit to nsajko/julia that referenced this issue Aug 23, 2024
@mikmoore
Copy link
Contributor

mikmoore commented Aug 26, 2024

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?

@tpapp
Copy link
Contributor

tpapp commented Aug 26, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
maths Mathematical functions
Projects
None yet
Development

Successfully merging a pull request may close this issue.