From 55a5d135cdc0232ec0a39717f9f49f2de350d14f Mon Sep 17 00:00:00 2001 From: Romeo Valentin Date: Sun, 19 Nov 2023 18:01:56 -0800 Subject: [PATCH 1/8] Define helpers for unitful interfaces. This commit adds two simple functions `similar_dims` (and `similar_units`) that return a `Quantity` type with the dimensions (and units) constrained to those of the parameters I found myself trying to write simulation code with strongly typed interfaces, i.e. including information about the units. Initially I wrote my interfaces like so: ```julia const Meters = typeof(1.0m); circumference_of_circle(r::Meters) = pi*r^2 ``` However, when trying to autodiff through this code, I run into a problem, because `Meters` has the numerical type `Float64` baked in, and autodiff evaluates on a type `Quantity{Dual{Float64}}` (roughly). We can instead define `Meters` like so: ```julia const Meters{T<:Real} = Quantity{T, dimension(1.0m), unit(1.0m)} circumference_of_circle(r::Meters{T}) where {T} = pi*r^2 circumference_of_circle(r::Quantity{T, dimension(1.0m), unit(1.0m)}) where {T} = pi*r^2 ``` but I thought a better approach would be to provide some syntactic sugar to this "unit constraint". With this PR, we can write ```julia circumference_of_circle(r::similar_dims(u"m")) where {T} = pi*r^2 circumference_of_circle(r::similar_units(u"m")) where {T} = pi*r^2 ``` The difference is that the first one only constrains the dimension, and the latter constrains both dimension and unit (i.e. doesn't allow e.g. `km`). I'm happy to receive any feedback on the idea and the naming. Other names could be e.g. `quantity_with_dims` (but too long for my taste), or `dims_as` etc., but `similar` is already Julia lingo and feels appropriate in this context. --- src/Unitful.jl | 1 + src/utils.jl | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/Unitful.jl b/src/Unitful.jl index e8919035..02c35639 100644 --- a/src/Unitful.jl +++ b/src/Unitful.jl @@ -27,6 +27,7 @@ import LinearAlgebra: istril, istriu, norm import Random export logunit, unit, absoluteunit, dimension, uconvert, ustrip, upreferred +export similar_units, similar_dims export @dimension, @derived_dimension, @refunit, @unit, @affineunit, @u_str export Quantity, DimensionlessQuantity, NoUnits, NoDims diff --git a/src/utils.jl b/src/utils.jl index 5d717015..13887215 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -243,6 +243,49 @@ struct DimensionError <: Exception y end +""" + similar_dims(q::Quantity) + similar_dims(u::Units) +Returns a type of [`Unitful.Quantity`](@ref) with the dimensions contrained to the +dimension of `q` or `u`. +Useful to build unitful interfaces that don't contrain the numeric type of the specific unit. + +Examples: + +```jldoctest +julia> circumference_of_square(side::similar_dims(u"m")) = 4*side; +julia> circumference_of_square((1//2)m) # works +2//1 m +julia> circumference_of_square((1//2)km) # also works +2//1 km +``` + +See also [`Unitful.similar_units`](@ref). +""" +similar_dims(q::Quantity) = Quantity{T, dimension(q), U} where {T<:Real, U<:Unitlike} +similar_dims(u::Units) = Quantity{T, dimension(u), U} where {T<:Real, U<:Unitlike} + +""" + similar_units(q::Quantity) + similar_units(u::Units) +Returns a type of [`Unitful.Quantity`](@ref) with the dimensions and units contrained to the +dimension and units of `q` or `u`. +Useful to build unitful interfaces that don't contrain the numeric type. + +Examples: + +```jldoctest +julia> circumference_of_square(side::similar_units(u"m")) = 4*side; +julia> circumference_of_square((1//2)m) # works +2//1 m +julia> # circumference_of_square((1//2)km) # doesn't work, constrained to exactly meters +``` + +See also [`Unitful.similar_dims`](@ref). +""" +similar_units(q::Quantity) = Quantity{T, dimension(q), unit(q)} where {T<:Real} +similar_units(u::Units) = Quantity{T, dimension(u), typeof(u)} where {T<:Real} + Base.showerror(io::IO, e::DimensionError) = print(io, "DimensionError: $(e.x) and $(e.y) are not dimensionally compatible."); From 74a73d16261634bcefc4838fb8b78925ca7bcdfb Mon Sep 17 00:00:00 2001 From: Romeo Valentin Date: Sun, 19 Nov 2023 18:28:51 -0800 Subject: [PATCH 2/8] Write some tests for unitful interfaces. --- test/runtests.jl | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/runtests.jl b/test/runtests.jl index 525462a3..648419e7 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2071,6 +2071,7 @@ end Base.:+(a::Num, b::Num) = Num(a.x + b.x) Base.:-(a::Num, b::Num) = Num(a.x - b.x) Base.:*(a::Num, b::Num) = Num(a.x * b.x) +Base.:<(a::Num, b::Num) = a.x < b.x Base.promote_rule(::Type{Num}, ::Type{<:Real}) = Num Base.ArithmeticStyle(::Type{Num}) = Base.ArithmeticRounds() Base.OrderStyle(::Type{Num}) = Base.Unordered() @@ -2079,6 +2080,18 @@ Base.OrderStyle(::Type{Num}) = Base.Unordered() # Test that @generated functions work with Quantities + custom types (#231) @test uconvert(u"°C", Num(373.15)u"K") == Num(100)u"°C" end +area_of_circle(radius::similar_dims(u"m")) = pi*radius^2 +area_of_square(side::similar_units(u"m")) = side^2 + +@testset "Unitful interfaces" begin + @test area_of_circle(Num(1.0)u"m") ≈ pi*m^2 + @test area_of_circle(Num(1.0)u"km") ≈ pi*km^2 + @test_throws MethodError area_of_circle(Num(1.0)u"s") + + @test area_of_square(Num(0.5)u"m") ≈ 0.25*m^2 + @test_throws MethodError area_of_square(Num(0.5)u"km") + @test_throws MethodError area_of_square(Num(0.5)u"s") +end @testset "Traits" begin @testset "> ArithmeticStyle" begin From a746d5c53c16d2f658755dc029a2269441434296 Mon Sep 17 00:00:00 2001 From: Romeo Valentin Date: Sun, 19 Nov 2023 23:23:12 -0800 Subject: [PATCH 3/8] Rename ``similar_dims` to `WithDims`, same for units --- src/Unitful.jl | 2 +- src/utils.jl | 24 ++++++++++++------------ test/runtests.jl | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Unitful.jl b/src/Unitful.jl index 02c35639..60290498 100644 --- a/src/Unitful.jl +++ b/src/Unitful.jl @@ -27,7 +27,7 @@ import LinearAlgebra: istril, istriu, norm import Random export logunit, unit, absoluteunit, dimension, uconvert, ustrip, upreferred -export similar_units, similar_dims +export WithUnits, WithDims export @dimension, @derived_dimension, @refunit, @unit, @affineunit, @u_str export Quantity, DimensionlessQuantity, NoUnits, NoDims diff --git a/src/utils.jl b/src/utils.jl index 13887215..cb6cf2fc 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -244,8 +244,8 @@ struct DimensionError <: Exception end """ - similar_dims(q::Quantity) - similar_dims(u::Units) + WithDims(q::Quantity) + WithDims(u::Units) Returns a type of [`Unitful.Quantity`](@ref) with the dimensions contrained to the dimension of `q` or `u`. Useful to build unitful interfaces that don't contrain the numeric type of the specific unit. @@ -253,21 +253,21 @@ Useful to build unitful interfaces that don't contrain the numeric type of the s Examples: ```jldoctest -julia> circumference_of_square(side::similar_dims(u"m")) = 4*side; +julia> circumference_of_square(side::WithDims(u"m")) = 4*side; julia> circumference_of_square((1//2)m) # works 2//1 m julia> circumference_of_square((1//2)km) # also works 2//1 km ``` -See also [`Unitful.similar_units`](@ref). +See also [`Unitful.WithUnits`](@ref). """ -similar_dims(q::Quantity) = Quantity{T, dimension(q), U} where {T<:Real, U<:Unitlike} -similar_dims(u::Units) = Quantity{T, dimension(u), U} where {T<:Real, U<:Unitlike} +WithDims(q::Quantity) = Quantity{T, dimension(q), U} where {T<:Real, U<:Unitlike} +WithDims(u::Units) = Quantity{T, dimension(u), U} where {T<:Real, U<:Unitlike} """ - similar_units(q::Quantity) - similar_units(u::Units) + WithUnits(q::Quantity) + WithUnits(u::Units) Returns a type of [`Unitful.Quantity`](@ref) with the dimensions and units contrained to the dimension and units of `q` or `u`. Useful to build unitful interfaces that don't contrain the numeric type. @@ -275,16 +275,16 @@ Useful to build unitful interfaces that don't contrain the numeric type. Examples: ```jldoctest -julia> circumference_of_square(side::similar_units(u"m")) = 4*side; +julia> circumference_of_square(side::WithUnits(u"m")) = 4*side; julia> circumference_of_square((1//2)m) # works 2//1 m julia> # circumference_of_square((1//2)km) # doesn't work, constrained to exactly meters ``` -See also [`Unitful.similar_dims`](@ref). +See also [`Unitful.WithDims`](@ref). """ -similar_units(q::Quantity) = Quantity{T, dimension(q), unit(q)} where {T<:Real} -similar_units(u::Units) = Quantity{T, dimension(u), typeof(u)} where {T<:Real} +WithUnits(q::Quantity) = Quantity{T, dimension(q), unit(q)} where {T<:Real} +WithUnits(u::Units) = Quantity{T, dimension(u), typeof(u)} where {T<:Real} Base.showerror(io::IO, e::DimensionError) = print(io, "DimensionError: $(e.x) and $(e.y) are not dimensionally compatible."); diff --git a/test/runtests.jl b/test/runtests.jl index 648419e7..a92cc5f2 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2080,8 +2080,8 @@ Base.OrderStyle(::Type{Num}) = Base.Unordered() # Test that @generated functions work with Quantities + custom types (#231) @test uconvert(u"°C", Num(373.15)u"K") == Num(100)u"°C" end -area_of_circle(radius::similar_dims(u"m")) = pi*radius^2 -area_of_square(side::similar_units(u"m")) = side^2 +area_of_circle(radius::WithDims(u"m")) = pi*radius^2 +area_of_square(side::WithUnits(u"m")) = side^2 @testset "Unitful interfaces" begin @test area_of_circle(Num(1.0)u"m") ≈ pi*m^2 From c5b82180671f6d09608e4bb1b9eb8239b8281774 Mon Sep 17 00:00:00 2001 From: Romeo Valentin Date: Sun, 3 Dec 2023 22:27:33 -0800 Subject: [PATCH 4/8] Fix some typos. --- src/utils.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index cb6cf2fc..57b6cad4 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -246,9 +246,9 @@ end """ WithDims(q::Quantity) WithDims(u::Units) -Returns a type of [`Unitful.Quantity`](@ref) with the dimensions contrained to the +Returns a subtype of [`Unitful.Quantity`](@ref) with the dimensions constrained to the dimension of `q` or `u`. -Useful to build unitful interfaces that don't contrain the numeric type of the specific unit. +Useful to build unitful interfaces that don't constrain the numeric type or the unit, just the dimension of a quantity. Examples: @@ -262,15 +262,15 @@ julia> circumference_of_square((1//2)km) # also works See also [`Unitful.WithUnits`](@ref). """ -WithDims(q::Quantity) = Quantity{T, dimension(q), U} where {T<:Real, U<:Unitlike} -WithDims(u::Units) = Quantity{T, dimension(u), U} where {T<:Real, U<:Unitlike} +WithDims(q::Quantity) = Quantity{T, dimension(q), U} where {T<:Real, U<:Unitlike} +WithDims(u::Units) = Quantity{T, dimension(u), U} where {T<:Real, U<:Unitlike} """ WithUnits(q::Quantity) WithUnits(u::Units) -Returns a type of [`Unitful.Quantity`](@ref) with the dimensions and units contrained to the +Returns a subtype of [`Unitful.Quantity`](@ref) with the dimensions and units constrained to the dimension and units of `q` or `u`. -Useful to build unitful interfaces that don't contrain the numeric type. +Useful to build unitful interfaces that don't constrain the unit, but not the numeric type of a quantity. Examples: From 15aa3eb8447cba0f8c59401d67bff3efca1d4de3 Mon Sep 17 00:00:00 2001 From: Romeo Valentin Date: Sun, 3 Dec 2023 22:51:09 -0800 Subject: [PATCH 5/8] Rewrite and expand test cases. Rewrite the tests as type-checks only as suggested in the PR review. Co-authored-by: Sebastian Stock <42280794+sostock@users.noreply.github.com> --- test/runtests.jl | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index a92cc5f2..ae6d8fd5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2071,7 +2071,6 @@ end Base.:+(a::Num, b::Num) = Num(a.x + b.x) Base.:-(a::Num, b::Num) = Num(a.x - b.x) Base.:*(a::Num, b::Num) = Num(a.x * b.x) -Base.:<(a::Num, b::Num) = a.x < b.x Base.promote_rule(::Type{Num}, ::Type{<:Real}) = Num Base.ArithmeticStyle(::Type{Num}) = Base.ArithmeticRounds() Base.OrderStyle(::Type{Num}) = Base.Unordered() @@ -2080,17 +2079,23 @@ Base.OrderStyle(::Type{Num}) = Base.Unordered() # Test that @generated functions work with Quantities + custom types (#231) @test uconvert(u"°C", Num(373.15)u"K") == Num(100)u"°C" end -area_of_circle(radius::WithDims(u"m")) = pi*radius^2 -area_of_square(side::WithUnits(u"m")) = side^2 -@testset "Unitful interfaces" begin - @test area_of_circle(Num(1.0)u"m") ≈ pi*m^2 - @test area_of_circle(Num(1.0)u"km") ≈ pi*km^2 - @test_throws MethodError area_of_circle(Num(1.0)u"s") - - @test area_of_square(Num(0.5)u"m") ≈ 0.25*m^2 - @test_throws MethodError area_of_square(Num(0.5)u"km") - @test_throws MethodError area_of_square(Num(0.5)u"s") +@testset "WithDims, WithUnits" begin + # built-in types + @test 1m isa WithDims(m) + @test 1.0m isa WithDims(m) + @test 1//1m isa WithDims(m) + @test 1.0m isa WithUnits(m) + # user-defined types + @test Num(1.0)m isa WithDims(m) + @test Num(1.0)km isa WithDims(m) + @test !(Num(1.0)s isa WithDims(m)) + @test Num(1.0)m isa WithUnits(m) + @test !(Num(1.0)km isa WithUnits(m)) + @test !(Num(1.0)s isa WithUnits(m)) + # composite units + @test 1kg*(1.0m/s)^2 isa WithDims(J) + @test 1kg*(1.0m/s)^2 isa WithUnits(kg*m^2/s^2) end @testset "Traits" begin From 6d28e4cc3cef1d74135b66f2659c5872da7262f1 Mon Sep 17 00:00:00 2001 From: Romeo Valentin Date: Sun, 3 Dec 2023 22:54:45 -0800 Subject: [PATCH 6/8] Include one more example in docstring. --- src/utils.jl | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index 57b6cad4..52e5ad38 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -253,11 +253,16 @@ Useful to build unitful interfaces that don't constrain the numeric type or the Examples: ```jldoctest -julia> circumference_of_square(side::WithDims(u"m")) = 4*side; +julia> using Unitful, Unitful.DefaultSymbols; import Unitful.hr +julia> circumference_of_square(side::WithDims(m)) = 4*side; julia> circumference_of_square((1//2)m) # works 2//1 m julia> circumference_of_square((1//2)km) # also works 2//1 km +# You can also constrain the return type. The numeric type is usually inferred automatically. +julia> kinetic_energy(mass::WithDims(kg), velocity::WithDims(m/s))::WithDims(J) = mass*velocity^2; +julia> kinetic_energy(1000kg, 100km/hr) +10000000 kg km^2 hr^-2 ``` See also [`Unitful.WithUnits`](@ref). @@ -275,10 +280,16 @@ Useful to build unitful interfaces that don't constrain the unit, but not the nu Examples: ```jldoctest -julia> circumference_of_square(side::WithUnits(u"m")) = 4*side; +julia> using Unitful, Unitful.DefaultSymbols; import Unitful.hr +julia> circumference_of_square(side::WithUnits(m)) = 4*side; julia> circumference_of_square((1//2)m) # works 2//1 m julia> # circumference_of_square((1//2)km) # doesn't work, constrained to exactly meters + +# You can also constrain the return type. The numeric type is usually inferred automatically. +julia> kinetic_energy(mass::WithUnits(kg), velocity::WithUnits(m/s))::WithUnits(J) = mass*velocity^2 |> x->uconvert(J, x) +julia> kinetic_energy(1000kg, uconvert(m/s, 100km/hr)) +62500000//81 J ``` See also [`Unitful.WithDims`](@ref). From f4888db768393abaad26a0a2fbe585ac27cbe69e Mon Sep 17 00:00:00 2001 From: Romeo Valentin Date: Wed, 5 Jun 2024 23:33:16 -0700 Subject: [PATCH 7/8] Bump to version 2.0 so that Local Registry prefers this one. --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 1e46baa2..66104d29 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "Unitful" uuid = "1986cc42-f94f-5a68-af5c-568840ba703d" -version = "1.18.0" +version = "2.0.0" [deps] ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9" From fd687cabed41bc95438d9e36da24153a8fc884b8 Mon Sep 17 00:00:00 2001 From: Romeo Valentin Date: Wed, 5 Jun 2024 23:57:13 -0700 Subject: [PATCH 8/8] Try to go back to Unitful 1 for compat. --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 66104d29..772b35ed 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "Unitful" uuid = "1986cc42-f94f-5a68-af5c-568840ba703d" -version = "2.0.0" +version = "1.9.0" [deps] ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9"