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

round(n; sigdigits) without explicit units #326

Open
mcabbott opened this issue May 11, 2020 · 11 comments
Open

round(n; sigdigits) without explicit units #326

mcabbott opened this issue May 11, 2020 · 11 comments

Comments

@mcabbott
Copy link
Contributor

mcabbott commented May 11, 2020

While being able to round in a unit-controlled way is neat (and should perhaps be encouraged n the docs), what's the reason to disallow simple rounding? I expected the second of these to work, and if it did, then code with & without units would behave the same:

len = (120//1)u"cm"
round(u"m", len, sigdigits=2)

tim = 3.0000001u"s"
round(tim, sigdigits=3) # ERROR: specify the type of the quantity to convert to when rounding

Edit: for sigdigits in particular, the objection that the rounding depends on your units does not apply, these agree:

tim_ns = tim |> u"ns"
round(ustrip(tim), sigdigits=3) * unit(tim)
round(ustrip(tim_ns), sigdigits=3) * unit(tim_ns)
@sostock
Copy link
Collaborator

sostock commented Oct 23, 2021

for sigdigits in particular, the objection that the rounding depends on your units does not apply, these agree:

tim_ns = tim |> u"ns"
round(ustrip(tim), sigdigits=3) * unit(tim)
round(ustrip(tim_ns), sigdigits=3) * unit(tim_ns)

They only agree because you are rounding in base 10 and s/ns = 10^9 is a power of 10 as well. For other units, they don’t agree:

julia> time_hr = 1.23456*u"hr"
1.23456 hr

julia> time_s = time_hr |> u"s"
4444.416 s

julia> round(ustrip(time_s), sigdigits=3) * unit(time_s)
4440.0 s

julia> round(ustrip(time_hr), sigdigits=3) * unit(time_hr)
1.23 hr

julia> ans |> u"s"       # 4440 s != 4428 s
4428.0 s

@mcabbott
Copy link
Contributor Author

mcabbott commented Oct 23, 2021

Sure. The more refined version of this is #328 (comment) : After declaring that you care about 3 digits, how surprising is it really that things don't agree beyond that?

("3 digits" is, of course, a crude way of saying "between 1 part in 10^2 and 1 in 10^3". It's specifying a logarithmic cutoff in linear space, sort-of. The error bar on the number of digits is about 1.)

@sostock
Copy link
Collaborator

sostock commented Oct 23, 2021

After declaring that you care about 3 digits, how surprising is it really that things don't agree beyond that?

I would be surprised if round(12345, sigdigits=3) != round(1.2345e4, sigdigits=3), because both are the same value, just different types. For me, the same applies to objects that have the same values, but different units.

However, I understand the desire for “unitless rounding” for the purpose of printing less digits (cf. #474), because we don’t have a nice way to do that (like @printf for unitless numbers) and it is clearly wanted/needed (cf. #474, #460). But IMO, the better answer to that would be to implement some formatting functions, not to add round methods that are not invariant under unit conversion.

@mcabbott
Copy link
Contributor Author

mcabbott commented Oct 23, 2021

But when you say "same value, just different types", what does "same" mean? If we are talking about physical quantities, with units, then it does not mean infinite precision, surely.

Even if we just have floating point numbers, we also have finite precision, and thus cannot trust that rounding commutes with changes of type:

julia> x = nextfloat(2.5);

julia> Float32(round(x, sigdigits=1))
3.0f0

julia> round(Float32(x), sigdigits=1)
2.0f0

However, these will "almost always" agree, where the meaning of "almost" depends on the precision we specified.

@sostock
Copy link
Collaborator

sostock commented Oct 23, 2021

Yes, floating-point numbers have finite precision and can introduce arbitrarily large errors. However, I don’t think we should give up unit-invariant arithmetic just because floating-point numbers can break it.

If we are talking about physical quantities, with units, then it does not mean infinite precision, surely.

I think we fundamentally disagree on that. I think physical quantities can have values with infinite precision. Measurements cannot, of course, but for example the speed of light is defined as exactly 299,792,458 m/s, with infinite precision.

In the example above, the numbers 12345 and 1.2345e4 are actually equal, with infinite precision, because 12345 can be exactly represented by a Float64. On the other hand x = nextfloat(2.5) cannot be exactly represented by a Float32, so x and Float32(x) are different numbers and are allowed to round to different numbers.

Generally, I would want exact unit-invariance when using exact arithmetic (i.e., when using an exact type like Rational 1, and also when using floats if the correct result can be exactly represented by the type).

However, these will "almost always" agree, where the meaning of "almost" depends on the precision we specified.

That’s where our disagreement may stem from. I don’t think of specifying sigdigits as specifying precision of the output. For a number with infinite precision, rounding with sigdigits should be well-defined and return a number of infinite precision. I view this issue as largely decoupled from floating-point precision.

Footnotes

  1. Admittedly, this does not even hold for unitless numbers right now, because round(::Rational, sigdigits) returns a floating-point number. I think that’s unfortunate, especially since round(::Rational) without sigdigits returns a Rational.

@KronosTheLate
Copy link
Contributor

I feel like perfect is the enemy of the good here.

When I want to round a Quantity, if I do not specify the output unit, I expect it to be assumed to be the input unit. It is just an extremely sensible default, and the only sensible default, as we would never want the output to have a different unit than the input by default.

The issues that arise with inequality when changing the order of rounding and unit conversion is a bummer, but there is no way around it - the information is lost during rounding. I honestly think that you are crazy if you expect strict commutativity between rounding and unit conversion, and that you would be foolish to use rounding and unit conversion without extreme care if you are doing anything important. And in the extreme care case, simply specify the output unit! But please allow the quick-and-dirty "let me remove some visual noise" rounding to be done without specifying the output unit, when such a sensible default exists.

@KronosTheLate
Copy link
Contributor

As for requiring the keyword argument sigdigits, I do not see why it should not default to the defaults from Base.round. If the user wants anything else they are free to specify it, but consistency with Base is really something that reduces friction in the ecosystem, and something that I greatly appreciate.

@david-macmahon
Copy link

I'm writing a Plots recipe and would like to support user specified rounding for tick labels, but because of this issue it ends up being somewhat complicated:

julia> x = 1.2345678
1.2345678

julia> xs = x * 1u"s"
1.2345678 s

julia> round(x, digits=2)
1.23

julia> round(xs, digits=2)
ERROR: specify the type of the quantity to convert to when rounding quantities. Example: round(typeof(1u"m"), 137u"cm").

julia> round(typeof(xs), xs, digits=2)
1.23 s

julia> round(typeof(x), x, digits=2)
ERROR: MethodError: no method matching round(::Type{Float64}, ::Float64; digits=2)

Because of this issue, I have to create my own rounding function that has different methods for Quantity types vs other types. Is that really the recommended way to handle this? Maybe the real question here is why does Julia's round not support rounding Float64 to Float64???

@david-macmahon
Copy link

I just realized that to create my own rounding function with a method for values of type Quantity means that I have to depend on Unitful rather than having the user's Quantity values "just work" without my package having to depend on Unitful. It seems like composability is being sacrificed for some purity-of-units concept that I don't understand. I don't think anyone expects rounding seconds to two decimal places to give the same result as rounding hours to two decimal places.

@michikawa07
Copy link
Contributor

It is just an extremely sensible default, and the only sensible default, as we would never want the output to have a different unit than the input by default.

I strongly agree with this opinion.
The default unit being the unit of input follows intuition and feels very natural.

@Lightup1
Copy link

neat solution:
Just add the following code before your main code

using Unitful
import Base.round
function round(x::Quantity;kwargs...)
    round(typeof(x),x;kwargs...)
end

It should be added inside Unitful.jl I think

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

No branches or pull requests

6 participants