From 1befe743f0ff65672a6442d8e0fbdecba73c858f Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Mon, 12 Jun 2023 07:47:10 -0400 Subject: [PATCH 01/28] Make `AbstractQuantity` and `AbstractDimensions` --- src/DynamicQuantities.jl | 1 + src/math.jl | 68 ++++++++++++++-------------- src/types.jl | 9 +++- src/utils.jl | 98 ++++++++++++++++++++-------------------- 4 files changed, 92 insertions(+), 84 deletions(-) diff --git a/src/DynamicQuantities.jl b/src/DynamicQuantities.jl index 0fe164fc..a636b171 100644 --- a/src/DynamicQuantities.jl +++ b/src/DynamicQuantities.jl @@ -1,5 +1,6 @@ module DynamicQuantities +export AbstractQuantity, AbstractDimensions export Quantity, Dimensions, DimensionError, ustrip, dimension, valid export ulength, umass, utime, ucurrent, utemperature, uluminosity, uamount export uparse, @u_str diff --git a/src/math.jl b/src/math.jl index d68ef524..7594c659 100644 --- a/src/math.jl +++ b/src/math.jl @@ -1,41 +1,41 @@ -Base.:*(l::Dimensions, r::Dimensions) = @map_dimensions(+, l, r) -Base.:*(l::Quantity, r::Quantity) = Quantity(l.value * r.value, l.dimensions * r.dimensions) -Base.:*(l::Quantity, r::Dimensions) = Quantity(l.value, l.dimensions * r) -Base.:*(l::Dimensions, r::Quantity) = Quantity(r.value, l * r.dimensions) -Base.:*(l::Quantity, r) = Quantity(l.value * r, l.dimensions) -Base.:*(l, r::Quantity) = Quantity(l * r.value, r.dimensions) -Base.:*(l::Dimensions, r) = Quantity(r, l) -Base.:*(l, r::Dimensions) = Quantity(l, r) +Base.:*(l::AbstractDimensions, r::AbstractDimensions) = @map_dimensions(+, l, r) +Base.:*(l::AbstractQuantity, r::AbstractQuantity) = quantity(ustrip(l) * ustrip(r), dimension(l) * dimension(r)) +Base.:*(l::AbstractQuantity, r::AbstractDimensions) = quantity(ustrip(l), dimension(l) * r) +Base.:*(l::AbstractDimensions, r::AbstractQuantity) = quantity(ustrip(r), l * dimension(r)) +Base.:*(l::AbstractQuantity, r) = quantity(ustrip(l) * r, dimension(l)) +Base.:*(l, r::AbstractQuantity) = quantity(l * ustrip(r), dimension(r)) +Base.:*(l::AbstractDimensions, r) = quantity(r, l) +Base.:*(l, r::AbstractDimensions) = quantity(l, r) -Base.:/(l::Dimensions, r::Dimensions) = @map_dimensions(-, l, r) -Base.:/(l::Quantity, r::Quantity) = Quantity(l.value / r.value, l.dimensions / r.dimensions) -Base.:/(l::Quantity, r::Dimensions) = Quantity(l.value, l.dimensions / r) -Base.:/(l::Dimensions, r::Quantity) = Quantity(inv(r.value), l / r.dimensions) -Base.:/(l::Quantity, r) = Quantity(l.value / r, l.dimensions) -Base.:/(l, r::Quantity) = l * inv(r) -Base.:/(l::Dimensions, r) = Quantity(inv(r), l) -Base.:/(l, r::Dimensions) = Quantity(l, inv(r)) +Base.:/(l::AbstractDimensions, r::AbstractDimensions) = @map_dimensions(-, l, r) +Base.:/(l::AbstractQuantity, r::AbstractQuantity) = quantity(ustrip(l) / ustrip(r), dimension(l) / dimension(r)) +Base.:/(l::AbstractQuantity, r::AbstractDimensions) = quantity(ustrip(l), dimension(l) / r) +Base.:/(l::AbstractDimensions, r::AbstractQuantity) = quantity(inv(ustrip(r)), l / dimension(r)) +Base.:/(l::AbstractQuantity, r) = quantity(ustrip(l) / r, dimension(l)) +Base.:/(l, r::AbstractQuantity) = l * inv(r) +Base.:/(l::AbstractDimensions, r) = quantity(inv(r), l) +Base.:/(l, r::AbstractDimensions) = quantity(l, inv(r)) -Base.:+(l::Quantity, r::Quantity) = dimension(l) == dimension(r) ? Quantity(l.value + r.value, l.dimensions) : throw(DimensionError(l, r)) -Base.:-(l::Quantity) = Quantity(-l.value, l.dimensions) -Base.:-(l::Quantity, r::Quantity) = l + (-r) +Base.:+(l::AbstractQuantity, r::AbstractQuantity) = dimension(l) == dimension(r) ? quantity(ustrip(l) + ustrip(r), dimension(l)) : throw(DimensionError(l, r)) +Base.:-(l::AbstractQuantity) = quantity(-ustrip(l), dimension(l)) +Base.:-(l::AbstractQuantity, r::AbstractQuantity) = l + (-r) -Base.:+(l::Quantity, r) = dimension(l) == dimension(r) ? Quantity(l.value + r, l.dimensions) : throw(DimensionError(l, r)) -Base.:+(l, r::Quantity) = dimension(l) == dimension(r) ? Quantity(l + r.value, r.dimensions) : throw(DimensionError(l, r)) -Base.:-(l::Quantity, r) = l + (-r) -Base.:-(l, r::Quantity) = l + (-r) +Base.:+(l::AbstractQuantity, r) = dimension(l) == dimension(r) ? quantity(ustrip(l) + r, dimension(l)) : throw(DimensionError(l, r)) +Base.:+(l, r::AbstractQuantity) = dimension(l) == dimension(r) ? quantity(l + ustrip(r), dimension(r)) : throw(DimensionError(l, r)) +Base.:-(l::AbstractQuantity, r) = l + (-r) +Base.:-(l, r::AbstractQuantity) = l + (-r) -_pow(l::Dimensions{R}, r::R) where {R} = @map_dimensions(Base.Fix1(*, r), l) -_pow(l::Quantity{T,R}, r::R) where {T,R} = Quantity(l.value^convert(T, r), _pow(l.dimensions, r)) -Base.:^(l::Dimensions{R}, r::Number) where {R} = _pow(l, tryrationalize(R, r)) -Base.:^(l::Quantity{T,R}, r::Number) where {T,R} = _pow(l, tryrationalize(R, r)) +_pow(l::AbstractDimensions{R}, r::R) where {R} = @map_dimensions(Base.Fix1(*, r), l) +_pow(l::AbstractQuantity{T,R}, r::R) where {T,R} = quantity(ustrip(l)^convert(T, r), _pow(dimension(l), r)) +Base.:^(l::AbstractDimensions{R}, r::Number) where {R} = _pow(l, tryrationalize(R, r)) +Base.:^(l::AbstractQuantity{T,R}, r::Number) where {T,R} = _pow(l, tryrationalize(R, r)) -Base.inv(d::Dimensions) = @map_dimensions(-, d) -Base.inv(q::Quantity) = Quantity(inv(q.value), inv(q.dimensions)) +Base.inv(d::AbstractDimensions) = @map_dimensions(-, d) +Base.inv(q::AbstractQuantity) = quantity(inv(ustrip(q)), inv(dimension(q))) -Base.sqrt(d::Dimensions{R}) where {R} = d^inv(convert(R, 2)) -Base.sqrt(q::Quantity) = Quantity(sqrt(q.value), sqrt(q.dimensions)) -Base.cbrt(d::Dimensions{R}) where {R} = d^inv(convert(R, 3)) -Base.cbrt(q::Quantity) = Quantity(cbrt(q.value), cbrt(q.dimensions)) +Base.sqrt(d::AbstractDimensions{R}) where {R} = d^inv(convert(R, 2)) +Base.sqrt(q::AbstractQuantity) = quantity(sqrt(ustrip(q)), sqrt(dimension(q))) +Base.cbrt(d::AbstractDimensions{R}) where {R} = d^inv(convert(R, 3)) +Base.cbrt(q::AbstractQuantity) = quantity(cbrt(ustrip(q)), cbrt(dimension(q))) -Base.abs(q::Quantity) = Quantity(abs(q.value), q.dimensions) +Base.abs(q::AbstractQuantity) = quantity(abs(ustrip(q)), dimension(q)) diff --git a/src/types.jl b/src/types.jl index c21ab666..3bffb5f8 100644 --- a/src/types.jl +++ b/src/types.jl @@ -1,6 +1,9 @@ const DEFAULT_DIM_TYPE = FixedRational{Int32, 2^4 * 3^2 * 5^2 * 7} const DEFAULT_VALUE_TYPE = Float64 +abstract type AbstractQuantity{T,R} end +abstract type AbstractDimensions{R} end + """ Dimensions @@ -18,7 +21,7 @@ example, the dimensions of velocity are `Dimensions(length=1, time=-1)`. - `luminosity`: luminosity dimension (i.e., cd^(luminosity)) - `amount`: amount dimension (i.e., mol^(amount)) """ -struct Dimensions{R <: Real} +struct Dimensions{R<:Real} <: AbstractDimensions{R} length::R mass::R time::R @@ -73,7 +76,7 @@ including `*`, `+`, `-`, `/`, `^`, `sqrt`, and `cbrt`. - `value::T`: value of the quantity of some type `T` - `dimensions::Dimensions`: dimensions of the quantity """ -struct Quantity{T, R} +struct Quantity{T,R} <: AbstractQuantity{T,R} value::T dimensions::Dimensions{R} @@ -84,6 +87,8 @@ struct Quantity{T, R} Quantity{T,R}(q::Quantity) where {T,R} = Quantity(convert(T, q.value), Dimensions{R}(dimension(q))) end +quantity(l, r) = Quantity(l, r) + struct DimensionError{Q1,Q2} <: Exception q1::Q1 q2::Q2 diff --git a/src/utils.jl b/src/utils.jl index 9d2c5473..5a9893bf 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -23,27 +23,28 @@ macro all_dimensions(f, l...) return output |> esc end -Base.float(q::Quantity{T}) where {T<:AbstractFloat} = convert(T, q) -Base.convert(::Type{T}, q::Quantity) where {T<:Real} = +Base.float(q::AbstractQuantity{T}) where {T<:AbstractFloat} = convert(T, q) +Base.convert(::Type{T}, q::AbstractQuantity) where {T<:Real} = let @assert iszero(q.dimensions) "Quantity $(q) has dimensions! Use `ustrip` instead." return convert(T, q.value) end -Base.isfinite(q::Quantity) = isfinite(q.value) -Base.keys(::Dimensions) = DIMENSION_NAMES -Base.iszero(d::Dimensions) = @all_dimensions(iszero, d) -Base.iszero(q::Quantity) = iszero(q.value) -Base.getindex(d::Dimensions, k::Symbol) = getfield(d, k) -Base.:(==)(l::Dimensions, r::Dimensions) = @all_dimensions(==, l, r) -Base.:(==)(l::Quantity, r::Quantity) = l.value == r.value && l.dimensions == r.dimensions -Base.isapprox(l::Quantity, r::Quantity; kws...) = isapprox(l.value, r.value; kws...) && l.dimensions == r.dimensions -Base.length(::Dimensions) = 1 -Base.length(::Quantity) = 1 -Base.iterate(d::Dimensions) = (d, nothing) -Base.iterate(::Dimensions, ::Nothing) = nothing -Base.iterate(q::Quantity) = (q, nothing) -Base.iterate(::Quantity, ::Nothing) = nothing +Base.isfinite(q::AbstractQuantity) = isfinite(ustrip(q)) +Base.keys(::D) where {D<:AbstractDimensions} = DIMENSION_NAMES +# TODO: Make this more generic. +Base.iszero(d::AbstractDimensions) = @all_dimensions(iszero, d) +Base.iszero(q::AbstractQuantity) = iszero(ustrip(q)) +Base.getindex(d::AbstractDimensions, k::Symbol) = getfield(d, k) +Base.:(==)(l::AbstractDimensions, r::AbstractDimensions) = @all_dimensions(==, l, r) +Base.:(==)(l::AbstractQuantity, r::AbstractQuantity) = ustrip(l) == ustrip(r) && dimension(l) == dimension(r) +Base.isapprox(l::AbstractQuantity, r::AbstractQuantity; kws...) = isapprox(ustrip(l), ustrip(r); kws...) && dimension(l) == dimension(r) +Base.length(::AbstractDimensions) = 1 +Base.length(::AbstractQuantity) = 1 +Base.iterate(d::AbstractDimensions) = (d, nothing) +Base.iterate(::AbstractDimensions, ::Nothing) = nothing +Base.iterate(q::AbstractQuantity) = (q, nothing) +Base.iterate(::AbstractQuantity, ::Nothing) = nothing # Multiplicative identities: Base.one(::Type{Quantity{T,R}}) where {T,R} = Quantity(one(T), R) @@ -66,7 +67,7 @@ Base.oneunit(::Dimensions) = error("There is no such thing as a dimensionful 1 f Base.oneunit(::Type{<:Quantity}) = error("Cannot create a dimensionful 1 for a `Quantity` type without knowing the dimensions. Please use `oneunit(::Quantity)` instead.") Base.oneunit(::Type{<:Dimensions}) = error("There is no such thing as a dimensionful 1 for a `Dimensions` type, as + is only defined for `Quantity`.") -Base.show(io::IO, d::Dimensions) = +Base.show(io::IO, d::AbstractDimensions) = let tmp_io = IOBuffer() for k in keys(d) if !iszero(d[k]) @@ -80,7 +81,7 @@ Base.show(io::IO, d::Dimensions) = s = replace(s, r"\s*$" => "") print(io, s) end -Base.show(io::IO, q::Quantity) = print(io, q.value, " ", q.dimensions) +Base.show(io::IO, q::AbstractQuantity) = print(io, ustrip(q), " ", dimension(q)) string_rational(x) = isinteger(x) ? string(round(Int, x)) : string(x) pretty_print_exponent(io::IO, x) = print(io, to_superscript(string_rational(x))) @@ -110,8 +111,8 @@ Base.convert(::Type{Dimensions{R}}, d::Dimensions) where {R} = Dimensions{R}(d) Remove the units from a quantity. """ -ustrip(q::Quantity) = q.value -ustrip(::Dimensions) = error("Cannot remove units from a `Dimensions` object.") +ustrip(q::AbstractQuantity) = q.value +ustrip(::AbstractDimensions) = error("Cannot remove units from a `Dimensions` object.") ustrip(q) = q """ @@ -119,69 +120,70 @@ ustrip(q) = q Get the dimensions of a quantity, returning a `Dimensions` object. """ -dimension(q::Quantity) = q.dimensions -dimension(d::Dimensions) = d +dimension(q::AbstractQuantity) = q.dimensions +dimension(d::AbstractDimensions) = d dimension(_) = Dimensions() +# TODO: Should we throw an error instead, because this is assuming a type? """ - ulength(q::Quantity) - ulength(d::Dimensions) + ulength(q::AbstractQuantity) + ulength(d::AbstractDimensions) Get the length dimension of a quantity (e.g., meters^(ulength)). """ -ulength(q::Quantity) = ulength(dimension(q)) -ulength(d::Dimensions) = d.length +ulength(q::AbstractQuantity) = ulength(dimension(q)) +ulength(d::AbstractDimensions) = d.length """ - umass(q::Quantity) - umass(d::Dimensions) + umass(q::AbstractQuantity) + umass(d::AbstractDimensions) Get the mass dimension of a quantity (e.g., kg^(umass)). """ -umass(q::Quantity) = umass(dimension(q)) -umass(d::Dimensions) = d.mass +umass(q::AbstractQuantity) = umass(dimension(q)) +umass(d::AbstractDimensions) = d.mass """ - utime(q::Quantity) - utime(d::Dimensions) + utime(q::AbstractQuantity) + utime(d::AbstractDimensions) Get the time dimension of a quantity (e.g., s^(utime)) """ -utime(q::Quantity) = utime(dimension(q)) -utime(d::Dimensions) = d.time +utime(q::AbstractQuantity) = utime(dimension(q)) +utime(d::AbstractDimensions) = d.time """ - ucurrent(q::Quantity) - ucurrent(d::Dimensions) + ucurrent(q::AbstractQuantity) + ucurrent(d::AbstractDimensions) Get the current dimension of a quantity (e.g., A^(ucurrent)). """ -ucurrent(q::Quantity) = ucurrent(dimension(q)) -ucurrent(d::Dimensions) = d.current +ucurrent(q::AbstractQuantity) = ucurrent(dimension(q)) +ucurrent(d::AbstractDimensions) = d.current """ utemperature(q::Quantity) - utemperature(d::Dimensions) + utemperature(d::AbstractDimensions) Get the temperature dimension of a quantity (e.g., K^(utemperature)). """ -utemperature(q::Quantity) = utemperature(dimension(q)) -utemperature(d::Dimensions) = d.temperature +utemperature(q::AbstractQuantity) = utemperature(dimension(q)) +utemperature(d::AbstractDimensions) = d.temperature """ uluminosity(q::Quantity) - uluminosity(d::Dimensions) + uluminosity(d::AbstractDimensions) Get the luminosity dimension of a quantity (e.g., cd^(uluminosity)). """ -uluminosity(q::Quantity) = uluminosity(dimension(q)) -uluminosity(d::Dimensions) = d.luminosity +uluminosity(q::AbstractQuantity) = uluminosity(dimension(q)) +uluminosity(d::AbstractDimensions) = d.luminosity """ - uamount(q::Quantity) - uamount(d::Dimensions) + uamount(q::AbstractQuantity) + uamount(d::AbstractDimensions) Get the amount dimension of a quantity (e.g., mol^(uamount)). """ -uamount(q::Quantity) = uamount(dimension(q)) -uamount(d::Dimensions) = d.amount +uamount(q::AbstractQuantity) = uamount(dimension(q)) +uamount(d::AbstractDimensions) = d.amount From a09c51241bc4e5513aff21c95086210c30287779 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Mon, 12 Jun 2023 07:48:40 -0400 Subject: [PATCH 02/28] Fix docstrings for abstract types --- src/utils.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index 5a9893bf..de44e82e 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -107,7 +107,7 @@ Base.convert(::Type{Dimensions}, d::Dimensions) = d Base.convert(::Type{Dimensions{R}}, d::Dimensions) where {R} = Dimensions{R}(d) """ - ustrip(q::Quantity) + ustrip(q::AbstractQuantity) Remove the units from a quantity. """ @@ -116,7 +116,7 @@ ustrip(::AbstractDimensions) = error("Cannot remove units from a `Dimensions` ob ustrip(q) = q """ - dimension(q::Quantity) + dimension(q::AbstractQuantity) Get the dimensions of a quantity, returning a `Dimensions` object. """ @@ -162,7 +162,7 @@ ucurrent(q::AbstractQuantity) = ucurrent(dimension(q)) ucurrent(d::AbstractDimensions) = d.current """ - utemperature(q::Quantity) + utemperature(q::AbstractQuantity) utemperature(d::AbstractDimensions) Get the temperature dimension of a quantity (e.g., K^(utemperature)). @@ -171,7 +171,7 @@ utemperature(q::AbstractQuantity) = utemperature(dimension(q)) utemperature(d::AbstractDimensions) = d.temperature """ - uluminosity(q::Quantity) + uluminosity(q::AbstractQuantity) uluminosity(d::AbstractDimensions) Get the luminosity dimension of a quantity (e.g., cd^(uluminosity)). From 645726c3a55c0cab85202203592ddff28786a415 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Mon, 12 Jun 2023 08:09:15 -0400 Subject: [PATCH 03/28] Implement new `quantity` function --- src/math.jl | 44 ++++++++++++++++++++++---------------------- src/types.jl | 3 ++- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/math.jl b/src/math.jl index 7594c659..744b4f04 100644 --- a/src/math.jl +++ b/src/math.jl @@ -1,41 +1,41 @@ Base.:*(l::AbstractDimensions, r::AbstractDimensions) = @map_dimensions(+, l, r) -Base.:*(l::AbstractQuantity, r::AbstractQuantity) = quantity(ustrip(l) * ustrip(r), dimension(l) * dimension(r)) -Base.:*(l::AbstractQuantity, r::AbstractDimensions) = quantity(ustrip(l), dimension(l) * r) -Base.:*(l::AbstractDimensions, r::AbstractQuantity) = quantity(ustrip(r), l * dimension(r)) -Base.:*(l::AbstractQuantity, r) = quantity(ustrip(l) * r, dimension(l)) -Base.:*(l, r::AbstractQuantity) = quantity(l * ustrip(r), dimension(r)) -Base.:*(l::AbstractDimensions, r) = quantity(r, l) -Base.:*(l, r::AbstractDimensions) = quantity(l, r) +Base.:*(l::AbstractQuantity, r::AbstractQuantity) = quantity(typeof(l), ustrip(l) * ustrip(r), dimension(l) * dimension(r)) +Base.:*(l::AbstractQuantity, r::AbstractDimensions) = quantity(typeof(l), ustrip(l), dimension(l) * r) +Base.:*(l::AbstractDimensions, r::AbstractQuantity) = quantity(typeof(r), ustrip(r), l * dimension(r)) +Base.:*(l::AbstractQuantity, r) = quantity(typeof(l), ustrip(l) * r, dimension(l)) +Base.:*(l, r::AbstractQuantity) = quantity(typeof(r), l * ustrip(r), dimension(r)) +Base.:*(l::AbstractDimensions, r) = quantity(typeof(l), r, l) +Base.:*(l, r::AbstractDimensions) = quantity(typeof(r), l, r) Base.:/(l::AbstractDimensions, r::AbstractDimensions) = @map_dimensions(-, l, r) -Base.:/(l::AbstractQuantity, r::AbstractQuantity) = quantity(ustrip(l) / ustrip(r), dimension(l) / dimension(r)) -Base.:/(l::AbstractQuantity, r::AbstractDimensions) = quantity(ustrip(l), dimension(l) / r) -Base.:/(l::AbstractDimensions, r::AbstractQuantity) = quantity(inv(ustrip(r)), l / dimension(r)) -Base.:/(l::AbstractQuantity, r) = quantity(ustrip(l) / r, dimension(l)) +Base.:/(l::AbstractQuantity, r::AbstractQuantity) = quantity(typeof(l), ustrip(l) / ustrip(r), dimension(l) / dimension(r)) +Base.:/(l::AbstractQuantity, r::AbstractDimensions) = quantity(typeof(l), ustrip(l), dimension(l) / r) +Base.:/(l::AbstractDimensions, r::AbstractQuantity) = quantity(typeof(r), inv(ustrip(r)), l / dimension(r)) +Base.:/(l::AbstractQuantity, r) = quantity(typeof(l), ustrip(l) / r, dimension(l)) Base.:/(l, r::AbstractQuantity) = l * inv(r) -Base.:/(l::AbstractDimensions, r) = quantity(inv(r), l) -Base.:/(l, r::AbstractDimensions) = quantity(l, inv(r)) +Base.:/(l::AbstractDimensions, r) = quantity(typeof(l), inv(r), l) +Base.:/(l, r::AbstractDimensions) = quantity(typeof(r), l, inv(r)) -Base.:+(l::AbstractQuantity, r::AbstractQuantity) = dimension(l) == dimension(r) ? quantity(ustrip(l) + ustrip(r), dimension(l)) : throw(DimensionError(l, r)) -Base.:-(l::AbstractQuantity) = quantity(-ustrip(l), dimension(l)) +Base.:+(l::AbstractQuantity, r::AbstractQuantity) = dimension(l) == dimension(r) ? quantity(typeof(l), ustrip(l) + ustrip(r), dimension(l)) : throw(DimensionError(l, r)) +Base.:-(l::AbstractQuantity) = quantity(typeof(l), -ustrip(l), dimension(l)) Base.:-(l::AbstractQuantity, r::AbstractQuantity) = l + (-r) -Base.:+(l::AbstractQuantity, r) = dimension(l) == dimension(r) ? quantity(ustrip(l) + r, dimension(l)) : throw(DimensionError(l, r)) -Base.:+(l, r::AbstractQuantity) = dimension(l) == dimension(r) ? quantity(l + ustrip(r), dimension(r)) : throw(DimensionError(l, r)) +Base.:+(l::AbstractQuantity, r) = dimension(l) == dimension(r) ? quantity(typeof(l), ustrip(l) + r, dimension(l)) : throw(DimensionError(l, r)) +Base.:+(l, r::AbstractQuantity) = dimension(l) == dimension(r) ? quantity(typeof(r), l + ustrip(r), dimension(r)) : throw(DimensionError(l, r)) Base.:-(l::AbstractQuantity, r) = l + (-r) Base.:-(l, r::AbstractQuantity) = l + (-r) _pow(l::AbstractDimensions{R}, r::R) where {R} = @map_dimensions(Base.Fix1(*, r), l) -_pow(l::AbstractQuantity{T,R}, r::R) where {T,R} = quantity(ustrip(l)^convert(T, r), _pow(dimension(l), r)) +_pow(l::AbstractQuantity{T,R}, r::R) where {T,R} = quantity(typeof(l), ustrip(l)^convert(T, r), _pow(dimension(l), r)) Base.:^(l::AbstractDimensions{R}, r::Number) where {R} = _pow(l, tryrationalize(R, r)) Base.:^(l::AbstractQuantity{T,R}, r::Number) where {T,R} = _pow(l, tryrationalize(R, r)) Base.inv(d::AbstractDimensions) = @map_dimensions(-, d) -Base.inv(q::AbstractQuantity) = quantity(inv(ustrip(q)), inv(dimension(q))) +Base.inv(q::AbstractQuantity) = quantity(typeof(q), inv(ustrip(q)), inv(dimension(q))) Base.sqrt(d::AbstractDimensions{R}) where {R} = d^inv(convert(R, 2)) -Base.sqrt(q::AbstractQuantity) = quantity(sqrt(ustrip(q)), sqrt(dimension(q))) +Base.sqrt(q::AbstractQuantity) = quantity(typeof(q), sqrt(ustrip(q)), sqrt(dimension(q))) Base.cbrt(d::AbstractDimensions{R}) where {R} = d^inv(convert(R, 3)) -Base.cbrt(q::AbstractQuantity) = quantity(cbrt(ustrip(q)), cbrt(dimension(q))) +Base.cbrt(q::AbstractQuantity) = quantity(typeof(q), cbrt(ustrip(q)), cbrt(dimension(q))) -Base.abs(q::AbstractQuantity) = quantity(abs(ustrip(q)), dimension(q)) +Base.abs(q::AbstractQuantity) = quantity(typeof(q), abs(ustrip(q)), dimension(q)) diff --git a/src/types.jl b/src/types.jl index 3bffb5f8..b426439b 100644 --- a/src/types.jl +++ b/src/types.jl @@ -87,7 +87,8 @@ struct Quantity{T,R} <: AbstractQuantity{T,R} Quantity{T,R}(q::Quantity) where {T,R} = Quantity(convert(T, q.value), Dimensions{R}(dimension(q))) end -quantity(l, r) = Quantity(l, r) +quantity(::Type{<:Quantity}, l, r) = Quantity(l, r) +quantity(::Type{<:Dimensions}, l, r) = Quantity(l, r) struct DimensionError{Q1,Q2} <: Exception q1::Q1 From 26355cc4649c718261717429357c3e6129d99c30 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Mon, 12 Jun 2023 08:28:16 -0400 Subject: [PATCH 04/28] Further extend abstract interface --- src/math.jl | 52 ++++++++++++++++++++++++++-------------------------- src/types.jl | 6 ++++-- src/utils.jl | 41 ++++++++++++++++++++++------------------- 3 files changed, 52 insertions(+), 47 deletions(-) diff --git a/src/math.jl b/src/math.jl index 744b4f04..7740a61c 100644 --- a/src/math.jl +++ b/src/math.jl @@ -1,41 +1,41 @@ -Base.:*(l::AbstractDimensions, r::AbstractDimensions) = @map_dimensions(+, l, r) -Base.:*(l::AbstractQuantity, r::AbstractQuantity) = quantity(typeof(l), ustrip(l) * ustrip(r), dimension(l) * dimension(r)) -Base.:*(l::AbstractQuantity, r::AbstractDimensions) = quantity(typeof(l), ustrip(l), dimension(l) * r) -Base.:*(l::AbstractDimensions, r::AbstractQuantity) = quantity(typeof(r), ustrip(r), l * dimension(r)) -Base.:*(l::AbstractQuantity, r) = quantity(typeof(l), ustrip(l) * r, dimension(l)) -Base.:*(l, r::AbstractQuantity) = quantity(typeof(r), l * ustrip(r), dimension(r)) -Base.:*(l::AbstractDimensions, r) = quantity(typeof(l), r, l) -Base.:*(l, r::AbstractDimensions) = quantity(typeof(r), l, r) +Base.:*(l::AbstractDimensions, r::AbstractDimensions) = map_dimensions(+, l, r) +Base.:*(l::AbstractQuantity, r::AbstractQuantity) = new_quantity(typeof(l), ustrip(l) * ustrip(r), dimension(l) * dimension(r)) +Base.:*(l::AbstractQuantity, r::AbstractDimensions) = new_quantity(typeof(l), ustrip(l), dimension(l) * r) +Base.:*(l::AbstractDimensions, r::AbstractQuantity) = new_quantity(typeof(r), ustrip(r), l * dimension(r)) +Base.:*(l::AbstractQuantity, r) = new_quantity(typeof(l), ustrip(l) * r, dimension(l)) +Base.:*(l, r::AbstractQuantity) = new_quantity(typeof(r), l * ustrip(r), dimension(r)) +Base.:*(l::AbstractDimensions, r) = new_quantity(typeof(l), r, l) +Base.:*(l, r::AbstractDimensions) = new_quantity(typeof(r), l, r) -Base.:/(l::AbstractDimensions, r::AbstractDimensions) = @map_dimensions(-, l, r) -Base.:/(l::AbstractQuantity, r::AbstractQuantity) = quantity(typeof(l), ustrip(l) / ustrip(r), dimension(l) / dimension(r)) -Base.:/(l::AbstractQuantity, r::AbstractDimensions) = quantity(typeof(l), ustrip(l), dimension(l) / r) -Base.:/(l::AbstractDimensions, r::AbstractQuantity) = quantity(typeof(r), inv(ustrip(r)), l / dimension(r)) -Base.:/(l::AbstractQuantity, r) = quantity(typeof(l), ustrip(l) / r, dimension(l)) +Base.:/(l::AbstractDimensions, r::AbstractDimensions) = map_dimensions(-, l, r) +Base.:/(l::AbstractQuantity, r::AbstractQuantity) = new_quantity(typeof(l), ustrip(l) / ustrip(r), dimension(l) / dimension(r)) +Base.:/(l::AbstractQuantity, r::AbstractDimensions) = new_quantity(typeof(l), ustrip(l), dimension(l) / r) +Base.:/(l::AbstractDimensions, r::AbstractQuantity) = new_quantity(typeof(r), inv(ustrip(r)), l / dimension(r)) +Base.:/(l::AbstractQuantity, r) = new_quantity(typeof(l), ustrip(l) / r, dimension(l)) Base.:/(l, r::AbstractQuantity) = l * inv(r) -Base.:/(l::AbstractDimensions, r) = quantity(typeof(l), inv(r), l) -Base.:/(l, r::AbstractDimensions) = quantity(typeof(r), l, inv(r)) +Base.:/(l::AbstractDimensions, r) = new_quantity(typeof(l), inv(r), l) +Base.:/(l, r::AbstractDimensions) = new_quantity(typeof(r), l, inv(r)) -Base.:+(l::AbstractQuantity, r::AbstractQuantity) = dimension(l) == dimension(r) ? quantity(typeof(l), ustrip(l) + ustrip(r), dimension(l)) : throw(DimensionError(l, r)) -Base.:-(l::AbstractQuantity) = quantity(typeof(l), -ustrip(l), dimension(l)) +Base.:+(l::AbstractQuantity, r::AbstractQuantity) = dimension(l) == dimension(r) ? new_quantity(typeof(l), ustrip(l) + ustrip(r), dimension(l)) : throw(DimensionError(l, r)) +Base.:-(l::AbstractQuantity) = new_quantity(typeof(l), -ustrip(l), dimension(l)) Base.:-(l::AbstractQuantity, r::AbstractQuantity) = l + (-r) -Base.:+(l::AbstractQuantity, r) = dimension(l) == dimension(r) ? quantity(typeof(l), ustrip(l) + r, dimension(l)) : throw(DimensionError(l, r)) -Base.:+(l, r::AbstractQuantity) = dimension(l) == dimension(r) ? quantity(typeof(r), l + ustrip(r), dimension(r)) : throw(DimensionError(l, r)) +Base.:+(l::AbstractQuantity, r) = dimension(l) == dimension(r) ? new_quantity(typeof(l), ustrip(l) + r, dimension(l)) : throw(DimensionError(l, r)) +Base.:+(l, r::AbstractQuantity) = dimension(l) == dimension(r) ? new_quantity(typeof(r), l + ustrip(r), dimension(r)) : throw(DimensionError(l, r)) Base.:-(l::AbstractQuantity, r) = l + (-r) Base.:-(l, r::AbstractQuantity) = l + (-r) -_pow(l::AbstractDimensions{R}, r::R) where {R} = @map_dimensions(Base.Fix1(*, r), l) -_pow(l::AbstractQuantity{T,R}, r::R) where {T,R} = quantity(typeof(l), ustrip(l)^convert(T, r), _pow(dimension(l), r)) +_pow(l::AbstractDimensions{R}, r::R) where {R} = map_dimensions(Base.Fix1(*, r), l) +_pow(l::AbstractQuantity{T,R}, r::R) where {T,R} = new_quantity(typeof(l), ustrip(l)^convert(T, r), _pow(dimension(l), r)) Base.:^(l::AbstractDimensions{R}, r::Number) where {R} = _pow(l, tryrationalize(R, r)) Base.:^(l::AbstractQuantity{T,R}, r::Number) where {T,R} = _pow(l, tryrationalize(R, r)) -Base.inv(d::AbstractDimensions) = @map_dimensions(-, d) -Base.inv(q::AbstractQuantity) = quantity(typeof(q), inv(ustrip(q)), inv(dimension(q))) +Base.inv(d::AbstractDimensions) = map_dimensions(-, d) +Base.inv(q::AbstractQuantity) = new_quantity(typeof(q), inv(ustrip(q)), inv(dimension(q))) Base.sqrt(d::AbstractDimensions{R}) where {R} = d^inv(convert(R, 2)) -Base.sqrt(q::AbstractQuantity) = quantity(typeof(q), sqrt(ustrip(q)), sqrt(dimension(q))) +Base.sqrt(q::AbstractQuantity) = new_quantity(typeof(q), sqrt(ustrip(q)), sqrt(dimension(q))) Base.cbrt(d::AbstractDimensions{R}) where {R} = d^inv(convert(R, 3)) -Base.cbrt(q::AbstractQuantity) = quantity(typeof(q), cbrt(ustrip(q)), cbrt(dimension(q))) +Base.cbrt(q::AbstractQuantity) = new_quantity(typeof(q), cbrt(ustrip(q)), cbrt(dimension(q))) -Base.abs(q::AbstractQuantity) = quantity(typeof(q), abs(ustrip(q)), dimension(q)) +Base.abs(q::AbstractQuantity) = new_quantity(typeof(q), abs(ustrip(q)), dimension(q)) diff --git a/src/types.jl b/src/types.jl index b426439b..c20f253b 100644 --- a/src/types.jl +++ b/src/types.jl @@ -54,6 +54,8 @@ struct Dimensions{R<:Real} <: AbstractDimensions{R} Dimensions{_R}(d::Dimensions) where {_R} = Dimensions{_R}(d.length, d.mass, d.time, d.current, d.temperature, d.luminosity, d.amount) end +new_dimensions(::Type{<:Dimensions}, l...) = Dimensions(l...) + const DIMENSION_NAMES = Base.fieldnames(Dimensions) const DIMENSION_SYNONYMS = (:m, :kg, :s, :A, :K, :cd, :mol) const SYNONYM_MAPPING = NamedTuple(DIMENSION_NAMES .=> DIMENSION_SYNONYMS) @@ -87,8 +89,8 @@ struct Quantity{T,R} <: AbstractQuantity{T,R} Quantity{T,R}(q::Quantity) where {T,R} = Quantity(convert(T, q.value), Dimensions{R}(dimension(q))) end -quantity(::Type{<:Quantity}, l, r) = Quantity(l, r) -quantity(::Type{<:Dimensions}, l, r) = Quantity(l, r) +new_quantity(::Type{<:Quantity}, value, dim) = Quantity(value, dim) +new_quantity(::Type{<:Dimensions}, value, dim) = Quantity(value, dim) struct DimensionError{Q1,Q2} <: Exception q1::Q1 diff --git a/src/utils.jl b/src/utils.jl index de44e82e..5159bed3 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1,26 +1,28 @@ -macro map_dimensions(f, l...) - # Create a new Dimensions object by applying f to each key - output = :(Dimensions()) - for dim in DIMENSION_NAMES - f_expr = :($f()) - for arg in l - push!(f_expr.args, :($arg.$dim)) +@generated function map_dimensions(f::F, args::AbstractDimensions...) where {F<:Function} + dimension_type = first(args) + dimension_names = Base.fieldnames(dimension_type) + output = :(new_dimensions($dimension_type)) + for dim in dimension_names + f_expr = :(f()) + for i=1:length(args) + push!(f_expr.args, :(args[$i].$dim)) end push!(output.args, f_expr) end - return output |> esc + return output end -macro all_dimensions(f, l...) - # Test a function over all dimensions +@generated function all_dimensions(f::F, args::AbstractDimensions...) where {F<:Function} + dimension_type = first(args) + dimension_names = Base.fieldnames(dimension_type) output = Expr(:&&) - for dim in DIMENSION_NAMES - f_expr = :($f()) - for arg in l - push!(f_expr.args, :($arg.$dim)) + for dim in dimension_names + f_expr = :(f()) + for i=1:length(args) + push!(f_expr.args, :(args[$i].$dim)) end push!(output.args, f_expr) end - return output |> esc + return output end Base.float(q::AbstractQuantity{T}) where {T<:AbstractFloat} = convert(T, q) @@ -31,12 +33,12 @@ Base.convert(::Type{T}, q::AbstractQuantity) where {T<:Real} = end Base.isfinite(q::AbstractQuantity) = isfinite(ustrip(q)) -Base.keys(::D) where {D<:AbstractDimensions} = DIMENSION_NAMES +Base.keys(d::AbstractDimensions) = Base.fieldnames(typeof(d)) # TODO: Make this more generic. -Base.iszero(d::AbstractDimensions) = @all_dimensions(iszero, d) +Base.iszero(d::AbstractDimensions) = all_dimensions(iszero, d) Base.iszero(q::AbstractQuantity) = iszero(ustrip(q)) Base.getindex(d::AbstractDimensions, k::Symbol) = getfield(d, k) -Base.:(==)(l::AbstractDimensions, r::AbstractDimensions) = @all_dimensions(==, l, r) +Base.:(==)(l::AbstractDimensions, r::AbstractDimensions) = all_dimensions(==, l, r) Base.:(==)(l::AbstractQuantity, r::AbstractQuantity) = ustrip(l) == ustrip(r) && dimension(l) == dimension(r) Base.isapprox(l::AbstractQuantity, r::AbstractQuantity; kws...) = isapprox(ustrip(l), ustrip(r); kws...) && dimension(l) == dimension(r) Base.length(::AbstractDimensions) = 1 @@ -67,11 +69,12 @@ Base.oneunit(::Dimensions) = error("There is no such thing as a dimensionful 1 f Base.oneunit(::Type{<:Quantity}) = error("Cannot create a dimensionful 1 for a `Quantity` type without knowing the dimensions. Please use `oneunit(::Quantity)` instead.") Base.oneunit(::Type{<:Dimensions}) = error("There is no such thing as a dimensionful 1 for a `Dimensions` type, as + is only defined for `Quantity`.") +dimension_name(::Dimensions, k::Symbol) = SYNONYM_MAPPING[k] Base.show(io::IO, d::AbstractDimensions) = let tmp_io = IOBuffer() for k in keys(d) if !iszero(d[k]) - print(tmp_io, SYNONYM_MAPPING[k]) + print(tmp_io, dimension_name(d, k)) isone(d[k]) || pretty_print_exponent(tmp_io, d[k]) print(tmp_io, " ") end From de114815690c93be8b16ae48c25e3b542d56bd0d Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Mon, 12 Jun 2023 14:16:59 -0400 Subject: [PATCH 05/28] Remove redundancy in abstract type interface --- src/types.jl | 11 ++++------- src/utils.jl | 1 - test/unittests.jl | 4 ++-- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/types.jl b/src/types.jl index c20f253b..1ece00e6 100644 --- a/src/types.jl +++ b/src/types.jl @@ -54,11 +54,6 @@ struct Dimensions{R<:Real} <: AbstractDimensions{R} Dimensions{_R}(d::Dimensions) where {_R} = Dimensions{_R}(d.length, d.mass, d.time, d.current, d.temperature, d.luminosity, d.amount) end -new_dimensions(::Type{<:Dimensions}, l...) = Dimensions(l...) - -const DIMENSION_NAMES = Base.fieldnames(Dimensions) -const DIMENSION_SYNONYMS = (:m, :kg, :s, :A, :K, :cd, :mol) -const SYNONYM_MAPPING = NamedTuple(DIMENSION_NAMES .=> DIMENSION_SYNONYMS) """ Quantity{T} @@ -89,8 +84,10 @@ struct Quantity{T,R} <: AbstractQuantity{T,R} Quantity{T,R}(q::Quantity) where {T,R} = Quantity(convert(T, q.value), Dimensions{R}(dimension(q))) end -new_quantity(::Type{<:Quantity}, value, dim) = Quantity(value, dim) -new_quantity(::Type{<:Dimensions}, value, dim) = Quantity(value, dim) +# All that is needed to make an `AbstractQuantity` work: +dimension_name(::Dimensions, k::Symbol) = (length="m", mass="kg", time="s", current="A", temperature="K", luminosity="cd", amount="mol")[k] +new_dimensions(::Type{<:Dimensions}, dims...) = Dimensions(dims...) +new_quantity(::Type{<:Union{<:Quantity,<:Dimensions}}, l, r) = Quantity(l, r) struct DimensionError{Q1,Q2} <: Exception q1::Q1 diff --git a/src/utils.jl b/src/utils.jl index 5159bed3..77b94192 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -69,7 +69,6 @@ Base.oneunit(::Dimensions) = error("There is no such thing as a dimensionful 1 f Base.oneunit(::Type{<:Quantity}) = error("Cannot create a dimensionful 1 for a `Quantity` type without knowing the dimensions. Please use `oneunit(::Quantity)` instead.") Base.oneunit(::Type{<:Dimensions}) = error("There is no such thing as a dimensionful 1 for a `Dimensions` type, as + is only defined for `Quantity`.") -dimension_name(::Dimensions, k::Symbol) = SYNONYM_MAPPING[k] Base.show(io::IO, d::AbstractDimensions) = let tmp_io = IOBuffer() for k in keys(d) diff --git a/test/unittests.jl b/test/unittests.jl index b832f241..b80f3a7c 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -1,5 +1,5 @@ using DynamicQuantities -using DynamicQuantities: DEFAULT_DIM_TYPE, DEFAULT_VALUE_TYPE, DIMENSION_NAMES +using DynamicQuantities: DEFAULT_DIM_TYPE, DEFAULT_VALUE_TYPE using Ratios: SimpleRatio using SaferIntegers: SafeInt16 using Test @@ -182,7 +182,7 @@ end @test Quantity(0.5, length=2) / Dimensions(length=1) == Quantity(0.5, length=1) @test Dimensions(length=1) / Quantity(0.5, length=2, mass=-5) == Quantity(2, length=-1, mass=5) - @test Dimensions{Int8}([0 for i=1:length(DIMENSION_NAMES)]...) == Dimensions{Int8}() + @test Dimensions{Int8}(zeros(Int, 7)...) == Dimensions{Int8}() @test zero(Quantity(0.0+0.0im)) + Quantity(1) == Quantity(1.0+0.0im, length=Int8(0)) @test oneunit(Quantity(0.0+0.0im)) - Quantity(1) == Quantity(0.0+0.0im, length=Int8(0)) From 88da738fb9620e390bd6ce2b37b42809ea1fa304 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Mon, 12 Jun 2023 15:52:13 -0400 Subject: [PATCH 06/28] Make generic constructors for Dimension --- src/types.jl | 36 +++++++++++++----------------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/src/types.jl b/src/types.jl index 1ece00e6..2e4d6202 100644 --- a/src/types.jl +++ b/src/types.jl @@ -4,6 +4,12 @@ const DEFAULT_VALUE_TYPE = Float64 abstract type AbstractQuantity{T,R} end abstract type AbstractDimensions{R} end +constructor_of(::Type{D}) where {D<:AbstractDimensions} = D +constructor_of(::Type{D}) where {R,D<:AbstractDimensions{R}} = D.name.wrapper +constructor_of(::Type{Q}) where {Q<:AbstractQuantity} = Q +constructor_of(::Type{Q}) where {T,Q<:AbstractQuantity{T}} = Q.body.name.wrapper +constructor_of(::Type{Q}) where {T,R,Q<:AbstractQuantity{T,R}} = Q.name.wrapper + """ Dimensions @@ -29,31 +35,15 @@ struct Dimensions{R<:Real} <: AbstractDimensions{R} temperature::R luminosity::R amount::R - - function Dimensions(length::_R, - mass::_R, - time::_R, - current::_R, - temperature::_R, - luminosity::_R, - amount::_R) where {_R<:Real} - new{_R}(length, mass, time, current, temperature, luminosity, amount) - end - Dimensions(; kws...) = Dimensions(DEFAULT_DIM_TYPE; kws...) - Dimensions(::Type{_R}; kws...) where {_R} = Dimensions( - tryrationalize(_R, get(kws, :length, zero(_R))), - tryrationalize(_R, get(kws, :mass, zero(_R))), - tryrationalize(_R, get(kws, :time, zero(_R))), - tryrationalize(_R, get(kws, :current, zero(_R))), - tryrationalize(_R, get(kws, :temperature, zero(_R))), - tryrationalize(_R, get(kws, :luminosity, zero(_R))), - tryrationalize(_R, get(kws, :amount, zero(_R))), - ) - Dimensions{_R}(; kws...) where {_R} = Dimensions(_R; kws...) - Dimensions{_R}(args...) where {_R} = Dimensions(Base.Fix1(convert, _R).(args)...) - Dimensions{_R}(d::Dimensions) where {_R} = Dimensions{_R}(d.length, d.mass, d.time, d.current, d.temperature, d.luminosity, d.amount) end +(::Type{D})(::Type{R}; kws...) where {R,D<:AbstractDimensions} = D{R}((tryrationalize(R, get(kws, k, zero(R))) for k in fieldnames(D))...) +(::Type{D})(; kws...) where {D<:AbstractDimensions} = D(DEFAULT_DIM_TYPE; kws...) + +(::Type{D})(args...) where {R,D<:AbstractDimensions{R}} = constructor_of(D)(Base.Fix1(convert, R).(args)...) +(::Type{D})(; kws...) where {R,D<:AbstractDimensions{R}} = constructor_of(D)(R; kws...) +(::Type{D})(d::AbstractDimensions) where {R,D<:AbstractDimensions{R}} = D((getfield(d, k) for k in fieldnames(D))...) + """ Quantity{T} From 41f6257368b9e1856ad600a4356526e6d58fc9b4 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Wed, 14 Jun 2023 17:51:42 -0400 Subject: [PATCH 07/28] Change functions user needs to define --- Project.toml | 3 ++- src/types.jl | 25 ++++++++++++------------- src/utils.jl | 41 ++++++++++++++++++----------------------- 3 files changed, 32 insertions(+), 37 deletions(-) diff --git a/Project.toml b/Project.toml index a68aec44..023ded69 100644 --- a/Project.toml +++ b/Project.toml @@ -5,6 +5,7 @@ version = "0.4.0" [deps] Requires = "ae029012-a4dd-5104-9daa-d747884805df" +Tricks = "410a4b4d-49e4-4fbc-ab6d-cb71b17b3775" [weakdeps] Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" @@ -19,8 +20,8 @@ julia = "1.6" [extras] Ratios = "c84ed2f1-dad5-54f0-aa8e-dbefe2724439" -SaferIntegers = "88634af6-177f-5301-88b8-7819386cfa38" SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" +SaferIntegers = "88634af6-177f-5301-88b8-7819386cfa38" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" diff --git a/src/types.jl b/src/types.jl index 0294db40..d03a82aa 100644 --- a/src/types.jl +++ b/src/types.jl @@ -1,15 +1,11 @@ +import Tricks: static_fieldnames + const DEFAULT_DIM_TYPE = FixedRational{Int32, 2^4 * 3^2 * 5^2 * 7} const DEFAULT_VALUE_TYPE = Float64 abstract type AbstractQuantity{T,R} end abstract type AbstractDimensions{R} end -constructor_of(::Type{D}) where {D<:AbstractDimensions} = D -constructor_of(::Type{D}) where {R,D<:AbstractDimensions{R}} = D.name.wrapper -constructor_of(::Type{Q}) where {Q<:AbstractQuantity} = Q -constructor_of(::Type{Q}) where {T,Q<:AbstractQuantity{T}} = Q.body.name.wrapper -constructor_of(::Type{Q}) where {T,R,Q<:AbstractQuantity{T,R}} = Q.name.wrapper - """ Dimensions{R} @@ -36,7 +32,6 @@ which is by default a rational number. - `Dimensions(::Type{R}; kws...)` or `Dimensions{R}(; kws...)`: Pass a subset of dimensions as keyword arguments, with the output type set to `Dimensions{R}`. - `Dimensions{R}(args...)`: Pass all the dimensions as arguments, with the output type set to `Dimensions{R}`. - `Dimensions{R}(d::Dimensions)`: Copy the dimensions from another `Dimensions` object, with the output type set to `Dimensions{R}`. - """ struct Dimensions{R<:Real} <: AbstractDimensions{R} length::R @@ -48,12 +43,14 @@ struct Dimensions{R<:Real} <: AbstractDimensions{R} amount::R end -(::Type{D})(::Type{R}; kws...) where {R,D<:AbstractDimensions} = D{R}((tryrationalize(R, get(kws, k, zero(R))) for k in fieldnames(D))...) +(::Type{D})(::Type{R}; kws...) where {R,D<:AbstractDimensions} = D{R}((tryrationalize(R, get(kws, k, zero(R))) for k in static_fieldnames(D))...) (::Type{D})(; kws...) where {D<:AbstractDimensions} = D(DEFAULT_DIM_TYPE; kws...) -(::Type{D})(args...) where {R,D<:AbstractDimensions{R}} = constructor_of(D)(Base.Fix1(convert, R).(args)...) -(::Type{D})(; kws...) where {R,D<:AbstractDimensions{R}} = constructor_of(D)(R; kws...) -(::Type{D})(d::AbstractDimensions) where {R,D<:AbstractDimensions{R}} = D((getfield(d, k) for k in fieldnames(D))...) +(::Type{D})(args...) where {R,D<:AbstractDimensions{R}} = dimension_constructor(D)(Base.Fix1(convert, R).(args)...) +(::Type{D})(; kws...) where {R,D<:AbstractDimensions{R}} = dimension_constructor(D)(R; kws...) +(::Type{D})(d::AbstractDimensions) where {R,D<:AbstractDimensions{R}} = D((getfield(d, k) for k in static_fieldnames(D))...) + +new_dimensions(::Type{D}, dims...) where {D<:AbstractDimensions} = dimension_constructor(D)(dims...) """ @@ -94,10 +91,12 @@ struct Quantity{T,R} <: AbstractQuantity{T,R} Quantity{T,R}(q::Quantity) where {T,R} = Quantity(convert(T, q.value), Dimensions{R}(dimension(q))) end +new_quantity(::Type{QD}, l, r) where {QD<:Union{AbstractQuantity,AbstractDimensions}} = quantity_constructor(QD)(l, r) + # All that is needed to make an `AbstractQuantity` work: dimension_name(::Dimensions, k::Symbol) = (length="m", mass="kg", time="s", current="A", temperature="K", luminosity="cd", amount="mol")[k] -new_dimensions(::Type{<:Dimensions}, dims...) = Dimensions(dims...) -new_quantity(::Type{<:Union{<:Quantity,<:Dimensions}}, l, r) = Quantity(l, r) +quantity_constructor(::Type{<:Union{Quantity,Dimensions}}) = Quantity +dimension_constructor(::Type{<:Union{Quantity,Dimensions}}) = Dimensions struct DimensionError{Q1,Q2} <: Exception q1::Q1 diff --git a/src/utils.jl b/src/utils.jl index a8272fbf..ab6faa67 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1,28 +1,23 @@ -@generated function map_dimensions(f::F, args::AbstractDimensions...) where {F<:Function} - dimension_type = first(args) - dimension_names = Base.fieldnames(dimension_type) - output = :(new_dimensions($dimension_type)) - for dim in dimension_names - f_expr = :(f()) - for i=1:length(args) - push!(f_expr.args, :(args[$i].$dim)) - end - push!(output.args, f_expr) - end - return output +import Tricks: static_fieldnames + +function map_dimensions(f::F, args::AbstractDimensions...) where {F<:Function} + dimension_type = promote_type(typeof(args).parameters...) + dimension_names = static_fieldnames(dimension_type) + return new_dimensions( + dimension_type, + ( + f((getfield(arg, dim) for arg in args)...) + for dim in dimension_names + )... + ) end -@generated function all_dimensions(f::F, args::AbstractDimensions...) where {F<:Function} - dimension_type = first(args) - dimension_names = Base.fieldnames(dimension_type) - output = Expr(:&&) +function all_dimensions(f::F, args::AbstractDimensions...) where {F<:Function} + dimension_type = promote_type(typeof(args).parameters...) + dimension_names = static_fieldnames(dimension_type) for dim in dimension_names - f_expr = :(f()) - for i=1:length(args) - push!(f_expr.args, :(args[$i].$dim)) - end - push!(output.args, f_expr) + f((getfield(arg, dim) for arg in args)...) || return false end - return output + return true end Base.float(q::AbstractQuantity{T}) where {T<:AbstractFloat} = convert(T, q) @@ -33,7 +28,7 @@ Base.convert(::Type{T}, q::AbstractQuantity) where {T<:Real} = end Base.isfinite(q::AbstractQuantity) = isfinite(ustrip(q)) -Base.keys(d::AbstractDimensions) = Base.fieldnames(typeof(d)) +Base.keys(d::AbstractDimensions) = static_fieldnames(typeof(d)) # TODO: Make this more generic. Base.iszero(d::AbstractDimensions) = all_dimensions(iszero, d) Base.iszero(q::AbstractQuantity) = iszero(ustrip(q)) From 68941b853cac269fdfa9f6e48ebef7319257a902 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Wed, 14 Jun 2023 18:00:52 -0400 Subject: [PATCH 08/28] Back to generated version --- src/utils.jl | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index ab6faa67..173cd5f5 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -11,13 +11,18 @@ function map_dimensions(f::F, args::AbstractDimensions...) where {F<:Function} )... ) end -function all_dimensions(f::F, args::AbstractDimensions...) where {F<:Function} - dimension_type = promote_type(typeof(args).parameters...) - dimension_names = static_fieldnames(dimension_type) - for dim in dimension_names - f((getfield(arg, dim) for arg in args)...) || return false +@generated function all_dimensions(f::F, args::AbstractDimensions...) where {F<:Function} + # Test a function over all dimensions + output = Expr(:&&) + dimension_type = promote_type(args...) + for dim in Base.fieldnames(dimension_type) + f_expr = :(f()) + for i=1:length(args) + push!(f_expr.args, :(args[$i].$dim)) + end + push!(output.args, f_expr) end - return true + return output end Base.float(q::AbstractQuantity{T}) where {T<:AbstractFloat} = convert(T, q) From 6cf61a68aaf242df132f1f7082e3d623a091e1cd Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 15 Jun 2023 00:35:16 -0400 Subject: [PATCH 09/28] `getproperty` instead of `getfield` for abstraction --- src/types.jl | 2 +- src/utils.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types.jl b/src/types.jl index d03a82aa..9ddf7ec4 100644 --- a/src/types.jl +++ b/src/types.jl @@ -48,7 +48,7 @@ end (::Type{D})(args...) where {R,D<:AbstractDimensions{R}} = dimension_constructor(D)(Base.Fix1(convert, R).(args)...) (::Type{D})(; kws...) where {R,D<:AbstractDimensions{R}} = dimension_constructor(D)(R; kws...) -(::Type{D})(d::AbstractDimensions) where {R,D<:AbstractDimensions{R}} = D((getfield(d, k) for k in static_fieldnames(D))...) +(::Type{D})(d::AbstractDimensions) where {R,D<:AbstractDimensions{R}} = D((getproperty(d, k) for k in static_fieldnames(D))...) new_dimensions(::Type{D}, dims...) where {D<:AbstractDimensions} = dimension_constructor(D)(dims...) diff --git a/src/utils.jl b/src/utils.jl index 173cd5f5..9ac06243 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -6,7 +6,7 @@ function map_dimensions(f::F, args::AbstractDimensions...) where {F<:Function} return new_dimensions( dimension_type, ( - f((getfield(arg, dim) for arg in args)...) + f((getproperty(arg, dim) for arg in args)...) for dim in dimension_names )... ) From 7a50e436aa4177f958084cd84684abdf60af58e2 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 15 Jun 2023 00:41:59 -0400 Subject: [PATCH 10/28] Default for `dimension_name` is to return key name --- src/utils.jl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/utils.jl b/src/utils.jl index 9ac06243..1e668530 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -90,6 +90,11 @@ Base.show(io::IO, d::AbstractDimensions) = end Base.show(io::IO, q::AbstractQuantity) = print(io, ustrip(q), " ", dimension(q)) +function dimension_name(::AbstractDimensions, k::Symbol) + default_dimensions = (length="m", mass="kg", time="s", current="A", temperature="K", luminosity="cd", amount="mol") + return get(default_dimensions, k, string(k)) +end + string_rational(x) = isinteger(x) ? string(round(Int, x)) : string(x) pretty_print_exponent(io::IO, x) = print(io, to_superscript(string_rational(x))) const SUPERSCRIPT_MAPPING = ['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹'] From 12ecee20fa3eb90ed842f2ddfb9f111c230d2c40 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 15 Jun 2023 03:58:18 -0400 Subject: [PATCH 11/28] Add missing converter to integers --- src/fixed_rational.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/fixed_rational.jl b/src/fixed_rational.jl index 2d914ac9..836873d0 100644 --- a/src/fixed_rational.jl +++ b/src/fixed_rational.jl @@ -47,6 +47,7 @@ Base.convert(::Type{F}, x::Rational) where {F<:FixedRational} = F(x) Base.convert(::Type{Rational{R}}, x::F) where {R,F<:FixedRational} = Rational{R}(x.num, denom(F)) Base.convert(::Type{Rational}, x::F) where {F<:FixedRational} = Rational{eltype(F)}(x.num, denom(F)) Base.convert(::Type{AF}, x::F) where {AF<:AbstractFloat,F<:FixedRational} = convert(AF, x.num) / convert(AF, denom(F)) +Base.convert(::Type{I}, x::F) where {I<:Integer,F<:FixedRational} = convert(I, convert(Rational, x)) Base.round(::Type{T}, x::F) where {T,F<:FixedRational} = div(convert(T, x.num), convert(T, denom(F)), RoundNearest) Base.promote(x::Integer, y::F) where {F<:FixedRational} = (F(x), y) Base.promote(x::F, y::Integer) where {F<:FixedRational} = reverse(promote(y, x)) From c04adc729d3bbd6cc3a1fe8cc114bb49f04f0b61 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 15 Jun 2023 04:09:11 -0400 Subject: [PATCH 12/28] Get abstract quantities working with constructors --- src/types.jl | 14 ++++++-------- test/unittests.jl | 23 +++++++++++++++++++++++ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/types.jl b/src/types.jl index 9ddf7ec4..d5fe4198 100644 --- a/src/types.jl +++ b/src/types.jl @@ -50,7 +50,7 @@ end (::Type{D})(; kws...) where {R,D<:AbstractDimensions{R}} = dimension_constructor(D)(R; kws...) (::Type{D})(d::AbstractDimensions) where {R,D<:AbstractDimensions{R}} = D((getproperty(d, k) for k in static_fieldnames(D))...) -new_dimensions(::Type{D}, dims...) where {D<:AbstractDimensions} = dimension_constructor(D)(dims...) +new_dimensions(::Type{QD}, dims...) where {QD<:Union{AbstractQuantity,AbstractDimensions}} = dimension_constructor(QD)(dims...) """ @@ -83,18 +83,16 @@ dimensions according to the operation. struct Quantity{T,R} <: AbstractQuantity{T,R} value::T dimensions::Dimensions{R} - - Quantity(x; kws...) = new{typeof(x), DEFAULT_DIM_TYPE}(x, Dimensions(; kws...)) - Quantity(x, ::Type{_R}; kws...) where {_R} = new{typeof(x), _R}(x, Dimensions(_R; kws...)) - Quantity(x, d::Dimensions{_R}) where {_R} = new{typeof(x), _R}(x, d) - Quantity{T}(q::Quantity) where {T} = Quantity(convert(T, q.value), dimension(q)) - Quantity{T,R}(q::Quantity) where {T,R} = Quantity(convert(T, q.value), Dimensions{R}(dimension(q))) end +(::Type{Q})(x, ::Type{R}; kws...) where {R,Q<:AbstractQuantity} = quantity_constructor(Q){typeof(x), R}(x, dimension_constructor(Q)(R; kws...)) +(::Type{Q})(x; kws...) where {Q<:AbstractQuantity} = Q(x, DEFAULT_DIM_TYPE; kws...) +(::Type{Q})(q::AbstractQuantity) where {T,Q<:AbstractQuantity{T}} = new_quantity(Q, convert(T, ustrip(q)), dimension(q)) +(::Type{Q})(q::AbstractQuantity) where {T,R,Q<:AbstractQuantity{T,R}} = new_quantity(Q, convert(T, ustrip(q)), dimension_constructor(Q){R}(dimension(q))) + new_quantity(::Type{QD}, l, r) where {QD<:Union{AbstractQuantity,AbstractDimensions}} = quantity_constructor(QD)(l, r) # All that is needed to make an `AbstractQuantity` work: -dimension_name(::Dimensions, k::Symbol) = (length="m", mass="kg", time="s", current="A", temperature="K", luminosity="cd", amount="mol")[k] quantity_constructor(::Type{<:Union{Quantity,Dimensions}}) = Quantity dimension_constructor(::Type{<:Union{Quantity,Dimensions}}) = Dimensions diff --git a/test/unittests.jl b/test/unittests.jl index 60030641..87e07479 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -1,5 +1,6 @@ using DynamicQuantities using DynamicQuantities: DEFAULT_DIM_TYPE, DEFAULT_VALUE_TYPE +import DynamicQuantities: quantity_constructor, dimension_constructor using Ratios: SimpleRatio using SaferIntegers: SafeInt16 using Test @@ -305,3 +306,25 @@ end @test_throws LoadError eval(:(u":x")) end + +struct MyDimensions{R} <: AbstractDimensions{R} + length::R + mass::R + time::R +end +struct MyQuantity{T,R} <: AbstractQuantity{T,R} + value::T + dimensions::MyDimensions{R} +end +quantity_constructor(::Type{<:Union{MyQuantity,MyDimensions}}) = MyQuantity +dimension_constructor(::Type{<:Union{MyQuantity,MyDimensions}}) = MyDimensions + +@testset "Custom dimensions" begin + for T in [Float32, Float64], R in [Rational{Int64}, Rational{Int32}] + x = MyQuantity(T(0.1), R, length=0.5) + @test x * x ≈ MyQuantity(T(0.01), R, length=1) + @test typeof(x * x) == MyQuantity{T,R} + end + @test MyQuantity(0.1, DEFAULT_DIM_TYPE, length=0.5) == MyQuantity(0.1, length=0.5) + @test MyQuantity(0.1, DEFAULT_DIM_TYPE, length=0.5) == MyQuantity(0.1, length=1//2) +end From ff844d14ce57a31d60100c0c362f435610529298 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 15 Jun 2023 05:00:49 -0400 Subject: [PATCH 13/28] Can now use abstract types without overloading functions --- src/types.jl | 34 ++++++++++++++++++++++++++++++---- test/unittests.jl | 10 ++++++++-- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/types.jl b/src/types.jl index d5fe4198..6c6d6155 100644 --- a/src/types.jl +++ b/src/types.jl @@ -1,4 +1,4 @@ -import Tricks: static_fieldnames +import Tricks: static_fieldnames, static_fieldtypes const DEFAULT_DIM_TYPE = FixedRational{Int32, 2^4 * 3^2 * 5^2 * 7} const DEFAULT_VALUE_TYPE = Float64 @@ -92,9 +92,35 @@ end new_quantity(::Type{QD}, l, r) where {QD<:Union{AbstractQuantity,AbstractDimensions}} = quantity_constructor(QD)(l, r) -# All that is needed to make an `AbstractQuantity` work: -quantity_constructor(::Type{<:Union{Quantity,Dimensions}}) = Quantity -dimension_constructor(::Type{<:Union{Quantity,Dimensions}}) = Dimensions +function _container_type(T::Type) + if isa(T, UnionAll) + return _container_type(T.body) + elseif isa(T, DataType) + return T.name.wrapper + else + error("Could not infer container of type: $(T)") + end +end +@generated function get_container_type(::Type{T}) where {T} + container = _container_type(T) + return :($container) +end +@generated function get_dim_type(::Type{Q}) where {Q<:AbstractQuantity} + quantity_type = get_container_type(Q) + field_type = NamedTuple(static_fieldnames(quantity_type) .=> static_fieldtypes(quantity_type))[:dimensions] + out = get_container_type(field_type) + return :($out) +end + +dimension_constructor(::Type{D}) where {D<:AbstractDimensions} = get_container_type(D) +dimension_constructor(::Type{Q}) where {Q<:AbstractQuantity} = get_dim_type(Q) + +quantity_constructor(::Type{Q}) where {Q<:AbstractQuantity} = get_container_type(Q) + +# This requires user override, as we don't know which Quantity +# to make from which Dimensions type: +quantity_constructor(::Type{D}) where {D<:AbstractDimensions} = Quantity + struct DimensionError{Q1,Q2} <: Exception q1::Q1 diff --git a/test/unittests.jl b/test/unittests.jl index 87e07479..d7cb4a3f 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -316,8 +316,6 @@ struct MyQuantity{T,R} <: AbstractQuantity{T,R} value::T dimensions::MyDimensions{R} end -quantity_constructor(::Type{<:Union{MyQuantity,MyDimensions}}) = MyQuantity -dimension_constructor(::Type{<:Union{MyQuantity,MyDimensions}}) = MyDimensions @testset "Custom dimensions" begin for T in [Float32, Float64], R in [Rational{Int64}, Rational{Int32}] @@ -327,4 +325,12 @@ dimension_constructor(::Type{<:Union{MyQuantity,MyDimensions}}) = MyDimensions end @test MyQuantity(0.1, DEFAULT_DIM_TYPE, length=0.5) == MyQuantity(0.1, length=0.5) @test MyQuantity(0.1, DEFAULT_DIM_TYPE, length=0.5) == MyQuantity(0.1, length=1//2) + + # Before we define `quantity_constructor`, we get an error: + @test_throws MethodError MyQuantity(0.1) + 0.1 * MyDimensions() + + # This is because it is still constructing `Quantity` from `0.1 * MyDimensions()`, + # so we need to override it: + @eval quantity_constructor(::Type{<:MyDimensions}) = MyQuantity + @test MyQuantity(0.1) + 0.1 * MyDimensions() ≈ MyQuantity(0.2) end From 6548c65940a8ea8d9adfb441485a79eb0b673c38 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 15 Jun 2023 05:11:06 -0400 Subject: [PATCH 14/28] Be explicit that user needs to define their own `quantity_constructor` --- src/types.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.jl b/src/types.jl index 6c6d6155..aa4eaed4 100644 --- a/src/types.jl +++ b/src/types.jl @@ -119,7 +119,7 @@ quantity_constructor(::Type{Q}) where {Q<:AbstractQuantity} = get_container_type # This requires user override, as we don't know which Quantity # to make from which Dimensions type: -quantity_constructor(::Type{D}) where {D<:AbstractDimensions} = Quantity +quantity_constructor(::Type{D}) where {D<:Dimensions} = Quantity struct DimensionError{Q1,Q2} <: Exception From 3060585f75b3a4ad24291cb20df40df319551b44 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 15 Jun 2023 05:18:18 -0400 Subject: [PATCH 15/28] Add docstrings on `quantity_constructor` --- src/types.jl | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/types.jl b/src/types.jl index aa4eaed4..c351186a 100644 --- a/src/types.jl +++ b/src/types.jl @@ -112,16 +112,45 @@ end return :($out) end +""" + dimension_constructor(::Type{<:AbstractDimensions}) + +This function returns the container for a particular `AbstractDimensions`. +For example, `Dimensions` will get returned as `Dimensions`, and +`Dimensions{Rational{Int64}}` will also get returned as `Dimensions`. +""" dimension_constructor(::Type{D}) where {D<:AbstractDimensions} = get_container_type(D) + +""" + dimension_constructor(::Type{<:AbstractQuantity}) + +This function returns the `Dimensions` type used inside +a particular `Quantity` type by reading the `.dimensions` field. +It also strips the type parameter (i.e., `Dimensions{R} -> Dimensions`). +""" dimension_constructor(::Type{Q}) where {Q<:AbstractQuantity} = get_dim_type(Q) +""" + quantity_constructor(::Type{<:AbstractQuantity}) + +This function returns the container for a particular `AbstractQuantity`. +For example, `Quantity` gets returned as `Quantity`, `Quantity{Float32}` also +as `Quantity`, and `Quantity{Float32,Rational{Int64}}` also as `Quantity`. +""" quantity_constructor(::Type{Q}) where {Q<:AbstractQuantity} = get_container_type(Q) -# This requires user override, as we don't know which Quantity -# to make from which Dimensions type: +""" + quantity_constructor(::Type{<:AbstractDimensions}) + +This function returns the `<:AbstractQuantity` type corresponding to +a given `<:AbstractDimensions`. For example, `Dimensions -> Quantity`. +If you define a custom dimensions type, you should overload this function +so it returns your custom quantity type that uses that dimensions type. +This is only needed if you wish to use the `(*)(::AbstractDimensions, ::Number)` +function; otherwise it won't be necessary. +""" quantity_constructor(::Type{D}) where {D<:Dimensions} = Quantity - struct DimensionError{Q1,Q2} <: Exception q1::Q1 q2::Q2 From 1e1cb561160a0e313ba68220c45951cac97096c5 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 15 Jun 2023 07:20:53 -0400 Subject: [PATCH 16/28] Abstract versions of zero, one, oneunit --- src/math.jl | 4 ++-- src/utils.jl | 45 +++++++++++++++++++++------------------------ test/unittests.jl | 11 ++++++++--- 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/math.jl b/src/math.jl index 72eba0b4..821f793c 100644 --- a/src/math.jl +++ b/src/math.jl @@ -20,8 +20,8 @@ Base.:+(l::AbstractQuantity, r::AbstractQuantity) = dimension(l) == dimension(r) Base.:-(l::AbstractQuantity) = new_quantity(typeof(l), -ustrip(l), dimension(l)) Base.:-(l::AbstractQuantity, r::AbstractQuantity) = l + (-r) -Base.:+(l::AbstractQuantity, r) = dimension(l) == dimension(r) ? new_quantity(typeof(l), ustrip(l) + r, dimension(l)) : throw(DimensionError(l, r)) -Base.:+(l, r::AbstractQuantity) = dimension(l) == dimension(r) ? new_quantity(typeof(r), l + ustrip(r), dimension(r)) : throw(DimensionError(l, r)) +Base.:+(l::AbstractQuantity, r) = iszero(dimension(l)) ? new_quantity(typeof(l), ustrip(l) + r, dimension(l)) : throw(DimensionError(l, r)) +Base.:+(l, r::AbstractQuantity) = iszero(dimension(r)) ? new_quantity(typeof(r), l + ustrip(r), dimension(r)) : throw(DimensionError(l, r)) Base.:-(l::AbstractQuantity, r) = l + (-r) Base.:-(l, r::AbstractQuantity) = l + (-r) diff --git a/src/utils.jl b/src/utils.jl index 1e668530..f685d666 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -28,7 +28,7 @@ end Base.float(q::AbstractQuantity{T}) where {T<:AbstractFloat} = convert(T, q) Base.convert(::Type{T}, q::AbstractQuantity) where {T<:Real} = let - @assert iszero(q.dimensions) "Quantity $(q) has dimensions! Use `ustrip` instead." + @assert iszero(q.dimensions) "$(typeof(q)): $(q) has dimensions! Use `ustrip` instead." return convert(T, q.value) end @@ -40,11 +40,11 @@ Base.iszero(q::AbstractQuantity) = iszero(ustrip(q)) Base.getindex(d::AbstractDimensions, k::Symbol) = getfield(d, k) Base.:(==)(l::AbstractDimensions, r::AbstractDimensions) = all_dimensions(==, l, r) Base.:(==)(l::AbstractQuantity, r::AbstractQuantity) = ustrip(l) == ustrip(r) && dimension(l) == dimension(r) -Base.:(==)(l, r::AbstractQuantity) = ustrip(l) == ustrip(r) && dimension(l) == dimension(r) -Base.:(==)(l::AbstractQuantity, r) = ustrip(l) == ustrip(r) && dimension(l) == dimension(r) +Base.:(==)(l, r::AbstractQuantity) = ustrip(l) == ustrip(r) && iszero(dimension(r)) +Base.:(==)(l::AbstractQuantity, r) = ustrip(l) == ustrip(r) && iszero(dimension(l)) Base.isless(l::AbstractQuantity, r::AbstractQuantity) = dimension(l) == dimension(r) ? isless(ustrip(l), ustrip(r)) : throw(DimensionError(l, r)) -Base.isless(l::AbstractQuantity, r) = dimension(l) == dimension(r) ? isless(ustrip(l), r) : throw(DimensionError(l, r)) -Base.isless(l, r::AbstractQuantity) = dimension(l) == dimension(r) ? isless(l, ustrip(r)) : throw(DimensionError(l, r)) +Base.isless(l::AbstractQuantity, r) = iszero(dimension(l)) ? isless(ustrip(l), r) : throw(DimensionError(l, r)) +Base.isless(l, r::AbstractQuantity) = iszero(dimension(r)) ? isless(l, ustrip(r)) : throw(DimensionError(l, r)) Base.isapprox(l::AbstractQuantity, r::AbstractQuantity; kws...) = isapprox(ustrip(l), ustrip(r); kws...) && dimension(l) == dimension(r) Base.length(::AbstractDimensions) = 1 Base.length(::AbstractQuantity) = 1 @@ -54,25 +54,24 @@ Base.iterate(q::AbstractQuantity) = (q, nothing) Base.iterate(::AbstractQuantity, ::Nothing) = nothing # Multiplicative identities: -Base.one(::Type{Quantity{T,R}}) where {T,R} = Quantity(one(T), R) -Base.one(::Type{Quantity{T}}) where {T} = one(Quantity{T,DEFAULT_DIM_TYPE}) -Base.one(::Type{Quantity}) = one(Quantity{DEFAULT_VALUE_TYPE}) -Base.one(::Type{Dimensions{R}}) where {R} = Dimensions{R}() -Base.one(::Type{Dimensions}) = one(Dimensions{DEFAULT_DIM_TYPE}) -Base.one(q::Quantity) = Quantity(one(ustrip(q)), one(dimension(q))) -Base.one(d::Dimensions) = one(typeof(d)) +Base.one(::Type{Q}) where {T,R,Q<:AbstractQuantity{T,R}} = new_quantity(Q, one(T), R) +Base.one(::Type{Q}) where {T,Q<:AbstractQuantity{T}} = new_quantity(Q, one(T), DEFAULT_DIM_TYPE) +Base.one(::Type{Q}) where {Q<:AbstractQuantity} = new_quantity(Q, one(DEFAULT_VALUE_TYPE), DEFAULT_DIM_TYPE) +Base.one(::Type{D}) where {D<:AbstractDimensions} = D() +Base.one(q::Q) where {Q<:AbstractQuantity} = new_quantity(Q, one(ustrip(q)), one(dimension(q))) +Base.one(::D) where {D<:AbstractDimensions} = one(D) # Additive identities: -Base.zero(q::Quantity) = Quantity(zero(ustrip(q)), dimension(q)) -Base.zero(::Dimensions) = error("There is no such thing as an additive identity for a `Dimensions` object, as + is only defined for `Quantity`.") -Base.zero(::Type{<:Quantity}) = error("Cannot create an additive identity for a `Quantity` type, as the dimensions are unknown. Please use `zero(::Quantity)` instead.") -Base.zero(::Type{<:Dimensions}) = error("There is no such thing as an additive identity for a `Dimensions` type, as + is only defined for `Quantity`.") +Base.zero(q::Q) where {Q<:AbstractQuantity} = new_quantity(Q, zero(ustrip(q)), dimension(q)) +Base.zero(::AbstractDimensions) = error("There is no such thing as an additive identity for a `AbstractDimensions` object, as + is only defined for `AbstractQuantity`.") +Base.zero(::Type{<:AbstractQuantity}) = error("Cannot create an additive identity for a `AbstractQuantity` type, as the dimensions are unknown. Please use `zero(::AbstractQuantity)` instead.") +Base.zero(::Type{<:AbstractDimensions}) = error("There is no such thing as an additive identity for a `AbstractDimensions` type, as + is only defined for `AbstractQuantity`.") # Dimensionful 1: -Base.oneunit(q::Quantity) = Quantity(oneunit(ustrip(q)), dimension(q)) -Base.oneunit(::Dimensions) = error("There is no such thing as a dimensionful 1 for a `Dimensions` object, as + is only defined for `Quantity`.") -Base.oneunit(::Type{<:Quantity}) = error("Cannot create a dimensionful 1 for a `Quantity` type without knowing the dimensions. Please use `oneunit(::Quantity)` instead.") -Base.oneunit(::Type{<:Dimensions}) = error("There is no such thing as a dimensionful 1 for a `Dimensions` type, as + is only defined for `Quantity`.") +Base.oneunit(q::Q) where {Q<:AbstractQuantity} = new_quantity(Q, oneunit(ustrip(q)), dimension(q)) +Base.oneunit(::AbstractDimensions) = error("There is no such thing as a dimensionful 1 for a `AbstractDimensions` object, as + is only defined for `AbstractQuantity`.") +Base.oneunit(::Type{<:AbstractQuantity}) = error("Cannot create a dimensionful 1 for a `AbstractQuantity` type without knowing the dimensions. Please use `oneunit(::AbstractQuantity)` instead.") +Base.oneunit(::Type{<:AbstractDimensions}) = error("There is no such thing as a dimensionful 1 for a `AbstractDimensions` type, as + is only defined for `AbstractQuantity`.") Base.show(io::IO, d::AbstractDimensions) = let tmp_io = IOBuffer() @@ -124,18 +123,16 @@ Base.convert(::Type{Dimensions{R}}, d::Dimensions) where {R} = Dimensions{R}(d) Remove the units from a quantity. """ ustrip(q::AbstractQuantity) = q.value -ustrip(::AbstractDimensions) = error("Cannot remove units from a `Dimensions` object.") +ustrip(::AbstractDimensions) = error("Cannot remove units from an `AbstractDimensions` object.") ustrip(q) = q """ dimension(q::AbstractQuantity) -Get the dimensions of a quantity, returning a `Dimensions` object. +Get the dimensions of a quantity, returning an `AbstractDimensions` object. """ dimension(q::AbstractQuantity) = q.dimensions dimension(d::AbstractDimensions) = d -dimension(_) = Dimensions() -# TODO: Should we throw an error instead, because this is assuming a type? """ ulength(q::AbstractQuantity) diff --git a/test/unittests.jl b/test/unittests.jl index d7cb4a3f..e4bd7c4d 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -131,10 +131,9 @@ end @testset "Fallbacks" begin @test ustrip(0.5) == 0.5 @test ustrip(ones(32)) == ones(32) - @test dimension(0.5) == Dimensions() - @test dimension(ones(32)) == Dimensions() @test dimension(Dimensions()) === Dimensions() + @test_throws MethodError Dimensions(1.0) @test_throws ErrorException ustrip(Dimensions()) end @@ -149,7 +148,6 @@ end @test dimension(prod(uX)) == prod([Dimensions(length=2.5, luminosity=0.5) for i in 1:10]) @test dimension(prod(uX)) == prod([Dimensions(R, length=2.5, luminosity=0.5) for i in 1:10]) @test typeof(dimension(prod(uX))) <: Dimensions{R} - @test dimension(X[1]) == Dimensions() uX = X .* Quantity(2, length=2.5, luminosity=0.5) @test sum(X) == 0.5 * ustrip(sum(uX)) @@ -322,6 +320,13 @@ end x = MyQuantity(T(0.1), R, length=0.5) @test x * x ≈ MyQuantity(T(0.01), R, length=1) @test typeof(x * x) == MyQuantity{T,R} + @test one(MyQuantity{T,R}) == MyQuantity(one(T), MyDimensions(R)) + @test zero(x) == MyQuantity(zero(T), R, length=0.5) + @test oneunit(x) + x == MyQuantity(T(1.1), R, length=0.5) + @test typeof(oneunit(x) + x) == MyQuantity{T,R} + + @test_throws ErrorException zero(MyQuantity{T,R}) + @test_throws ErrorException oneunit(MyQuantity{T,R}) end @test MyQuantity(0.1, DEFAULT_DIM_TYPE, length=0.5) == MyQuantity(0.1, length=0.5) @test MyQuantity(0.1, DEFAULT_DIM_TYPE, length=0.5) == MyQuantity(0.1, length=1//2) From 9271cdec4e52eb5d37075512b62fcb73d5588725 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 15 Jun 2023 10:56:45 -0400 Subject: [PATCH 17/28] Clean up container type --- src/types.jl | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/src/types.jl b/src/types.jl index c351186a..8f3d7534 100644 --- a/src/types.jl +++ b/src/types.jl @@ -92,23 +92,13 @@ end new_quantity(::Type{QD}, l, r) where {QD<:Union{AbstractQuantity,AbstractDimensions}} = quantity_constructor(QD)(l, r) -function _container_type(T::Type) - if isa(T, UnionAll) - return _container_type(T.body) - elseif isa(T, DataType) - return T.name.wrapper - else - error("Could not infer container of type: $(T)") - end -end -@generated function get_container_type(::Type{T}) where {T} - container = _container_type(T) - return :($container) +function container_type(::Type{T}) where {T} + return Base.typename(T).wrapper end @generated function get_dim_type(::Type{Q}) where {Q<:AbstractQuantity} - quantity_type = get_container_type(Q) + quantity_type = container_type(Q) field_type = NamedTuple(static_fieldnames(quantity_type) .=> static_fieldtypes(quantity_type))[:dimensions] - out = get_container_type(field_type) + out = container_type(field_type) return :($out) end @@ -119,7 +109,7 @@ This function returns the container for a particular `AbstractDimensions`. For example, `Dimensions` will get returned as `Dimensions`, and `Dimensions{Rational{Int64}}` will also get returned as `Dimensions`. """ -dimension_constructor(::Type{D}) where {D<:AbstractDimensions} = get_container_type(D) +dimension_constructor(::Type{D}) where {D<:AbstractDimensions} = container_type(D) """ dimension_constructor(::Type{<:AbstractQuantity}) @@ -137,7 +127,7 @@ This function returns the container for a particular `AbstractQuantity`. For example, `Quantity` gets returned as `Quantity`, `Quantity{Float32}` also as `Quantity`, and `Quantity{Float32,Rational{Int64}}` also as `Quantity`. """ -quantity_constructor(::Type{Q}) where {Q<:AbstractQuantity} = get_container_type(Q) +quantity_constructor(::Type{Q}) where {Q<:AbstractQuantity} = container_type(Q) """ quantity_constructor(::Type{<:AbstractDimensions}) From 0970d71a5bf5450832ddc4cba1922fab78724fea Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Fri, 16 Jun 2023 07:45:33 -0400 Subject: [PATCH 18/28] Clean up container type --- src/types.jl | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/types.jl b/src/types.jl index 8f3d7534..3b9aa81b 100644 --- a/src/types.jl +++ b/src/types.jl @@ -50,8 +50,6 @@ end (::Type{D})(; kws...) where {R,D<:AbstractDimensions{R}} = dimension_constructor(D)(R; kws...) (::Type{D})(d::AbstractDimensions) where {R,D<:AbstractDimensions{R}} = D((getproperty(d, k) for k in static_fieldnames(D))...) -new_dimensions(::Type{QD}, dims...) where {QD<:Union{AbstractQuantity,AbstractDimensions}} = dimension_constructor(QD)(dims...) - """ Quantity{T,R} @@ -90,15 +88,16 @@ end (::Type{Q})(q::AbstractQuantity) where {T,Q<:AbstractQuantity{T}} = new_quantity(Q, convert(T, ustrip(q)), dimension(q)) (::Type{Q})(q::AbstractQuantity) where {T,R,Q<:AbstractQuantity{T,R}} = new_quantity(Q, convert(T, ustrip(q)), dimension_constructor(Q){R}(dimension(q))) +new_dimensions(::Type{QD}, dims...) where {QD<:Union{AbstractQuantity,AbstractDimensions}} = dimension_constructor(QD)(dims...) new_quantity(::Type{QD}, l, r) where {QD<:Union{AbstractQuantity,AbstractDimensions}} = quantity_constructor(QD)(l, r) -function container_type(::Type{T}) where {T} +function constructor_of(::Type{T}) where {T} return Base.typename(T).wrapper end @generated function get_dim_type(::Type{Q}) where {Q<:AbstractQuantity} - quantity_type = container_type(Q) + quantity_type = constructor_of(Q) field_type = NamedTuple(static_fieldnames(quantity_type) .=> static_fieldtypes(quantity_type))[:dimensions] - out = container_type(field_type) + out = constructor_of(field_type) return :($out) end @@ -109,7 +108,7 @@ This function returns the container for a particular `AbstractDimensions`. For example, `Dimensions` will get returned as `Dimensions`, and `Dimensions{Rational{Int64}}` will also get returned as `Dimensions`. """ -dimension_constructor(::Type{D}) where {D<:AbstractDimensions} = container_type(D) +dimension_constructor(::Type{D}) where {D<:AbstractDimensions} = constructor_of(D) """ dimension_constructor(::Type{<:AbstractQuantity}) @@ -127,7 +126,7 @@ This function returns the container for a particular `AbstractQuantity`. For example, `Quantity` gets returned as `Quantity`, `Quantity{Float32}` also as `Quantity`, and `Quantity{Float32,Rational{Int64}}` also as `Quantity`. """ -quantity_constructor(::Type{Q}) where {Q<:AbstractQuantity} = container_type(Q) +quantity_constructor(::Type{Q}) where {Q<:AbstractQuantity} = constructor_of(Q) """ quantity_constructor(::Type{<:AbstractDimensions}) From 255ecf8d131b24b7827bd1889427e7ea56156ed6 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sat, 24 Jun 2023 17:44:11 -0400 Subject: [PATCH 19/28] Disable math directly on `AbstractDimensions` --- src/math.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/math.jl b/src/math.jl index 821f793c..dc968571 100644 --- a/src/math.jl +++ b/src/math.jl @@ -4,8 +4,8 @@ Base.:*(l::AbstractQuantity, r::AbstractDimensions) = new_quantity(typeof(l), us Base.:*(l::AbstractDimensions, r::AbstractQuantity) = new_quantity(typeof(r), ustrip(r), l * dimension(r)) Base.:*(l::AbstractQuantity, r) = new_quantity(typeof(l), ustrip(l) * r, dimension(l)) Base.:*(l, r::AbstractQuantity) = new_quantity(typeof(r), l * ustrip(r), dimension(r)) -Base.:*(l::AbstractDimensions, r) = new_quantity(typeof(l), r, l) -Base.:*(l, r::AbstractDimensions) = new_quantity(typeof(r), l, r) +Base.:*(l::AbstractDimensions, r) = error("Please use an `AbstractQuantity` for multiplication. You used multiplication on types: $(typeof(l)) and $(typeof(r)).") +Base.:*(l, r::AbstractDimensions) = error("Please use an `AbstractQuantity` for multiplication. You used multiplication on types: $(typeof(l)) and $(typeof(r)).") Base.:/(l::AbstractDimensions, r::AbstractDimensions) = map_dimensions(-, l, r) Base.:/(l::AbstractQuantity, r::AbstractQuantity) = new_quantity(typeof(l), ustrip(l) / ustrip(r), dimension(l) / dimension(r)) @@ -13,8 +13,8 @@ Base.:/(l::AbstractQuantity, r::AbstractDimensions) = new_quantity(typeof(l), us Base.:/(l::AbstractDimensions, r::AbstractQuantity) = new_quantity(typeof(r), inv(ustrip(r)), l / dimension(r)) Base.:/(l::AbstractQuantity, r) = new_quantity(typeof(l), ustrip(l) / r, dimension(l)) Base.:/(l, r::AbstractQuantity) = l * inv(r) -Base.:/(l::AbstractDimensions, r) = new_quantity(typeof(l), inv(r), l) -Base.:/(l, r::AbstractDimensions) = new_quantity(typeof(r), l, inv(r)) +Base.:/(l::AbstractDimensions, r) = error("Please use an `AbstractQuantity` for division. You used division on types: $(typeof(l)) and $(typeof(r)).") +Base.:/(l, r::AbstractDimensions) = error("Please use an `AbstractQuantity` for division. You used division on types: $(typeof(l)) and $(typeof(r)).") Base.:+(l::AbstractQuantity, r::AbstractQuantity) = dimension(l) == dimension(r) ? new_quantity(typeof(l), ustrip(l) + ustrip(r), dimension(l)) : throw(DimensionError(l, r)) Base.:-(l::AbstractQuantity) = new_quantity(typeof(l), -ustrip(l), dimension(l)) @@ -30,8 +30,8 @@ _pow(l::AbstractQuantity{T}, r) where {T} = new_quantity(typeof(l), ustrip(l)^r, _pow_as_T(l::AbstractQuantity{T}, r) where {T} = new_quantity(typeof(l), ustrip(l)^convert(T, r), _pow(l.dimensions, r)) Base.:^(l::AbstractDimensions{R}, r::Integer) where {R} = _pow(l, r) Base.:^(l::AbstractDimensions{R}, r::Number) where {R} = _pow(l, tryrationalize(R, r)) -Base.:^(l::AbstractQuantity{T,R}, r::Integer) where {T,R} = _pow(l, r) -Base.:^(l::AbstractQuantity{T,R}, r::Number) where {T,R} = _pow_as_T(l, tryrationalize(R, r)) +Base.:^(l::AbstractQuantity{T,D}, r::Integer) where {T,R,D<:AbstractDimensions{R}} = _pow(l, r) +Base.:^(l::AbstractQuantity{T,D}, r::Number) where {T,R,D<:AbstractDimensions{R}} = _pow_as_T(l, tryrationalize(R, r)) Base.inv(d::AbstractDimensions) = map_dimensions(-, d) Base.inv(q::AbstractQuantity) = new_quantity(typeof(q), inv(ustrip(q)), inv(dimension(q))) From 6f00d473debc26e03d4a707a7061577c91cf58e3 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sat, 24 Jun 2023 17:45:51 -0400 Subject: [PATCH 20/28] Refactor quantities to parametrize on dimensions --- ext/DynamicQuantitiesUnitfulExt.jl | 4 +- src/types.jl | 70 ++++++-------- src/utils.jl | 12 +-- test/test_unitful.jl | 11 ++- test/unittests.jl | 142 ++++++++++++++++++----------- 5 files changed, 132 insertions(+), 107 deletions(-) diff --git a/ext/DynamicQuantitiesUnitfulExt.jl b/ext/DynamicQuantitiesUnitfulExt.jl index 80fb227e..16dbaf39 100644 --- a/ext/DynamicQuantitiesUnitfulExt.jl +++ b/ext/DynamicQuantitiesUnitfulExt.jl @@ -35,10 +35,10 @@ Base.convert(::Type{Unitful.Quantity}, x::DynamicQuantities.Quantity) = end Base.convert(::Type{DynamicQuantities.Quantity}, x::Unitful.Quantity{T}) where {T} = convert(DynamicQuantities.Quantity{T,DynamicQuantities.DEFAULT_DIM_TYPE}, x) -Base.convert(::Type{DynamicQuantities.Quantity{T,R}}, x::Unitful.Quantity) where {T,R} = +Base.convert(::Type{DynamicQuantities.Quantity{T,D}}, x::Unitful.Quantity) where {T,R,D<:DynamicQuantities.AbstractDimensions{R}} = let value = Unitful.ustrip(Unitful.upreferred(x)) - dimension = convert(DynamicQuantities.Dimensions{R}, Unitful.dimension(x)) + dimension = convert(D, Unitful.dimension(x)) return DynamicQuantities.Quantity(convert(T, value), dimension) end diff --git a/src/types.jl b/src/types.jl index 3b9aa81b..5c55f717 100644 --- a/src/types.jl +++ b/src/types.jl @@ -1,9 +1,9 @@ import Tricks: static_fieldnames, static_fieldtypes -const DEFAULT_DIM_TYPE = FixedRational{Int32, 2^4 * 3^2 * 5^2 * 7} +const DEFAULT_DIM_BASE_TYPE = FixedRational{Int32, 2^4 * 3^2 * 5^2 * 7} const DEFAULT_VALUE_TYPE = Float64 -abstract type AbstractQuantity{T,R} end +abstract type AbstractQuantity{T,D} end abstract type AbstractDimensions{R} end """ @@ -27,10 +27,10 @@ which is by default a rational number. # Constructors -- `Dimensions(args...)`: Pass all the dimensions as arguments. `R` is set to `DEFAULT_DIM_TYPE`. -- `Dimensions(; kws...)`: Pass a subset of dimensions as keyword arguments. `R` is set to `DEFAULT_DIM_TYPE`. +- `Dimensions(args...)`: Pass all the dimensions as arguments. `R` is set to `DEFAULT_DIM_BASE_TYPE`. +- `Dimensions(; kws...)`: Pass a subset of dimensions as keyword arguments. `R` is set to `DEFAULT_DIM_BASE_TYPE`. - `Dimensions(::Type{R}; kws...)` or `Dimensions{R}(; kws...)`: Pass a subset of dimensions as keyword arguments, with the output type set to `Dimensions{R}`. -- `Dimensions{R}(args...)`: Pass all the dimensions as arguments, with the output type set to `Dimensions{R}`. +- `Dimensions{R}()`: Create a dimensionless object typed as `Dimensions{R}`. - `Dimensions{R}(d::Dimensions)`: Copy the dimensions from another `Dimensions` object, with the output type set to `Dimensions{R}`. """ struct Dimensions{R<:Real} <: AbstractDimensions{R} @@ -44,17 +44,18 @@ struct Dimensions{R<:Real} <: AbstractDimensions{R} end (::Type{D})(::Type{R}; kws...) where {R,D<:AbstractDimensions} = D{R}((tryrationalize(R, get(kws, k, zero(R))) for k in static_fieldnames(D))...) -(::Type{D})(; kws...) where {D<:AbstractDimensions} = D(DEFAULT_DIM_TYPE; kws...) - -(::Type{D})(args...) where {R,D<:AbstractDimensions{R}} = dimension_constructor(D)(Base.Fix1(convert, R).(args)...) +(::Type{D})(::Type{R}; kws...) where {R,D<:AbstractDimensions{R}} = constructor_of(D)(R; kws...) +(::Type{D})(; kws...) where {D<:AbstractDimensions} = D(DEFAULT_DIM_BASE_TYPE; kws...) (::Type{D})(; kws...) where {R,D<:AbstractDimensions{R}} = dimension_constructor(D)(R; kws...) +(::Type{D})(args...) where {R,D<:AbstractDimensions{R}} = dimension_constructor(D)(Base.Fix1(convert, R).(args)...) (::Type{D})(d::AbstractDimensions) where {R,D<:AbstractDimensions{R}} = D((getproperty(d, k) for k in static_fieldnames(D))...) +const DEFAULT_DIM_TYPE = Dimensions{DEFAULT_DIM_BASE_TYPE} """ - Quantity{T,R} + Quantity{T,D} -Physical quantity with value `value` of type `T` and dimensions `dimensions` of type `Dimensions{R}`. +Physical quantity with value `value` of type `T` and dimensions `dimensions` of type `D`. For example, the velocity of an object with mass 1 kg and velocity 2 m/s is `Quantity(2, mass=1, length=1, time=-1)`. You should access these fields with `ustrip(q)`, and `dimensions(q)`. @@ -68,38 +69,39 @@ dimensions according to the operation. # Fields - `value::T`: value of the quantity of some type `T`. Access with `ustrip(::Quantity)` -- `dimensions::Dimensions{R}`: dimensions of the quantity with dimension type `R`. Access with `dimension(::Quantity)` +- `dimensions::D`: dimensions of the quantity. Access with `dimension(::Quantity)` # Constructors - `Quantity(x; kws...)`: Construct a quantity with value `x` and dimensions given by the keyword arguments. The value type is inferred from `x`. `R` is set to `DEFAULT_DIM_TYPE`. -- `Quantity(x, ::Type{R}; kws...)`: Construct a quantity with value `x`. The dimensions parametric type is set to `R`. -- `Quantity(x, d::Dimensions{R})`: Construct a quantity with value `x` and dimensions `d`. -- `Quantity{T}(q::Quantity)`: Construct a quantity with value `q.value` and dimensions `q.dimensions`, but with value type converted to `T`. -- `Quantity{T,R}(q::Quantity)`: Construct a quantity with value `q.value` and dimensions `q.dimensions`, but with value type converted to `T` and dimensions parametric type set to `R`. +- `Quantity(x, ::Type{D}; kws...)`: Construct a quantity with value `x` with no dimensions, and the dimensions type set to `D`. +- `Quantity(x, d::D)`: Construct a quantity with value `x` and dimensions `d` of type `D`. +- `Quantity{T}(...)`: As above, but converting the value to type `T`. You may also pass a `Quantity` as input. +- `Quantity{T,D}(...)`: As above, but converting the value to type `T` and dimensions to `D`. You may also pass a `Quantity` as input. """ -struct Quantity{T,R} <: AbstractQuantity{T,R} +struct Quantity{T,D<:AbstractDimensions} <: AbstractQuantity{T,D} value::T - dimensions::Dimensions{R} + dimensions::D end +(::Type{Q})(x, ::Type{D}; kws...) where {R,D<:AbstractDimensions{R},T,Q<:AbstractQuantity{T}} = quantity_constructor(Q){T, D}(convert(T, x), D(R; kws...)) +(::Type{Q})(x, ::Type{D}; kws...) where {R,D<:AbstractDimensions{R},Q<:AbstractQuantity} = quantity_constructor(Q){typeof(x), D}(x, D(R; kws...)) + +(::Type{Q})(x, ::Type{D}; kws...) where {D<:AbstractDimensions,T,Q<:AbstractQuantity{T}} = quantity_constructor(Q){T, D}(convert(T, x), D(DEFAULT_DIM_BASE_TYPE; kws...)) +(::Type{Q})(x, ::Type{D}; kws...) where {D<:AbstractDimensions,Q<:AbstractQuantity} = quantity_constructor(Q){typeof(x), D}(x, D(DEFAULT_DIM_BASE_TYPE; kws...)) + +(::Type{Q})(x; kws...) where {T,D<:AbstractDimensions,Q<:AbstractQuantity{T,D}} = quantity_constructor(Q)(convert(T, x), D; kws...) +(::Type{Q})(x; kws...) where {T,Q<:AbstractQuantity{T}} = quantity_constructor(Q)(convert(T, x), DEFAULT_DIM_TYPE; kws...) +(::Type{Q})(x; kws...) where {Q<:AbstractQuantity} = quantity_constructor(Q)(x, DEFAULT_DIM_TYPE; kws...) -(::Type{Q})(x, ::Type{R}; kws...) where {R,Q<:AbstractQuantity} = quantity_constructor(Q){typeof(x), R}(x, dimension_constructor(Q)(R; kws...)) -(::Type{Q})(x; kws...) where {Q<:AbstractQuantity} = Q(x, DEFAULT_DIM_TYPE; kws...) (::Type{Q})(q::AbstractQuantity) where {T,Q<:AbstractQuantity{T}} = new_quantity(Q, convert(T, ustrip(q)), dimension(q)) -(::Type{Q})(q::AbstractQuantity) where {T,R,Q<:AbstractQuantity{T,R}} = new_quantity(Q, convert(T, ustrip(q)), dimension_constructor(Q){R}(dimension(q))) +(::Type{Q})(q::AbstractQuantity) where {T,D<:AbstractDimensions,Q<:AbstractQuantity{T,D}} = new_quantity(Q, convert(T, ustrip(q)), convert(D, dimension(q))) new_dimensions(::Type{QD}, dims...) where {QD<:Union{AbstractQuantity,AbstractDimensions}} = dimension_constructor(QD)(dims...) -new_quantity(::Type{QD}, l, r) where {QD<:Union{AbstractQuantity,AbstractDimensions}} = quantity_constructor(QD)(l, r) +new_quantity(::Type{QD}, l, r) where {QD<:Union{AbstractQuantity}} = quantity_constructor(QD)(l, r) function constructor_of(::Type{T}) where {T} return Base.typename(T).wrapper end -@generated function get_dim_type(::Type{Q}) where {Q<:AbstractQuantity} - quantity_type = constructor_of(Q) - field_type = NamedTuple(static_fieldnames(quantity_type) .=> static_fieldtypes(quantity_type))[:dimensions] - out = constructor_of(field_type) - return :($out) -end """ dimension_constructor(::Type{<:AbstractDimensions}) @@ -117,7 +119,7 @@ This function returns the `Dimensions` type used inside a particular `Quantity` type by reading the `.dimensions` field. It also strips the type parameter (i.e., `Dimensions{R} -> Dimensions`). """ -dimension_constructor(::Type{Q}) where {Q<:AbstractQuantity} = get_dim_type(Q) +dimension_constructor(::Type{Q}) where {T,D<:AbstractDimensions,Q<:AbstractQuantity{T,D}} = constructor_of(D) """ quantity_constructor(::Type{<:AbstractQuantity}) @@ -128,18 +130,6 @@ as `Quantity`, and `Quantity{Float32,Rational{Int64}}` also as `Quantity`. """ quantity_constructor(::Type{Q}) where {Q<:AbstractQuantity} = constructor_of(Q) -""" - quantity_constructor(::Type{<:AbstractDimensions}) - -This function returns the `<:AbstractQuantity` type corresponding to -a given `<:AbstractDimensions`. For example, `Dimensions -> Quantity`. -If you define a custom dimensions type, you should overload this function -so it returns your custom quantity type that uses that dimensions type. -This is only needed if you wish to use the `(*)(::AbstractDimensions, ::Number)` -function; otherwise it won't be necessary. -""" -quantity_constructor(::Type{D}) where {D<:Dimensions} = Quantity - struct DimensionError{Q1,Q2} <: Exception q1::Q1 q2::Q2 diff --git a/src/utils.jl b/src/utils.jl index f685d666..df2e117f 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -54,7 +54,7 @@ Base.iterate(q::AbstractQuantity) = (q, nothing) Base.iterate(::AbstractQuantity, ::Nothing) = nothing # Multiplicative identities: -Base.one(::Type{Q}) where {T,R,Q<:AbstractQuantity{T,R}} = new_quantity(Q, one(T), R) +Base.one(::Type{Q}) where {T,D,Q<:AbstractQuantity{T,D}} = new_quantity(Q, one(T), D) Base.one(::Type{Q}) where {T,Q<:AbstractQuantity{T}} = new_quantity(Q, one(T), DEFAULT_DIM_TYPE) Base.one(::Type{Q}) where {Q<:AbstractQuantity} = new_quantity(Q, one(DEFAULT_VALUE_TYPE), DEFAULT_DIM_TYPE) Base.one(::Type{D}) where {D<:AbstractDimensions} = D() @@ -110,12 +110,12 @@ tryrationalize(::Type{R}, x) where {R} = isinteger(x) ? convert(R, round(Int, x) Base.showerror(io::IO, e::DimensionError) = print(io, "DimensionError: ", e.q1, " and ", e.q2, " have incompatible dimensions") -Base.convert(::Type{Quantity}, q::Quantity) = q -Base.convert(::Type{Quantity{T}}, q::Quantity) where {T} = Quantity{T}(q) -Base.convert(::Type{Quantity{T,R}}, q::Quantity) where {T,R} = Quantity{T,R}(q) +Base.convert(::Type{Q}, q::AbstractQuantity) where {Q<:AbstractQuantity} = q +Base.convert(::Type{Q}, q::AbstractQuantity) where {T,Q<:AbstractQuantity{T}} = new_quantity(Q, convert(T, ustrip(q)), dimension(q)) +Base.convert(::Type{Q}, q::AbstractQuantity) where {T,D,Q<:AbstractQuantity{T,D}} = new_quantity(Q, convert(T, ustrip(q)), convert(D, dimension(q))) -Base.convert(::Type{Dimensions}, d::Dimensions) = d -Base.convert(::Type{Dimensions{R}}, d::Dimensions) where {R} = Dimensions{R}(d) +Base.convert(::Type{D}, d::AbstractDimensions) where {D<:AbstractDimensions} = d +Base.convert(::Type{D}, d::AbstractDimensions) where {R,D<:AbstractDimensions{R}} = D(d) """ ustrip(q::AbstractQuantity) diff --git a/test/test_unitful.jl b/test/test_unitful.jl index eac5ab66..fe652c08 100644 --- a/test/test_unitful.jl +++ b/test/test_unitful.jl @@ -1,5 +1,5 @@ import DynamicQuantities -using DynamicQuantities: DEFAULT_DIM_TYPE, DEFAULT_VALUE_TYPE +using DynamicQuantities: DEFAULT_DIM_BASE_TYPE, DEFAULT_VALUE_TYPE import Unitful import Unitful: @u_str import Ratios: SimpleRatio @@ -11,14 +11,15 @@ risapprox(x::Unitful.Quantity, y::Unitful.Quantity; kws...) = return isapprox(xfloat, yfloat; kws...) end -for T in [DEFAULT_VALUE_TYPE, Float16, Float32, Float64], R in [DEFAULT_DIM_TYPE, Rational{Int16}, Rational{Int32}, SimpleRatio{Int}, SimpleRatio{SafeInt16}] - x = DynamicQuantities.Quantity(T(0.2), R, length=1, amount=2, current=-1 // 2, luminosity=2 // 5) +for T in [DEFAULT_VALUE_TYPE, Float16, Float32, Float64], R in [DEFAULT_DIM_BASE_TYPE, Rational{Int16}, Rational{Int32}, SimpleRatio{Int}, SimpleRatio{SafeInt16}] + D = DynamicQuantities.Dimensions{R} + x = DynamicQuantities.Quantity(T(0.2), D, length=1, amount=2, current=-1 // 2, luminosity=2 // 5) x_unitful = T(0.2)u"m*mol^2*A^(-1//2)*cd^(2//5)" @test risapprox(convert(Unitful.Quantity, x), x_unitful; atol=1e-6) @test typeof(convert(DynamicQuantities.Quantity, convert(Unitful.Quantity, x))) <: DynamicQuantities.Quantity{T,DynamicQuantities.DEFAULT_DIM_TYPE} @test isapprox(convert(DynamicQuantities.Quantity, convert(Unitful.Quantity, x)), x; atol=1e-6) - @test isapprox(convert(DynamicQuantities.Quantity{T,R}, x_unitful), x; atol=1e-6) - @test risapprox(convert(Unitful.Quantity, convert(DynamicQuantities.Quantity{T,R}, x_unitful)), Unitful.upreferred(x_unitful); atol=1e-6) + @test isapprox(convert(DynamicQuantities.Quantity{T,D}, x_unitful), x; atol=1e-6) + @test risapprox(convert(Unitful.Quantity, convert(DynamicQuantities.Quantity{T,D}, x_unitful)), Unitful.upreferred(x_unitful); atol=1e-6) end diff --git a/test/unittests.jl b/test/unittests.jl index e4bd7c4d..b9760d73 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -1,5 +1,5 @@ using DynamicQuantities -using DynamicQuantities: DEFAULT_DIM_TYPE, DEFAULT_VALUE_TYPE +using DynamicQuantities: DEFAULT_DIM_BASE_TYPE, DEFAULT_DIM_TYPE, DEFAULT_VALUE_TYPE import DynamicQuantities: quantity_constructor, dimension_constructor using Ratios: SimpleRatio using SaferIntegers: SafeInt16 @@ -7,11 +7,12 @@ using Test @testset "Basic utilities" begin - for T in [DEFAULT_VALUE_TYPE, Float16, Float32, Float64], R in [DEFAULT_DIM_TYPE, Rational{Int16}, Rational{Int32}, SimpleRatio{Int}, SimpleRatio{SafeInt16}] - x = Quantity(T(0.2), R, length=1, mass=2.5) + for T in [DEFAULT_VALUE_TYPE, Float16, Float32, Float64], R in [DEFAULT_DIM_BASE_TYPE, Rational{Int16}, Rational{Int32}, SimpleRatio{Int}, SimpleRatio{SafeInt16}] + D = Dimensions{R} + x = Quantity(T(0.2), D, length=1, mass=2.5) @test typeof(x).parameters[1] == T - @test typeof(x).parameters[2] == R + @test typeof(x).parameters[2] == D @test ulength(x) == R(1 // 1) @test umass(x) == R(5 // 2) @test ustrip(x) ≈ T(0.2) @@ -24,7 +25,7 @@ using Test y = x^2 @test typeof(x).parameters[1] == T - @test typeof(x).parameters[2] == R + @test typeof(x).parameters[2] == D @test ulength(y) == R(2 // 1) @test umass(y) == (5 // 1) @test ustrip(y) ≈ T(0.04) @@ -72,25 +73,25 @@ using Test @test iszero(x.dimensions) == false @test iszero(y.dimensions) == true - y = Quantity(T(2 // 10), R, length=1, mass=5 // 2) + y = Quantity(T(2 // 10), D, length=1, mass=5 // 2) @test y ≈ x - y = Quantity(T(2 // 10), R, length=1, mass=6 // 2) + y = Quantity(T(2 // 10), D, length=1, mass=6 // 2) @test !(y ≈ x) y = x * Inf32 @test typeof(y).parameters[1] == promote_type(T, Float32) - @test typeof(y).parameters[2] == R + @test typeof(y).parameters[2] == D @test isfinite(x) @test !isfinite(y) y = x^2.1 @test typeof(y).parameters[1] == T # Should not promote! Expect 2.1 to be converted to 21//10 - @test typeof(y).parameters[2] == R + @test typeof(y).parameters[2] == D @test ulength(y) == R(1 * (21 // 10)) @test umass(y) == R((5 // 2) * (21 // 10)) @test utime(y) == R(0) @@ -100,26 +101,26 @@ using Test @test uamount(y) == R(0) @test ustrip(y) ≈ T(0.2^2.1) - dimensionless = Quantity(one(T), R) + dimensionless = Quantity(one(T), D) y = T(2) + dimensionless @test ustrip(y) == T(3) @test dimension(y) == Dimensions(R) - @test typeof(y) == Quantity{T,R} + @test typeof(y) == Quantity{T,D} y = T(2) - dimensionless @test ustrip(y) == T(1) @test dimension(y) == Dimensions(R) - @test typeof(y) == Quantity{T,R} + @test typeof(y) == Quantity{T,D} y = dimensionless + T(2) @test ustrip(y) == T(3) y = dimensionless - T(2) @test ustrip(y) == T(-1) - @test_throws DimensionError Quantity(one(T), R, length=1) + 1.0 - @test_throws DimensionError Quantity(one(T), R, length=1) - 1.0 - @test_throws DimensionError 1.0 + Quantity(one(T), R, length=1) - @test_throws DimensionError 1.0 - Quantity(one(T), R, length=1) + @test_throws DimensionError Quantity(one(T), D, length=1) + 1.0 + @test_throws DimensionError Quantity(one(T), D, length=1) - 1.0 + @test_throws DimensionError 1.0 + Quantity(one(T), D, length=1) + @test_throws DimensionError 1.0 - Quantity(one(T), D, length=1) end x = Quantity(-1.2, length=2 // 5) @@ -139,15 +140,17 @@ end @testset "Arrays" begin for T in [Float16, Float32, Float64], R in [Rational{Int16}, Rational{Int32}, SimpleRatio{Int}, SimpleRatio{SafeInt16}] + D = Dimensions{R} + X = randn(T, 10) - uX = X .* Dimensions{R}(length=2.5, luminosity=0.5) + uX = X .* Quantity{T,D}(1, length=2.5, luminosity=0.5) - @test eltype(uX) <: Quantity{T,R} - @test typeof(sum(uX)) <: Quantity{T,R} + @test eltype(uX) <: Quantity{T,D} + @test typeof(sum(uX)) <: Quantity{T,D} @test sum(X) == ustrip(sum(uX)) - @test dimension(prod(uX)) == prod([Dimensions(length=2.5, luminosity=0.5) for i in 1:10]) - @test dimension(prod(uX)) == prod([Dimensions(R, length=2.5, luminosity=0.5) for i in 1:10]) - @test typeof(dimension(prod(uX))) <: Dimensions{R} + @test dimension(prod(uX)) == dimension(prod([Quantity(T(1), D, length=2.5, luminosity=0.5) for i in 1:10])) + @test dimension(prod(uX)) == dimension(prod([Quantity(T(1), D, length=2.5, luminosity=0.5) for i in 1:10])) + @test typeof(dimension(prod(uX))) <: D uX = X .* Quantity(2, length=2.5, luminosity=0.5) @test sum(X) == 0.5 * ustrip(sum(uX)) @@ -156,9 +159,9 @@ end @test ustrip(x + ones(T, 32))[32] == 2 @test typeof(x + ones(T, 32)) <: Quantity{Vector{T}} @test typeof(x - ones(T, 32)) <: Quantity{Vector{T}} - @test typeof(ones(T, 32) * Dimensions(length=1)) <: Quantity{Vector{T}} - @test typeof(ones(T, 32) / Dimensions(length=1)) <: Quantity{Vector{T}} - @test ones(T, 32) / Dimensions(length=1) == Quantity(ones(T, 32), length=-1) + @test typeof(ones(T, 32) * Quantity(T(1), D, length=1)) <: Quantity{Vector{T}} + @test typeof(ones(T, 32) / Quantity(T(1), D, length=1)) <: Quantity{Vector{T}} + @test ones(T, 32) / Quantity(T(1), length=1) == Quantity(ones(T, 32), length=-1) end end @@ -172,9 +175,11 @@ end @test dimension(z) == Dimensions(length=1, mass=2) @test float(z / (z * -1 / 52)) ≈ ustrip(z) - @test Dimensions(length=1) / 0.5 == Quantity(2.0, length=1) - @test 0.5 / Dimensions(length=1) == Quantity(0.5, length=-1) - @test Dimensions(length=1) * 0.5 == Quantity(0.5, length=1) + # Invalid ways to make a quantity: + @test_throws ErrorException Dimensions(length=1) / 0.5 == Quantity(2.0, length=1) + @test_throws ErrorException 0.5 / Dimensions(length=1) + @test_throws ErrorException Dimensions(length=1) * 0.5 + @test 0.5 / Quantity(1, length=1) == Quantity(0.5, length=-1) @test 0.5 * Quantity(1, length=1) == Quantity(0.5, length=1) @test Quantity(0.5) / Dimensions(length=1) == Quantity(0.5, length=-1) @@ -218,7 +223,7 @@ end @test sqrt(z * -1) == Quantity(sqrt(52), length=1 // 2, mass=1) @test cbrt(z) == Quantity(cbrt(-52), length=1 // 3, mass=2 // 3) - @test 1.0 * (Dimensions(length=3)^2) == Quantity(1.0, length=6) + @test_throws ErrorException 1.0 * (Dimensions(length=3)^2) x = 0.9u"km/s" y = 0.3 * x @@ -253,9 +258,9 @@ end @test convert(Dimensions, d) === d q = Quantity(0.5, d) - q32_32 = convert(Quantity{Float32,Rational{Int32}}, q) - @test typeof(q) == Quantity{Float64,Rational{Int16}} - @test typeof(q32_32) == Quantity{Float32,Rational{Int32}} + q32_32 = convert(Quantity{Float32,Dimensions{Rational{Int32}}}, q) + @test typeof(q) == Quantity{Float64,Dimensions{Rational{Int16}}} + @test typeof(q32_32) == Quantity{Float32,Dimensions{Rational{Int32}}} @test ustrip(q) == 0.5 @test ustrip(q32_32) == 0.5 @test typeof(ustrip(q)) == Float64 @@ -264,8 +269,28 @@ end @test umass(q) == 2 @test umass(q32_32) == 2 @test typeof(umass(q32_32)) == Rational{Int32} - @test typeof(convert(Quantity{Float16}, q)) == Quantity{Float16,Rational{Int16}} + @test typeof(convert(Quantity{Float16}, q)) == Quantity{Float16,Dimensions{Rational{Int16}}} @test convert(Quantity, q) === q + + # Automatic conversions via constructor: + for T in [Float16, Float32, Float64, BigFloat], R in [DEFAULT_DIM_BASE_TYPE, Rational{Int16}, Rational{Int32}, SimpleRatio{Int}, SimpleRatio{SafeInt16}] + D = Dimensions{R} + q = Quantity{T,D}(2, length=1.5) + @test typeof(q) == Quantity{T,D} + @test typeof(ustrip(q)) == T + @test typeof(ulength(q)) == R + + # Now, without R, the default will be DEFAULT_DIM_BASE_TYPE: + q = Quantity{T}(2, length=1.5) + @test typeof(q) == Quantity{T,DEFAULT_DIM_TYPE} + @test typeof(ustrip(q)) == T + @test typeof(ulength(q)) == DEFAULT_DIM_BASE_TYPE + + # Just dimensions: + d = D(length=1.5) + @test typeof(d) == D + @test typeof(ulength(d)) == R + end end @testset "Units" begin @@ -285,8 +310,8 @@ end @test ustrip(y) ≈ 0.0003 @test ulength(y) == 2 - y32 = convert(Quantity{Float32,Rational{Int16}}, y) - @test typeof(y32) == Quantity{Float32,Rational{Int16}} + y32 = convert(Quantity{Float32,Dimensions{Rational{Int16}}}, y) + @test typeof(y32) == Quantity{Float32,Dimensions{Rational{Int16}}} @test ustrip(y32) ≈ 0.0003 z = u"yr" @@ -310,32 +335,41 @@ struct MyDimensions{R} <: AbstractDimensions{R} mass::R time::R end -struct MyQuantity{T,R} <: AbstractQuantity{T,R} +struct MyQuantity{T,D<:AbstractDimensions} <: AbstractQuantity{T,D} value::T - dimensions::MyDimensions{R} + dimensions::D end @testset "Custom dimensions" begin for T in [Float32, Float64], R in [Rational{Int64}, Rational{Int32}] - x = MyQuantity(T(0.1), R, length=0.5) - @test x * x ≈ MyQuantity(T(0.01), R, length=1) - @test typeof(x * x) == MyQuantity{T,R} - @test one(MyQuantity{T,R}) == MyQuantity(one(T), MyDimensions(R)) - @test zero(x) == MyQuantity(zero(T), R, length=0.5) - @test oneunit(x) + x == MyQuantity(T(1.1), R, length=0.5) - @test typeof(oneunit(x) + x) == MyQuantity{T,R} - - @test_throws ErrorException zero(MyQuantity{T,R}) - @test_throws ErrorException oneunit(MyQuantity{T,R}) + D = MyDimensions{R} + x = MyQuantity(T(0.1), D, length=0.5) + @test x * x ≈ MyQuantity(T(0.01), D, length=1) + @test typeof(x * x) == MyQuantity{T,D} + @test one(MyQuantity{T,D}) == MyQuantity(one(T), MyDimensions(R)) + @test zero(x) == MyQuantity(zero(T), D, length=0.5) + @test oneunit(x) + x == MyQuantity(T(1.1), D, length=0.5) + @test typeof(oneunit(x) + x) == MyQuantity{T,D} + + # Automatic conversions: + @test typeof(MyQuantity{T,D}(0.1, length=0.5)) == MyQuantity{T,D} + @test typeof(0.5 * MyQuantity{T,D}(0.1, length=0.5)) == MyQuantity{promote_type(T,Float64),D} + + # Using MyDimensions inside regular Quantity: + x = Quantity(T(0.1), MyDimensions(R, length=0.5)) + @test typeof(x) == Quantity{T,MyDimensions{R}} + @test typeof(x * x) == Quantity{T,MyDimensions{R}} + @test ulength(x * x) == 1 + @test dimension(x * x) == MyDimensions(R, length=1) + + # Errors: + @test_throws ErrorException zero(MyQuantity{T,D}) + @test_throws ErrorException oneunit(MyQuantity{T,D}) + @test_throws ErrorException 1.0 * MyDimensions() end @test MyQuantity(0.1, DEFAULT_DIM_TYPE, length=0.5) == MyQuantity(0.1, length=0.5) @test MyQuantity(0.1, DEFAULT_DIM_TYPE, length=0.5) == MyQuantity(0.1, length=1//2) - # Before we define `quantity_constructor`, we get an error: - @test_throws MethodError MyQuantity(0.1) + 0.1 * MyDimensions() - - # This is because it is still constructing `Quantity` from `0.1 * MyDimensions()`, - # so we need to override it: - @eval quantity_constructor(::Type{<:MyDimensions}) = MyQuantity - @test MyQuantity(0.1) + 0.1 * MyDimensions() ≈ MyQuantity(0.2) + # But, we always need to use a quantity when mixing with mathematical operations: + @test_throws ErrorException MyQuantity(0.1) + 0.1 * MyDimensions() end From f32120573a771bf192324299d1a09f2b16409bd0 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sat, 24 Jun 2023 19:01:03 -0400 Subject: [PATCH 21/28] Fix constructor ambiguity issues --- src/types.jl | 68 ++++++++++++++--------------------------------- src/utils.jl | 3 --- test/unittests.jl | 11 ++++++-- 3 files changed, 29 insertions(+), 53 deletions(-) diff --git a/src/types.jl b/src/types.jl index 5c55f717..5209e8c1 100644 --- a/src/types.jl +++ b/src/types.jl @@ -1,6 +1,6 @@ import Tricks: static_fieldnames, static_fieldtypes -const DEFAULT_DIM_BASE_TYPE = FixedRational{Int32, 2^4 * 3^2 * 5^2 * 7} +const DEFAULT_DIM_BASE_TYPE = FixedRational{Int32,2^4 * 3^2 * 5^2 * 7} const DEFAULT_VALUE_TYPE = Float64 abstract type AbstractQuantity{T,D} end @@ -43,11 +43,15 @@ struct Dimensions{R<:Real} <: AbstractDimensions{R} amount::R end -(::Type{D})(::Type{R}; kws...) where {R,D<:AbstractDimensions} = D{R}((tryrationalize(R, get(kws, k, zero(R))) for k in static_fieldnames(D))...) -(::Type{D})(::Type{R}; kws...) where {R,D<:AbstractDimensions{R}} = constructor_of(D)(R; kws...) +(::Type{D})(args...) where {R,D<:AbstractDimensions{R}} = + begin + @assert length(args) == length(static_fieldnames(D)) # (just to prevent stack overflows from misuse) + constructor_of(D){R}(Base.Fix1(convert, R).(args)...) + end +(::Type{D})(args...) where {D<:AbstractDimensions} = constructor_of(D){DEFAULT_DIM_BASE_TYPE}(args...) +(::Type{D})(::Type{R}; kws...) where {R,D<:AbstractDimensions} = constructor_of(D){R}((tryrationalize(R, get(kws, k, zero(R))) for k in static_fieldnames(D))...) +(::Type{D})(; kws...) where {R,D<:AbstractDimensions{R}} = constructor_of(D)(R; kws...) (::Type{D})(; kws...) where {D<:AbstractDimensions} = D(DEFAULT_DIM_BASE_TYPE; kws...) -(::Type{D})(; kws...) where {R,D<:AbstractDimensions{R}} = dimension_constructor(D)(R; kws...) -(::Type{D})(args...) where {R,D<:AbstractDimensions{R}} = dimension_constructor(D)(Base.Fix1(convert, R).(args)...) (::Type{D})(d::AbstractDimensions) where {R,D<:AbstractDimensions{R}} = D((getproperty(d, k) for k in static_fieldnames(D))...) const DEFAULT_DIM_TYPE = Dimensions{DEFAULT_DIM_BASE_TYPE} @@ -83,52 +87,20 @@ struct Quantity{T,D<:AbstractDimensions} <: AbstractQuantity{T,D} value::T dimensions::D end -(::Type{Q})(x, ::Type{D}; kws...) where {R,D<:AbstractDimensions{R},T,Q<:AbstractQuantity{T}} = quantity_constructor(Q){T, D}(convert(T, x), D(R; kws...)) -(::Type{Q})(x, ::Type{D}; kws...) where {R,D<:AbstractDimensions{R},Q<:AbstractQuantity} = quantity_constructor(Q){typeof(x), D}(x, D(R; kws...)) +(::Type{Q})(x::T, ::Type{D}; kws...) where {D<:AbstractDimensions,T,T2,Q<:AbstractQuantity{T2}} = constructor_of(Q)(convert(T2, x), D(; kws...)) +(::Type{Q})(x, ::Type{D}; kws...) where {D<:AbstractDimensions,Q<:AbstractQuantity} = constructor_of(Q)(x, D(; kws...)) +(::Type{Q})(x::T; kws...) where {T,T2,Q<:AbstractQuantity{T2}} = constructor_of(Q)(convert(T2, x), dim_type(Q)(; kws...)) +(::Type{Q})(x; kws...) where {Q<:AbstractQuantity} = constructor_of(Q)(x, dim_type(Q)(; kws...)) -(::Type{Q})(x, ::Type{D}; kws...) where {D<:AbstractDimensions,T,Q<:AbstractQuantity{T}} = quantity_constructor(Q){T, D}(convert(T, x), D(DEFAULT_DIM_BASE_TYPE; kws...)) -(::Type{Q})(x, ::Type{D}; kws...) where {D<:AbstractDimensions,Q<:AbstractQuantity} = quantity_constructor(Q){typeof(x), D}(x, D(DEFAULT_DIM_BASE_TYPE; kws...)) +(::Type{Q})(q::AbstractQuantity) where {T,D<:AbstractDimensions,Q<:AbstractQuantity{T,D}} = constructor_of(Q)(convert(T, ustrip(q)), convert(D, dimension(q))) +(::Type{Q})(q::AbstractQuantity) where {T,Q<:AbstractQuantity{T}} = constructor_of(Q)(convert(T, ustrip(q)), dimension(q)) -(::Type{Q})(x; kws...) where {T,D<:AbstractDimensions,Q<:AbstractQuantity{T,D}} = quantity_constructor(Q)(convert(T, x), D; kws...) -(::Type{Q})(x; kws...) where {T,Q<:AbstractQuantity{T}} = quantity_constructor(Q)(convert(T, x), DEFAULT_DIM_TYPE; kws...) -(::Type{Q})(x; kws...) where {Q<:AbstractQuantity} = quantity_constructor(Q)(x, DEFAULT_DIM_TYPE; kws...) +new_dimensions(::Type{D}, dims...) where {D<:AbstractDimensions} = constructor_of(D)(dims...) +new_quantity(::Type{Q}, l, r) where {Q<:AbstractQuantity} = constructor_of(Q)(l, r) -(::Type{Q})(q::AbstractQuantity) where {T,Q<:AbstractQuantity{T}} = new_quantity(Q, convert(T, ustrip(q)), dimension(q)) -(::Type{Q})(q::AbstractQuantity) where {T,D<:AbstractDimensions,Q<:AbstractQuantity{T,D}} = new_quantity(Q, convert(T, ustrip(q)), convert(D, dimension(q))) - -new_dimensions(::Type{QD}, dims...) where {QD<:Union{AbstractQuantity,AbstractDimensions}} = dimension_constructor(QD)(dims...) -new_quantity(::Type{QD}, l, r) where {QD<:Union{AbstractQuantity}} = quantity_constructor(QD)(l, r) - -function constructor_of(::Type{T}) where {T} - return Base.typename(T).wrapper -end - -""" - dimension_constructor(::Type{<:AbstractDimensions}) - -This function returns the container for a particular `AbstractDimensions`. -For example, `Dimensions` will get returned as `Dimensions`, and -`Dimensions{Rational{Int64}}` will also get returned as `Dimensions`. -""" -dimension_constructor(::Type{D}) where {D<:AbstractDimensions} = constructor_of(D) - -""" - dimension_constructor(::Type{<:AbstractQuantity}) - -This function returns the `Dimensions` type used inside -a particular `Quantity` type by reading the `.dimensions` field. -It also strips the type parameter (i.e., `Dimensions{R} -> Dimensions`). -""" -dimension_constructor(::Type{Q}) where {T,D<:AbstractDimensions,Q<:AbstractQuantity{T,D}} = constructor_of(D) - -""" - quantity_constructor(::Type{<:AbstractQuantity}) - -This function returns the container for a particular `AbstractQuantity`. -For example, `Quantity` gets returned as `Quantity`, `Quantity{Float32}` also -as `Quantity`, and `Quantity{Float32,Rational{Int64}}` also as `Quantity`. -""" -quantity_constructor(::Type{Q}) where {Q<:AbstractQuantity} = constructor_of(Q) +dim_type(::Type{Q}) where {T,D<:AbstractDimensions,Q<:AbstractQuantity{T,D}} = D +dim_type(::Type{<:AbstractQuantity}) = DEFAULT_DIM_TYPE +constructor_of(::Type{T}) where {T} = Base.typename(T).wrapper struct DimensionError{Q1,Q2} <: Exception q1::Q1 diff --git a/src/utils.jl b/src/utils.jl index df2e117f..8792bb56 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -46,10 +46,7 @@ Base.isless(l::AbstractQuantity, r::AbstractQuantity) = dimension(l) == dimensio Base.isless(l::AbstractQuantity, r) = iszero(dimension(l)) ? isless(ustrip(l), r) : throw(DimensionError(l, r)) Base.isless(l, r::AbstractQuantity) = iszero(dimension(r)) ? isless(l, ustrip(r)) : throw(DimensionError(l, r)) Base.isapprox(l::AbstractQuantity, r::AbstractQuantity; kws...) = isapprox(ustrip(l), ustrip(r); kws...) && dimension(l) == dimension(r) -Base.length(::AbstractDimensions) = 1 Base.length(::AbstractQuantity) = 1 -Base.iterate(d::AbstractDimensions) = (d, nothing) -Base.iterate(::AbstractDimensions, ::Nothing) = nothing Base.iterate(q::AbstractQuantity) = (q, nothing) Base.iterate(::AbstractQuantity, ::Nothing) = nothing diff --git a/test/unittests.jl b/test/unittests.jl index b9760d73..8b73463a 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -1,6 +1,5 @@ using DynamicQuantities using DynamicQuantities: DEFAULT_DIM_BASE_TYPE, DEFAULT_DIM_TYPE, DEFAULT_VALUE_TYPE -import DynamicQuantities: quantity_constructor, dimension_constructor using Ratios: SimpleRatio using SaferIntegers: SafeInt16 using Test @@ -134,7 +133,7 @@ end @test ustrip(ones(32)) == ones(32) @test dimension(Dimensions()) === Dimensions() - @test_throws MethodError Dimensions(1.0) + @test_throws AssertionError Dimensions(1.0) @test_throws ErrorException ustrip(Dimensions()) end @@ -352,9 +351,17 @@ end @test typeof(oneunit(x) + x) == MyQuantity{T,D} # Automatic conversions: + @test typeof(MyQuantity(1, MyDimensions, length=0.5)) == MyQuantity{typeof(1),MyDimensions{DEFAULT_DIM_BASE_TYPE}} + @test typeof(MyQuantity{T}(1, MyDimensions, length=0.5)) == MyQuantity{T,MyDimensions{DEFAULT_DIM_BASE_TYPE}} + @test typeof(MyQuantity{T}(1, D, length=0.5)) == MyQuantity{T,D} @test typeof(MyQuantity{T,D}(0.1, length=0.5)) == MyQuantity{T,D} @test typeof(0.5 * MyQuantity{T,D}(0.1, length=0.5)) == MyQuantity{promote_type(T,Float64),D} + x = MyQuantity(big(0.1), length=1) + @test typeof(x) == MyQuantity{BigFloat,DEFAULT_DIM_TYPE} + @test typeof(MyQuantity{T}(x)) == MyQuantity{T,DEFAULT_DIM_TYPE} + @test typeof(MyQuantity{T,D}(x)) == MyQuantity{T,D} + # Using MyDimensions inside regular Quantity: x = Quantity(T(0.1), MyDimensions(R, length=0.5)) @test typeof(x) == Quantity{T,MyDimensions{R}} From 895c69e36051be9b4e49d04ee74f297f2c48409f Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sat, 24 Jun 2023 19:20:04 -0400 Subject: [PATCH 22/28] Simplify abstract constructors --- src/types.jl | 6 ------ test/unittests.jl | 12 +++++++++++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/types.jl b/src/types.jl index 5209e8c1..208f6549 100644 --- a/src/types.jl +++ b/src/types.jl @@ -43,12 +43,6 @@ struct Dimensions{R<:Real} <: AbstractDimensions{R} amount::R end -(::Type{D})(args...) where {R,D<:AbstractDimensions{R}} = - begin - @assert length(args) == length(static_fieldnames(D)) # (just to prevent stack overflows from misuse) - constructor_of(D){R}(Base.Fix1(convert, R).(args)...) - end -(::Type{D})(args...) where {D<:AbstractDimensions} = constructor_of(D){DEFAULT_DIM_BASE_TYPE}(args...) (::Type{D})(::Type{R}; kws...) where {R,D<:AbstractDimensions} = constructor_of(D){R}((tryrationalize(R, get(kws, k, zero(R))) for k in static_fieldnames(D))...) (::Type{D})(; kws...) where {R,D<:AbstractDimensions{R}} = constructor_of(D)(R; kws...) (::Type{D})(; kws...) where {D<:AbstractDimensions} = D(DEFAULT_DIM_BASE_TYPE; kws...) diff --git a/test/unittests.jl b/test/unittests.jl index 8b73463a..65509f63 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -1,4 +1,5 @@ using DynamicQuantities +using DynamicQuantities: FixedRational using DynamicQuantities: DEFAULT_DIM_BASE_TYPE, DEFAULT_DIM_TYPE, DEFAULT_VALUE_TYPE using Ratios: SimpleRatio using SaferIntegers: SafeInt16 @@ -133,7 +134,7 @@ end @test ustrip(ones(32)) == ones(32) @test dimension(Dimensions()) === Dimensions() - @test_throws AssertionError Dimensions(1.0) + @test_throws MethodError Dimensions(1.0) @test_throws ErrorException ustrip(Dimensions()) end @@ -329,6 +330,11 @@ end @test_throws LoadError eval(:(u":x")) end +@testset "Additional tests of FixedRational" begin + @test convert(Int64, FixedRational{Int64,1000}(2 // 1)) == 2 + @test convert(Int32, FixedRational{Int64,1000}(3 // 1)) == 3 +end + struct MyDimensions{R} <: AbstractDimensions{R} length::R mass::R @@ -377,6 +383,10 @@ end @test MyQuantity(0.1, DEFAULT_DIM_TYPE, length=0.5) == MyQuantity(0.1, length=0.5) @test MyQuantity(0.1, DEFAULT_DIM_TYPE, length=0.5) == MyQuantity(0.1, length=1//2) + # Can construct using args directly: + @test typeof(MyDimensions(1, 1, 1)) == MyDimensions{Int} + @test typeof(MyDimensions{Float64}(1, 1, 1)) == MyDimensions{Float64} + # But, we always need to use a quantity when mixing with mathematical operations: @test_throws ErrorException MyQuantity(0.1) + 0.1 * MyDimensions() end From b583e464b930244946f7560b427018851d2f9f67 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sat, 24 Jun 2023 19:22:20 -0400 Subject: [PATCH 23/28] Clean up docs --- src/types.jl | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/types.jl b/src/types.jl index 208f6549..8639e491 100644 --- a/src/types.jl +++ b/src/types.jl @@ -71,11 +71,14 @@ dimensions according to the operation. # Constructors -- `Quantity(x; kws...)`: Construct a quantity with value `x` and dimensions given by the keyword arguments. The value type is inferred from `x`. `R` is set to `DEFAULT_DIM_TYPE`. -- `Quantity(x, ::Type{D}; kws...)`: Construct a quantity with value `x` with no dimensions, and the dimensions type set to `D`. +- `Quantity(x; kws...)`: Construct a quantity with value `x` and dimensions given by the keyword arguments. The value + type is inferred from `x`. `R` is set to `DEFAULT_DIM_TYPE`. +- `Quantity(x, ::Type{D}; kws...)`: Construct a quantity with value `x` with dimensions given by the keyword arguments, + and the dimensions type set to `D`. - `Quantity(x, d::D)`: Construct a quantity with value `x` and dimensions `d` of type `D`. - `Quantity{T}(...)`: As above, but converting the value to type `T`. You may also pass a `Quantity` as input. -- `Quantity{T,D}(...)`: As above, but converting the value to type `T` and dimensions to `D`. You may also pass a `Quantity` as input. +- `Quantity{T,D}(...)`: As above, but converting the value to type `T` and dimensions to `D`. You may also pass a + `Quantity` as input. """ struct Quantity{T,D<:AbstractDimensions} <: AbstractQuantity{T,D} value::T From 5c7f06941074731eeeddc62ffc77d21e7856fbb9 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sat, 24 Jun 2023 19:46:35 -0400 Subject: [PATCH 24/28] Type conversion tests --- test/unittests.jl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/unittests.jl b/test/unittests.jl index 65509f63..396baecc 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -272,6 +272,13 @@ end @test typeof(convert(Quantity{Float16}, q)) == Quantity{Float16,Dimensions{Rational{Int16}}} @test convert(Quantity, q) === q + # Test that regular type promotion applies: + q = Quantity(2, d) + @test typeof(q) == Quantity{Int64,typeof(d)} + @test typeof(q ^ 2) == Quantity{Int64,typeof(d)} + @test typeof(0.5 * q) == Quantity{Float64,typeof(d)} + @test typeof(inv(q)) == Quantity{Float64,typeof(d)} + # Automatic conversions via constructor: for T in [Float16, Float32, Float64, BigFloat], R in [DEFAULT_DIM_BASE_TYPE, Rational{Int16}, Rational{Int32}, SimpleRatio{Int}, SimpleRatio{SafeInt16}] D = Dimensions{R} From 53a9a330e71afc43aaa1b69a50eb4cd5e8b4288a Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 25 Jun 2023 15:56:27 -0400 Subject: [PATCH 25/28] Print non-reals with parentheses --- src/utils.jl | 3 ++- test/unittests.jl | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/utils.jl b/src/utils.jl index 8792bb56..0e89e8f4 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -84,7 +84,8 @@ Base.show(io::IO, d::AbstractDimensions) = s = replace(s, r"\s*$" => "") print(io, s) end -Base.show(io::IO, q::AbstractQuantity) = print(io, ustrip(q), " ", dimension(q)) +Base.show(io::IO, q::AbstractQuantity{<:Real}) = print(io, ustrip(q), " ", dimension(q)) +Base.show(io::IO, q::AbstractQuantity) = print(io, "(", ustrip(q), ") ", dimension(q)) function dimension_name(::AbstractDimensions, k::Symbol) default_dimensions = (length="m", mass="kg", time="s", current="A", temperature="K", luminosity="cd", amount="mol") diff --git a/test/unittests.jl b/test/unittests.jl index 396baecc..8274bc2e 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -123,6 +123,8 @@ using Test @test_throws DimensionError 1.0 - Quantity(one(T), D, length=1) end + @test string((0.5 + 0.5im)*u"km/s") == "(500.0 + 500.0im) m s⁻¹" + x = Quantity(-1.2, length=2 // 5) @test abs(x) == Quantity(1.2, length=2 // 5) From 12308def9b795f19fe4d4d6fecb2706b7929998c Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 25 Jun 2023 16:06:36 -0400 Subject: [PATCH 26/28] Fix promotion rules for complex --- src/math.jl | 14 ++++++++------ test/unittests.jl | 11 +++++++++-- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/math.jl b/src/math.jl index dc968571..9f59e381 100644 --- a/src/math.jl +++ b/src/math.jl @@ -25,13 +25,15 @@ Base.:+(l, r::AbstractQuantity) = iszero(dimension(r)) ? new_quantity(typeof(r), Base.:-(l::AbstractQuantity, r) = l + (-r) Base.:-(l, r::AbstractQuantity) = l + (-r) -_pow(l::AbstractDimensions, r) = map_dimensions(Base.Fix1(*, r), l) -_pow(l::AbstractQuantity{T}, r) where {T} = new_quantity(typeof(l), ustrip(l)^r, _pow(dimension(l), r)) -_pow_as_T(l::AbstractQuantity{T}, r) where {T} = new_quantity(typeof(l), ustrip(l)^convert(T, r), _pow(l.dimensions, r)) -Base.:^(l::AbstractDimensions{R}, r::Integer) where {R} = _pow(l, r) +# We don't promote on the dimension types: +_pow(l::AbstractDimensions{R}, r::R) where {R} = map_dimensions(Base.Fix1(*, r), l) Base.:^(l::AbstractDimensions{R}, r::Number) where {R} = _pow(l, tryrationalize(R, r)) -Base.:^(l::AbstractQuantity{T,D}, r::Integer) where {T,R,D<:AbstractDimensions{R}} = _pow(l, r) -Base.:^(l::AbstractQuantity{T,D}, r::Number) where {T,R,D<:AbstractDimensions{R}} = _pow_as_T(l, tryrationalize(R, r)) +Base.:^(l::AbstractQuantity{T,D}, r::Integer) where {T,R,D<:AbstractDimensions{R}} = new_quantity(typeof(l), ustrip(l)^r, dimension(l)^r) +Base.:^(l::AbstractQuantity{T,D}, r::Number) where {T,R,D<:AbstractDimensions{R}} = + let dim_pow = tryrationalize(R, r), val_pow = convert(T, dim_pow) + # Need to ensure we take the numerical power by the rationalized quantity: + return new_quantity(typeof(l), ustrip(l)^val_pow, dimension(l)^dim_pow) + end Base.inv(d::AbstractDimensions) = map_dimensions(-, d) Base.inv(q::AbstractQuantity) = new_quantity(typeof(q), inv(ustrip(q)), inv(dimension(q))) diff --git a/test/unittests.jl b/test/unittests.jl index 8274bc2e..cea8a5e5 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -123,14 +123,21 @@ using Test @test_throws DimensionError 1.0 - Quantity(one(T), D, length=1) end - @test string((0.5 + 0.5im)*u"km/s") == "(500.0 + 500.0im) m s⁻¹" - x = Quantity(-1.2, length=2 // 5) @test abs(x) == Quantity(1.2, length=2 // 5) @test abs(x) == abs(Quantity(1.2, length=2 // 5)) end +@testset "Complex numbers" begin + x = (0.5 + 0.5im) * u"km/s" + @test string(x) == "(500.0 + 500.0im) m s⁻¹" + @test typeof(x) == Quantity{Complex{Float64}, DEFAULT_DIM_TYPE} + @test typeof(x^2) == Quantity{Complex{Float64}, DEFAULT_DIM_TYPE} + @test x^2/u"km/s"^2 == Quantity(0.5im) + @test x^2.5 ≈ (-5.088059320440205e6 + 1.2283661817565577e7im) * u"m^(5/2) * s^(-5/2)" +end + @testset "Fallbacks" begin @test ustrip(0.5) == 0.5 @test ustrip(ones(32)) == ones(32) From 6b93f9748e90e4e02eeb9775f3a12c36b8bee709 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 25 Jun 2023 19:34:35 -0400 Subject: [PATCH 27/28] Fix upreferred validation --- .github/workflows/CI.yml | 29 +++++++++++++++++++++++++---- ext/DynamicQuantitiesUnitfulExt.jl | 18 ++++++++++++++---- test/runtests.jl | 19 ++++++++++++------- test/test_ban_upreferred.jl | 13 +++++++++++++ test/test_unitful.jl | 2 ++ 5 files changed, 66 insertions(+), 15 deletions(-) create mode 100644 test/test_ban_upreferred.jl diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index ef5d9520..aeb0be86 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -32,10 +32,31 @@ jobs: arch: ${{ matrix.arch }} - uses: julia-actions/cache@v1 - uses: julia-actions/julia-buildpkg@v1 - - uses: julia-actions/julia-runtest@v1 - - uses: julia-actions/julia-uploadcoveralls@v1 - env: - COVERALLS_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + - name: "Run tests" + run: | + julia --color=yes --project=. -e 'import Pkg; Pkg.add("Coverage")' + julia --color=yes --threads=auto --check-bounds=yes --depwarn=yes --code-coverage=user --project=. -e 'import Pkg; Pkg.test(coverage=true)' + DQ_TEST_UPREFERRED=true julia --color=yes --threads=auto --check-bounds=yes --depwarn=yes --code-coverage=user --project=. -e 'import Pkg; Pkg.test(coverage=true)' + julia --color=yes --project=. coverage.jl + - name: "Coveralls" + uses: coverallsapp/github-action@v2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + file: lcov.info + parallel: true + flag-name: julia-${{ matrix.version }}-${{ matrix.os }}-${{ matrix.arch }} + + coveralls: + name: Indicate completion to coveralls + runs-on: ubuntu-latest + needs: test + steps: + - name: Finish + uses: coverallsapp/github-action@v2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + parallel-finished: true + docs: name: Documentation runs-on: ubuntu-latest diff --git a/ext/DynamicQuantitiesUnitfulExt.jl b/ext/DynamicQuantitiesUnitfulExt.jl index 16dbaf39..b4eda721 100644 --- a/ext/DynamicQuantitiesUnitfulExt.jl +++ b/ext/DynamicQuantitiesUnitfulExt.jl @@ -10,19 +10,28 @@ else import ..Unitful: @u_str end -# This lets the user override the preferred units: -function unitful_equivalences() - si_units = (length=u"m", mass=u"kg", time=u"s", current=u"A", temperature=u"K", luminosity=u"cd", amount=u"mol") +function get_si_units() + return (length=u"m", mass=u"kg", time=u"s", current=u"A", temperature=u"K", luminosity=u"cd", amount=u"mol") +end + +function validate_upreferred() + si_units = get_si_units() for k in keys(si_units) if Unitful.upreferred(si_units[k]) !== si_units[k] error("Found custom `Unitful.preferunits`. This is not supported when interfacing Unitful and DynamicQuantities: you must leave the default `Unitful.upreferred`, which is the SI base units.") end end + return true +end + +function unitful_equivalences() + si_units = get_si_units() return NamedTuple((k => si_units[k] for k in keys(si_units))) end Base.convert(::Type{Unitful.Quantity}, x::DynamicQuantities.Quantity) = let + validate_upreferred() cumulator = DynamicQuantities.ustrip(x) dims = DynamicQuantities.dimension(x) equiv = unitful_equivalences() @@ -42,9 +51,10 @@ Base.convert(::Type{DynamicQuantities.Quantity{T,D}}, x::Unitful.Quantity) where return DynamicQuantities.Quantity(convert(T, value), dimension) end -Base.convert(::Type{DynamicQuantities.Dimensions}, d::Unitful.Dimensions) = convert(DynamicQuantities.Dimensions{DynamicQuantities.DEFAULT_DIM_TYPE}, d) +Base.convert(::Type{DynamicQuantities.Dimensions}, d::Unitful.Dimensions) = convert(DynamicQuantities.DEFAULT_DIM_TYPE, d) Base.convert(::Type{DynamicQuantities.Dimensions{R}}, d::Unitful.Dimensions{D}) where {R,D} = let + validate_upreferred() cumulator = DynamicQuantities.Dimensions{R}() for dim in D dim_symbol = _map_dim_name_to_dynamic_units(typeof(dim)) diff --git a/test/runtests.jl b/test/runtests.jl index 212af775..9193bbf4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -5,10 +5,15 @@ import Ratios: SimpleRatio @eval Base.round(T, x::SimpleRatio) = round(T, x.num // x.den) end -@safetestset "Unitful.jl integration tests" begin - include("test_unitful.jl") -end - -@safetestset "Unit tests" begin - include("unittests.jl") -end +if parse(Bool, get(ENV, "DQ_TEST_UPREFERRED", "false")) + @safetestset "Test upreferred disallowed" begin + include("test_ban_upreferred.jl") + end +else + @safetestset "Unitful.jl integration tests" begin + include("test_unitful.jl") + end + @safetestset "Unit tests" begin + include("unittests.jl") + end +end \ No newline at end of file diff --git a/test/test_ban_upreferred.jl b/test/test_ban_upreferred.jl new file mode 100644 index 00000000..63db6118 --- /dev/null +++ b/test/test_ban_upreferred.jl @@ -0,0 +1,13 @@ +### These are tests we need to run with a fresh Julia runtime + +import Unitful +import Unitful: @u_str +Unitful.preferunits(u"km") +using Test +import DynamicQuantities + +x_unitful = 1.5u"km" +x_dq = DynamicQuantities.Quantity(1500.0, length=1) + +@test_throws ErrorException convert(DynamicQuantities.Quantity, x_unitful) +@test_throws ErrorException convert(Unitful.Quantity, x_dq) diff --git a/test/test_unitful.jl b/test/test_unitful.jl index fe652c08..4153e567 100644 --- a/test/test_unitful.jl +++ b/test/test_unitful.jl @@ -22,4 +22,6 @@ for T in [DEFAULT_VALUE_TYPE, Float16, Float32, Float64], R in [DEFAULT_DIM_BASE @test isapprox(convert(DynamicQuantities.Quantity{T,D}, x_unitful), x; atol=1e-6) @test risapprox(convert(Unitful.Quantity, convert(DynamicQuantities.Quantity{T,D}, x_unitful)), Unitful.upreferred(x_unitful); atol=1e-6) + + @test typeof(convert(DynamicQuantities.Dimensions, Unitful.dimension(x_unitful))) == DynamicQuantities.Dimensions{DEFAULT_DIM_BASE_TYPE} end From a365da00aa9566089539ba6681cfdfaa234e5732 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 25 Jun 2023 20:07:38 -0400 Subject: [PATCH 28/28] Test custom unit raises error --- test/runtests.jl | 2 +- test/test_unitful.jl | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 9193bbf4..82e5973e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -16,4 +16,4 @@ else @safetestset "Unit tests" begin include("unittests.jl") end -end \ No newline at end of file +end diff --git a/test/test_unitful.jl b/test/test_unitful.jl index 4153e567..ac8cf72f 100644 --- a/test/test_unitful.jl +++ b/test/test_unitful.jl @@ -25,3 +25,16 @@ for T in [DEFAULT_VALUE_TYPE, Float16, Float32, Float64], R in [DEFAULT_DIM_BASE @test typeof(convert(DynamicQuantities.Dimensions, Unitful.dimension(x_unitful))) == DynamicQuantities.Dimensions{DEFAULT_DIM_BASE_TYPE} end + +module MyScaleUnit + using Unitful + @dimension(𝐒, "𝐒", Scale) + @refunit(scale, "scale", Scale, 𝐒, false) +end + +Unitful.register(MyScaleUnit) + +x = 1.0u"scale" +@test typeof(x) <: Unitful.Quantity{Float64, MyScaleUnit.𝐒} +@test_throws ErrorException convert(DynamicQuantities.Quantity, x) +# These are not supported because there is no SI equivalency