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
Show file tree
Hide file tree
Changes from 54 commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
a491992
Define `AbstractQuantity <: Number`
MilesCranmer Sep 19, 2023
69f3fd5
Remove defiitions for `Missing`
MilesCranmer Sep 19, 2023
a8f25f7
Create AbstractUnionQuantity for multiple abstract quantities
MilesCranmer Sep 24, 2023
1410e05
Create GenericQuantity
MilesCranmer Sep 24, 2023
cd1dd2e
Update array to allow generic quantities
MilesCranmer Sep 25, 2023
37cb7e6
Fix some method ambiguities
MilesCranmer Sep 25, 2023
b282ad1
Start separate file for ambiguities
MilesCranmer Sep 25, 2023
66113e2
Fix ambiguity in AbstractDimensions + Quantity
MilesCranmer Sep 25, 2023
670ab84
Fix ambiguity in powers
MilesCranmer Sep 25, 2023
3e920f9
Fix ambiguity in constructors
MilesCranmer Sep 25, 2023
7491a93
Fix ambiguity in getindex
MilesCranmer Sep 25, 2023
613b2f0
Move other methods to ambiguities
MilesCranmer Sep 25, 2023
b735283
Rename to `disambiguities`
MilesCranmer Sep 25, 2023
37289c8
Fix ambiguities in promotion rules
MilesCranmer Sep 25, 2023
d1414ea
Merge branch 'main' into union-type
gaurav-arya Oct 15, 2023
810ed86
Export generic quantity
gaurav-arya Oct 15, 2023
561ba11
Merge branch 'main' into union-type
MilesCranmer Oct 17, 2023
679057c
Add docstrings for generic quantities
MilesCranmer Oct 22, 2023
61b35bf
More types on missing
MilesCranmer Oct 22, 2023
042d3eb
Prevent invalid dimension conversions
MilesCranmer Oct 22, 2023
d24d3ac
Remove old missing test
MilesCranmer Oct 22, 2023
4e8b750
Fix string test for local version
MilesCranmer Oct 22, 2023
f38d630
Fix method hierarchy in constructors
MilesCranmer Oct 22, 2023
913228b
Add GenericQuantity tests
MilesCranmer Oct 22, 2023
d68b041
Revert "Remove old missing test"
MilesCranmer Oct 22, 2023
22d12d0
Fix rounding behavior
MilesCranmer Oct 22, 2023
ad2ed1c
Remove overkill promotion function
MilesCranmer Oct 22, 2023
cc519ea
Formatting
MilesCranmer Oct 22, 2023
72addf4
Add missing promotion utility between symbolic and regular
MilesCranmer Oct 22, 2023
6b1607c
Merge branch 'prevent-invalid-conversions' into union-type
MilesCranmer Oct 22, 2023
bca03f2
Make SymbolicDimensions conversion work for union type
MilesCranmer Oct 22, 2023
b6b3245
Test promotion with both symbolic/non and quantity/generic
MilesCranmer Oct 22, 2023
0567e47
Remove unused constructors
MilesCranmer Oct 22, 2023
1d85f85
Formatting
MilesCranmer Oct 22, 2023
9ac4df0
More generic quantity tests
MilesCranmer Oct 22, 2023
b20998b
Add promotion rule for generic numbers
MilesCranmer Oct 22, 2023
c49c648
Add explicit promote_type tests
MilesCranmer Oct 22, 2023
097cd31
Test conversion to `Vector{Any}`
MilesCranmer Oct 22, 2023
3a73997
Merge branch 'main' into union-type
MilesCranmer Oct 22, 2023
f437e6e
Add docs for union quantity
MilesCranmer Oct 23, 2023
bc5a9f4
Add docs on `GenericQuantity`
MilesCranmer Oct 23, 2023
21c9823
Clean up docs
MilesCranmer Oct 23, 2023
8267824
Refactor + and - definitions
MilesCranmer Oct 23, 2023
0c68bb9
More docs
MilesCranmer Oct 23, 2023
ddd4894
Add more examples
MilesCranmer Oct 23, 2023
452d18c
Add projectile motion example
MilesCranmer Oct 23, 2023
c495b83
Split out projectile motion example
MilesCranmer Oct 23, 2023
a65d8ba
Add custom dimensions example
MilesCranmer Oct 24, 2023
78b87ef
Fix documentation number
MilesCranmer Oct 24, 2023
aa60738
Expand unittest coverage
MilesCranmer Oct 24, 2023
9ebd6bc
Example of custom quantity
MilesCranmer Oct 24, 2023
5248189
Add example of `uconvert` on `GenericQuantity`
MilesCranmer Oct 24, 2023
f8b6e98
Improve cookie example
MilesCranmer Oct 24, 2023
3bb9452
Refactor internal utilities
MilesCranmer Oct 28, 2023
ce9d527
Add compatibility for generic quantities in measurements
MilesCranmer Oct 28, 2023
d02b466
Add GenericQuantity to array tests
MilesCranmer Oct 28, 2023
1f0b39d
More GenericQuantity tests
MilesCranmer Oct 28, 2023
3faa4ee
Ensure convert(Number, q) is tested
MilesCranmer Oct 28, 2023
e9e7b8c
Apply suggestions from code review
MilesCranmer Oct 28, 2023
697037a
Reformat tests
MilesCranmer Oct 28, 2023
108aada
Refactor QuantityArray for GenericQuantity
MilesCranmer Oct 29, 2023
46abb61
Fix documentation build
MilesCranmer Oct 29, 2023
68ff3c1
Clean up quantity array docs
MilesCranmer Oct 29, 2023
cc28197
Update name to UnionAbstractQuantity
MilesCranmer Oct 31, 2023
a401e86
Update fieldname check from code review
MilesCranmer Nov 1, 2023
2a1c7ad
Update internal_utils.jl
MilesCranmer Nov 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
351 changes: 351 additions & 0 deletions docs/src/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ On your chemistry homework, you are faced with the following problem on the phot
> The energy of the incident UV light is ``7.2 \cdot 10^{-19} \mathrm{J}`` per photon. Calculate the wavelength of the ejected electrons, in nanometers.

Let's solve this problem with `DynamicQuantities.jl`!

```jldoctest examples
julia> using DynamicQuantities

Expand All @@ -30,6 +31,356 @@ julia> λ = h / p # wavelength of ejected electrons
julia> uconvert(us"nm", λ) # return answer in nanometers
3.0294912478780556 nm
```

Since units are automatically propagated, we can verify the dimension of our answer and all intermediates.
Also, using `DynamicQuantities.Constants`, we were able to obtain the (dimensionful!) values of all necessary constants without typing them ourselves.

## 2. Projectile motion

Let's solve a simple projectile motion problem.
First load the `DynamicQuantities` module:

```julia
using DynamicQuantities
```

Set up initial conditions as quantities:

```julia
y0 = 10u"km"
v0 = 250u"m/s"
θ = deg2rad(60)
g = 9.81u"m/s^2"
```

Next, we use trig functions to calculate x and y components of initial velocity.
`vx0` is the x component and
`vy0` is the y component:

```julia
vx0 = v0 * cos(θ)
vy0 = v0 * sin(θ)
```

Next, let's create a time vector from 0 to 1.3 minutes.
MilesCranmer marked this conversation as resolved.
Show resolved Hide resolved
Note that these are the same dimension (time), so it's fine to treat
them as dimensionally equivalent!

```julia
t = range(0u"s", 1.3u"min", length=100)
```

Next, use kinematic equations to calculate x and y as a function of time.
`x(t)` is x position at time t, and
MilesCranmer marked this conversation as resolved.
Show resolved Hide resolved
`y(t)` is the y position
MilesCranmer marked this conversation as resolved.
Show resolved Hide resolved

```julia
x(t) = vx0*t
y(t) = vy0*t - 0.5*g*t^2 + y0
```

These are functions, so let's evaluate them:

```julia
x_si = x.(t)
y_si = y.(t)
```

These are regular vectors of quantities
with `Dimensions` for physical dimensions.

Next, let's plot the trajectory.
First convert to km and strip units:

```julia
x_km = ustrip.(uconvert(us"km").(x_si))
y_km = ustrip.(uconvert(us"km").(y_si))
```

Now, we plot:

```julia
plot(x_km, y_km, label="Trajectory", xlabel="x [km]", ylabel="y [km]")
```

## 3. Various Simple Examples

This section demonstrates miscellaneous examples of using `DynamicQuantities.jl`.

### Conversion

Convert a quantity to have a new type for the value:

```julia
quantity = 1.5u"m"

convert_q = Quantity{Float32}(quantity)

println("Converted Quantity to Float32: ", convert_q)
```

### Array basics

Create a `QuantityArray` (an array of quantities with
the same dimension) by passing an array and a single quantity:

```julia
x = QuantityArray(randn(32), u"km/s")
```

or, by passing an array of individual quantities:

```julia
y = randn(32)
y_q = QuantityArray(y .* u"m * cd / s")
```

We can take advantage of this being `<:AbstractArray`:

```julia
println("Sum x: ", sum(x))
```

We can also do things like setting a particular element:

```julia
y_q[5] = Quantity(5, length=1, luminosity=1, time=-1)
println("5th element of y_q: ", y_q[5])
```

We can get back the original array with `ustrip`:

```julia
println("Stripped y_q: ", ustrip(y_q))
```

This `QuantityArray` is useful for broadcasting:

```julia
f_square(v) = v^2 * 1.5 - v^2
println("Applying function to y_q: ", sum(f_square.(y_q)))
```

### Fill

We can also make `QuantityArray` using `fill`:

```julia
filled_q = fill(u"m/s", 10)
println("Filled QuantityArray: ", filled_q)
```

`fill` works for 0 dimensional `QuantityArray`s as well:

```julia
empty_q = fill(u"m/s", ())
println("0 dimensional QuantityArray: ", empty_q)
```

### Similar

Likewise, we can create a `QuantityArray` with the same properties as another `QuantityArray`:

```julia
qa = QuantityArray(rand(3, 4), u"m")

new_qa = similar(qa)

println("Similar qa: ", new_qa)
```

### Promotion

Promotion rules are defined for `QuantityArray`s:

```julia
qarr1 = QuantityArray(randn(32), convert(Dimensions{Rational{Int32}}, dimension(u"km/s")))
qarr2 = QuantityArray(randn(Float16, 32), convert(Dimensions{Rational{Int64}}, dimension(u"km/s")))
```

See what type they promote to:

```julia
println("Promoted type: ", typeof(promote(qarr1, qarr2)))
```

### Array Concatenation

Likewise, we can take advantage of array concatenation,
which will ensure we have the same dimensions:

```julia
qarr1 = QuantityArray(randn(3) .* u"km/s")
qarr2 = QuantityArray(randn(3) .* u"km/s")
```

Concatenate them:

```julia
concat_qarr = hcat(qarr1, qarr2)
println("Concatenated QuantityArray: ", concat_qarr)
```

### Symbolic Units

We can use arbitrary `AbstractQuantity` and `AbstractDimensions`
in a `QuantityArray`, including `SymbolicDimensions`:

```julia
z_ar = randn(32)
z = QuantityArray(z_ar, us"Constants.M_sun * km/s")
```

Expand to standard units:

```julia
z_expanded = uexpand(z)
println("Expanded z: ", z_expanded)
```


### GenericQuantity Construction

In addition to `Quantity`, we can also use `GenericQuantity`:


```julia
x = GenericQuantity(1.5)
y = GenericQuantity(0.2u"km")
println(y)
```

This `GenericQuantity` is subtyped to `Any`,
rather than `Number`, and thus can also store
custom non-scalar types.

For example, we can work with `Coords`, and
wrap it in a single `GenericQuantity` type:

```julia
struct Coords
x::Float64
y::Float64
end

# Define arithmetic operations on Coords
Base.:+(a::Coords, b::Coords) = Coords(a.x + b.x, a.y + b.y)
Base.:-(a::Coords, b::Coords) = Coords(a.x - b.x, a.y - b.y)
Base.:*(a::Coords, b::Number) = Coords(a.x * b, a.y * b)
Base.:*(a::Number, b::Coords) = Coords(a * b.x, a * b.y)
Base.:/(a::Coords, b::Number) = Coords(a.x / b, a.y / b)
```

We can then build a `GenericQuantity` out of this:

```julia
coord1 = GenericQuantity(Coords(0.3, 0.9), length=1)
coord2 = GenericQuantity(Coords(0.2, -0.1), length=1)
```

and perform operations on these:

```julia
coord1 + coord2 |> uconvert(us"cm")
# (Coords(50.0, 80.0)) cm
```

The nice part about this is it only stores a single Dimensions
(or `SymbolicDimensions`) for the entire struct!

### GenericQuantity and Quantity Promotion

When we combine a `GenericQuantity` and a `Quantity`,
the result is another `GenericQuantity`:

```julia
x = GenericQuantity(1.5f0)
y = Quantity(1.5, length=1)
println("Promoted type of x and y: ", typeof(x * y))
```

### Custom Dimensions

We can create custom dimensions by subtyping to
`AbstractDimensions`:

```julia
struct MyDimensions{R} <: AbstractDimensions{R}
cookie::R
milk::R
end
```

Many constructors and functions are defined on `AbstractDimensions`,
so this can be used out-of-the-box.
We can then use this in a `Quantity`, and all operations will work as expected:

```julia
x = Quantity(1.5, MyDimensions(cookie=1, milk=-1))
y = Quantity(2.0, MyDimensions(milk=1))

x * y
```

which gives us `3.0 cookie` computed from a rate of `1.5 cookie milk⁻¹` multiplied
by `2.0 milk`. Likewise, we can use these in a `QuantityArray`:

```julia
x_qa = QuantityArray(randn(32), MyDimensions(cookie=1, milk=-1))

x_qa .^ 2
```

### Custom Quantities

We can also create custom dimensions by subtyping
to either `AbstractQuantity` (for `<:Number`) or
`AbstractGenericQuantity` (for `<:Any`):

```julia
struct MyQuantity{T,D} <: AbstractQuantity{T,D}
value::T
dimensions::D
end
```

Since `AbstractQuantity <: Number`, this will also be a number.
Keep in mind that you must call these fields `value` and `dimensions`
for `ustrip(...)` and `dimension(...)` to work. Otherwise, simply
redefine those.

We can use this custom quantity just like we would use `Quantity`:

```julia
q1 = MyQuantity(1.2, Dimensions(length=-2))
# prints as `1.2 m⁻²`

q2 = MyQuantity(1.5, MyDimensions(cookie=1))
# prints as `1.5 cookie`
```

Including mathematical operations:

```julia
q2 ^ 2
# `2.25 cookie²`
```

The main reason you would use a custom quantity is if you want
to change built-in behavior, or maybe have special methods for
different types of quantities.

Note that you can declare a method on `AbstractQuantity`, or
`AbstractGenericQuantity` to allow their respective inputs.

**Note**: In general, you should probably
specialize on `AbstractUnionQuantity` which is
the union of these two abstract quantities, _as well as any other future abstract quantity types_,
MilesCranmer marked this conversation as resolved.
Show resolved Hide resolved
such as the planned `AbstractRealQuantity`.

```julia
function my_func(x::AbstractUnionQuantity{T,D}) where {T,D}
# value has type T and dimensions has type D
return x / ustrip(x)
end
```
12 changes: 12 additions & 0 deletions docs/src/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,15 @@ SymbolicDimensions
```@docs
QuantityArray
```

## Generic quantities

Whereas `Quantity` is subtyped to `Number`,
a more general type of quantity is `GenericQuantity`,
which is subtyped to `Any`.

```@docs
GenericQuantity
AbstractGenericQuantity
AbstractUnionQuantity
```
4 changes: 2 additions & 2 deletions ext/DynamicQuantitiesLinearAlgebraExt.jl
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
module DynamicQuantitiesLinearAlgebraExt

import LinearAlgebra: norm
import DynamicQuantities: AbstractQuantity, ustrip, dimension, new_quantity
import DynamicQuantities: AbstractUnionQuantity, ustrip, dimension, new_quantity

norm(q::AbstractQuantity, p::Real=2) = new_quantity(typeof(q), norm(ustrip(q), p), dimension(q))
norm(q::AbstractUnionQuantity, p::Real=2) = new_quantity(typeof(q), norm(ustrip(q), p), dimension(q))

end
Loading