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

Support dispatches on Number via union type #49

Merged
merged 66 commits into from
Nov 1, 2023
Merged

Conversation

MilesCranmer
Copy link
Member

@MilesCranmer MilesCranmer commented Sep 25, 2023

This fixes #44 and refactors much of the internals to support two abstract quantities:

  • AbstractQuantity{T,D} <: Number. Same name, but now only Number
  • AbstractGenericQuantity{T,D}. New name, supports Any

It does this by creating UnionAbstractQuantity which is a union of these two types:

const UnionAbstractQuantity{T,D} = Union{AbstractQuantity{T,D},AbstractGenericQuantity{T,D}}

There is also now GenericQuantity as a built-in AbstractGenericQuantity, similar to how Quantity is the built-in AbstractQuantity.

In principle one could also add RealQuantity <: AbstractRealQuantity and so on. I chose to not add those yet until someone needs it.

@gaurav-arya what do you think?


The downside of this PR is that it hurts readability due to the use of @eval (required to avoid method ambiguities, and if we don't want to manually write a hundred extra methods...)

@github-actions
Copy link

github-actions bot commented Sep 25, 2023

Benchmark Results

main 2a1c7ad... t[main]/t[2a1c7ad...]
Quantity/creation/Quantity(x) 4.3 ± 0 ns 4.3 ± 0 ns 1
Quantity/creation/Quantity(x, length=y) 4.3 ± 0 ns 4.3 ± 0 ns 1
Quantity/with_numbers/*real 3.9 ± 0 ns 3.9 ± 0 ns 1
Quantity/with_numbers/^int 13.2 ± 3.9 ns 13.2 ± 3.9 ns 1
Quantity/with_numbers/^int * real 14.3 ± 4.1 ns 14.1 ± 4.3 ns 1.01
Quantity/with_quantity/+y 6.5 ± 0 ns 6.5 ± 0 ns 1
Quantity/with_quantity//y 4.7 ± 0.1 ns 4.9 ± 0 ns 0.959
Quantity/with_self/dimension 2 ± 0.1 ns 2 ± 0.1 ns 1
Quantity/with_self/inv 4.3 ± 0 ns 4.7 ± 0 ns 0.915
Quantity/with_self/ustrip 2 ± 0.1 ns 2 ± 0.1 ns 1
QuantityArray/broadcasting/multi_array_of_quantities 0.355 ± 0.18 ms 0.371 ± 0.2 ms 0.956
QuantityArray/broadcasting/multi_normal_array 0.104 ± 0.017 ms 0.0987 ± 0.029 ms 1.05
QuantityArray/broadcasting/multi_quantity_array 0.343 ± 0.062 ms 0.341 ± 0.06 ms 1.01
QuantityArray/broadcasting/x^2_array_of_quantities 0.0753 ± 0.034 ms 0.0749 ± 0.026 ms 1.01
QuantityArray/broadcasting/x^2_normal_array 12.9 ± 1.2 μs 12.9 ± 1.1 μs 1
QuantityArray/broadcasting/x^2_quantity_array 13.9 ± 1.1 μs 13.8 ± 1.3 μs 1.01
QuantityArray/broadcasting/x^4_array_of_quantities 0.184 ± 0.095 ms 0.185 ± 0.088 ms 0.995
QuantityArray/broadcasting/x^4_normal_array 0.0936 ± 0.016 ms 0.0907 ± 0.013 ms 1.03
QuantityArray/broadcasting/x^4_quantity_array 0.118 ± 0.016 ms 0.111 ± 0.019 ms 1.06
time_to_load 0.248 ± 0.031 s 0.312 ± 0.047 s 0.797

Benchmark Plots

A plot of the benchmark results have been uploaded as an artifact to the workflow run for this PR.
Go to "Actions"->"Benchmark a pull request"->[the most recent run]->"Artifacts" (at the bottom).

Copy link
Collaborator

@gaurav-arya gaurav-arya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an extremely cool PR: I don't know of any other number type packages that have made the effort to work around the MyStruct{T} <: T conundrum. (the closest might be symbolics having both generic variables and a special Num <: Real?).

I have admittedly not had time yet to review line by line, but some general comments:

Naming: TL;DR everything seems fine:) I wondered whether AbstractUnionQuantity should really be AbstractQuantity. However, I think it's important for it to be clear that this is a union type (and can't e.g. be subtyped), so I think you've already made the right choice. Relatedly, should AbstractQuantity <: Number really be AbstractNumberQuantity <: Number. I'm fine with your choice here too: special casing Number as the shortest name / default type seems reasonable, as that's what Unitful worked with. Presumably future additions would look like AbstractRealQuantity <: Real etc.

Code readability: I don't think code readability is hurt that much in this PR (the net increase in code lines in impressively small). Further, I hope in a future PR to substantially reduce the code size of e.g.math.jl by classifying the overloads into their maybe 3 or 4 different kinds and then having a simple loop over functions for each (or a convenience macro to define the overload). #64 is also somewhat related. That would hopefully increase readability further, as what we're seeing now is the multiplicative explosion of the looping logic together with the original repeated-overload complexity.

Unit parsing: Right now, AFAIU unit strings are always parsed as an instance of Quantity. This will be an interesting thing to revisit if were to want e.g. an AbstractRealQuantity in the future, as we may want 1.0 * u"m" <: Real for example. But I don't think there's any issue right now.

src/utils.jl Outdated Show resolved Hide resolved
@MilesCranmer
Copy link
Member Author

Awesome, thanks so much @gaurav-arya!!

Relatedly, should AbstractQuantity <: Number really be AbstractNumberQuantity <: Number. I'm fine with your choice here too: special casing Number as the shortest name / default type seems reasonable, as that's what Unitful worked with. Presumably future additions would look like AbstractRealQuantity <: Real etc.

Exactly my thoughts as well. i.e., keeping it AbstractQuantity just for Unitful compatibility. We could even have AbstractNumberQuantity as a synonym for it if needed, in case someone is confused why there is AbstractRealQuantity but no Number equivalent.

Further, I hope in a future PR to substantially reduce the code size of e.g.math.jl by classifying the overloads into their maybe 3 or 4 different kinds and then having a simple loop over functions for each (or a convenience macro to define the overload).

Great idea! We could use TypedDelegation.jl for this too if needed.

Unit parsing: Right now, AFAIU unit strings are always parsed as an instance of Quantity. This will be an interesting thing to revisit if were to want e.g. an AbstractRealQuantity in the future, as we may want 1.0 * u"m" <: Real for example. But I don't think there's any issue right now.

Sounds good, we can bring this part up again later!

@MilesCranmer
Copy link
Member Author

Not sure what’s going on with the automated benchmarks, but if I run the benchmarks locally it seems to have no effect on the performance. Although I’m using 1.10 locally so perhaps there’s some difference on 1.9...

Copy link
Collaborator

@gaurav-arya gaurav-arya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some small comments:)

docs/src/examples.md Outdated Show resolved Hide resolved
docs/src/examples.md Outdated Show resolved Hide resolved
docs/src/examples.md Outdated Show resolved Hide resolved
docs/src/examples.md Show resolved Hide resolved
src/symbolic_dimensions.jl Outdated Show resolved Hide resolved
@gaurav-arya
Copy link
Collaborator

The performance regression on 1.9 appears to be real:

julia> using DynamicQuantities

julia> q = 1u"m"
1.0 m

julia> @btime inv($q)
  254.961 ns (8 allocations: 768 bytes)

src/types.jl Outdated Show resolved Hide resolved
@MilesCranmer
Copy link
Member Author

I fixed the performance regression. Looks like it was just something that the Julia compiler was able to inline on Julia 1.10 but not the earlier versions. I just moved it to a generated function now.

@MilesCranmer
Copy link
Member Author

MilesCranmer commented Oct 29, 2023

Thanks for the review! Here are the changes since your last pass over: 3bb9452...union-type

Regarding test coverage, it is good to worry about but I'm not that worried because GenericQuantity should be the exact same as Quantity. The only cases we need to worry about are: (1) where both GenericQuantity and Quantity can be used as inputs, like where there are two AbstractUnionQuantity arguments, and (2) where Quantity is the default for something (we should have a GenericQuantity alternative). I just added another field to ABSTRACT_QUANTITY_TYPES to set the default concrete type in such cases so its easier to loop.

Also, just to be safe, I'm looping over both quantity types in a lot of the unittests now, particularly the basic tests and the QuantityArray tests.

@MilesCranmer
Copy link
Member Author

MilesCranmer commented Oct 29, 2023

Btw, should it be UnionAbstractQuantity instead? Because we want to have AbstractXQuantity be subtyped to X, it might have better semantics, since it’s not an abstract type itself.

Also, maybe AbstractAnyQuantity and AnyQuantity instead of Generic - wdyt?

@MilesCranmer
Copy link
Member Author

Okay I changed it to UnionAbstractQuantity. However I'm going to leave the generic one as GenericQuantity since AnyQuantity makes me think it is an Any, which is bad.

Any last comments or do you think it's good to go?

Copy link
Collaborator

@gaurav-arya gaurav-arya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the slow review -- nearly all looks good to me! I just have a note about the fieldnames check.

src/internal_utils.jl Outdated Show resolved Hide resolved
src/internal_utils.jl Outdated Show resolved Hide resolved
@MilesCranmer
Copy link
Member Author

Thanks for the reviews!!

@MilesCranmer MilesCranmer merged commit 13998ee into main Nov 1, 2023
7 checks passed
@MilesCranmer MilesCranmer deleted the union-type branch November 1, 2023 20:24
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

Successfully merging this pull request may close these issues.

Support dispatches on Number, Real, etc.
2 participants