From f4b811233ffac535c05a77f28fec5c70cc541c03 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 12 Nov 2023 23:32:40 +0000 Subject: [PATCH 01/39] [WIP] Trying to add RealQuantity --- src/DynamicQuantities.jl | 2 +- src/arrays.jl | 3 +-- src/constants.jl | 2 +- src/fixed_rational.jl | 11 +++++++++- src/symbolic_dimensions.jl | 35 +++++++++++++++++--------------- src/types.jl | 41 +++++++++++++++++++++++++++++++++++--- src/units.jl | 2 +- src/uparse.jl | 16 +++++++-------- src/utils.jl | 41 ++++++++++++++++++++++++++++---------- 9 files changed, 110 insertions(+), 43 deletions(-) diff --git a/src/DynamicQuantities.jl b/src/DynamicQuantities.jl index 08d06121..a78cb026 100644 --- a/src/DynamicQuantities.jl +++ b/src/DynamicQuantities.jl @@ -2,7 +2,7 @@ module DynamicQuantities export Units, Constants export AbstractDimensions, AbstractQuantity, AbstractGenericQuantity, UnionAbstractQuantity -export Quantity, GenericQuantity, Dimensions, SymbolicDimensions, QuantityArray, DimensionError +export Quantity, GenericQuantity, RealQuantity, Dimensions, SymbolicDimensions, QuantityArray, DimensionError export ustrip, dimension export ulength, umass, utime, ucurrent, utemperature, uluminosity, uamount export uparse, @u_str, sym_uparse, @us_str, uexpand, uconvert diff --git a/src/arrays.jl b/src/arrays.jl index 775b7ce7..1ca73be1 100644 --- a/src/arrays.jl +++ b/src/arrays.jl @@ -15,7 +15,7 @@ and so can be used in most places where a normal array would be used, including # Constructors - `QuantityArray(v::AbstractArray, d::AbstractDimensions)`: Create a `QuantityArray` with value `v` and dimensions `d`, - using `Quantity` if the eltype of `v` is numeric, and `GenericQuantity` otherwise. + using `RealQuantity` if the eltype of `v` is real, `Quantity` if it is numeric, and `GenericQuantity` otherwise. - `QuantityArray(v::AbstractArray{<:Number}, q::AbstractQuantity)`: Create a `QuantityArray` with value `v` and dimensions inferred with `dimension(q)`. This is so that you can easily create an array with the units module, like so: ```julia @@ -52,7 +52,6 @@ struct QuantityArray{T,N,D<:AbstractDimensions,Q<:UnionAbstractQuantity{T,D},V<: end end -# Construct with a Quantity (easier, as you can use the units): QuantityArray(v::AbstractArray; kws...) = QuantityArray(v, DEFAULT_DIM_TYPE(; kws...)) for (type, base_type, default_type) in ABSTRACT_QUANTITY_TYPES @eval begin diff --git a/src/constants.jl b/src/constants.jl index 736a71b4..d69420ad 100644 --- a/src/constants.jl +++ b/src/constants.jl @@ -1,7 +1,7 @@ module Constants import ..DEFAULT_QUANTITY_TYPE -import ..Quantity +import ..RealQuantity import ..Units as U import ..Units: _add_prefixes diff --git a/src/fixed_rational.jl b/src/fixed_rational.jl index a6a3313e..2f2ae061 100644 --- a/src/fixed_rational.jl +++ b/src/fixed_rational.jl @@ -83,11 +83,20 @@ end function Base.promote_rule(::Type{<:FixedRational{T1}}, ::Type{Rational{T2}}) where {T1,T2} return Rational{promote_type(T1,T2)} end +function Base.promote_rule(::Type{Rational{T2}}, ::Type{<:FixedRational{T1}}) where {T1,T2} + return Rational{promote_type(T1,T2)} +end function Base.promote_rule(::Type{<:FixedRational{T1}}, ::Type{T2}) where {T1,T2<:Real} return promote_type(Rational{T1}, T2) end +function Base.promote_rule(::Type{T2}, ::Type{<:FixedRational{T1}}) where {T1,T2<:Real} + return promote_type(Rational{T1}, T2) +end +# Want to consume integers: function Base.promote_rule(::Type{F}, ::Type{<:Integer}) where {F<:FixedRational} - # Want to consume integers: + return F +end +function Base.promote_rule(::Type{<:Integer}, ::Type{F}) where {F<:FixedRational} return F end diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index 8638f705..8cf37c4d 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -104,9 +104,9 @@ end uexpand(q::UnionAbstractQuantity{<:Any,<:SymbolicDimensions}) Expand the symbolic units in a quantity to their base SI form. -In other words, this converts a `Quantity` with `SymbolicDimensions` +In other words, this converts a quantity with `SymbolicDimensions` to one with `Dimensions`. The opposite of this function is `uconvert`, -for converting to specific symbolic units, or `convert(Quantity{<:Any,<:SymbolicDimensions}, q)`, +for converting to specific symbolic units, or, e.g., `convert(Quantity{<:Any,<:SymbolicDimensions}, q)`, for assuming SI units as the output symbols. """ function uexpand(q::Q) where {T,R,D<:SymbolicDimensions{R},Q<:UnionAbstractQuantity{T,D}} @@ -277,7 +277,7 @@ module SymbolicUnitsParse import ..SYMBOL_CONFLICTS import ..SymbolicDimensions - import ...Quantity + import ...RealQuantity import ...DEFAULT_VALUE_TYPE import ...DEFAULT_DIM_BASE_TYPE @@ -287,7 +287,7 @@ module SymbolicUnitsParse import ..SYMBOL_CONFLICTS import ..SymbolicDimensions - import ..Quantity + import ..RealQuantity import ..DEFAULT_VALUE_TYPE import ..DEFAULT_DIM_BASE_TYPE @@ -299,11 +299,11 @@ module SymbolicUnitsParse CONSTANT_SYMBOLS_EXIST[] || lock(CONSTANT_SYMBOLS_LOCK) do CONSTANT_SYMBOLS_EXIST[] && return nothing for unit in setdiff(CONSTANT_SYMBOLS, SYMBOL_CONFLICTS) - @eval const $unit = Quantity(DEFAULT_VALUE_TYPE(1.0), SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}; $(unit)=1) + @eval const $unit = RealQuantity(DEFAULT_VALUE_TYPE(1.0), SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}; $(unit)=1) end # Evaluate conflicting symbols to non-symbolic form: for unit in SYMBOL_CONFLICTS - @eval const $unit = convert(Quantity{DEFAULT_VALUE_TYPE,SymbolicDimensions}, EagerConstants.$unit) + @eval const $unit = convert(RealQuantity{DEFAULT_VALUE_TYPE,SymbolicDimensions}, EagerConstants.$unit) end CONSTANT_SYMBOLS_EXIST[] = true end @@ -318,7 +318,7 @@ module SymbolicUnitsParse UNIT_SYMBOLS_EXIST[] || lock(UNIT_SYMBOLS_LOCK) do UNIT_SYMBOLS_EXIST[] && return nothing for unit in UNIT_SYMBOLS - @eval const $unit = Quantity(DEFAULT_VALUE_TYPE(1.0), SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}; $(unit)=1) + @eval const $unit = RealQuantity(DEFAULT_VALUE_TYPE(1.0), SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}; $(unit)=1) end UNIT_SYMBOLS_EXIST[] = true end @@ -329,27 +329,27 @@ module SymbolicUnitsParse sym_uparse(raw_string::AbstractString) Parse a string containing an expression of units and return the - corresponding `Quantity` object with `Float64` value. + corresponding `RealQuantity` object with `Float64` value. However, that unlike the regular `u"..."` macro, this macro uses `SymbolicDimensions` for the dimension type, which means that all units and constants are stored symbolically and will not automatically expand to SI units. For example, `sym_uparse("km/s^2")` would be parsed to - `Quantity(1.0, SymbolicDimensions, km=1, s=-2)`. + `RealQuantity(1.0, SymbolicDimensions, km=1, s=-2)`. Note that inside this expression, you also have access to the `Constants` module. So, for example, `sym_uparse("Constants.c^2 * Hz^2")` would evaluate to - `Quantity(1.0, SymbolicDimensions, c=2, Hz=2)`. However, note that due to + `RealQuantity(1.0, SymbolicDimensions, c=2, Hz=2)`. However, note that due to namespace collisions, a few physical constants are automatically converted. """ function sym_uparse(raw_string::AbstractString) _generate_unit_symbols() Constants._generate_unit_symbols() raw_result = eval(Meta.parse(raw_string)) - return copy(as_quantity(raw_result))::Quantity{DEFAULT_VALUE_TYPE,SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}} + return copy(as_quantity(raw_result))::RealQuantity{DEFAULT_VALUE_TYPE,SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}} end - as_quantity(q::Quantity) = q - as_quantity(x::Number) = Quantity(convert(DEFAULT_VALUE_TYPE, x), SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}) + as_quantity(q::RealQuantity) = q + as_quantity(x::Number) = RealQuantity(convert(DEFAULT_VALUE_TYPE, x), SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}) as_quantity(x) = error("Unexpected type evaluated: $(typeof(x))") end @@ -359,15 +359,15 @@ import .SymbolicUnitsParse: sym_uparse us"[unit expression]" Parse a string containing an expression of units and return the -corresponding `Quantity` object with `Float64` value. However, +corresponding `RealQuantity` object with `Float64` value. However, unlike the regular `u"..."` macro, this macro uses `SymbolicDimensions` for the dimension type, which means that all units and constants are stored symbolically and will not automatically expand to SI units. -For example, `us"km/s^2"` would be parsed to `Quantity(1.0, SymbolicDimensions, km=1, s=-2)`. +For example, `us"km/s^2"` would be parsed to `RealQuantity(1.0, SymbolicDimensions, km=1, s=-2)`. Note that inside this expression, you also have access to the `Constants` module. So, for example, `us"Constants.c^2 * Hz^2"` would evaluate to -`Quantity(1.0, SymbolicDimensions, c=2, Hz=2)`. However, note that due to +`RealQuantity(1.0, SymbolicDimensions, c=2, Hz=2)`. However, note that due to namespace collisions, a few physical constants are automatically converted. """ macro us_str(s) @@ -380,3 +380,6 @@ end function Base.promote_rule(::Type{SymbolicDimensions{R1}}, ::Type{Dimensions{R2}}) where {R1,R2} return Dimensions{promote_type(R1,R2)} end +function Base.promote_rule(::Type{Dimensions{R2}}, ::Type{SymbolicDimensions{R1}}) where {R1,R2} + return Dimensions{promote_type(R1,R2)} +end diff --git a/src/types.jl b/src/types.jl index 116dd883..727bb746 100644 --- a/src/types.jl +++ b/src/types.jl @@ -56,6 +56,14 @@ _as well as any other future abstract quantity types_, """ abstract type AbstractGenericQuantity{T,D} end +""" + AbstractRealQuantity{T,D} <: Real + +This has the same behavior as `AbstractQuantity` but is subtyped to `Real` rather +than `Number`. +""" +abstract type AbstractRealQuantity{T,D} <: Real end + """ UnionAbstractQuantity{T,D} @@ -64,7 +72,7 @@ It is used throughout the library to declare methods which can take both types. You should generally specialize on this type, rather than its constituents, as it will also include future abstract quantity types. """ -const UnionAbstractQuantity{T,D} = Union{AbstractQuantity{T,D},AbstractGenericQuantity{T,D}} +const UnionAbstractQuantity{T,D} = Union{AbstractQuantity{T,D},AbstractGenericQuantity{T,D},AbstractRealQuantity{T,D}} """ Dimensions{R<:Real} <: AbstractDimensions{R} @@ -164,6 +172,18 @@ struct GenericQuantity{T,D<:AbstractDimensions} <: AbstractGenericQuantity{T,D} GenericQuantity(x::_T, dimensions::_D) where {_T,_D<:AbstractDimensions} = new{_T,_D}(x, dimensions) end +""" + RealQuantity{T<:Real,D<:AbstractDimensions} <: AbstractRealQuantity{T,D} <: Real + +This has the same behavior as `Quantity` but is subtyped to `AbstractRealQuantity <: Real`. +""" +struct RealQuantity{T<:Real,D<:AbstractDimensions} <: AbstractRealQuantity{T,D} + value::T + dimensions::D + + RealQuantity(x::_T, dimensions::_D) where {_T,_D<:AbstractDimensions} = new{_T,_D}(x, dimensions) +end + """ ABSTRACT_QUANTITY_TYPES @@ -171,7 +191,18 @@ A constant tuple of the existing abstract quantity types, each as a tuple with (1) the abstract type, (2) the base type, and (3) the default exported concrete type. """ -const ABSTRACT_QUANTITY_TYPES = ((AbstractQuantity, Number, Quantity), (AbstractGenericQuantity, Any, GenericQuantity)) +const ABSTRACT_QUANTITY_TYPES = ((AbstractQuantity, Number, Quantity), (AbstractGenericQuantity, Any, GenericQuantity), (AbstractRealQuantity, Real, RealQuantity)) + +""" + promote_quantity(::Type{<:UnionAbstractQuantity}, t::Type{<:Any}) + +Find the next quantity type in the hierarchy that can accommodate the type `t`. +If the current quantity type can already accommodate `t`, then the current type is returned. +""" +promote_quantity(::Type{<:Union{GenericQuantity,Quantity,RealQuantity}}, ::Type{<:Any}) = GenericQuantity +promote_quantity(::Type{<:Union{Quantity,RealQuantity}}, ::Type{<:Number}) = Quantity +promote_quantity(::Type{<:RealQuantity}, ::Type{<:Real}) = RealQuantity +promote_quantity(T, _) = t for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES @eval begin @@ -189,7 +220,7 @@ for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES end end -const DEFAULT_QUANTITY_TYPE = Quantity{DEFAULT_VALUE_TYPE, DEFAULT_DIM_TYPE} +const DEFAULT_QUANTITY_TYPE = RealQuantity{DEFAULT_VALUE_TYPE, DEFAULT_DIM_TYPE} new_dimensions(::Type{D}, dims...) where {D<:AbstractDimensions} = constructorof(D)(dims...) new_quantity(::Type{Q}, l, r) where {Q<:UnionAbstractQuantity} = constructorof(Q)(l, r) @@ -208,6 +239,7 @@ if you need custom behavior. constructorof(::Type{<:Dimensions}) = Dimensions constructorof(::Type{<:Quantity}) = Quantity constructorof(::Type{<:GenericQuantity}) = GenericQuantity +constructorof(::Type{<:RealQuantity}) = RealQuantity """ with_type_parameters(::Type{<:AbstractDimensions}, ::Type{R}) @@ -226,6 +258,9 @@ end function with_type_parameters(::Type{<:GenericQuantity}, ::Type{T}, ::Type{D}) where {T,D} return GenericQuantity{T,D} end +function with_type_parameters(::Type{<:RealQuantity}, ::Type{T}, ::Type{D}) where {T,D} + return RealQuantity{T,D} +end # The following functions should be overloaded for special types function constructorof(::Type{T}) where {T<:Union{UnionAbstractQuantity,AbstractDimensions}} diff --git a/src/units.jl b/src/units.jl index 4b8b831e..430dc049 100644 --- a/src/units.jl +++ b/src/units.jl @@ -3,7 +3,7 @@ module Units import ..DEFAULT_DIM_TYPE import ..DEFAULT_VALUE_TYPE import ..DEFAULT_QUANTITY_TYPE -import ..Quantity +import ..RealQuantity @assert DEFAULT_VALUE_TYPE == Float64 "`units.jl` must be updated to support a different default value type." diff --git a/src/uparse.jl b/src/uparse.jl index 68ee5b52..90762f5d 100644 --- a/src/uparse.jl +++ b/src/uparse.jl @@ -1,6 +1,6 @@ module UnitsParse -import ..Quantity +import ..RealQuantity import ..DEFAULT_DIM_TYPE import ..DEFAULT_VALUE_TYPE import ..Units: UNIT_SYMBOLS @@ -24,8 +24,8 @@ end uparse(s::AbstractString) Parse a string containing an expression of units and return the -corresponding `Quantity` object with `Float64` value. For example, -`uparse("m/s")` would be parsed to `Quantity(1.0, length=1, time=-1)`. +corresponding `RealQuantity` object with `Float64` value. For example, +`uparse("m/s")` would be parsed to `RealQuantity(1.0, length=1, time=-1)`. Note that inside this expression, you also have access to the `Constants` module. So, for example, `uparse("Constants.c^2 * Hz^2")` would evaluate to @@ -33,19 +33,19 @@ the quantity corresponding to the speed of light multiplied by Hertz, squared. """ function uparse(s::AbstractString) - return as_quantity(eval(Meta.parse(s)))::Quantity{DEFAULT_VALUE_TYPE,DEFAULT_DIM_TYPE} + return as_quantity(eval(Meta.parse(s)))::RealQuantity{DEFAULT_VALUE_TYPE,DEFAULT_DIM_TYPE} end -as_quantity(q::Quantity) = q -as_quantity(x::Number) = Quantity(convert(DEFAULT_VALUE_TYPE, x), DEFAULT_DIM_TYPE) +as_quantity(q::RealQuantity) = q +as_quantity(x::Number) = RealQuantity(convert(DEFAULT_VALUE_TYPE, x), DEFAULT_DIM_TYPE) as_quantity(x) = error("Unexpected type evaluated: $(typeof(x))") """ u"[unit expression]" Parse a string containing an expression of units and return the -corresponding `Quantity` object with `Float64` value. For example, -`u"km/s^2"` would be parsed to `Quantity(1000.0, length=1, time=-2)`. +corresponding `RealQuantity` object with `Float64` value. For example, +`u"km/s^2"` would be parsed to `RealQuantity(1000.0, length=1, time=-2)`. Note that inside this expression, you also have access to the `Constants` module. So, for example, `u"Constants.c^2 * Hz^2"` would evaluate to diff --git a/src/utils.jl b/src/utils.jl index d31791a4..7d29125b 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -36,10 +36,29 @@ end function Base.promote_rule(::Type{<:GenericQuantity{T1,D1}}, ::Type{<:GenericQuantity{T2,D2}}) where {T1,T2,D1,D2} return GenericQuantity{promote_type(T1,T2),promote_type(D1,D2)} end +function Base.promote_rule(::Type{<:Quantity{T1,D1}}, ::Type{<:Quantity{T2,D2}}) where {T1,T2,D1,D2} + return Quantity{promote_type(T1,T2),promote_type(D1,D2)} +end +function Base.promote_rule(::Type{<:RealQuantity{T1,D1}}, ::Type{<:RealQuantity{T2,D2}}) where {T1,T2,D1,D2} + return RealQuantity{promote_type(T1,T2),promote_type(D1,D2)} +end + function Base.promote_rule(::Type{<:Quantity{T1,D1}}, ::Type{<:GenericQuantity{T2,D2}}) where {T1,T2,D1,D2} return GenericQuantity{promote_type(T1,T2),promote_type(D1,D2)} end -function Base.promote_rule(::Type{<:Quantity{T1,D1}}, ::Type{<:Quantity{T2,D2}}) where {T1,T2,D1,D2} +function Base.promote_rule(::Type{<:GenericQuantity{T1,D1}}, ::Type{<:Quantity{T2,D2}}) where {T1,T2,D1,D2} + return GenericQuantity{promote_type(T1,T2),promote_type(D1,D2)} +end +function Base.promote_rule(::Type{<:GenericQuantity{T1,D1}}, ::Type{<:RealQuantity{T2,D2}}) where {T1,T2,D1,D2} + return GenericQuantity{promote_type(T1,T2),promote_type(D1,D2)} +end +function Base.promote_rule(::Type{<:RealQuantity{T1,D1}}, ::Type{<:GenericQuantity{T2,D2}}) where {T1,T2,D1,D2} + return GenericQuantity{promote_type(T1,T2),promote_type(D1,D2)} +end +function Base.promote_rule(::Type{<:Quantity{T1,D1}}, ::Type{<:RealQuantity{T2,D2}}) where {T1,T2,D1,D2} + return Quantity{promote_type(T1,T2),promote_type(D1,D2)} +end +function Base.promote_rule(::Type{<:RealQuantity{T1,D1}}, ::Type{<:Quantity{T2,D2}}) where {T1,T2,D1,D2} return Quantity{promote_type(T1,T2),promote_type(D1,D2)} end @@ -57,17 +76,19 @@ const BASE_NUMERIC_TYPES = Union{ Rational{Int64}, Rational{UInt64}, Rational{Int128}, Rational{UInt128}, Rational{BigInt}, } -for (type, _, _) in ABSTRACT_QUANTITY_TYPES - @eval function Base.promote_rule(::Type{Q}, ::Type{T2}) where {T,D,Q<:$type{T,D},T2<:BASE_NUMERIC_TYPES} - return with_type_parameters(Q, promote_type(T, T2), D) - end - @eval function Base.convert(::Type{Q}, x::BASE_NUMERIC_TYPES) where {T,D,Q<:$type{T,D}} - return new_quantity(Q, convert(T, x), D()) +for (type, _, _) in ABSTRACT_QUANTITY_TYPES + @eval begin + function Base.convert(::Type{Q}, x::BASE_NUMERIC_TYPES) where {T,D,Q<:$type{T,D}} + return new_quantity(Q, convert(T, x), D()) + end + function Base.promote_rule(::Type{Q}, ::Type{T2}) where {T,D,Q<:$type{T,D},T2<:BASE_NUMERIC_TYPES} + return with_type_parameters(promote_quantity(Q, T2), promote_type(T, T2), D) + end + function Base.promote_rule(::Type{T2}, ::Type{Q}) where {T,D,Q<:$type{T,D},T2<:BASE_NUMERIC_TYPES} + return with_type_parameters(promote_quantity(Q, T2), promote_type(T, T2), D) + end end end -function Base.promote_rule(::Type{<:AbstractQuantity}, ::Type{<:Number}) - return Number -end """ promote_except_value(q1::UnionAbstractQuantity, q2::UnionAbstractQuantity) From af8815fb1cf64f059a0c61e0d599b7861ec64b9d Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 19 Nov 2023 17:30:22 +0000 Subject: [PATCH 02/39] `isapprox` should throw error on mismatched dimensions --- src/utils.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index 7d29125b..a7f15d3f 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -141,7 +141,8 @@ Base.keys(q::UnionAbstractQuantity) = keys(ustrip(q)) # Numeric checks function Base.isapprox(l::UnionAbstractQuantity, r::UnionAbstractQuantity; kws...) l, r = promote_except_value(l, r) - return isapprox(ustrip(l), ustrip(r); kws...) && dimension(l) == dimension(r) + dimension(l) == dimension(r) || throw(DimensionError(l, r)) + return isapprox(ustrip(l), ustrip(r); kws...) end function Base.isapprox(l::Number, r::UnionAbstractQuantity; kws...) iszero(dimension(r)) || throw(DimensionError(l, r)) @@ -151,7 +152,6 @@ function Base.isapprox(l::UnionAbstractQuantity, r::Number; kws...) iszero(dimension(l)) || throw(DimensionError(l, r)) return isapprox(ustrip(l), r; kws...) end -Base.iszero(d::AbstractDimensions) = all_dimensions(iszero, d) function Base.:(==)(l::UnionAbstractQuantity, r::UnionAbstractQuantity) l, r = promote_except_value(l, r) ustrip(l) == ustrip(r) && dimension(l) == dimension(r) From c8118458f4b5251e686fa99f07ebbe6adaa445af Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 19 Nov 2023 18:18:26 +0000 Subject: [PATCH 03/39] Fix isapprox usage in tests --- test/unittests.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/unittests.jl b/test/unittests.jl index f7636d19..cf1199cc 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -14,6 +14,9 @@ function record_show(s, f=show) f(io, s) return String(take!(io)) end +function unsafe_isapprox(x, y; kwargs...) + return isapprox(ustrip(x), ustrip(y); kwargs...) && dimension(x) == dimension(y) +end @testset "Basic utilities" begin From 54bb1060cdb4f190bed80fa082921ad1d93325db Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 19 Nov 2023 18:19:59 +0000 Subject: [PATCH 04/39] Generalize comparison operators --- src/utils.jl | 96 +++++++++++++++++++++++++++++++++-------------- test/unittests.jl | 2 +- 2 files changed, 68 insertions(+), 30 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index a7f15d3f..2518c350 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -139,40 +139,76 @@ Base.keys(q::UnionAbstractQuantity) = keys(ustrip(q)) # Numeric checks -function Base.isapprox(l::UnionAbstractQuantity, r::UnionAbstractQuantity; kws...) - l, r = promote_except_value(l, r) - dimension(l) == dimension(r) || throw(DimensionError(l, r)) - return isapprox(ustrip(l), ustrip(r); kws...) -end -function Base.isapprox(l::Number, r::UnionAbstractQuantity; kws...) - iszero(dimension(r)) || throw(DimensionError(l, r)) - return isapprox(l, ustrip(r); kws...) -end -function Base.isapprox(l::UnionAbstractQuantity, r::Number; kws...) - iszero(dimension(l)) || throw(DimensionError(l, r)) - return isapprox(ustrip(l), r; kws...) -end -function Base.:(==)(l::UnionAbstractQuantity, r::UnionAbstractQuantity) - l, r = promote_except_value(l, r) - ustrip(l) == ustrip(r) && dimension(l) == dimension(r) +for op in (:(<=), :(<), :(>=), :(>), :isless, :isgreater), + (type, base_type, _) in ABSTRACT_QUANTITY_TYPES + + @eval begin + function Base.$(op)(l::$type, r::$type) + l, r = promote_except_value(l, r) + dimension(l) == dimension(r) || throw(DimensionError(l, r)) + return $(op)(ustrip(l), ustrip(r)) + end + function Base.$(op)(l::$type, r::$base_type) + iszero(dimension(l)) || throw(DimensionError(l, r)) + return $(op)(ustrip(l), r) + end + function Base.$(op)(l::$base_type, r::$type) + iszero(dimension(r)) || throw(DimensionError(l, r)) + return $(op)(l, ustrip(r)) + end + end end -Base.:(==)(l::Number, r::UnionAbstractQuantity) = ustrip(l) == ustrip(r) && iszero(dimension(r)) -Base.:(==)(l::UnionAbstractQuantity, r::Number) = ustrip(l) == ustrip(r) && iszero(dimension(l)) -Base.:(==)(l::AbstractDimensions, r::AbstractDimensions) = all_dimensions(==, l, r) -function Base.isless(l::UnionAbstractQuantity, r::UnionAbstractQuantity) - l, r = promote_except_value(l, r) - dimension(l) == dimension(r) || throw(DimensionError(l, r)) - return isless(ustrip(l), ustrip(r)) +for op in (:isequal, :(==)), (type, base_type, _) in ABSTRACT_QUANTITY_TYPES + @eval begin + function Base.$(op)(l::$type, r::$type) + l, r = promote_except_value(l, r) + return $(op)(ustrip(l), ustrip(r)) && dimension(l) == dimension(r) + end + function Base.$(op)(l::$type, r::$base_type) + return $(op)(ustrip(l), r) && iszero(dimension(l)) + end + function Base.$(op)(l::$base_type, r::$type) + return $(op)(l, ustrip(r)) && iszero(dimension(r)) + end + end end -function Base.isless(l::UnionAbstractQuantity, r::Number) - iszero(dimension(l)) || throw(DimensionError(l, r)) - return isless(ustrip(l), r) +for op in (:(<=), :(<), :(>=), :(>), :isless, :isgreater, :isequal, :(==)), + (t1, _, _) in ABSTRACT_QUANTITY_TYPES, + (t2, _, _) in ABSTRACT_QUANTITY_TYPES + + t1 == t2 && continue + + @eval function Base.$(op)(l::$t1, r::$t2) + return $(op)(promote_except_value(l, r)...) + end end -function Base.isless(l::Number, r::UnionAbstractQuantity) - iszero(dimension(r)) || throw(DimensionError(l, r)) - return isless(l, ustrip(r)) +# Define isapprox: +for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES + @eval begin + function Base.isapprox(l::$type, r::$type; kws...) + dimension(l) == dimension(r) || throw(DimensionError(l, r)) + return isapprox(ustrip(l), ustrip(r); kws...) + end + function Base.isapprox(l::$base_type, r::$type; kws...) + iszero(dimension(r)) || throw(DimensionError(l, r)) + return isapprox(l, ustrip(r); kws...) + end + function Base.isapprox(l::$type, r::$base_type; kws...) + iszero(dimension(l)) || throw(DimensionError(l, r)) + return isapprox(ustrip(l), r; kws...) + end + end + for (type2, _, _) in ABSTRACT_QUANTITY_TYPES + + type == type2 && continue + + @eval function Base.isapprox(l::$type, r::$type2; kws...) + return isapprox(promote_except_value(l, r)...; kws...) + end + end end + # Simple flags: for f in ( :iszero, :isfinite, :isinf, :isnan, :isreal, :signbit, @@ -180,6 +216,8 @@ for f in ( ) @eval Base.$f(q::UnionAbstractQuantity) = $f(ustrip(q)) end +Base.iszero(d::AbstractDimensions) = all_dimensions(iszero, d) +Base.:(==)(l::AbstractDimensions, r::AbstractDimensions) = all_dimensions(==, l, r) # Base.one, typemin, typemax diff --git a/test/unittests.jl b/test/unittests.jl index cf1199cc..16cfd7cf 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -97,7 +97,7 @@ end y = Q(T(2 // 10), D, length=1, mass=6 // 2) - @test !(y ≈ x) + @test !(unsafe_isapprox(y, x)) y = x * Inf32 From adee292909e34edb2dc99b8c94cced505d26118d Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 19 Nov 2023 19:58:41 +0000 Subject: [PATCH 05/39] Clean up more ambiguities --- src/disambiguities.jl | 53 +++++++++++++++++++++++++++++------ src/fixed_rational.jl | 64 ++++++++++++++++++++++--------------------- src/math.jl | 13 +++++++-- 3 files changed, 88 insertions(+), 42 deletions(-) diff --git a/src/disambiguities.jl b/src/disambiguities.jl index a0571754..f1cfd10d 100644 --- a/src/disambiguities.jl +++ b/src/disambiguities.jl @@ -1,14 +1,51 @@ -Base.isless(::AbstractQuantity, ::Missing) = missing -Base.isless(::Missing, ::AbstractQuantity) = missing -Base.:(==)(::AbstractQuantity, ::Missing) = missing -Base.:(==)(::Missing, ::AbstractQuantity) = missing -Base.isapprox(::AbstractQuantity, ::Missing; kws...) = missing -Base.isapprox(::Missing, ::AbstractQuantity; kws...) = missing +for op in (:isless, :(==), :isequal, :(<)), (type, _, _) in ABSTRACT_QUANTITY_TYPES + @eval begin + Base.$(op)(::$type, ::Missing) = missing + Base.$(op)(::Missing, ::$type) = missing + end +end +for op in (:isapprox,), (type, _, _) in ABSTRACT_QUANTITY_TYPES + @eval begin + Base.$(op)(::$type, ::Missing; kws...) = missing + Base.$(op)(::Missing, ::$type; kws...) = missing + end +end -Base.:(==)(::AbstractQuantity, ::WeakRef) = error("Cannot compare a quantity to a weakref") -Base.:(==)(::WeakRef, ::AbstractQuantity) = error("Cannot compare a weakref to a quantity") +for (type, _, _) in ABSTRACT_QUANTITY_TYPES + @eval begin + Base.:(==)(::$type, ::WeakRef) = error("Cannot compare a quantity to a weakref") + Base.:(==)(::WeakRef, ::$type) = error("Cannot compare a weakref to a quantity") + end +end Base.:*(l::AbstractDimensions, r::Number) = error("Please use an `UnionAbstractQuantity` for multiplication. You used multiplication on types: $(typeof(l)) and $(typeof(r)).") Base.:*(l::Number, r::AbstractDimensions) = error("Please use an `UnionAbstractQuantity` for multiplication. You used multiplication on types: $(typeof(l)) and $(typeof(r)).") Base.:/(l::AbstractDimensions, r::Number) = error("Please use an `UnionAbstractQuantity` for division. You used division on types: $(typeof(l)) and $(typeof(r)).") Base.:/(l::Number, r::AbstractDimensions) = error("Please use an `UnionAbstractQuantity` for division. You used division on types: $(typeof(l)) and $(typeof(r)).") + +# Promotion ambiguities +function Base.promote_rule(::Type{F}, ::Type{Bool}) where {F<:FixedRational} + return F +end +function Base.promote_rule(::Type{Bool}, ::Type{F}) where {F<:FixedRational} + return F +end +function Base.promote_rule(::Type{F}, ::Type{BigFloat}) where {F<:FixedRational} + return promote_type(Rational{num_type(F)}, BigFloat) +end +function Base.promote_rule(::Type{BigFloat}, ::Type{F}) where {F<:FixedRational} + return promote_type(Rational{num_type(F)}, BigFloat) +end +function Base.promote_rule(::Type{F}, ::Type{T}) where {F<:FixedRational,T<:AbstractIrrational} + return promote_type(Rational{num_type(F)}, T) +end +function Base.promote_rule(::Type{T}, ::Type{F}) where {F<:FixedRational,T<:AbstractIrrational} + return promote_type(Rational{num_type(F)}, T) +end + +# Assorted calls found by Aqua: +for type in (Signed, Float64, Float32, Rational), op in (:flipsign, :copysign) + @eval function Base.$(op)(x::$type, y::AbstractRealQuantity) + return $(op)(x, ustrip(y)) + end +end diff --git a/src/fixed_rational.jl b/src/fixed_rational.jl index 2f2ae061..6ab5f6d5 100644 --- a/src/fixed_rational.jl +++ b/src/fixed_rational.jl @@ -28,25 +28,25 @@ denom(x::FixedRational) = denom(typeof(x)) # Otherwise, we would have type instability. val_denom(::Type{F}) where {T,den,F<:FixedRational{T,den}} = Val(den) -Base.eltype(::Type{F}) where {T,F<:FixedRational{T}} = T +num_type(::Type{F}) where {T,F<:FixedRational{T}} = T const DEFAULT_NUMERATOR_TYPE = Int32 const DEFAULT_DENOM = DEFAULT_NUMERATOR_TYPE(2^4 * 3^2 * 5^2 * 7) (::Type{F})(x::F) where {F<:FixedRational} = x -(::Type{F})(x::F2) where {T,T2,den,F<:FixedRational{T,den},F2<:FixedRational{T2,den}} = unsafe_fixed_rational(x.num, eltype(F), val_denom(F)) -(::Type{F})(x::Integer) where {F<:FixedRational} = unsafe_fixed_rational(x * denom(F), eltype(F), val_denom(F)) -(::Type{F})(x::Rational) where {F<:FixedRational} = unsafe_fixed_rational(widemul(x.num, denom(F)) ÷ x.den, eltype(F), val_denom(F)) +(::Type{F})(x::F2) where {T,T2,den,F<:FixedRational{T,den},F2<:FixedRational{T2,den}} = unsafe_fixed_rational(x.num, num_type(F), val_denom(F)) +(::Type{F})(x::Integer) where {F<:FixedRational} = unsafe_fixed_rational(x * denom(F), num_type(F), val_denom(F)) +(::Type{F})(x::Rational) where {F<:FixedRational} = unsafe_fixed_rational(widemul(x.num, denom(F)) ÷ x.den, num_type(F), val_denom(F)) -Base.:*(l::F, r::F) where {F<:FixedRational} = unsafe_fixed_rational(widemul(l.num, r.num) ÷ denom(F), eltype(F), val_denom(F)) -Base.:+(l::F, r::F) where {F<:FixedRational} = unsafe_fixed_rational(l.num + r.num, eltype(F), val_denom(F)) -Base.:-(l::F, r::F) where {F<:FixedRational} = unsafe_fixed_rational(l.num - r.num, eltype(F), val_denom(F)) -Base.:-(x::F) where {F<:FixedRational} = unsafe_fixed_rational(-x.num, eltype(F), val_denom(F)) +Base.:*(l::F, r::F) where {F<:FixedRational} = unsafe_fixed_rational(widemul(l.num, r.num) ÷ denom(F), num_type(F), val_denom(F)) +Base.:+(l::F, r::F) where {F<:FixedRational} = unsafe_fixed_rational(l.num + r.num, num_type(F), val_denom(F)) +Base.:-(l::F, r::F) where {F<:FixedRational} = unsafe_fixed_rational(l.num - r.num, num_type(F), val_denom(F)) +Base.:-(x::F) where {F<:FixedRational} = unsafe_fixed_rational(-x.num, num_type(F), val_denom(F)) -Base.inv(x::F) where {F<:FixedRational} = unsafe_fixed_rational(widemul(denom(F), denom(F)) ÷ x.num, eltype(F), val_denom(F)) +Base.inv(x::F) where {F<:FixedRational} = unsafe_fixed_rational(widemul(denom(F), denom(F)) ÷ x.num, num_type(F), val_denom(F)) -Base.:*(l::F, r::Integer) where {F<:FixedRational} = unsafe_fixed_rational(l.num * r, eltype(F), val_denom(F)) -Base.:*(l::Integer, r::F) where {F<:FixedRational} = unsafe_fixed_rational(l * r.num, eltype(F), val_denom(F)) +Base.:*(l::F, r::Integer) where {F<:FixedRational} = unsafe_fixed_rational(l.num * r, num_type(F), val_denom(F)) +Base.:*(l::Integer, r::F) where {F<:FixedRational} = unsafe_fixed_rational(l * r.num, num_type(F), val_denom(F)) for comp in (:(==), :isequal, :<, :(isless), :<=) @eval Base.$comp(x::F, y::F) where {F<:FixedRational} = $comp(x.num, y.num) @@ -57,7 +57,7 @@ Base.isone(x::F) where {F<:FixedRational} = x.num == denom(F) Base.isinteger(x::F) where {F<:FixedRational} = iszero(x.num % denom(F)) Rational{R}(x::F) where {R,F<:FixedRational} = Rational{R}(x.num, denom(F)) -Rational(x::F) where {F<:FixedRational} = Rational{eltype(F)}(x) +Rational(x::F) where {F<:FixedRational} = Rational{num_type(F)}(x) (::Type{AF})(x::F) where {AF<:AbstractFloat,F<:FixedRational} = convert(AF, x.num) / convert(AF, denom(F)) (::Type{I})(x::F) where {I<:Integer,F<:FixedRational} = let @@ -73,26 +73,20 @@ Rational(x::F) where {F<:FixedRational} = Rational{eltype(F)}(x) Base.round(::Type{T}, x::F, r::RoundingMode=RoundNearest) where {T,F<:FixedRational} = div(convert(T, x.num), convert(T, denom(F)), r) Base.decompose(x::F) where {T,F<:FixedRational{T}} = (x.num, zero(T), denom(F)) -# Promotion rules: -function Base.promote_rule(::Type{<:FixedRational{T1,den1}}, ::Type{<:FixedRational{T2,den2}}) where {T1,T2,den1,den2} - return error("Refusing to promote `FixedRational` types with mixed denominators. Use `Rational` instead.") +# Promotion with self or rational-like +function Base.promote_rule(::Type{F1}, ::Type{F2}) where {F1<:FixedRational,F2<:FixedRational} + denom(F1) == denom(F2) || + error("Refusing to promote `FixedRational` types with mixed denominators. Use `Rational` instead.") + return FixedRational{promote_type(num_type(F1), num_type(F2)),denom(F1)} end -function Base.promote_rule(::Type{<:FixedRational{T1,den}}, ::Type{<:FixedRational{T2,den}}) where {T1,T2,den} - return FixedRational{promote_type(T1,T2),den} +function Base.promote_rule(::Type{F}, ::Type{Rational{T2}}) where {F<:FixedRational,T2} + return Rational{promote_type(num_type(F),T2)} end -function Base.promote_rule(::Type{<:FixedRational{T1}}, ::Type{Rational{T2}}) where {T1,T2} - return Rational{promote_type(T1,T2)} +function Base.promote_rule(::Type{Rational{T2}}, ::Type{F}) where {F<:FixedRational,T2} + return Rational{promote_type(num_type(F),T2)} end -function Base.promote_rule(::Type{Rational{T2}}, ::Type{<:FixedRational{T1}}) where {T1,T2} - return Rational{promote_type(T1,T2)} -end -function Base.promote_rule(::Type{<:FixedRational{T1}}, ::Type{T2}) where {T1,T2<:Real} - return promote_type(Rational{T1}, T2) -end -function Base.promote_rule(::Type{T2}, ::Type{<:FixedRational{T1}}) where {T1,T2<:Real} - return promote_type(Rational{T1}, T2) -end -# Want to consume integers: + +# We want to consume integers function Base.promote_rule(::Type{F}, ::Type{<:Integer}) where {F<:FixedRational} return F end @@ -100,9 +94,17 @@ function Base.promote_rule(::Type{<:Integer}, ::Type{F}) where {F<:FixedRational return F end +# Promotion with general types promotes like a rational +function Base.promote_rule(::Type{T}, ::Type{T2}) where {T2<:Real,T<:FixedRational} + return promote_type(Rational{num_type(T)}, T2) +end +function Base.promote_rule(::Type{T2}, ::Type{T}) where {T2<:Real,T<:FixedRational} + return promote_type(Rational{num_type(T)}, T2) +end + Base.string(x::FixedRational) = let - isinteger(x) && return string(convert(eltype(x), x)) + isinteger(x) && return string(convert(num_type(x), x)) g = gcd(x.num, denom(x)) return string(div(x.num, g)) * "//" * string(div(denom(x), g)) end @@ -110,7 +112,7 @@ Base.show(io::IO, x::FixedRational) = print(io, string(x)) tryrationalize(::Type{F}, x::F) where {F<:FixedRational} = x tryrationalize(::Type{F}, x::Union{Rational,Integer}) where {F<:FixedRational} = convert(F, x) -tryrationalize(::Type{F}, x) where {F<:FixedRational} = unsafe_fixed_rational(round(eltype(F), x * denom(F)), eltype(F), val_denom(F)) +tryrationalize(::Type{F}, x) where {F<:FixedRational} = unsafe_fixed_rational(round(num_type(F), x * denom(F)), num_type(F), val_denom(F)) # Fix method ambiguities Base.round(::Type{T}, x::F, r::RoundingMode=RoundNearest) where {T>:Missing, F<:FixedRational} = round(Base.nonmissingtype_checked(T), x, r) diff --git a/src/math.jl b/src/math.jl index 3fb8e03b..8558fc0d 100644 --- a/src/math.jl +++ b/src/math.jl @@ -20,7 +20,7 @@ for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES function Base.:/(l::$type, r::$base_type) new_quantity(typeof(l), ustrip(l) / r, dimension(l)) end - function Base.div(x::$type, y::Number, r::RoundingMode=RoundToZero) + function Base.div(x::$type, y::$base_type, r::RoundingMode=RoundToZero) new_quantity(typeof(x), div(ustrip(x), y, r), dimension(x)) end @@ -30,7 +30,7 @@ for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES function Base.:/(l::$base_type, r::$type) new_quantity(typeof(r), l / ustrip(r), inv(dimension(r))) end - function Base.div(x::Number, y::$type, r::RoundingMode=RoundToZero) + function Base.div(x::$base_type, y::$type, r::RoundingMode=RoundToZero) new_quantity(typeof(y), div(x, ustrip(y), r), inv(dimension(y))) end @@ -75,7 +75,7 @@ end Base.:-(l::UnionAbstractQuantity) = new_quantity(typeof(l), -ustrip(l), dimension(l)) # Combining different abstract types -for op in (:*, :/, :+, :-, :div, :atan, :atand, :copysign, :flipsign, :mod), +for op in (:*, :/, :+, :-, :atan, :atand, :copysign, :flipsign, :mod), (t1, _, _) in ABSTRACT_QUANTITY_TYPES, (t2, _, _) in ABSTRACT_QUANTITY_TYPES @@ -83,6 +83,13 @@ for op in (:*, :/, :+, :-, :div, :atan, :atand, :copysign, :flipsign, :mod), @eval Base.$op(l::$t1, r::$t2) = $op(promote_except_value(l, r)...) end +# different methods needed: +for (t1, _, _) in ABSTRACT_QUANTITY_TYPES, (t2, _, _) in ABSTRACT_QUANTITY_TYPES + + t1 == t2 && continue + + @eval Base.div(x::$t1, y::$t2, r::RoundingMode=RoundToZero) = div(promote_except_value(x, y)..., r) +end # We don't promote on the dimension types: function Base.:^(l::AbstractDimensions{R}, r::Integer) where {R} From d4cda51f4c7fe0430f284ba8261a6402c6489cea Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 19 Nov 2023 20:31:55 +0000 Subject: [PATCH 06/39] Get simple complex operations working for RealQuantity --- src/math.jl | 22 ++++++++++++++++++++++ src/types.jl | 17 +++++++++++++---- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/math.jl b/src/math.jl index 8558fc0d..02ca0b0e 100644 --- a/src/math.jl +++ b/src/math.jl @@ -50,6 +50,24 @@ for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES end end +# Complex multiplication +for (type, _, _) in ABSTRACT_QUANTITY_TYPES + @eval begin + function Base.:*(l::Complex, r::$type) + new_quantity(typeof(r), l * ustrip(r), dimension(r)) + end + function Base.:*(l::$type, r::Complex) + new_quantity(typeof(l), ustrip(l) * r, dimension(l)) + end + function Base.:/(l::Complex, r::$type) + new_quantity(typeof(r), l / ustrip(r), inv(dimension(r))) + end + function Base.:/(l::$type, r::Complex) + new_quantity(typeof(r), ustrip(l) / r, dimension(r)) + end + end +end + Base.:*(l::AbstractDimensions, r::AbstractDimensions) = map_dimensions(+, l, r) Base.:/(l::AbstractDimensions, r::AbstractDimensions) = map_dimensions(-, l, r) @@ -125,6 +143,10 @@ for (type, _, _) in ABSTRACT_QUANTITY_TYPES Base.:^(l::$type, r::Integer) = _pow_int(l, r) Base.:^(l::$type, r::Number) = _pow(l, r) Base.:^(l::$type, r::Rational) = _pow(l, r) + function Base.:^(l::$type, r::Complex) + iszero(dimension(l)) || throw(DimensionError(l)) + return new_quantity(typeof(l), ustrip(l)^r, dimension(l)) + end end end @inline Base.literal_pow(::typeof(^), l::AbstractDimensions, ::Val{p}) where {p} = map_dimensions(Base.Fix1(*, p), l) diff --git a/src/types.jl b/src/types.jl index 727bb746..9690a4c8 100644 --- a/src/types.jl +++ b/src/types.jl @@ -191,7 +191,11 @@ A constant tuple of the existing abstract quantity types, each as a tuple with (1) the abstract type, (2) the base type, and (3) the default exported concrete type. """ -const ABSTRACT_QUANTITY_TYPES = ((AbstractQuantity, Number, Quantity), (AbstractGenericQuantity, Any, GenericQuantity), (AbstractRealQuantity, Real, RealQuantity)) +const ABSTRACT_QUANTITY_TYPES = ( + (AbstractQuantity, Number, Quantity), + (AbstractGenericQuantity, Any, GenericQuantity), + (AbstractRealQuantity, Real, RealQuantity) +) """ promote_quantity(::Type{<:UnionAbstractQuantity}, t::Type{<:Any}) @@ -202,7 +206,7 @@ If the current quantity type can already accommodate `t`, then the current type promote_quantity(::Type{<:Union{GenericQuantity,Quantity,RealQuantity}}, ::Type{<:Any}) = GenericQuantity promote_quantity(::Type{<:Union{Quantity,RealQuantity}}, ::Type{<:Number}) = Quantity promote_quantity(::Type{<:RealQuantity}, ::Type{<:Real}) = RealQuantity -promote_quantity(T, _) = t +promote_quantity(T, _) = T for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES @eval begin @@ -222,8 +226,13 @@ end const DEFAULT_QUANTITY_TYPE = RealQuantity{DEFAULT_VALUE_TYPE, DEFAULT_DIM_TYPE} -new_dimensions(::Type{D}, dims...) where {D<:AbstractDimensions} = constructorof(D)(dims...) -new_quantity(::Type{Q}, l, r) where {Q<:UnionAbstractQuantity} = constructorof(Q)(l, r) +@inline function new_dimensions(::Type{D}, dims...) where {D<:AbstractDimensions} + return constructorof(D)(dims...) +end +@inline function new_quantity(::Type{Q}, val, dims) where {Q<:UnionAbstractQuantity} + Qout = promote_quantity(Q, typeof(val)) + return constructorof(Qout)(val, dims) +end dim_type(::Type{Q}) where {T,D<:AbstractDimensions,Q<:UnionAbstractQuantity{T,D}} = D dim_type(::Type{<:UnionAbstractQuantity}) = DEFAULT_DIM_TYPE From 9424a227fcd8ae8102bef74ddf53cd39b4e34ece Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 19 Nov 2023 20:39:29 +0000 Subject: [PATCH 07/39] More ambiguities found by Aqua --- src/disambiguities.jl | 27 +++++++++++++++++++++++++++ src/utils.jl | 21 +++++++++++++++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/disambiguities.jl b/src/disambiguities.jl index f1cfd10d..f8463d96 100644 --- a/src/disambiguities.jl +++ b/src/disambiguities.jl @@ -49,3 +49,30 @@ for type in (Signed, Float64, Float32, Rational), op in (:flipsign, :copysign) return $(op)(x, ustrip(y)) end end + +function Base.:*(l::Complex{Bool}, r::AbstractRealQuantity) + return new_quantity(typeof(r), l * ustrip(r), dimension(r)) +end +function Base.:*(l::AbstractRealQuantity, r::Complex{Bool}) + return new_quantity(typeof(l), ustrip(l) * r, dimension(l)) +end + +for op in (:(==), :isequal), base_type in (AbstractIrrational, AbstractFloat) + @eval begin + function Base.$(op)(l::AbstractRealQuantity, r::$base_type) + return $(op)(ustrip(l), r) && iszero(dimension(l)) + end + function Base.$(op)(l::$base_type, r::AbstractRealQuantity) + return $(op)(l, ustrip(r)) && iszero(dimension(r)) + end + end +end + +function Base.isless(l::AbstractRealQuantity, r::AbstractFloat) + iszero(dimension(l)) || throw(DimensionError(l, r)) + return isless(ustrip(l), r) +end +function Base.isless(l::AbstractFloat, r::AbstractRealQuantity) + iszero(dimension(r)) || throw(DimensionError(l, r)) + return isless(l, ustrip(r)) +end diff --git a/src/utils.jl b/src/utils.jl index 2518c350..bb8f87e4 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -68,14 +68,18 @@ end # abstract number packages which may try to do the same thing. # (which would lead to ambiguities) const BASE_NUMERIC_TYPES = Union{ - Bool, Int8, UInt8, Int16, UInt16, Int32, UInt32, + Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64, Int128, UInt128, Float16, Float32, - Float64, BigFloat, BigInt, ComplexF16, ComplexF32, + Float64, BigInt, ComplexF16, ComplexF32, ComplexF64, Complex{BigFloat}, Rational{Int8}, Rational{UInt8}, Rational{Int16}, Rational{UInt16}, Rational{Int32}, Rational{UInt32}, Rational{Int64}, Rational{UInt64}, Rational{Int128}, Rational{UInt128}, Rational{BigInt}, } +# The following types require explicit promotion, +# as putting them in a union type creates different ambiguities +const AMBIGUOUS_NUMERIC_TYPES = (Bool, BigFloat) + for (type, _, _) in ABSTRACT_QUANTITY_TYPES @eval begin function Base.convert(::Type{Q}, x::BASE_NUMERIC_TYPES) where {T,D,Q<:$type{T,D}} @@ -88,6 +92,19 @@ for (type, _, _) in ABSTRACT_QUANTITY_TYPES return with_type_parameters(promote_quantity(Q, T2), promote_type(T, T2), D) end end + for numeric_type in AMBIGUOUS_NUMERIC_TYPES + @eval begin + function Base.convert(::Type{Q}, x::$numeric_type) where {T,D,Q<:$type{T,D}} + return new_quantity(Q, convert(T, x), D()) + end + function Base.promote_rule(::Type{Q}, ::Type{$numeric_type}) where {T,D,Q<:$type{T,D}} + return with_type_parameters(promote_quantity(Q, $numeric_type), promote_type(T, $numeric_type), D) + end + function Base.promote_rule(::Type{$numeric_type}, ::Type{Q}) where {T,D,Q<:$type{T,D}} + return with_type_parameters(promote_quantity(Q, $numeric_type), promote_type(T, $numeric_type), D) + end + end + end end """ From 8f844a2a8f5fe3cd04a2815c8274894bbce4189b Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 19 Nov 2023 21:23:15 +0000 Subject: [PATCH 08/39] Get majority of tests working with RealQuantity --- ext/DynamicQuantitiesUnitfulExt.jl | 48 +++++++++++--------- src/fixed_rational.jl | 12 ++--- test/test_unitful.jl | 8 ++-- test/unittests.jl | 71 +++++++++++++++--------------- 4 files changed, 73 insertions(+), 66 deletions(-) diff --git a/ext/DynamicQuantitiesUnitfulExt.jl b/ext/DynamicQuantitiesUnitfulExt.jl index 045c027e..16f09420 100644 --- a/ext/DynamicQuantitiesUnitfulExt.jl +++ b/ext/DynamicQuantitiesUnitfulExt.jl @@ -1,6 +1,7 @@ module DynamicQuantitiesUnitfulExt -import DynamicQuantities +using DynamicQuantities: DynamicQuantities, ABSTRACT_QUANTITY_TYPES + import Unitful import Unitful: @u_str @@ -23,30 +24,33 @@ function unitful_equivalences() 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) - if dims isa DynamicQuantities.SymbolicDimensions - throw(ArgumentError("Conversion of a `DynamicQuantities.Quantity` to a `Unitful.Quantity` is not defined with dimensions of type `SymbolicDimensions`. Instead, you can first use the `uexpand` function to convert the dimensions to their base SI form of type `Dimensions`, then convert this quantity to a `Unitful.Quantity`.")) +for (_, _, Q) in ABSTRACT_QUANTITY_TYPES + @eval begin + function Base.convert(::Type{Unitful.Quantity}, x::$Q) + validate_upreferred() + cumulator = DynamicQuantities.ustrip(x) + dims = DynamicQuantities.dimension(x) + if dims isa DynamicQuantities.SymbolicDimensions + throw(ArgumentError("Conversion of a `DynamicQuantities." * string($Q) * "` to a `Unitful.Quantity` is not defined with dimensions of type `SymbolicDimensions`. Instead, you can first use the `uexpand` function to convert the dimensions to their base SI form of type `Dimensions`, then convert this quantity to a `Unitful.Quantity`.")) + end + equiv = unitful_equivalences() + for dim in keys(dims) + value = dims[dim] + iszero(value) && continue + cumulator *= equiv[dim]^value + end + cumulator end - equiv = unitful_equivalences() - for dim in keys(dims) - value = dims[dim] - iszero(value) && continue - cumulator *= equiv[dim]^value + function Base.convert(::Type{$Q}, x::Unitful.Quantity{T}) where {T} + return convert($Q{T,DynamicQuantities.DEFAULT_DIM_TYPE}, x) + end + function Base.convert(::Type{$Q{T,D}}, x::Unitful.Quantity) where {T,R,D<:DynamicQuantities.AbstractDimensions{R}} + value = Unitful.ustrip(Unitful.upreferred(x)) + dimension = convert(D, Unitful.dimension(x)) + return $Q(convert(T, value), dimension) end - cumulator - 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,D}}, x::Unitful.Quantity) where {T,R,D<:DynamicQuantities.AbstractDimensions{R}} = - let - value = Unitful.ustrip(Unitful.upreferred(x)) - dimension = convert(D, Unitful.dimension(x)) - return DynamicQuantities.Quantity(convert(T, value), dimension) end +end 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} = diff --git a/src/fixed_rational.jl b/src/fixed_rational.jl index 6ab5f6d5..42ac7149 100644 --- a/src/fixed_rational.jl +++ b/src/fixed_rational.jl @@ -14,6 +14,7 @@ struct FixedRational{T<:Integer,den} <: Real num::T global unsafe_fixed_rational(num::Integer, ::Type{T}, ::Val{den}) where {T,den} = new{T,den}(num) end +@inline _denom(::Type{F}) where {T,den,F<:FixedRational{T,den}} = den """ denom(F::FixedRational) @@ -21,14 +22,15 @@ end Since `den` can be a different type than `T`, this function is used to get the denominator as a `T`. """ -denom(::Type{F}) where {T,den,F<:FixedRational{T,den}} = convert(T, den) +denom(::Type{<:F}) where {T,F<:FixedRational{T}} = convert(T, _denom(F)) denom(x::FixedRational) = denom(typeof(x)) # But, for Val(den), we need to use the same type as at init. # Otherwise, we would have type instability. -val_denom(::Type{F}) where {T,den,F<:FixedRational{T,den}} = Val(den) +val_denom(::Type{<:F}) where {F<:FixedRational} = Val(_denom(F)) -num_type(::Type{F}) where {T,F<:FixedRational{T}} = T +num_type(::Type{<:FixedRational{T}}) where {T} = T +num_type(x::FixedRational) = num_type(typeof(x)) const DEFAULT_NUMERATOR_TYPE = Int32 const DEFAULT_DENOM = DEFAULT_NUMERATOR_TYPE(2^4 * 3^2 * 5^2 * 7) @@ -75,9 +77,9 @@ Base.decompose(x::F) where {T,F<:FixedRational{T}} = (x.num, zero(T), denom(F)) # Promotion with self or rational-like function Base.promote_rule(::Type{F1}, ::Type{F2}) where {F1<:FixedRational,F2<:FixedRational} - denom(F1) == denom(F2) || + _denom(F1) == _denom(F2) || error("Refusing to promote `FixedRational` types with mixed denominators. Use `Rational` instead.") - return FixedRational{promote_type(num_type(F1), num_type(F2)),denom(F1)} + return FixedRational{promote_type(num_type(F1), num_type(F2)), _denom(F1)} end function Base.promote_rule(::Type{F}, ::Type{Rational{T2}}) where {F<:FixedRational,T2} return Rational{promote_type(num_type(F),T2)} diff --git a/test/test_unitful.jl b/test/test_unitful.jl index 8da1945e..d0dfddcd 100644 --- a/test/test_unitful.jl +++ b/test/test_unitful.jl @@ -6,10 +6,10 @@ import Ratios: SimpleRatio import SaferIntegers: SafeInt16 using Test -risapprox(x::Unitful.Quantity, y::Unitful.Quantity; kws...) = - let (xfloat, yfloat) = (Unitful.ustrip ∘ Unitful.upreferred).((x, y)) - return isapprox(xfloat, yfloat; kws...) - end +function risapprox(x::Unitful.Quantity, y::Unitful.Quantity; kws...) + (xfloat, yfloat) = (Unitful.ustrip ∘ Unitful.upreferred).((x, y)) + return isapprox(xfloat, yfloat; kws...) +end 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} diff --git a/test/unittests.jl b/test/unittests.jl index 16cfd7cf..95366aa6 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -1,8 +1,8 @@ using DynamicQuantities using DynamicQuantities: FixedRational -using DynamicQuantities: DEFAULT_DIM_BASE_TYPE, DEFAULT_DIM_TYPE, DEFAULT_VALUE_TYPE +using DynamicQuantities: DEFAULT_QUANTITY_TYPE, DEFAULT_DIM_BASE_TYPE, DEFAULT_DIM_TYPE, DEFAULT_VALUE_TYPE using DynamicQuantities: array_type, value_type, dim_type, quantity_type -using DynamicQuantities: GenericQuantity +using DynamicQuantities: GenericQuantity, with_type_parameters, constructorof using Ratios: SimpleRatio using SaferIntegers: SafeInt16 using StaticArrays: SArray, MArray @@ -20,7 +20,7 @@ end @testset "Basic utilities" begin - for Q in [Quantity, GenericQuantity], T in [DEFAULT_VALUE_TYPE, Float16, Float32, Float64], R in [DEFAULT_DIM_BASE_TYPE, Rational{Int16}, Rational{Int32}, SimpleRatio{Int}, SimpleRatio{SafeInt16}] + for Q in [Quantity, GenericQuantity, RealQuantity], 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 = Q(T(0.2), D, length=1, mass=2.5) @@ -390,18 +390,18 @@ end @test utime(x) == -2 y = 0.9u"sqrt(mΩ)" - @test typeof(y) == Quantity{Float64,DEFAULT_DIM_TYPE} + @test typeof(y) == with_type_parameters(DEFAULT_QUANTITY_TYPE, Float64, DEFAULT_DIM_TYPE) @test ustrip(y) ≈ 0.02846049894151541 @test ucurrent(y) == -1 @test ulength(y) == 1 y = BigFloat(0.3) * u"mΩ" - @test typeof(y) == Quantity{BigFloat,DEFAULT_DIM_TYPE} + @test typeof(y) == with_type_parameters(DEFAULT_QUANTITY_TYPE, BigFloat, DEFAULT_DIM_TYPE) @test ustrip(y) ≈ 0.0003 @test ulength(y) == 2 - y32 = convert(Quantity{Float32,Dimensions{Rational{Int16}}}, y) - @test typeof(y32) == Quantity{Float32,Dimensions{Rational{Int16}}} + y32 = convert(with_type_parameters(DEFAULT_QUANTITY_TYPE, Float32, Dimensions{Rational{Int16}}), y) + @test typeof(y32) == with_type_parameters(DEFAULT_QUANTITY_TYPE, Float32, Dimensions{Rational{Int16}}) @test ustrip(y32) ≈ 0.0003 z = u"yr" @@ -409,13 +409,13 @@ end @test ustrip(z) ≈ 60 * 60 * 24 * 365.25 # Test type stability of extreme range of units - @test typeof(u"1") == Quantity{Float64,DEFAULT_DIM_TYPE} - @test typeof(u"1f0") == Quantity{Float64,DEFAULT_DIM_TYPE} - @test typeof(u"s"^2) == Quantity{Float64,DEFAULT_DIM_TYPE} - @test typeof(u"Ω") == Quantity{Float64,DEFAULT_DIM_TYPE} - @test typeof(u"Gyr") == Quantity{Float64,DEFAULT_DIM_TYPE} - @test typeof(u"fm") == Quantity{Float64,DEFAULT_DIM_TYPE} - @test typeof(u"fm"^2) == Quantity{Float64,DEFAULT_DIM_TYPE} + @test typeof(u"1") == DEFAULT_QUANTITY_TYPE + @test typeof(u"1f0") == DEFAULT_QUANTITY_TYPE + @test typeof(u"s"^2) == DEFAULT_QUANTITY_TYPE + @test typeof(u"Ω") == DEFAULT_QUANTITY_TYPE + @test typeof(u"Gyr") == DEFAULT_QUANTITY_TYPE + @test typeof(u"fm") == DEFAULT_QUANTITY_TYPE + @test typeof(u"fm"^2) == DEFAULT_QUANTITY_TYPE @test_throws LoadError eval(:(u":x")) end @@ -486,10 +486,10 @@ end a = 0.5u"km/s" b = MyNumber(0.5) ar = [a, b] - @test ar isa Vector{Number} + @test ar isa Vector{Real} @test a === ar[1] @test b === ar[2] - @test promote_type(MyNumber, typeof(a)) == Number + @test promote_type(MyNumber, typeof(a)) == Real # Explicit conversion so coverage can see it: D = DEFAULT_DIM_TYPE @@ -609,7 +609,7 @@ end q = 1.5us"km/s" @test q == 1.5 * us"km" / us"s" - @test typeof(q) <: Quantity{Float64,<:SymbolicDimensions} + @test typeof(q) <: with_type_parameters(DEFAULT_QUANTITY_TYPE, Float64, SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}) @test string(dimension(q)) == "s⁻¹ km" @test uexpand(q) == 1.5u"km/s" @test string(dimension(us"Constants.au^1.5")) == "au³ᐟ²" @@ -683,13 +683,13 @@ end @test_throws DimensionError uconvert(us"nm * J", 5e-9u"m") # Types: - @test typeof(uconvert(us"nm", 5e-9u"m")) <: Quantity{Float64,<:SymbolicDimensions} + @test typeof(uconvert(us"nm", 5e-9u"m")) <: RealQuantity{Float64,<:SymbolicDimensions} @test typeof(uconvert(us"nm", GenericQuantity(5e-9u"m"))) <: GenericQuantity{Float64,<:SymbolicDimensions} @test uconvert(GenericQuantity(us"nm"), GenericQuantity(5e-9u"m")) ≈ 5us"nm" @test uconvert(GenericQuantity(us"nm"), GenericQuantity(5e-9u"m")) ≈ GenericQuantity(5us"nm") # We only want to convert the dimensions, and ignore the quantity type: - @test typeof(uconvert(GenericQuantity(us"nm"), 5e-9u"m")) <: Quantity{Float64,<:SymbolicDimensions} + @test typeof(uconvert(GenericQuantity(us"nm"), 5e-9u"m")) <: RealQuantity{Float64,<:SymbolicDimensions} q = 1.5u"Constants.M_sun" qs = uconvert(us"Constants.M_sun", 5.0 * q) @@ -704,7 +704,7 @@ end VERSION >= v"1.8" && @test_throws "You passed a quantity" uconvert(1.2us"m", 1.0u"m") - for Q in (Quantity, GenericQuantity) + for Q in (RealQuantity, Quantity, GenericQuantity) # Different types require converting both arguments: q = convert(Q{Float16}, 1.5u"g") qs = uconvert(convert(Q{Float16}, us"g"), 5 * q) @@ -769,10 +769,10 @@ end x = 1.0u"m" y = x ^ (3//2) @test y == Quantity(1.0, length=3//2) - @test typeof(y) == Quantity{Float64,DEFAULT_DIM_TYPE} + @test typeof(y) == RealQuantity{Float64,DEFAULT_DIM_TYPE} end -for Q in (Quantity, GenericQuantity) +for Q in (RealQuantity, Quantity, GenericQuantity) @testset "Arrays" begin @testset "Basics" begin x = QuantityArray(randn(32), Q(u"km/s")) @@ -798,7 +798,7 @@ for Q in (Quantity, GenericQuantity) # Test default constructors: @test QuantityArray(ones(3), u"m/s") == QuantityArray(ones(3), length=1, time=-1) - @test typeof(QuantityArray(ones(3), u"m/s")) <: QuantityArray{Float64,1,<:Dimensions,<:Quantity,<:Array} + @test typeof(QuantityArray(ones(3), u"m/s")) <: QuantityArray{Float64,1,<:Dimensions,<:constructorof(DEFAULT_QUANTITY_TYPE),<:Array} # We can create quantity arrays with generic quantity @test typeof(QuantityArray([[1.0], [2.0, 3.0]], dimension(u"m/s"))) <: QuantityArray{<:Any,1,<:Dimensions,<:GenericQuantity,<:Array} @@ -964,14 +964,14 @@ for Q in (Quantity, GenericQuantity) @test ustrip(x .* y) == ustrip(x) .* ustrip(y) end - Q == Quantity && @testset "Broadcast different arrays" begin + Q in (Quantity, RealQuantity) && @testset "Broadcast different arrays" begin f(x, y, z, w) = x * y + z * w g(x, y, z, w) = f.(x, y, z, w) x = randn(32) y = QuantityArray(randn(32), u"km/s") z = rand(1:10, 32) - w = Quantity{Float32}(u"m/s") + w = Q{Float32}(u"m/s") @test typeof(g(x, y, z, w)) <: QuantityArray{Float64} y32 = QuantityArray(ustrip(y), dimension(y)) @@ -990,7 +990,7 @@ for Q in (Quantity, GenericQuantity) @test typeof(b .* y) <: QuantityArray{Float64} end - Q == Quantity && @testset "Broadcast scalars" begin + Q in (RealQuantity, Quantity) && @testset "Broadcast scalars" begin for (x, qx) in ((0.5, 0.5u"s"), ([0.5, 0.2], GenericQuantity([0.5, 0.2], time=1))) @test size(qx) == size(x) @test length(qx) == length(x) @@ -1128,8 +1128,8 @@ end qy = QuantityArray(y; length=1) @test typeof(convert(typeof(qx), qy)) == typeof(qx) - @test convert(typeof(qx), qy)[1] isa Quantity{Float64} - @test convert(typeof(qx), qy)[1] == convert(Quantity{Float64}, qy[1]) + @test convert(typeof(qx), qy)[1] isa RealQuantity{Float64} + @test convert(typeof(qx), qy)[1] == convert(RealQuantity{Float64}, qy[1]) end end @@ -1152,21 +1152,21 @@ end :log, :log2, :log10, :log1p, :exp, :exp2, :exp10, :expm1, :frexp, :exponent, :atan, :atand ) - for Q in (Quantity, GenericQuantity), D in (Dimensions, SymbolicDimensions), f in functions + for Q in (RealQuantity, Quantity, GenericQuantity), D in (Dimensions, SymbolicDimensions), f in functions # Only test on valid domain valid_inputs = filter( x -> is_input_valid(eval(f), x), 5rand(100) .- 2.5 ) for x in valid_inputs[1:3] - qx_dimensionless = Quantity(x, D) - qx_dimensions = Quantity(x, convert(D, dimension(u"m/s"))) + qx_dimensionless = Q(x, D) + qx_dimensions = Q(x, convert(D, dimension(u"m/s"))) @eval @test $f($qx_dimensionless) == $f($x) @eval @test_throws DimensionError $f($qx_dimensions) if f in (:atan, :atand) for y in valid_inputs[end-3:end] - qy_dimensionless = Quantity(y, D) - qy_dimensions = Quantity(y, convert(D, dimension(u"m/s"))) + qy_dimensionless = Q(y, D) + qy_dimensions = Q(y, convert(D, dimension(u"m/s"))) @eval @test $f($y, $qx_dimensionless) == $f($y, $x) @eval @test $f($qy_dimensionless, $x) == $f($y, $x) @eval @test $f($qy_dimensionless, $qx_dimensionless) == $f($y, $x) @@ -1189,8 +1189,9 @@ end :floor, :trunc, :ceil, :significand, :ldexp, :round, ) - for Q in (Quantity, GenericQuantity), D in (Dimensions, SymbolicDimensions), f in functions + for Q in (RealQuantity, Quantity, GenericQuantity), D in (Dimensions, SymbolicDimensions), f in functions T = f in (:abs, :real, :imag, :conj) ? ComplexF64 : Float64 + T <: Complex && Q == RealQuantity && continue if f == :modf # Functions that return multiple outputs for x in 5rand(T, 3) .- 2.5 dim = convert(D, dimension(u"m/s")) @@ -1251,7 +1252,7 @@ end end @testset "Test div" begin - for Q in (Quantity, GenericQuantity) + for Q in (RealQuantity, Quantity, GenericQuantity) x = Q{Int}(10, length=1) y = Q{Int}(3, mass=-1) @test div(x, y) == Q{Int}(3, length=1, mass=1) From 27247a29e4305cf15b1731fcbe0a07ee2640cc48 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 19 Nov 2023 21:59:44 +0000 Subject: [PATCH 09/39] Fix scitypes test --- test/test_scitypes.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_scitypes.jl b/test/test_scitypes.jl index 5f1897a7..51afe69d 100644 --- a/test/test_scitypes.jl +++ b/test/test_scitypes.jl @@ -1,4 +1,5 @@ using DynamicQuantities +using DynamicQuantities: DEFAULT_QUANTITY_TYPE, constructorof using ScientificTypes import ScientificTypes as ST @@ -18,6 +19,6 @@ sch = schema(X) @test first(sch.names) == :x @test first(sch.scitypes) == Continuous -@test first(sch.types) <: Quantity{Float64} +@test first(sch.types) <: constructorof(DEFAULT_QUANTITY_TYPE){Float64} @test first(schema((; x=rand(1:10, 5) .* Quantity{Int}(u"m/s"))).scitypes) == Count From 7bfbe2e2c9d1403a42b007028753e6e053b3b4f5 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 19 Nov 2023 23:04:10 +0000 Subject: [PATCH 10/39] Fix conversion to base type --- src/utils.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/utils.jl b/src/utils.jl index bb8f87e4..081d6ec3 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -25,7 +25,9 @@ end return output end -Base.convert(::Type{Number}, q::AbstractQuantity) = q +for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES + @eval Base.convert(::Type{$base_type}, q::$type) = q +end function Base.convert(::Type{T}, q::UnionAbstractQuantity) where {T<:Number} @assert iszero(dimension(q)) "$(typeof(q)): $(q) has dimensions! Use `ustrip` instead." return convert(T, ustrip(q)) From 0e412614c205f2ffd9ea266d97e74cb45415b49f Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 19 Nov 2023 23:13:33 +0000 Subject: [PATCH 11/39] Change back to `eltype` --- src/disambiguities.jl | 8 ++++---- src/fixed_rational.jl | 39 +++++++++++++++++++-------------------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/disambiguities.jl b/src/disambiguities.jl index f8463d96..891b3309 100644 --- a/src/disambiguities.jl +++ b/src/disambiguities.jl @@ -31,16 +31,16 @@ function Base.promote_rule(::Type{Bool}, ::Type{F}) where {F<:FixedRational} return F end function Base.promote_rule(::Type{F}, ::Type{BigFloat}) where {F<:FixedRational} - return promote_type(Rational{num_type(F)}, BigFloat) + return promote_type(Rational{eltype(F)}, BigFloat) end function Base.promote_rule(::Type{BigFloat}, ::Type{F}) where {F<:FixedRational} - return promote_type(Rational{num_type(F)}, BigFloat) + return promote_type(Rational{eltype(F)}, BigFloat) end function Base.promote_rule(::Type{F}, ::Type{T}) where {F<:FixedRational,T<:AbstractIrrational} - return promote_type(Rational{num_type(F)}, T) + return promote_type(Rational{eltype(F)}, T) end function Base.promote_rule(::Type{T}, ::Type{F}) where {F<:FixedRational,T<:AbstractIrrational} - return promote_type(Rational{num_type(F)}, T) + return promote_type(Rational{eltype(F)}, T) end # Assorted calls found by Aqua: diff --git a/src/fixed_rational.jl b/src/fixed_rational.jl index 42ac7149..7b50f616 100644 --- a/src/fixed_rational.jl +++ b/src/fixed_rational.jl @@ -29,26 +29,25 @@ denom(x::FixedRational) = denom(typeof(x)) # Otherwise, we would have type instability. val_denom(::Type{<:F}) where {F<:FixedRational} = Val(_denom(F)) -num_type(::Type{<:FixedRational{T}}) where {T} = T -num_type(x::FixedRational) = num_type(typeof(x)) +Base.eltype(::Type{<:FixedRational{T}}) where {T} = T const DEFAULT_NUMERATOR_TYPE = Int32 const DEFAULT_DENOM = DEFAULT_NUMERATOR_TYPE(2^4 * 3^2 * 5^2 * 7) (::Type{F})(x::F) where {F<:FixedRational} = x -(::Type{F})(x::F2) where {T,T2,den,F<:FixedRational{T,den},F2<:FixedRational{T2,den}} = unsafe_fixed_rational(x.num, num_type(F), val_denom(F)) -(::Type{F})(x::Integer) where {F<:FixedRational} = unsafe_fixed_rational(x * denom(F), num_type(F), val_denom(F)) -(::Type{F})(x::Rational) where {F<:FixedRational} = unsafe_fixed_rational(widemul(x.num, denom(F)) ÷ x.den, num_type(F), val_denom(F)) +(::Type{F})(x::F2) where {T,T2,den,F<:FixedRational{T,den},F2<:FixedRational{T2,den}} = unsafe_fixed_rational(x.num, eltype(F), val_denom(F)) +(::Type{F})(x::Integer) where {F<:FixedRational} = unsafe_fixed_rational(x * denom(F), eltype(F), val_denom(F)) +(::Type{F})(x::Rational) where {F<:FixedRational} = unsafe_fixed_rational(widemul(x.num, denom(F)) ÷ x.den, eltype(F), val_denom(F)) -Base.:*(l::F, r::F) where {F<:FixedRational} = unsafe_fixed_rational(widemul(l.num, r.num) ÷ denom(F), num_type(F), val_denom(F)) -Base.:+(l::F, r::F) where {F<:FixedRational} = unsafe_fixed_rational(l.num + r.num, num_type(F), val_denom(F)) -Base.:-(l::F, r::F) where {F<:FixedRational} = unsafe_fixed_rational(l.num - r.num, num_type(F), val_denom(F)) -Base.:-(x::F) where {F<:FixedRational} = unsafe_fixed_rational(-x.num, num_type(F), val_denom(F)) +Base.:*(l::F, r::F) where {F<:FixedRational} = unsafe_fixed_rational(widemul(l.num, r.num) ÷ denom(F), eltype(F), val_denom(F)) +Base.:+(l::F, r::F) where {F<:FixedRational} = unsafe_fixed_rational(l.num + r.num, eltype(F), val_denom(F)) +Base.:-(l::F, r::F) where {F<:FixedRational} = unsafe_fixed_rational(l.num - r.num, eltype(F), val_denom(F)) +Base.:-(x::F) where {F<:FixedRational} = unsafe_fixed_rational(-x.num, eltype(F), val_denom(F)) -Base.inv(x::F) where {F<:FixedRational} = unsafe_fixed_rational(widemul(denom(F), denom(F)) ÷ x.num, num_type(F), val_denom(F)) +Base.inv(x::F) where {F<:FixedRational} = unsafe_fixed_rational(widemul(denom(F), denom(F)) ÷ x.num, eltype(F), val_denom(F)) -Base.:*(l::F, r::Integer) where {F<:FixedRational} = unsafe_fixed_rational(l.num * r, num_type(F), val_denom(F)) -Base.:*(l::Integer, r::F) where {F<:FixedRational} = unsafe_fixed_rational(l * r.num, num_type(F), val_denom(F)) +Base.:*(l::F, r::Integer) where {F<:FixedRational} = unsafe_fixed_rational(l.num * r, eltype(F), val_denom(F)) +Base.:*(l::Integer, r::F) where {F<:FixedRational} = unsafe_fixed_rational(l * r.num, eltype(F), val_denom(F)) for comp in (:(==), :isequal, :<, :(isless), :<=) @eval Base.$comp(x::F, y::F) where {F<:FixedRational} = $comp(x.num, y.num) @@ -59,7 +58,7 @@ Base.isone(x::F) where {F<:FixedRational} = x.num == denom(F) Base.isinteger(x::F) where {F<:FixedRational} = iszero(x.num % denom(F)) Rational{R}(x::F) where {R,F<:FixedRational} = Rational{R}(x.num, denom(F)) -Rational(x::F) where {F<:FixedRational} = Rational{num_type(F)}(x) +Rational(x::F) where {F<:FixedRational} = Rational{eltype(F)}(x) (::Type{AF})(x::F) where {AF<:AbstractFloat,F<:FixedRational} = convert(AF, x.num) / convert(AF, denom(F)) (::Type{I})(x::F) where {I<:Integer,F<:FixedRational} = let @@ -79,13 +78,13 @@ Base.decompose(x::F) where {T,F<:FixedRational{T}} = (x.num, zero(T), denom(F)) function Base.promote_rule(::Type{F1}, ::Type{F2}) where {F1<:FixedRational,F2<:FixedRational} _denom(F1) == _denom(F2) || error("Refusing to promote `FixedRational` types with mixed denominators. Use `Rational` instead.") - return FixedRational{promote_type(num_type(F1), num_type(F2)), _denom(F1)} + return FixedRational{promote_type(eltype(F1), eltype(F2)), _denom(F1)} end function Base.promote_rule(::Type{F}, ::Type{Rational{T2}}) where {F<:FixedRational,T2} - return Rational{promote_type(num_type(F),T2)} + return Rational{promote_type(eltype(F),T2)} end function Base.promote_rule(::Type{Rational{T2}}, ::Type{F}) where {F<:FixedRational,T2} - return Rational{promote_type(num_type(F),T2)} + return Rational{promote_type(eltype(F),T2)} end # We want to consume integers @@ -98,15 +97,15 @@ end # Promotion with general types promotes like a rational function Base.promote_rule(::Type{T}, ::Type{T2}) where {T2<:Real,T<:FixedRational} - return promote_type(Rational{num_type(T)}, T2) + return promote_type(Rational{eltype(T)}, T2) end function Base.promote_rule(::Type{T2}, ::Type{T}) where {T2<:Real,T<:FixedRational} - return promote_type(Rational{num_type(T)}, T2) + return promote_type(Rational{eltype(T)}, T2) end Base.string(x::FixedRational) = let - isinteger(x) && return string(convert(num_type(x), x)) + isinteger(x) && return string(convert(eltype(x), x)) g = gcd(x.num, denom(x)) return string(div(x.num, g)) * "//" * string(div(denom(x), g)) end @@ -114,7 +113,7 @@ Base.show(io::IO, x::FixedRational) = print(io, string(x)) tryrationalize(::Type{F}, x::F) where {F<:FixedRational} = x tryrationalize(::Type{F}, x::Union{Rational,Integer}) where {F<:FixedRational} = convert(F, x) -tryrationalize(::Type{F}, x) where {F<:FixedRational} = unsafe_fixed_rational(round(num_type(F), x * denom(F)), num_type(F), val_denom(F)) +tryrationalize(::Type{F}, x) where {F<:FixedRational} = unsafe_fixed_rational(round(eltype(F), x * denom(F)), eltype(F), val_denom(F)) # Fix method ambiguities Base.round(::Type{T}, x::F, r::RoundingMode=RoundNearest) where {T>:Missing, F<:FixedRational} = round(Base.nonmissingtype_checked(T), x, r) From 4667128315dcd4173541f6ebe9e0a4a424a34b8e Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 19 Nov 2023 23:20:55 +0000 Subject: [PATCH 12/39] Refactor some promotion rules --- src/utils.jl | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index 081d6ec3..917671aa 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -35,14 +35,10 @@ end function Base.promote_rule(::Type{Dimensions{R1}}, ::Type{Dimensions{R2}}) where {R1,R2} return Dimensions{promote_type(R1,R2)} end -function Base.promote_rule(::Type{<:GenericQuantity{T1,D1}}, ::Type{<:GenericQuantity{T2,D2}}) where {T1,T2,D1,D2} - return GenericQuantity{promote_type(T1,T2),promote_type(D1,D2)} -end -function Base.promote_rule(::Type{<:Quantity{T1,D1}}, ::Type{<:Quantity{T2,D2}}) where {T1,T2,D1,D2} - return Quantity{promote_type(T1,T2),promote_type(D1,D2)} -end -function Base.promote_rule(::Type{<:RealQuantity{T1,D1}}, ::Type{<:RealQuantity{T2,D2}}) where {T1,T2,D1,D2} - return RealQuantity{promote_type(T1,T2),promote_type(D1,D2)} +for (_, _, concrete_type) in ABSTRACT_QUANTITY_TYPES + @eval function Base.promote_rule(::Type{<:$concrete_type{T1,D1}}, ::Type{<:$concrete_type{T2,D2}}) where {T1,T2,D1,D2} + return $concrete_type{promote_type(T1,T2),promote_type(D1,D2)} + end end function Base.promote_rule(::Type{<:Quantity{T1,D1}}, ::Type{<:GenericQuantity{T2,D2}}) where {T1,T2,D1,D2} From 745cd9e8a395838ba51766e9dc228ad0bb23df99 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Tue, 21 Nov 2023 01:11:06 +0000 Subject: [PATCH 13/39] No need to redefine `identity` --- src/math.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/math.jl b/src/math.jl index 02ca0b0e..8922d307 100644 --- a/src/math.jl +++ b/src/math.jl @@ -204,7 +204,7 @@ end ############################## Same dimension as input ################################## for f in ( :float, :abs, :real, :imag, :conj, :adjoint, :unsigned, - :nextfloat, :prevfloat, :identity, :transpose, :significand + :nextfloat, :prevfloat, :transpose, :significand ) @eval function Base.$f(q::UnionAbstractQuantity) return new_quantity(typeof(q), $f(ustrip(q)), dimension(q)) From 79bfd781c4e0a57e700867f53cd1c9459281ae49 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Tue, 21 Nov 2023 01:54:34 +0000 Subject: [PATCH 14/39] Fix ambiguity in `div` --- src/math.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/math.jl b/src/math.jl index 8922d307..e666d000 100644 --- a/src/math.jl +++ b/src/math.jl @@ -1,4 +1,5 @@ for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES + div_base_type = type == AbstractGenericQuantity ? Number : base_type @eval begin function Base.:*(l::$type, r::$type) l, r = promote_except_value(l, r) @@ -20,7 +21,7 @@ for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES function Base.:/(l::$type, r::$base_type) new_quantity(typeof(l), ustrip(l) / r, dimension(l)) end - function Base.div(x::$type, y::$base_type, r::RoundingMode=RoundToZero) + function Base.div(x::$type, y::$div_base_type, r::RoundingMode=RoundToZero) new_quantity(typeof(x), div(ustrip(x), y, r), dimension(x)) end @@ -30,7 +31,7 @@ for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES function Base.:/(l::$base_type, r::$type) new_quantity(typeof(r), l / ustrip(r), inv(dimension(r))) end - function Base.div(x::$base_type, y::$type, r::RoundingMode=RoundToZero) + function Base.div(x::$div_base_type, y::$type, r::RoundingMode=RoundToZero) new_quantity(typeof(y), div(x, ustrip(y), r), inv(dimension(y))) end From 037b6f289a3a76a2e8e38da92388cf0f6201e3c3 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Tue, 21 Nov 2023 04:44:53 +0000 Subject: [PATCH 15/39] Refactor promotion rules --- src/types.jl | 12 +-------- src/utils.jl | 71 ++++++++++++++++++++++++++++++++++------------------ 2 files changed, 47 insertions(+), 36 deletions(-) diff --git a/src/types.jl b/src/types.jl index 9690a4c8..16eaec61 100644 --- a/src/types.jl +++ b/src/types.jl @@ -197,16 +197,6 @@ const ABSTRACT_QUANTITY_TYPES = ( (AbstractRealQuantity, Real, RealQuantity) ) -""" - promote_quantity(::Type{<:UnionAbstractQuantity}, t::Type{<:Any}) - -Find the next quantity type in the hierarchy that can accommodate the type `t`. -If the current quantity type can already accommodate `t`, then the current type is returned. -""" -promote_quantity(::Type{<:Union{GenericQuantity,Quantity,RealQuantity}}, ::Type{<:Any}) = GenericQuantity -promote_quantity(::Type{<:Union{Quantity,RealQuantity}}, ::Type{<:Number}) = Quantity -promote_quantity(::Type{<:RealQuantity}, ::Type{<:Real}) = RealQuantity -promote_quantity(T, _) = T for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES @eval begin @@ -230,7 +220,7 @@ const DEFAULT_QUANTITY_TYPE = RealQuantity{DEFAULT_VALUE_TYPE, DEFAULT_DIM_TYPE} return constructorof(D)(dims...) end @inline function new_quantity(::Type{Q}, val, dims) where {Q<:UnionAbstractQuantity} - Qout = promote_quantity(Q, typeof(val)) + Qout = promote_quantity_on_value(Q, typeof(val)) return constructorof(Qout)(val, dims) end diff --git a/src/utils.jl b/src/utils.jl index 917671aa..9cfe76d8 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -35,30 +35,51 @@ end function Base.promote_rule(::Type{Dimensions{R1}}, ::Type{Dimensions{R2}}) where {R1,R2} return Dimensions{promote_type(R1,R2)} end -for (_, _, concrete_type) in ABSTRACT_QUANTITY_TYPES - @eval function Base.promote_rule(::Type{<:$concrete_type{T1,D1}}, ::Type{<:$concrete_type{T2,D2}}) where {T1,T2,D1,D2} - return $concrete_type{promote_type(T1,T2),promote_type(D1,D2)} + +# Define all the quantity x quantity promotion rules +""" + promote_quantity_on_value(Q::Type, T::Type) + +Find the next quantity type in the hierarchy that can accommodate the type `T`. +If the current quantity type can already accommodate `T`, then the current type is returned. +For example, `promote_quantity_on_value(Quantity, Float64)` would return `Quantity`, and +`promote_quantity_on_value(RealQuantity, String)` would return `GenericQuantity`. +The user should overload this function to define a custom type hierarchy. + +Also see `promote_quantity_on_quantity`. +""" +@inline promote_quantity_on_value(::Type{<:Union{GenericQuantity,Quantity,RealQuantity}}, ::Type{<:Any}) = GenericQuantity +@inline promote_quantity_on_value(::Type{<:Union{Quantity,RealQuantity}}, ::Type{<:Number}) = Quantity +@inline promote_quantity_on_value(::Type{<:RealQuantity}, ::Type{<:Real}) = RealQuantity +@inline promote_quantity_on_value(T, _) = T + +""" + promote_quantity_on_quantity(Q1, Q2) + +Defines the type hierarchy for quantities, returning the most specific type +that is compatible with both input quantity types. For example, +`promote_quantity_on_quantity(Quantity, GenericQuantity)` would return `GenericQuantity`, +as it can store both `Quantity` and `GenericQuantity` values. +Similarly, `promote_quantity_on_quantity(RealQuantity, RealQuantity)` would return `RealQuantity`, +as that is the most specific type. + +Also see `promote_quantity_on_value`. +""" +@inline promote_quantity_on_quantity(::Type{<:Union{GenericQuantity,Quantity,RealQuantity}}, ::Type{<:Union{GenericQuantity,Quantity,RealQuantity}}) = GenericQuantity +@inline promote_quantity_on_quantity(::Type{<:Union{Quantity,RealQuantity}}, ::Type{<:Union{Quantity,RealQuantity}}) = Quantity +@inline promote_quantity_on_quantity(::Type{<:RealQuantity}, ::Type{<:RealQuantity}) = RealQuantity +@inline promote_quantity_on_quantity(::Type{Q}, ::Type{Q}) where {Q<:UnionAbstractQuantity} = Q + +for (type1, _, _) in ABSTRACT_QUANTITY_TYPES, (type2, _, _) in ABSTRACT_QUANTITY_TYPES + @eval function Base.promote_rule(::Type{Q1}, ::Type{Q2}) where {T1,T2,D1,D2,Q1<:$type1{T1,D1},Q2<:$type2{T2,D2}} + return with_type_parameters( + promote_quantity_on_quantity(Q1, Q2), + promote_type(T1, T2), + promote_type(D1, D2), + ) end end -function Base.promote_rule(::Type{<:Quantity{T1,D1}}, ::Type{<:GenericQuantity{T2,D2}}) where {T1,T2,D1,D2} - return GenericQuantity{promote_type(T1,T2),promote_type(D1,D2)} -end -function Base.promote_rule(::Type{<:GenericQuantity{T1,D1}}, ::Type{<:Quantity{T2,D2}}) where {T1,T2,D1,D2} - return GenericQuantity{promote_type(T1,T2),promote_type(D1,D2)} -end -function Base.promote_rule(::Type{<:GenericQuantity{T1,D1}}, ::Type{<:RealQuantity{T2,D2}}) where {T1,T2,D1,D2} - return GenericQuantity{promote_type(T1,T2),promote_type(D1,D2)} -end -function Base.promote_rule(::Type{<:RealQuantity{T1,D1}}, ::Type{<:GenericQuantity{T2,D2}}) where {T1,T2,D1,D2} - return GenericQuantity{promote_type(T1,T2),promote_type(D1,D2)} -end -function Base.promote_rule(::Type{<:Quantity{T1,D1}}, ::Type{<:RealQuantity{T2,D2}}) where {T1,T2,D1,D2} - return Quantity{promote_type(T1,T2),promote_type(D1,D2)} -end -function Base.promote_rule(::Type{<:RealQuantity{T1,D1}}, ::Type{<:Quantity{T2,D2}}) where {T1,T2,D1,D2} - return Quantity{promote_type(T1,T2),promote_type(D1,D2)} -end # Define promotion rules for all basic numeric types, individually. # We don't want to define an opinionated promotion on <:Number, @@ -84,10 +105,10 @@ for (type, _, _) in ABSTRACT_QUANTITY_TYPES return new_quantity(Q, convert(T, x), D()) end function Base.promote_rule(::Type{Q}, ::Type{T2}) where {T,D,Q<:$type{T,D},T2<:BASE_NUMERIC_TYPES} - return with_type_parameters(promote_quantity(Q, T2), promote_type(T, T2), D) + return with_type_parameters(promote_quantity_on_value(Q, T2), promote_type(T, T2), D) end function Base.promote_rule(::Type{T2}, ::Type{Q}) where {T,D,Q<:$type{T,D},T2<:BASE_NUMERIC_TYPES} - return with_type_parameters(promote_quantity(Q, T2), promote_type(T, T2), D) + return with_type_parameters(promote_quantity_on_value(Q, T2), promote_type(T, T2), D) end end for numeric_type in AMBIGUOUS_NUMERIC_TYPES @@ -96,10 +117,10 @@ for (type, _, _) in ABSTRACT_QUANTITY_TYPES return new_quantity(Q, convert(T, x), D()) end function Base.promote_rule(::Type{Q}, ::Type{$numeric_type}) where {T,D,Q<:$type{T,D}} - return with_type_parameters(promote_quantity(Q, $numeric_type), promote_type(T, $numeric_type), D) + return with_type_parameters(promote_quantity_on_value(Q, $numeric_type), promote_type(T, $numeric_type), D) end function Base.promote_rule(::Type{$numeric_type}, ::Type{Q}) where {T,D,Q<:$type{T,D}} - return with_type_parameters(promote_quantity(Q, $numeric_type), promote_type(T, $numeric_type), D) + return with_type_parameters(promote_quantity_on_value(Q, $numeric_type), promote_type(T, $numeric_type), D) end end end From e90d04677911e5fc7988302e664e5ec9af70954b Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Tue, 21 Nov 2023 05:37:35 +0000 Subject: [PATCH 16/39] Test all ambiguities --- src/disambiguities.jl | 18 ++++-- src/math.jl | 2 +- src/utils.jl | 24 ++----- test/unittests.jl | 141 ++++++++++++++++++++++++++++++++++++------ 4 files changed, 142 insertions(+), 43 deletions(-) diff --git a/src/disambiguities.jl b/src/disambiguities.jl index 891b3309..744330f5 100644 --- a/src/disambiguities.jl +++ b/src/disambiguities.jl @@ -43,20 +43,21 @@ function Base.promote_rule(::Type{T}, ::Type{F}) where {F<:FixedRational,T<:Abst return promote_type(Rational{eltype(F)}, T) end -# Assorted calls found by Aqua: +################################################################################ +# Assorted calls found by Aqua: ################################################ +################################################################################ + for type in (Signed, Float64, Float32, Rational), op in (:flipsign, :copysign) @eval function Base.$(op)(x::$type, y::AbstractRealQuantity) return $(op)(x, ustrip(y)) end end - function Base.:*(l::Complex{Bool}, r::AbstractRealQuantity) return new_quantity(typeof(r), l * ustrip(r), dimension(r)) end function Base.:*(l::AbstractRealQuantity, r::Complex{Bool}) return new_quantity(typeof(l), ustrip(l) * r, dimension(l)) end - for op in (:(==), :isequal), base_type in (AbstractIrrational, AbstractFloat) @eval begin function Base.$(op)(l::AbstractRealQuantity, r::$base_type) @@ -67,7 +68,6 @@ for op in (:(==), :isequal), base_type in (AbstractIrrational, AbstractFloat) end end end - function Base.isless(l::AbstractRealQuantity, r::AbstractFloat) iszero(dimension(l)) || throw(DimensionError(l, r)) return isless(ustrip(l), r) @@ -76,3 +76,13 @@ function Base.isless(l::AbstractFloat, r::AbstractRealQuantity) iszero(dimension(r)) || throw(DimensionError(l, r)) return isless(l, ustrip(r)) end +for (type, _, _) in ABSTRACT_QUANTITY_TYPES, numeric_type in (Bool, BigFloat) + @eval begin + function Base.promote_rule(::Type{Q}, ::Type{$numeric_type}) where {T,D,Q<:$type{T,D}} + return with_type_parameters(promote_quantity_on_value(Q, $numeric_type), promote_type(T, $numeric_type), D) + end + function Base.promote_rule(::Type{$numeric_type}, ::Type{Q}) where {T,D,Q<:$type{T,D}} + return with_type_parameters(promote_quantity_on_value(Q, $numeric_type), promote_type(T, $numeric_type), D) + end + end +end diff --git a/src/math.jl b/src/math.jl index e666d000..f364540c 100644 --- a/src/math.jl +++ b/src/math.jl @@ -64,7 +64,7 @@ for (type, _, _) in ABSTRACT_QUANTITY_TYPES new_quantity(typeof(r), l / ustrip(r), inv(dimension(r))) end function Base.:/(l::$type, r::Complex) - new_quantity(typeof(r), ustrip(l) / r, dimension(r)) + new_quantity(typeof(l), ustrip(l) / r, dimension(l)) end end end diff --git a/src/utils.jl b/src/utils.jl index 9cfe76d8..088ae06e 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -87,17 +87,14 @@ end # abstract number packages which may try to do the same thing. # (which would lead to ambiguities) const BASE_NUMERIC_TYPES = Union{ - Int8, UInt8, Int16, UInt16, Int32, UInt32, + Bool, Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64, Int128, UInt128, Float16, Float32, - Float64, BigInt, ComplexF16, ComplexF32, + Float64, BigFloat, BigInt, ComplexF16, ComplexF32, ComplexF64, Complex{BigFloat}, Rational{Int8}, Rational{UInt8}, Rational{Int16}, Rational{UInt16}, Rational{Int32}, Rational{UInt32}, Rational{Int64}, Rational{UInt64}, Rational{Int128}, Rational{UInt128}, Rational{BigInt}, } -# The following types require explicit promotion, -# as putting them in a union type creates different ambiguities -const AMBIGUOUS_NUMERIC_TYPES = (Bool, BigFloat) for (type, _, _) in ABSTRACT_QUANTITY_TYPES @eval begin @@ -111,19 +108,6 @@ for (type, _, _) in ABSTRACT_QUANTITY_TYPES return with_type_parameters(promote_quantity_on_value(Q, T2), promote_type(T, T2), D) end end - for numeric_type in AMBIGUOUS_NUMERIC_TYPES - @eval begin - function Base.convert(::Type{Q}, x::$numeric_type) where {T,D,Q<:$type{T,D}} - return new_quantity(Q, convert(T, x), D()) - end - function Base.promote_rule(::Type{Q}, ::Type{$numeric_type}) where {T,D,Q<:$type{T,D}} - return with_type_parameters(promote_quantity_on_value(Q, $numeric_type), promote_type(T, $numeric_type), D) - end - function Base.promote_rule(::Type{$numeric_type}, ::Type{Q}) where {T,D,Q<:$type{T,D}} - return with_type_parameters(promote_quantity_on_value(Q, $numeric_type), promote_type(T, $numeric_type), D) - end - end - end end """ @@ -175,7 +159,7 @@ Base.keys(q::UnionAbstractQuantity) = keys(ustrip(q)) # Numeric checks -for op in (:(<=), :(<), :(>=), :(>), :isless, :isgreater), +for op in (:(<=), :(<), :(>=), :(>), :isless), (type, base_type, _) in ABSTRACT_QUANTITY_TYPES @eval begin @@ -247,7 +231,7 @@ end # Simple flags: for f in ( - :iszero, :isfinite, :isinf, :isnan, :isreal, :signbit, + :isone, :iszero, :isfinite, :isinf, :isnan, :isreal, :signbit, :isempty, :iseven, :isodd, :isinteger, :ispow2 ) @eval Base.$f(q::UnionAbstractQuantity) = $f(ustrip(q)) diff --git a/test/unittests.jl b/test/unittests.jl index 95366aa6..db8c94ea 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -3,6 +3,7 @@ using DynamicQuantities: FixedRational using DynamicQuantities: DEFAULT_QUANTITY_TYPE, DEFAULT_DIM_BASE_TYPE, DEFAULT_DIM_TYPE, DEFAULT_VALUE_TYPE using DynamicQuantities: array_type, value_type, dim_type, quantity_type using DynamicQuantities: GenericQuantity, with_type_parameters, constructorof +using DynamicQuantities: promote_quantity_on_quantity, promote_quantity_on_value using Ratios: SimpleRatio using SaferIntegers: SafeInt16 using StaticArrays: SArray, MArray @@ -169,6 +170,8 @@ end @test iseven(Quantity(3, length=1)) == false @test isodd(Quantity(2, length=1)) == false @test isodd(Quantity(3, length=1)) == true + @test isone(Quantity(1, length=1)) == true + @test isone(Quantity(2, length=1)) == false @test isinteger(Quantity(2, length=1)) == true @test isinteger(Quantity(2.1, length=1)) == false @test ispow2(Quantity(2, length=1)) == true @@ -200,6 +203,19 @@ end @test conj(x) == (0.5 - 0.6im) * u"km/s" @test angle(x) == angle(ustrip(x)) @test adjoint(ustrip(x^2)) ≈ adjoint(x^2) / u"m/s"^2 + + # Can create by division as well: + x = 1.0u"km/s" / (1.0 + 0.5im) + @test typeof(x) == Quantity{Complex{Float64}, DEFAULT_DIM_TYPE} + @test ustrip(x) ≈ 1000.0 / (1.0 + 0.5im) + @test ulength(x) == 1.0 + @test utime(x) == -1.0 + + x = (1.0 + 0.5im) / (1.0u"km/s") + @test typeof(x) == Quantity{Complex{Float64}, DEFAULT_DIM_TYPE} + @test ustrip(x) ≈ (1.0 + 0.5im) / 1000.0 + @test ulength(x) == -1.0 + @test utime(x) == 1.0 end @testset "Fallbacks" begin @@ -561,6 +577,9 @@ end # But, we always need to use a quantity when mixing with mathematical operations: @test_throws ErrorException MyQuantity(0.1) + 0.1 * MyDimensions() + + # Explicitly test that `promote_quantity_on_quantity` has a reasonable default + @test promote_quantity_on_quantity(typeof(MyQuantity(0.1)), typeof(MyQuantity(0.1))) == MyQuantity{Float64,DEFAULT_DIM_TYPE} end @testset "Symbolic dimensions" begin @@ -749,27 +768,71 @@ end @testset "Test ambiguities" begin - R = DEFAULT_DIM_BASE_TYPE - x = convert(R, 10) - y = convert(R, 5) - @test promote(x, y) == (x, y) - @test_throws ErrorException promote(x, convert(FixedRational{Int32,100}, 10)) - @test promote_type(typeof(u"km/s"), typeof(convert(Quantity{Float32}, u"km/s"))) <: Quantity{Float64} + @testset "FixedRational" begin + R = DEFAULT_DIM_BASE_TYPE + x = convert(R, 10) + y = convert(R, 5) + @test promote(x, y) == (x, y) + @test_throws ErrorException promote(x, convert(FixedRational{Int32,100}, 10)) + @test promote_type(typeof(u"km/s"), typeof(convert(Quantity{Float32}, u"km/s"))) <: Quantity{Float64} + + x = FixedRational{Int32,100}(1) + @test promote_type(typeof(x), typeof(true)) == typeof(x) + @test promote_type(typeof(true), typeof(x)) == typeof(x) + @test promote_type(typeof(x), typeof(BigFloat(1))) == promote_type(Rational{Int32}, BigFloat) + @test promote_type(typeof(BigFloat(1)), typeof(x)) == promote_type(Rational{Int32}, BigFloat) + @test promote_type(typeof(x), typeof(π)) == promote_type(Rational{Int32}, typeof(π)) + @test promote_type(typeof(π), typeof(x)) == promote_type(Rational{Int32}, typeof(π)) + end - x = 1.0u"m" - s = "test" - y = WeakRef(s) - @test_throws ErrorException x == y - @test_throws ErrorException y == x + @testset "Weakref" begin + x = 1.0u"m" + s = "test" + y = WeakRef(s) + @test_throws ErrorException x == y + @test_throws ErrorException y == x + end - qarr1 = QuantityArray(randn(3), u"km/s") - qarr2 = qarr1 - @test convert(typeof(qarr2), qarr2) === qarr1 + @testset "Arrays" begin + qarr1 = QuantityArray(randn(3), u"km/s") + qarr2 = qarr1 + @test convert(typeof(qarr2), qarr2) === qarr1 + end - x = 1.0u"m" - y = x ^ (3//2) - @test y == Quantity(1.0, length=3//2) - @test typeof(y) == RealQuantity{Float64,DEFAULT_DIM_TYPE} + @testset "Rational power law" begin + x = 1.0u"m" + y = x ^ (3//2) + @test y == Quantity(1.0, length=3//2) + @test typeof(y) == RealQuantity{Float64,DEFAULT_DIM_TYPE} + end + + @testset "Numeric promotion rules" begin + for Q in (RealQuantity, Quantity, GenericQuantity) + x = Q(1.0u"m") + @test promote_type(typeof(x), Bool) == typeof(x) + @test promote_type(Bool, typeof(x)) == typeof(x) + @test promote_type(typeof(x), BigFloat) == with_type_parameters(Q, BigFloat, DEFAULT_DIM_TYPE) + @test promote_type(BigFloat, typeof(x)) == with_type_parameters(Q, BigFloat, DEFAULT_DIM_TYPE) + end + end + + @testset "Complex numbers" begin + for Q in (RealQuantity, Quantity, GenericQuantity) + # Bool stuff + x = true * im + y = Q(0.5u"m") + @test typeof(x * y) == with_type_parameters(promote_quantity_on_value(Q, ComplexF64), Complex{Float64}, DEFAULT_DIM_TYPE) + @test typeof(y * x) == with_type_parameters(promote_quantity_on_value(Q, ComplexF64), Complex{Float64}, DEFAULT_DIM_TYPE) + @test ustrip(x * y) == 0.5im + @test ustrip(y * x) == 0.5im + + # Complex powers + x = Q(0.5u"1") + out = x ^ (1 + 2im) + @test typeof(out) == with_type_parameters(promote_quantity_on_value(Q, ComplexF64), Complex{Float64}, DEFAULT_DIM_TYPE) + @test ustrip(out) ≈ 0.5 ^ (1 + 2im) + end + end end for Q in (RealQuantity, Quantity, GenericQuantity) @@ -1251,6 +1314,40 @@ end end end +@testset "Assorted comparison functions" begin + functions = ( + :(<=), :(<), :(>=), :(>), :isless, :isequal, :(==), + ) + x = 5randn(10) .- 2.5 + y = 5randn(10) .- 2.5 + for Q in (RealQuantity, Quantity, GenericQuantity), D in (Dimensions, SymbolicDimensions), f in functions + ground_truth = @eval $f.($x, $y) + dim = convert(D, dimension(u"m/s")) + qx_dimensions = [Q(xi, dim) for xi in x] + qy_dimensions = [Q(yi, dim) for yi in y] + @eval @test all($f.($qx_dimensions, $qy_dimensions) .== $ground_truth) + if f in (:isequal, :(==)) + # These include a dimension check in the result, rather than + # throwing an error + @eval @test !any($f.($qx_dimensions, $y)) + @eval @test !any($f.($x, $qy_dimensions)) + else + @eval @test_throws DimensionError $f($qx_dimensions[1], $y[1]) + @eval @test_throws DimensionError $f($x[1], $qy_dimensions[1]) + end + qx_dimensionless = [Q(xi, D) for xi in x] + qy_dimensionless = [Q(yi, D) for yi in y] + @eval @test all($f.($qx_dimensionless, $y) .== $ground_truth) + @eval @test all($f.($x, $qy_dimensionless) .== $ground_truth) + + qx_real_dimensions = [RealQuantity(xi, dim) for xi in x] + qy_real_dimensions = [RealQuantity(yi, dim) for yi in y] + # Mixed quantity input + @eval @test all($f.($qx_real_dimensions, $qy_dimensions) .== $ground_truth) + @eval @test all($f.($qx_dimensions, $qy_real_dimensions) .== $ground_truth) + end +end + @testset "Test div" begin for Q in (RealQuantity, Quantity, GenericQuantity) x = Q{Int}(10, length=1) @@ -1264,4 +1361,12 @@ end @test div(10, y, RoundFromZero) == Q{Int}(4, mass=1) end end + # Also test mixed quantities: + x = RealQuantity{Int}(10, length=1) + y = Quantity{Int}(3, mass=-1) + @test div(x, y) == Quantity{Int}(3, length=1, mass=1) + @test typeof(div(x, y)) <: Quantity{Int} + if VERSION >= v"1.9" + @test div(x, y, RoundFromZero) == Quantity{Int}(4, length=1, mass=1) + end end From 814fa7ef28790e2599f7a578b4d363cfb0fb04b7 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Tue, 21 Nov 2023 05:45:14 +0000 Subject: [PATCH 17/39] Help coveralls see coverage --- test/unittests.jl | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/test/unittests.jl b/test/unittests.jl index db8c94ea..3c1aef81 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -515,6 +515,8 @@ end @test promote_type(GenericQuantity{Float32,D}, GenericQuantity{Float64,D}) == GenericQuantity{Float64,D} @test promote_type(SymbolicDimensions{Rational{Int}}, SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}) == SymbolicDimensions{Rational{Int}} @test promote_type(Dimensions{Rational{Int}}, SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}) == Dimensions{Rational{Int}} + + @test promote_quantity_on_quantity(RealQuantity, RealQuantity) == RealQuantity end struct MyDimensions{R} <: AbstractDimensions{R} @@ -777,12 +779,13 @@ end @test promote_type(typeof(u"km/s"), typeof(convert(Quantity{Float32}, u"km/s"))) <: Quantity{Float64} x = FixedRational{Int32,100}(1) - @test promote_type(typeof(x), typeof(true)) == typeof(x) - @test promote_type(typeof(true), typeof(x)) == typeof(x) - @test promote_type(typeof(x), typeof(BigFloat(1))) == promote_type(Rational{Int32}, BigFloat) - @test promote_type(typeof(BigFloat(1)), typeof(x)) == promote_type(Rational{Int32}, BigFloat) - @test promote_type(typeof(x), typeof(π)) == promote_type(Rational{Int32}, typeof(π)) - @test promote_type(typeof(π), typeof(x)) == promote_type(Rational{Int32}, typeof(π)) + # Need explicit `promote_rule` calls here so coverage picks it up + @test promote_rule(typeof(x), typeof(true)) == typeof(x) + @test promote_rule(typeof(true), typeof(x)) == typeof(x) + @test promote_rule(typeof(x), typeof(BigFloat(1))) == promote_type(Rational{Int32}, BigFloat) + @test promote_rule(typeof(BigFloat(1)), typeof(x)) == promote_type(Rational{Int32}, BigFloat) + @test promote_rule(typeof(x), typeof(π)) == promote_type(Rational{Int32}, typeof(π)) + @test promote_rule(typeof(π), typeof(x)) == promote_type(Rational{Int32}, typeof(π)) end @testset "Weakref" begin From d84bb6ac6580d709e083231c85608b81b4ade426 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Tue, 21 Nov 2023 05:54:37 +0000 Subject: [PATCH 18/39] Reduce unnecessary methods --- src/disambiguities.jl | 12 ++++++++++++ src/math.jl | 18 ------------------ 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/disambiguities.jl b/src/disambiguities.jl index 744330f5..ff40f30a 100644 --- a/src/disambiguities.jl +++ b/src/disambiguities.jl @@ -52,6 +52,18 @@ for type in (Signed, Float64, Float32, Rational), op in (:flipsign, :copysign) return $(op)(x, ustrip(y)) end end +function Base.:*(l::Complex, r::AbstractRealQuantity) + new_quantity(typeof(r), l * ustrip(r), dimension(r)) +end +function Base.:*(l::AbstractRealQuantity, r::Complex) + new_quantity(typeof(l), ustrip(l) * r, dimension(l)) +end +function Base.:/(l::Complex, r::AbstractRealQuantity) + new_quantity(typeof(r), l / ustrip(r), inv(dimension(r))) +end +function Base.:/(l::AbstractRealQuantity, r::Complex) + new_quantity(typeof(l), ustrip(l) / r, dimension(l)) +end function Base.:*(l::Complex{Bool}, r::AbstractRealQuantity) return new_quantity(typeof(r), l * ustrip(r), dimension(r)) end diff --git a/src/math.jl b/src/math.jl index f364540c..6877606a 100644 --- a/src/math.jl +++ b/src/math.jl @@ -51,24 +51,6 @@ for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES end end -# Complex multiplication -for (type, _, _) in ABSTRACT_QUANTITY_TYPES - @eval begin - function Base.:*(l::Complex, r::$type) - new_quantity(typeof(r), l * ustrip(r), dimension(r)) - end - function Base.:*(l::$type, r::Complex) - new_quantity(typeof(l), ustrip(l) * r, dimension(l)) - end - function Base.:/(l::Complex, r::$type) - new_quantity(typeof(r), l / ustrip(r), inv(dimension(r))) - end - function Base.:/(l::$type, r::Complex) - new_quantity(typeof(l), ustrip(l) / r, dimension(l)) - end - end -end - Base.:*(l::AbstractDimensions, r::AbstractDimensions) = map_dimensions(+, l, r) Base.:/(l::AbstractDimensions, r::AbstractDimensions) = map_dimensions(-, l, r) From 664b188644692eda9add5717ada95207f579c4e5 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Tue, 21 Nov 2023 06:19:54 +0000 Subject: [PATCH 19/39] Improve coverage --- test/unittests.jl | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/unittests.jl b/test/unittests.jl index 3c1aef81..ea4416c8 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -821,7 +821,14 @@ end @testset "Complex numbers" begin for Q in (RealQuantity, Quantity, GenericQuantity) - # Bool stuff + x = 1.0im + y = Q(0.5u"m") + @test typeof(x * y) == with_type_parameters(promote_quantity_on_value(Q, ComplexF64), Complex{Float64}, DEFAULT_DIM_TYPE) + @test typeof(y * x) == with_type_parameters(promote_quantity_on_value(Q, ComplexF64), Complex{Float64}, DEFAULT_DIM_TYPE) + @test ustrip(x * y) == 0.5im + @test ustrip(y * x) == 0.5im + + # Bool version x = true * im y = Q(0.5u"m") @test typeof(x * y) == with_type_parameters(promote_quantity_on_value(Q, ComplexF64), Complex{Float64}, DEFAULT_DIM_TYPE) From c643ea64723ddc08b07bfa59632a9932ce61f6a6 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Tue, 21 Nov 2023 06:47:13 +0000 Subject: [PATCH 20/39] Back to `Quantity` as default --- src/arrays.jl | 10 ++++++---- src/constants.jl | 1 - src/symbolic_dimensions.jl | 32 ++++++++++++++++++-------------- src/types.jl | 2 +- src/units.jl | 1 - src/uparse.jl | 17 +++++++++-------- test/unittests.jl | 12 ++++++------ 7 files changed, 40 insertions(+), 35 deletions(-) diff --git a/src/arrays.jl b/src/arrays.jl index 1ca73be1..5cd765df 100644 --- a/src/arrays.jl +++ b/src/arrays.jl @@ -15,7 +15,7 @@ and so can be used in most places where a normal array would be used, including # Constructors - `QuantityArray(v::AbstractArray, d::AbstractDimensions)`: Create a `QuantityArray` with value `v` and dimensions `d`, - using `RealQuantity` if the eltype of `v` is real, `Quantity` if it is numeric, and `GenericQuantity` otherwise. + using `Quantity` if it is numeric, and `GenericQuantity` otherwise. - `QuantityArray(v::AbstractArray{<:Number}, q::AbstractQuantity)`: Create a `QuantityArray` with value `v` and dimensions inferred with `dimension(q)`. This is so that you can easily create an array with the units module, like so: ```julia @@ -54,9 +54,11 @@ end QuantityArray(v::AbstractArray; kws...) = QuantityArray(v, DEFAULT_DIM_TYPE(; kws...)) for (type, base_type, default_type) in ABSTRACT_QUANTITY_TYPES - @eval begin - QuantityArray(v::AbstractArray{<:$base_type}, q::$type) = QuantityArray(v .* ustrip(q), dimension(q), typeof(q)) - QuantityArray(v::AbstractArray{<:$base_type}, d::AbstractDimensions) = QuantityArray(v, d, $default_type) + @eval QuantityArray(v::AbstractArray{<:$base_type}, q::$type) = QuantityArray(v .* ustrip(q), dimension(q), typeof(q)) + + # Only define defaults for Quantity and GenericQuantity. Other types, the user needs to declare explicitly. + if type in (AbstractQuantity, AbstractGenericQuantity) + @eval QuantityArray(v::AbstractArray{<:$base_type}, d::AbstractDimensions) = QuantityArray(v, d, $default_type) end end QuantityArray(v::QA) where {Q<:UnionAbstractQuantity,QA<:AbstractArray{Q}} = diff --git a/src/constants.jl b/src/constants.jl index d69420ad..6a39ca2f 100644 --- a/src/constants.jl +++ b/src/constants.jl @@ -1,7 +1,6 @@ module Constants import ..DEFAULT_QUANTITY_TYPE -import ..RealQuantity import ..Units as U import ..Units: _add_prefixes diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index 8cf37c4d..46435978 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -264,6 +264,8 @@ function map_dimensions(op::O, l::SymbolicDimensions{L}, r::SymbolicDimensions{R return SymbolicDimensions(I, V) end +const DEFAULT_SYMBOLIC_QUANTITY_TYPE = with_type_parameters(DEFAULT_QUANTITY_TYPE, DEFAULT_VALUE_TYPE, SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}) + """ SymbolicUnitsParse @@ -277,7 +279,8 @@ module SymbolicUnitsParse import ..SYMBOL_CONFLICTS import ..SymbolicDimensions - import ...RealQuantity + import ...constructorof + import ...DEFAULT_SYMBOLIC_QUANTITY_TYPE import ...DEFAULT_VALUE_TYPE import ...DEFAULT_DIM_BASE_TYPE @@ -287,7 +290,8 @@ module SymbolicUnitsParse import ..SYMBOL_CONFLICTS import ..SymbolicDimensions - import ..RealQuantity + import ..constructorof + import ..DEFAULT_SYMBOLIC_QUANTITY_TYPE import ..DEFAULT_VALUE_TYPE import ..DEFAULT_DIM_BASE_TYPE @@ -299,11 +303,11 @@ module SymbolicUnitsParse CONSTANT_SYMBOLS_EXIST[] || lock(CONSTANT_SYMBOLS_LOCK) do CONSTANT_SYMBOLS_EXIST[] && return nothing for unit in setdiff(CONSTANT_SYMBOLS, SYMBOL_CONFLICTS) - @eval const $unit = RealQuantity(DEFAULT_VALUE_TYPE(1.0), SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}; $(unit)=1) + @eval const $unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)(DEFAULT_VALUE_TYPE(1.0), SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}; $(unit)=1) end # Evaluate conflicting symbols to non-symbolic form: for unit in SYMBOL_CONFLICTS - @eval const $unit = convert(RealQuantity{DEFAULT_VALUE_TYPE,SymbolicDimensions}, EagerConstants.$unit) + @eval const $unit = convert(DEFAULT_SYMBOLIC_QUANTITY_TYPE, EagerConstants.$unit) end CONSTANT_SYMBOLS_EXIST[] = true end @@ -318,7 +322,7 @@ module SymbolicUnitsParse UNIT_SYMBOLS_EXIST[] || lock(UNIT_SYMBOLS_LOCK) do UNIT_SYMBOLS_EXIST[] && return nothing for unit in UNIT_SYMBOLS - @eval const $unit = RealQuantity(DEFAULT_VALUE_TYPE(1.0), SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}; $(unit)=1) + @eval const $unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)(DEFAULT_VALUE_TYPE(1.0), SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}; $(unit)=1) end UNIT_SYMBOLS_EXIST[] = true end @@ -329,27 +333,27 @@ module SymbolicUnitsParse sym_uparse(raw_string::AbstractString) Parse a string containing an expression of units and return the - corresponding `RealQuantity` object with `Float64` value. + corresponding `Quantity` object with `Float64` value. However, that unlike the regular `u"..."` macro, this macro uses `SymbolicDimensions` for the dimension type, which means that all units and constants are stored symbolically and will not automatically expand to SI units. For example, `sym_uparse("km/s^2")` would be parsed to - `RealQuantity(1.0, SymbolicDimensions, km=1, s=-2)`. + `Quantity(1.0, SymbolicDimensions, km=1, s=-2)`. Note that inside this expression, you also have access to the `Constants` module. So, for example, `sym_uparse("Constants.c^2 * Hz^2")` would evaluate to - `RealQuantity(1.0, SymbolicDimensions, c=2, Hz=2)`. However, note that due to + `Quantity(1.0, SymbolicDimensions, c=2, Hz=2)`. However, note that due to namespace collisions, a few physical constants are automatically converted. """ function sym_uparse(raw_string::AbstractString) _generate_unit_symbols() Constants._generate_unit_symbols() raw_result = eval(Meta.parse(raw_string)) - return copy(as_quantity(raw_result))::RealQuantity{DEFAULT_VALUE_TYPE,SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}} + return copy(as_quantity(raw_result))::DEFAULT_SYMBOLIC_QUANTITY_TYPE end - as_quantity(q::RealQuantity) = q - as_quantity(x::Number) = RealQuantity(convert(DEFAULT_VALUE_TYPE, x), SymbolicDimensions{DEFAULT_DIM_BASE_TYPE}) + as_quantity(q::DEFAULT_SYMBOLIC_QUANTITY_TYPE) = q + as_quantity(x::Number) = convert(DEFAULT_SYMBOLIC_QUANTITY_TYPE, x) as_quantity(x) = error("Unexpected type evaluated: $(typeof(x))") end @@ -359,15 +363,15 @@ import .SymbolicUnitsParse: sym_uparse us"[unit expression]" Parse a string containing an expression of units and return the -corresponding `RealQuantity` object with `Float64` value. However, +corresponding `Quantity` object with `Float64` value. However, unlike the regular `u"..."` macro, this macro uses `SymbolicDimensions` for the dimension type, which means that all units and constants are stored symbolically and will not automatically expand to SI units. -For example, `us"km/s^2"` would be parsed to `RealQuantity(1.0, SymbolicDimensions, km=1, s=-2)`. +For example, `us"km/s^2"` would be parsed to `Quantity(1.0, SymbolicDimensions, km=1, s=-2)`. Note that inside this expression, you also have access to the `Constants` module. So, for example, `us"Constants.c^2 * Hz^2"` would evaluate to -`RealQuantity(1.0, SymbolicDimensions, c=2, Hz=2)`. However, note that due to +`Quantity(1.0, SymbolicDimensions, c=2, Hz=2)`. However, note that due to namespace collisions, a few physical constants are automatically converted. """ macro us_str(s) diff --git a/src/types.jl b/src/types.jl index 16eaec61..68f519b0 100644 --- a/src/types.jl +++ b/src/types.jl @@ -214,7 +214,7 @@ for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES end end -const DEFAULT_QUANTITY_TYPE = RealQuantity{DEFAULT_VALUE_TYPE, DEFAULT_DIM_TYPE} +const DEFAULT_QUANTITY_TYPE = Quantity{DEFAULT_VALUE_TYPE, DEFAULT_DIM_TYPE} @inline function new_dimensions(::Type{D}, dims...) where {D<:AbstractDimensions} return constructorof(D)(dims...) diff --git a/src/units.jl b/src/units.jl index 430dc049..81d0d3a5 100644 --- a/src/units.jl +++ b/src/units.jl @@ -3,7 +3,6 @@ module Units import ..DEFAULT_DIM_TYPE import ..DEFAULT_VALUE_TYPE import ..DEFAULT_QUANTITY_TYPE -import ..RealQuantity @assert DEFAULT_VALUE_TYPE == Float64 "`units.jl` must be updated to support a different default value type." diff --git a/src/uparse.jl b/src/uparse.jl index 90762f5d..e913d044 100644 --- a/src/uparse.jl +++ b/src/uparse.jl @@ -1,6 +1,7 @@ module UnitsParse -import ..RealQuantity +import ..constructorof +import ..DEFAULT_QUANTITY_TYPE import ..DEFAULT_DIM_TYPE import ..DEFAULT_VALUE_TYPE import ..Units: UNIT_SYMBOLS @@ -24,8 +25,8 @@ end uparse(s::AbstractString) Parse a string containing an expression of units and return the -corresponding `RealQuantity` object with `Float64` value. For example, -`uparse("m/s")` would be parsed to `RealQuantity(1.0, length=1, time=-1)`. +corresponding `Quantity` object with `Float64` value. For example, +`uparse("m/s")` would be parsed to `Quantity(1.0, length=1, time=-1)`. Note that inside this expression, you also have access to the `Constants` module. So, for example, `uparse("Constants.c^2 * Hz^2")` would evaluate to @@ -33,19 +34,19 @@ the quantity corresponding to the speed of light multiplied by Hertz, squared. """ function uparse(s::AbstractString) - return as_quantity(eval(Meta.parse(s)))::RealQuantity{DEFAULT_VALUE_TYPE,DEFAULT_DIM_TYPE} + return as_quantity(eval(Meta.parse(s)))::DEFAULT_QUANTITY_TYPE end -as_quantity(q::RealQuantity) = q -as_quantity(x::Number) = RealQuantity(convert(DEFAULT_VALUE_TYPE, x), DEFAULT_DIM_TYPE) +as_quantity(q::DEFAULT_QUANTITY_TYPE) = q +as_quantity(x::Number) = convert(DEFAULT_QUANTITY_TYPE, x) as_quantity(x) = error("Unexpected type evaluated: $(typeof(x))") """ u"[unit expression]" Parse a string containing an expression of units and return the -corresponding `RealQuantity` object with `Float64` value. For example, -`u"km/s^2"` would be parsed to `RealQuantity(1000.0, length=1, time=-2)`. +corresponding `Quantity` object with `Float64` value. For example, +`u"km/s^2"` would be parsed to `Quantity(1000.0, length=1, time=-2)`. Note that inside this expression, you also have access to the `Constants` module. So, for example, `u"Constants.c^2 * Hz^2"` would evaluate to diff --git a/test/unittests.jl b/test/unittests.jl index ea4416c8..796e6423 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -499,7 +499,7 @@ end @eval struct MyNumber <: Real x::Float64 end - a = 0.5u"km/s" + a = RealQuantity(0.5u"km/s") b = MyNumber(0.5) ar = [a, b] @test ar isa Vector{Real} @@ -704,13 +704,13 @@ end @test_throws DimensionError uconvert(us"nm * J", 5e-9u"m") # Types: - @test typeof(uconvert(us"nm", 5e-9u"m")) <: RealQuantity{Float64,<:SymbolicDimensions} + @test typeof(uconvert(us"nm", 5e-9u"m")) <: constructorof(DEFAULT_QUANTITY_TYPE){Float64,<:SymbolicDimensions} @test typeof(uconvert(us"nm", GenericQuantity(5e-9u"m"))) <: GenericQuantity{Float64,<:SymbolicDimensions} @test uconvert(GenericQuantity(us"nm"), GenericQuantity(5e-9u"m")) ≈ 5us"nm" @test uconvert(GenericQuantity(us"nm"), GenericQuantity(5e-9u"m")) ≈ GenericQuantity(5us"nm") # We only want to convert the dimensions, and ignore the quantity type: - @test typeof(uconvert(GenericQuantity(us"nm"), 5e-9u"m")) <: RealQuantity{Float64,<:SymbolicDimensions} + @test typeof(uconvert(GenericQuantity(us"nm"), 5e-9u"m")) <: constructorof(DEFAULT_QUANTITY_TYPE){Float64,<:SymbolicDimensions} q = 1.5u"Constants.M_sun" qs = uconvert(us"Constants.M_sun", 5.0 * q) @@ -803,7 +803,7 @@ end end @testset "Rational power law" begin - x = 1.0u"m" + x = RealQuantity(1.0u"m") y = x ^ (3//2) @test y == Quantity(1.0, length=3//2) @test typeof(y) == RealQuantity{Float64,DEFAULT_DIM_TYPE} @@ -1201,8 +1201,8 @@ end qy = QuantityArray(y; length=1) @test typeof(convert(typeof(qx), qy)) == typeof(qx) - @test convert(typeof(qx), qy)[1] isa RealQuantity{Float64} - @test convert(typeof(qx), qy)[1] == convert(RealQuantity{Float64}, qy[1]) + @test convert(typeof(qx), qy)[1] isa Quantity{Float64} + @test convert(typeof(qx), qy)[1] == convert(Quantity{Float64}, qy[1]) end end From 72d19eb294a3d5a155cafccdcc0fe796d2e8d6c0 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Tue, 21 Nov 2023 06:49:37 +0000 Subject: [PATCH 21/39] Cleanup docs --- src/arrays.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/arrays.jl b/src/arrays.jl index 5cd765df..b01d8ced 100644 --- a/src/arrays.jl +++ b/src/arrays.jl @@ -15,7 +15,7 @@ and so can be used in most places where a normal array would be used, including # Constructors - `QuantityArray(v::AbstractArray, d::AbstractDimensions)`: Create a `QuantityArray` with value `v` and dimensions `d`, - using `Quantity` if it is numeric, and `GenericQuantity` otherwise. + using `Quantity` if the eltype of `v` is numeric, and `GenericQuantity` otherwise. - `QuantityArray(v::AbstractArray{<:Number}, q::AbstractQuantity)`: Create a `QuantityArray` with value `v` and dimensions inferred with `dimension(q)`. This is so that you can easily create an array with the units module, like so: ```julia From 75fa9e033ac8e33ddcd8018730b186a8818ff3f8 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Tue, 21 Nov 2023 07:23:14 +0000 Subject: [PATCH 22/39] Fix coverage --- test/unittests.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unittests.jl b/test/unittests.jl index 796e6423..3745f6eb 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -205,13 +205,13 @@ end @test adjoint(ustrip(x^2)) ≈ adjoint(x^2) / u"m/s"^2 # Can create by division as well: - x = 1.0u"km/s" / (1.0 + 0.5im) + x = RealQuantity(1.0u"km/s") / (1.0 + 0.5im) @test typeof(x) == Quantity{Complex{Float64}, DEFAULT_DIM_TYPE} @test ustrip(x) ≈ 1000.0 / (1.0 + 0.5im) @test ulength(x) == 1.0 @test utime(x) == -1.0 - x = (1.0 + 0.5im) / (1.0u"km/s") + x = (1.0 + 0.5im) / RealQuantity(1.0u"km/s") @test typeof(x) == Quantity{Complex{Float64}, DEFAULT_DIM_TYPE} @test ustrip(x) ≈ (1.0 + 0.5im) / 1000.0 @test ulength(x) == -1.0 From bc1a90bf3c1ffe622a6ef5c7caf73e23a66f1f18 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Tue, 21 Nov 2023 19:04:47 +0000 Subject: [PATCH 23/39] Export `AbstractRealQuantity` --- src/DynamicQuantities.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DynamicQuantities.jl b/src/DynamicQuantities.jl index a78cb026..bd0e8d8b 100644 --- a/src/DynamicQuantities.jl +++ b/src/DynamicQuantities.jl @@ -1,7 +1,7 @@ module DynamicQuantities export Units, Constants -export AbstractDimensions, AbstractQuantity, AbstractGenericQuantity, UnionAbstractQuantity +export AbstractDimensions, AbstractQuantity, AbstractGenericQuantity, AbstractRealQuantity, UnionAbstractQuantity export Quantity, GenericQuantity, RealQuantity, Dimensions, SymbolicDimensions, QuantityArray, DimensionError export ustrip, dimension export ulength, umass, utime, ucurrent, utemperature, uluminosity, uamount From 2694747a07a87f3e54350036be020b45dda8fc76 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Tue, 21 Nov 2023 19:05:01 +0000 Subject: [PATCH 24/39] Proper definition of `mod` and `rem` --- src/math.jl | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/src/math.jl b/src/math.jl index 6877606a..cf95eda5 100644 --- a/src/math.jl +++ b/src/math.jl @@ -76,7 +76,7 @@ end Base.:-(l::UnionAbstractQuantity) = new_quantity(typeof(l), -ustrip(l), dimension(l)) # Combining different abstract types -for op in (:*, :/, :+, :-, :atan, :atand, :copysign, :flipsign, :mod), +for op in (:*, :/, :+, :-, :atan, :atand, :copysign, :flipsign), (t1, _, _) in ABSTRACT_QUANTITY_TYPES, (t2, _, _) in ABSTRACT_QUANTITY_TYPES @@ -193,7 +193,7 @@ for f in ( return new_quantity(typeof(q), $f(ustrip(q)), dimension(q)) end end -for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES, f in (:copysign, :flipsign, :mod) +for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES, f in (:copysign, :flipsign,) # These treat the x as the magnitude, so we take the dimensions from there, # and ignore any dimensions on y, since those will cancel out. @eval begin @@ -209,6 +209,41 @@ for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES, f in (:copysign, :flipsign, end end end +for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES, f in (:rem, :mod) + # Need to define all rounding modes to avoid ambiguities + rounding_modes = if f == :rem + (RoundingMode, typeof.((RoundToZero, RoundDown, RoundUp, RoundFromZero))...) + else + (nothing,) + end + for rounding_mode in rounding_modes + param, extra_f_args = if rounding_mode == RoundingMode + # Add default: + ((), (:RoundToZero,)) + elseif f == :rem + ((:(::$rounding_mode),), (:($rounding_mode()),)) + else # :mod + ((), ()) + end + for (type2, _, _) in ABSTRACT_QUANTITY_TYPES + @eval function Base.$f(x::$type, y::$type2, $(param...)) + x, y = promote_except_value(x, y) + dimension(x) == dimension(y) || throw(DimensionError(x, y)) + return new_quantity(typeof(x), $f(ustrip(x), ustrip(y), $(extra_f_args...)), dimension(x)) + end + end + @eval begin + function Base.$f(x::$type, y::$base_type, $(param...)) + iszero(dimension(x)) || throw(DimensionError(x)) + return new_quantity(typeof(x), $f(ustrip(x), y, $(extra_f_args...)), dimension(x)) + end + function Base.$f(x::$base_type, y::$type, $(param...)) + iszero(dimension(y)) || throw(DimensionError(y)) + return new_quantity(typeof(y), $f(x, ustrip(y), $(extra_f_args...)), dimension(y)) + end + end + end +end function Base.ldexp(x::UnionAbstractQuantity, n::Integer) return new_quantity(typeof(x), ldexp(ustrip(x), n), dimension(x)) end From 8c24478b3cdb5f30c80a043c06350891a2611407 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Tue, 21 Nov 2023 19:05:20 +0000 Subject: [PATCH 25/39] Clean up conversion to numbers --- src/disambiguities.jl | 12 ++++++++++++ src/utils.jl | 32 ++++++++++++++++++++++---------- test/unittests.jl | 6 +++--- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/disambiguities.jl b/src/disambiguities.jl index ff40f30a..17c6f240 100644 --- a/src/disambiguities.jl +++ b/src/disambiguities.jl @@ -47,6 +47,18 @@ end # Assorted calls found by Aqua: ################################################ ################################################################################ +function Complex(q::AbstractRealQuantity) + @assert iszero(dimension(q)) "$(typeof(q)): $(q) has dimensions! Use `ustrip` instead." + return Complex(ustrip(q)) +end +function Complex{T}(q::AbstractRealQuantity) where {T<:Real} + @assert iszero(dimension(q)) "$(typeof(q)): $(q) has dimensions! Use `ustrip` instead." + return Complex{T}(ustrip(q)) +end +function Bool(q::AbstractRealQuantity) + @assert iszero(dimension(q)) "$(typeof(q)): $(q) has dimensions! Use `ustrip` instead." + return Bool(ustrip(q)) +end for type in (Signed, Float64, Float32, Rational), op in (:flipsign, :copysign) @eval function Base.$(op)(x::$type, y::AbstractRealQuantity) return $(op)(x, ustrip(y)) diff --git a/src/utils.jl b/src/utils.jl index 088ae06e..da149aa3 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -25,13 +25,6 @@ end return output end -for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES - @eval Base.convert(::Type{$base_type}, q::$type) = q -end -function Base.convert(::Type{T}, q::UnionAbstractQuantity) where {T<:Number} - @assert iszero(dimension(q)) "$(typeof(q)): $(q) has dimensions! Use `ustrip` instead." - return convert(T, ustrip(q)) -end function Base.promote_rule(::Type{Dimensions{R1}}, ::Type{Dimensions{R2}}) where {R1,R2} return Dimensions{promote_type(R1,R2)} end @@ -110,6 +103,21 @@ for (type, _, _) in ABSTRACT_QUANTITY_TYPES end end +for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES + @eval begin + function (::Type{T})(q::$type) where {T<:Number} + @assert iszero(dimension(q)) "$(typeof(q)): $(q) has dimensions! Use `ustrip` instead." + return convert(T, ustrip(q)) + end + function Base.convert(::Type{T}, q::$type) where {T<:Number} + return T(q) + end + function Base.convert(::Type{$base_type}, q::$type) + return q + end + end +end + """ promote_except_value(q1::UnionAbstractQuantity, q2::UnionAbstractQuantity) @@ -298,9 +306,13 @@ 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.showerror(io::IO, e::DimensionError{<:Any,Nothing}) = print(io, "DimensionError: ", e.q1, " is not dimensionless") -Base.convert(::Type{Q}, q::UnionAbstractQuantity) where {Q<:UnionAbstractQuantity} = q -Base.convert(::Type{Q}, q::UnionAbstractQuantity) where {T,Q<:UnionAbstractQuantity{T}} = new_quantity(Q, convert(T, ustrip(q)), dimension(q)) -Base.convert(::Type{Q}, q::UnionAbstractQuantity) where {T,D,Q<:UnionAbstractQuantity{T,D}} = new_quantity(Q, convert(T, ustrip(q)), convert(D, dimension(q))) +for (type, _, _) in ABSTRACT_QUANTITY_TYPES + @eval begin + Base.convert(::Type{Q}, q::$type) where {Q<:UnionAbstractQuantity} = q + Base.convert(::Type{Q}, q::$type) where {T,Q<:UnionAbstractQuantity{T}} = new_quantity(Q, convert(T, ustrip(q)), dimension(q)) + Base.convert(::Type{Q}, q::$type) where {T,D,Q<:UnionAbstractQuantity{T,D}} = new_quantity(Q, convert(T, ustrip(q)), convert(D, dimension(q))) + end +end Base.convert(::Type{D}, d::AbstractDimensions) where {D<:AbstractDimensions} = d Base.convert(::Type{D}, d::AbstractDimensions) where {R,D<:AbstractDimensions{R}} = D(d) diff --git a/test/unittests.jl b/test/unittests.jl index 3745f6eb..a0a8bee4 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -1258,9 +1258,9 @@ end functions = ( :float, :abs, :real, :imag, :conj, :adjoint, :unsigned, :nextfloat, :prevfloat, :identity, :transpose, - :copysign, :flipsign, :mod, :modf, + :copysign, :flipsign, :modf, :floor, :trunc, :ceil, :significand, - :ldexp, :round, + :ldexp, :round, # :mod ) for Q in (RealQuantity, Quantity, GenericQuantity), D in (Dimensions, SymbolicDimensions), f in functions T = f in (:abs, :real, :imag, :conj) ? ComplexF64 : Float64 @@ -1274,7 +1274,7 @@ end @eval @test $f($qx_dimensions)[$i] == $Q($f($x)[$i], $dim) end end - elseif f in (:copysign, :flipsign, :rem, :mod) # Functions that need multiple inputs + elseif f in (:copysign, :flipsign, :rem) # Functions that need multiple inputs for x in 5rand(T, 3) .- 2.5 for y in 5rand(T, 3) .- 2.5 dim = convert(D, dimension(u"m/s")) From 807bd213dd4abfaa22ef6b1b56592d275451859d Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 23 Nov 2023 14:52:20 +0000 Subject: [PATCH 26/39] Refactor div definition --- src/math.jl | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/math.jl b/src/math.jl index cf95eda5..27e18388 100644 --- a/src/math.jl +++ b/src/math.jl @@ -76,20 +76,17 @@ end Base.:-(l::UnionAbstractQuantity) = new_quantity(typeof(l), -ustrip(l), dimension(l)) # Combining different abstract types -for op in (:*, :/, :+, :-, :atan, :atand, :copysign, :flipsign), +for op in (:*, :/, :+, :-, :atan, :atand, :copysign, :flipsign, :div), (t1, _, _) in ABSTRACT_QUANTITY_TYPES, (t2, _, _) in ABSTRACT_QUANTITY_TYPES t1 == t2 && continue - @eval Base.$op(l::$t1, r::$t2) = $op(promote_except_value(l, r)...) -end -# different methods needed: -for (t1, _, _) in ABSTRACT_QUANTITY_TYPES, (t2, _, _) in ABSTRACT_QUANTITY_TYPES - - t1 == t2 && continue - - @eval Base.div(x::$t1, y::$t2, r::RoundingMode=RoundToZero) = div(promote_except_value(x, y)..., r) + if op == :div + @eval Base.$op(x::$t1, y::$t2, r::RoundingMode=RoundToZero) = $op(promote_except_value(x, y)..., r) + else + @eval Base.$op(l::$t1, r::$t2) = $op(promote_except_value(l, r)...) + end end # We don't promote on the dimension types: @@ -211,11 +208,7 @@ for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES, f in (:copysign, :flipsign, end for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES, f in (:rem, :mod) # Need to define all rounding modes to avoid ambiguities - rounding_modes = if f == :rem - (RoundingMode, typeof.((RoundToZero, RoundDown, RoundUp, RoundFromZero))...) - else - (nothing,) - end + rounding_modes = f == :rem ? (RoundingMode, typeof.((RoundToZero, RoundDown, RoundUp, RoundFromZero))...) : (nothing,) for rounding_mode in rounding_modes param, extra_f_args = if rounding_mode == RoundingMode # Add default: From 7d9aad8ad0f9dd1aad0833eab914136ea799f61b Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 23 Nov 2023 14:52:27 +0000 Subject: [PATCH 27/39] Add unittest for ranges --- test/unittests.jl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/unittests.jl b/test/unittests.jl index a0a8bee4..56dfad6f 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -189,6 +189,12 @@ end end +@testset "Ranges" begin + x = [xi for xi in 0.0u"km/s":0.1u"km/s":1.0u"km/s"] + @test x[2] == 0.1u"km/s" + @test x[end] == 1.0u"km/s" +end + @testset "Complex numbers" begin x = (0.5 + 0.6im) * u"km/s" @test string(x) == "(500.0 + 600.0im) m s⁻¹" From 8eb83f7546d6be0ecca7c946b60f860df757b2ff Mon Sep 17 00:00:00 2001 From: Miles Cranmer Date: Thu, 23 Nov 2023 14:55:01 +0000 Subject: [PATCH 28/39] Use `===` instead of `==` Co-authored-by: Gaurav Arya --- src/math.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/math.jl b/src/math.jl index 27e18388..460e5dc1 100644 --- a/src/math.jl +++ b/src/math.jl @@ -1,5 +1,5 @@ for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES - div_base_type = type == AbstractGenericQuantity ? Number : base_type + div_base_type = type === AbstractGenericQuantity ? Number : base_type @eval begin function Base.:*(l::$type, r::$type) l, r = promote_except_value(l, r) From 88ea317395c43663d22661a9a3c98baee2d187d0 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 23 Nov 2023 15:03:00 +0000 Subject: [PATCH 29/39] Refactor disambiguities --- src/disambiguities.jl | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/disambiguities.jl b/src/disambiguities.jl index 17c6f240..6eb75bd9 100644 --- a/src/disambiguities.jl +++ b/src/disambiguities.jl @@ -64,11 +64,15 @@ for type in (Signed, Float64, Float32, Rational), op in (:flipsign, :copysign) return $(op)(x, ustrip(y)) end end -function Base.:*(l::Complex, r::AbstractRealQuantity) - new_quantity(typeof(r), l * ustrip(r), dimension(r)) -end -function Base.:*(l::AbstractRealQuantity, r::Complex) - new_quantity(typeof(l), ustrip(l) * r, dimension(l)) +for type in (Complex, Complex{Bool}) + @eval begin + function Base.:*(l::$type, r::AbstractRealQuantity) + new_quantity(typeof(r), l * ustrip(r), dimension(r)) + end + function Base.:*(l::AbstractRealQuantity, r::$type) + new_quantity(typeof(l), ustrip(l) * r, dimension(l)) + end + end end function Base.:/(l::Complex, r::AbstractRealQuantity) new_quantity(typeof(r), l / ustrip(r), inv(dimension(r))) @@ -76,12 +80,6 @@ end function Base.:/(l::AbstractRealQuantity, r::Complex) new_quantity(typeof(l), ustrip(l) / r, dimension(l)) end -function Base.:*(l::Complex{Bool}, r::AbstractRealQuantity) - return new_quantity(typeof(r), l * ustrip(r), dimension(r)) -end -function Base.:*(l::AbstractRealQuantity, r::Complex{Bool}) - return new_quantity(typeof(l), ustrip(l) * r, dimension(l)) -end for op in (:(==), :isequal), base_type in (AbstractIrrational, AbstractFloat) @eval begin function Base.$(op)(l::AbstractRealQuantity, r::$base_type) From 5b2b067c4e25cac2031d2d7d352686f82a9ca428 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 23 Nov 2023 15:42:07 +0000 Subject: [PATCH 30/39] Split up `rem`/`mod` definitions --- src/math.jl | 79 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 29 deletions(-) diff --git a/src/math.jl b/src/math.jl index 460e5dc1..c3e963a1 100644 --- a/src/math.jl +++ b/src/math.jl @@ -1,5 +1,6 @@ for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES - div_base_type = type === AbstractGenericQuantity ? Number : base_type + # For div, we don't want to go more generic than `Number` + div_base_type = base_type <: Number ? base_type : Number @eval begin function Base.:*(l::$type, r::$type) l, r = promote_except_value(l, r) @@ -206,34 +207,54 @@ for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES, f in (:copysign, :flipsign, end end end -for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES, f in (:rem, :mod) - # Need to define all rounding modes to avoid ambiguities - rounding_modes = f == :rem ? (RoundingMode, typeof.((RoundToZero, RoundDown, RoundUp, RoundFromZero))...) : (nothing,) - for rounding_mode in rounding_modes - param, extra_f_args = if rounding_mode == RoundingMode - # Add default: - ((), (:RoundToZero,)) - elseif f == :rem - ((:(::$rounding_mode),), (:($rounding_mode()),)) - else # :mod - ((), ()) - end - for (type2, _, _) in ABSTRACT_QUANTITY_TYPES - @eval function Base.$f(x::$type, y::$type2, $(param...)) - x, y = promote_except_value(x, y) - dimension(x) == dimension(y) || throw(DimensionError(x, y)) - return new_quantity(typeof(x), $f(ustrip(x), ustrip(y), $(extra_f_args...)), dimension(x)) - end - end - @eval begin - function Base.$f(x::$type, y::$base_type, $(param...)) - iszero(dimension(x)) || throw(DimensionError(x)) - return new_quantity(typeof(x), $f(ustrip(x), y, $(extra_f_args...)), dimension(x)) - end - function Base.$f(x::$base_type, y::$type, $(param...)) - iszero(dimension(y)) || throw(DimensionError(y)) - return new_quantity(typeof(y), $f(x, ustrip(y), $(extra_f_args...)), dimension(y)) - end +# Define :rem +for (type, _base_type, _) in ABSTRACT_QUANTITY_TYPES, rounding_mode in (RoundingMode, RoundingMode{:ToZero}, RoundingMode{:Down}, RoundingMode{:Up}, RoundingMode{:FromZero}) + + # We don't want to go more generic than `Number` for mod and rem + base_type = _base_type <: Number ? _base_type : Number + # Add extra args: + param = rounding_mode === RoundingMode ? (()) : (:(::$rounding_mode),) + extra_f_args = rounding_mode === RoundingMode ? (:RoundToZero,) : (:($rounding_mode()),) + + for (type2, _, _) in ABSTRACT_QUANTITY_TYPES + @eval function Base.rem(x::$type, y::$type2, $(param...)) + x, y = promote_except_value(x, y) + dimension(x) == dimension(y) || throw(DimensionError(x, y)) + return new_quantity(typeof(x), rem(ustrip(x), ustrip(y), $(extra_f_args...)), dimension(x)) + end + end + @eval begin + function Base.rem(x::$type, y::$base_type, $(param...)) + iszero(dimension(x)) || throw(DimensionError(x)) + return new_quantity(typeof(x), rem(ustrip(x), y, $(extra_f_args...)), dimension(x)) + end + function Base.rem(x::$base_type, y::$type, $(param...)) + iszero(dimension(y)) || throw(DimensionError(y)) + return new_quantity(typeof(y), rem(x, ustrip(y), $(extra_f_args...)), dimension(y)) + end + end +end +# Define :mod +for (type, _base_type, _) in ABSTRACT_QUANTITY_TYPES + + # We don't want to go more generic than `Number` for mod and rem + base_type = _base_type <: Number ? _base_type : Number + + for (type2, _, _) in ABSTRACT_QUANTITY_TYPES + @eval function Base.mod(x::$type, y::$type2) + x, y = promote_except_value(x, y) + dimension(x) == dimension(y) || throw(DimensionError(x, y)) + return new_quantity(typeof(x), mod(ustrip(x), ustrip(y)), dimension(x)) + end + end + @eval begin + function Base.mod(x::$type, y::$base_type) + iszero(dimension(x)) || throw(DimensionError(x)) + return new_quantity(typeof(x), mod(ustrip(x), y), dimension(x)) + end + function Base.mod(x::$base_type, y::$type) + iszero(dimension(y)) || throw(DimensionError(y)) + return new_quantity(typeof(y), mod(x, ustrip(y)), dimension(y)) end end end From cd339b66e132947cef3554e99f41429193aaaaa0 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 23 Nov 2023 16:32:57 +0000 Subject: [PATCH 31/39] Reduce invalidations --- src/math.jl | 17 +++++++++++++---- src/utils.jl | 22 +++++++++++++--------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/math.jl b/src/math.jl index c3e963a1..48fe5bab 100644 --- a/src/math.jl +++ b/src/math.jl @@ -50,6 +50,15 @@ for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES new_quantity(typeof(r), inv(ustrip(r)), l / dimension(r)) end end + # Comparison with exact value type, in case we had to artificially limit it to Number + !(base_type <: Number) && @eval begin + function Base.div(x::$type{T}, y::T, r::RoundingMode=RoundToZero) where {T<:$base_type} + new_quantity(typeof(x), div(ustrip(x), y, r), dimension(x)) + end + function Base.div(x::T, y::$type{T}, r::RoundingMode=RoundToZero) where {T<:$base_type} + new_quantity(typeof(y), div(x, ustrip(y), r), inv(dimension(y))) + end + end end Base.:*(l::AbstractDimensions, r::AbstractDimensions) = map_dimensions(+, l, r) @@ -208,10 +217,10 @@ for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES, f in (:copysign, :flipsign, end end # Define :rem -for (type, _base_type, _) in ABSTRACT_QUANTITY_TYPES, rounding_mode in (RoundingMode, RoundingMode{:ToZero}, RoundingMode{:Down}, RoundingMode{:Up}, RoundingMode{:FromZero}) +for (type, true_base_type, _) in ABSTRACT_QUANTITY_TYPES, rounding_mode in (RoundingMode, RoundingMode{:ToZero}, RoundingMode{:Down}, RoundingMode{:Up}, RoundingMode{:FromZero}) # We don't want to go more generic than `Number` for mod and rem - base_type = _base_type <: Number ? _base_type : Number + base_type = true_base_type <: Number ? true_base_type : Number # Add extra args: param = rounding_mode === RoundingMode ? (()) : (:(::$rounding_mode),) extra_f_args = rounding_mode === RoundingMode ? (:RoundToZero,) : (:($rounding_mode()),) @@ -235,10 +244,10 @@ for (type, _base_type, _) in ABSTRACT_QUANTITY_TYPES, rounding_mode in (Rounding end end # Define :mod -for (type, _base_type, _) in ABSTRACT_QUANTITY_TYPES +for (type, true_base_type, _) in ABSTRACT_QUANTITY_TYPES # We don't want to go more generic than `Number` for mod and rem - base_type = _base_type <: Number ? _base_type : Number + base_type = true_base_type <: Number ? true_base_type : Number for (type2, _, _) in ABSTRACT_QUANTITY_TYPES @eval function Base.mod(x::$type, y::$type2) diff --git a/src/utils.jl b/src/utils.jl index da149aa3..ea4f7480 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -167,9 +167,9 @@ Base.keys(q::UnionAbstractQuantity) = keys(ustrip(q)) # Numeric checks -for op in (:(<=), :(<), :(>=), :(>), :isless), - (type, base_type, _) in ABSTRACT_QUANTITY_TYPES - +for op in (:(<=), :(<), :(>=), :(>), :isless), (type, true_base_type, _) in ABSTRACT_QUANTITY_TYPES + # Avoid creating overly generic operations on these: + base_type = true_base_type <: Number ? true_base_type : Number @eval begin function Base.$(op)(l::$type, r::$type) l, r = promote_except_value(l, r) @@ -186,7 +186,9 @@ for op in (:(<=), :(<), :(>=), :(>), :isless), end end end -for op in (:isequal, :(==)), (type, base_type, _) in ABSTRACT_QUANTITY_TYPES +for op in (:isequal, :(==)), (type, true_base_type, _) in ABSTRACT_QUANTITY_TYPES + # Avoid creating overly generic operations on these: + base_type = true_base_type <: Number ? true_base_type : Number @eval begin function Base.$(op)(l::$type, r::$type) l, r = promote_except_value(l, r) @@ -211,7 +213,9 @@ for op in (:(<=), :(<), :(>=), :(>), :isless, :isgreater, :isequal, :(==)), end end # Define isapprox: -for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES +for (type, true_base_type, _) in ABSTRACT_QUANTITY_TYPES + # Avoid creating overly generic operations on these: + base_type = true_base_type <: Number ? true_base_type : Number @eval begin function Base.isapprox(l::$type, r::$type; kws...) dimension(l) == dimension(r) || throw(DimensionError(l, r)) @@ -306,11 +310,11 @@ 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.showerror(io::IO, e::DimensionError{<:Any,Nothing}) = print(io, "DimensionError: ", e.q1, " is not dimensionless") -for (type, _, _) in ABSTRACT_QUANTITY_TYPES +for (type, _, _) in ABSTRACT_QUANTITY_TYPES, (type2, _, _) in ABSTRACT_QUANTITY_TYPES @eval begin - Base.convert(::Type{Q}, q::$type) where {Q<:UnionAbstractQuantity} = q - Base.convert(::Type{Q}, q::$type) where {T,Q<:UnionAbstractQuantity{T}} = new_quantity(Q, convert(T, ustrip(q)), dimension(q)) - Base.convert(::Type{Q}, q::$type) where {T,D,Q<:UnionAbstractQuantity{T,D}} = new_quantity(Q, convert(T, ustrip(q)), convert(D, dimension(q))) + Base.convert(::Type{Q}, q::$type) where {Q<:$type2} = q + Base.convert(::Type{Q}, q::$type) where {T,Q<:$type2{T}} = new_quantity(Q, convert(T, ustrip(q)), dimension(q)) + Base.convert(::Type{Q}, q::$type) where {T,D,Q<:$type2{T,D}} = new_quantity(Q, convert(T, ustrip(q)), convert(D, dimension(q))) end end From 4786b65be14a7ecd70c6ddcbed7921eb5a84d7ca Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 23 Nov 2023 16:59:46 +0000 Subject: [PATCH 32/39] Expand measurements testing --- src/utils.jl | 12 +++++++++--- test/test_measurements.jl | 10 ++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index ea4f7480..a5eade8c 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -89,11 +89,13 @@ const BASE_NUMERIC_TYPES = Union{ Rational{BigInt}, } -for (type, _, _) in ABSTRACT_QUANTITY_TYPES - @eval begin +for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES + !(base_type <: Number) && @eval begin function Base.convert(::Type{Q}, x::BASE_NUMERIC_TYPES) where {T,D,Q<:$type{T,D}} return new_quantity(Q, convert(T, x), D()) end + end + @eval begin function Base.promote_rule(::Type{Q}, ::Type{T2}) where {T,D,Q<:$type{T,D},T2<:BASE_NUMERIC_TYPES} return with_type_parameters(promote_quantity_on_value(Q, T2), promote_type(T, T2), D) end @@ -268,7 +270,7 @@ end Base.one(::Type{D}) where {D<:AbstractDimensions} = D() Base.one(::D) where {D<:AbstractDimensions} = one(D) -# Additive identities (zero) +# Additive identities (zero). We have to invalidate these due to different behavior with conversion Base.zero(q::Q) where {Q<:UnionAbstractQuantity} = 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 `UnionAbstractQuantity`.") Base.zero(::Type{<:UnionAbstractQuantity}) = error("Cannot create an additive identity for a `UnionAbstractQuantity` type, as the dimensions are unknown. Please use `zero(::UnionAbstractQuantity)` instead.") @@ -316,6 +318,10 @@ for (type, _, _) in ABSTRACT_QUANTITY_TYPES, (type2, _, _) in ABSTRACT_QUANTITY_ Base.convert(::Type{Q}, q::$type) where {T,Q<:$type2{T}} = new_quantity(Q, convert(T, ustrip(q)), dimension(q)) Base.convert(::Type{Q}, q::$type) where {T,D,Q<:$type2{T,D}} = new_quantity(Q, convert(T, ustrip(q)), convert(D, dimension(q))) end + # TODO: This invalidates some methods. But we have to, because + # the conversion in `number.jl` has a type assertion step, whereas + # we want to allow things like `convert(Quantity{Float64}, 1.0u"m")`, + # with the type for the dimensions being inferred. end Base.convert(::Type{D}, d::AbstractDimensions) where {D<:AbstractDimensions} = d diff --git a/test/test_measurements.jl b/test/test_measurements.jl index a02ab642..b76bc809 100644 --- a/test/test_measurements.jl +++ b/test/test_measurements.jl @@ -2,7 +2,7 @@ using DynamicQuantities using Measurements using Measurements: value, uncertainty -for Q in (Quantity, GenericQuantity) +for Q in (RealQuantity, Quantity, GenericQuantity) x = Q(1.0u"m/s") ± Q(0.1u"m/s") @test ustrip(x^2) == ustrip(x)^2 @@ -11,7 +11,9 @@ for Q in (Quantity, GenericQuantity) @test dimension(x)^2 == dimension(x^2) @test_throws DimensionError 0.5u"m" ± 0.1u"s" - # Mixed types: - y = Q{Float16}(0.1u"m/s") ± Q{Float32}(0.1u"m/s") - @test typeof(y) <: Q{Measurement{Float32}} + if Q in (Quantity, GenericQuantity) + # Mixed types: + y = Q{Float16}(0.1u"m/s") ± Q{Float32}(0.1u"m/s") + @test typeof(y) <: Q{Measurement{Float32}} + end end From 466c907d98b3dadcd5bb77619898bf298ffc8dec Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 23 Nov 2023 17:00:53 +0000 Subject: [PATCH 33/39] Clean up method invalidations --- src/utils.jl | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index a5eade8c..2cb32ac1 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -105,18 +105,13 @@ for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES end end -for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES +for (type, _, _) in ABSTRACT_QUANTITY_TYPES @eval begin function (::Type{T})(q::$type) where {T<:Number} + q isa T && return q @assert iszero(dimension(q)) "$(typeof(q)): $(q) has dimensions! Use `ustrip` instead." return convert(T, ustrip(q)) end - function Base.convert(::Type{T}, q::$type) where {T<:Number} - return T(q) - end - function Base.convert(::Type{$base_type}, q::$type) - return q - end end end From b05cf1db951040ed606cf5a54c3fa090b3834aa5 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 23 Nov 2023 17:12:49 +0000 Subject: [PATCH 34/39] Delete useless `div` methods --- src/math.jl | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/math.jl b/src/math.jl index 48fe5bab..96bb150a 100644 --- a/src/math.jl +++ b/src/math.jl @@ -50,15 +50,6 @@ for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES new_quantity(typeof(r), inv(ustrip(r)), l / dimension(r)) end end - # Comparison with exact value type, in case we had to artificially limit it to Number - !(base_type <: Number) && @eval begin - function Base.div(x::$type{T}, y::T, r::RoundingMode=RoundToZero) where {T<:$base_type} - new_quantity(typeof(x), div(ustrip(x), y, r), dimension(x)) - end - function Base.div(x::T, y::$type{T}, r::RoundingMode=RoundToZero) where {T<:$base_type} - new_quantity(typeof(y), div(x, ustrip(y), r), inv(dimension(y))) - end - end end Base.:*(l::AbstractDimensions, r::AbstractDimensions) = map_dimensions(+, l, r) @@ -216,7 +207,7 @@ for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES, f in (:copysign, :flipsign, end end end -# Define :rem +# Define :rem (unfortunately we have to create a method for each rounding mode to avoid ambiguity) for (type, true_base_type, _) in ABSTRACT_QUANTITY_TYPES, rounding_mode in (RoundingMode, RoundingMode{:ToZero}, RoundingMode{:Down}, RoundingMode{:Up}, RoundingMode{:FromZero}) # We don't want to go more generic than `Number` for mod and rem From 80cd5d34c44f20a1842978401c415754a4d768dd Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 23 Nov 2023 17:18:07 +0000 Subject: [PATCH 35/39] Refactor definition of `mod` --- src/math.jl | 30 ++++-------------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/src/math.jl b/src/math.jl index 96bb150a..53db2d5c 100644 --- a/src/math.jl +++ b/src/math.jl @@ -56,7 +56,9 @@ Base.:*(l::AbstractDimensions, r::AbstractDimensions) = map_dimensions(+, l, r) Base.:/(l::AbstractDimensions, r::AbstractDimensions) = map_dimensions(-, l, r) # Defines + and - -for (type, base_type, _) in ABSTRACT_QUANTITY_TYPES, op in (:+, :-) +for (type, true_base_type, _) in ABSTRACT_QUANTITY_TYPES, op in (:+, :-, :mod) + # Only define `mod` on `Number` types: + base_type = (op == :mod && !(true_base_type <: Number)) ? Number : true_base_type @eval begin function Base.$op(l::$type, r::$type) l, r = promote_except_value(l, r) @@ -77,7 +79,7 @@ end Base.:-(l::UnionAbstractQuantity) = new_quantity(typeof(l), -ustrip(l), dimension(l)) # Combining different abstract types -for op in (:*, :/, :+, :-, :atan, :atand, :copysign, :flipsign, :div), +for op in (:*, :/, :+, :-, :atan, :atand, :copysign, :flipsign, :div, :mod), (t1, _, _) in ABSTRACT_QUANTITY_TYPES, (t2, _, _) in ABSTRACT_QUANTITY_TYPES @@ -234,30 +236,6 @@ for (type, true_base_type, _) in ABSTRACT_QUANTITY_TYPES, rounding_mode in (Roun end end end -# Define :mod -for (type, true_base_type, _) in ABSTRACT_QUANTITY_TYPES - - # We don't want to go more generic than `Number` for mod and rem - base_type = true_base_type <: Number ? true_base_type : Number - - for (type2, _, _) in ABSTRACT_QUANTITY_TYPES - @eval function Base.mod(x::$type, y::$type2) - x, y = promote_except_value(x, y) - dimension(x) == dimension(y) || throw(DimensionError(x, y)) - return new_quantity(typeof(x), mod(ustrip(x), ustrip(y)), dimension(x)) - end - end - @eval begin - function Base.mod(x::$type, y::$base_type) - iszero(dimension(x)) || throw(DimensionError(x)) - return new_quantity(typeof(x), mod(ustrip(x), y), dimension(x)) - end - function Base.mod(x::$base_type, y::$type) - iszero(dimension(y)) || throw(DimensionError(y)) - return new_quantity(typeof(y), mod(x, ustrip(y)), dimension(y)) - end - end -end function Base.ldexp(x::UnionAbstractQuantity, n::Integer) return new_quantity(typeof(x), ldexp(ustrip(x), n), dimension(x)) end From 7999158c332c1b8f8c889679f0f9db3c3f261070 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 23 Nov 2023 17:36:35 +0000 Subject: [PATCH 36/39] Improve test coverage --- src/math.jl | 2 +- test/unittests.jl | 40 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/math.jl b/src/math.jl index 53db2d5c..67608330 100644 --- a/src/math.jl +++ b/src/math.jl @@ -55,7 +55,7 @@ end Base.:*(l::AbstractDimensions, r::AbstractDimensions) = map_dimensions(+, l, r) Base.:/(l::AbstractDimensions, r::AbstractDimensions) = map_dimensions(-, l, r) -# Defines + and - +# Defines +, -, and mod for (type, true_base_type, _) in ABSTRACT_QUANTITY_TYPES, op in (:+, :-, :mod) # Only define `mod` on `Number` types: base_type = (op == :mod && !(true_base_type <: Number)) ? Number : true_base_type diff --git a/test/unittests.jl b/test/unittests.jl index 56dfad6f..b159f3b9 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -847,6 +847,26 @@ end out = x ^ (1 + 2im) @test typeof(out) == with_type_parameters(promote_quantity_on_value(Q, ComplexF64), Complex{Float64}, DEFAULT_DIM_TYPE) @test ustrip(out) ≈ 0.5 ^ (1 + 2im) + + for CT in (Complex, Complex{Float32}) + x = Q(1.0) + @test CT(x) == CT(1.0) + @test typeof(CT(x)) <: CT + x = Q(1.0, length=1) + @test_throws AssertionError CT(x) + end + end + end + + @testset "Bool" begin + for Q in (RealQuantity, Quantity, GenericQuantity) + x = Q(1.0u"1") + @test Bool(x) == true + @test Bool(ustrip(x)) == true + @test Bool(Q(0.0u"m")) == false + @test Bool(ustrip(Q(0.0u"m"))) == false + x = Q(1.0u"m") + @test_throws AssertionError Bool(x) end end end @@ -1266,7 +1286,7 @@ end :nextfloat, :prevfloat, :identity, :transpose, :copysign, :flipsign, :modf, :floor, :trunc, :ceil, :significand, - :ldexp, :round, # :mod + :ldexp, :round, :mod, :rem ) for Q in (RealQuantity, Quantity, GenericQuantity), D in (Dimensions, SymbolicDimensions), f in functions T = f in (:abs, :real, :imag, :conj) ? ComplexF64 : Float64 @@ -1280,17 +1300,31 @@ end @eval @test $f($qx_dimensions)[$i] == $Q($f($x)[$i], $dim) end end - elseif f in (:copysign, :flipsign, :rem) # Functions that need multiple inputs + elseif f in (:copysign, :flipsign, :rem, :mod) # Functions that need multiple inputs for x in 5rand(T, 3) .- 2.5 for y in 5rand(T, 3) .- 2.5 dim = convert(D, dimension(u"m/s")) qx_dimensions = Q(x, dim) qy_dimensions = Q(y, dim) @eval @test $f($qx_dimensions, $qy_dimensions) == $Q($f($x, $y), $dim) - if f in (:copysign, :flipsign, :mod) + if f in (:copysign, :flipsign) # Also do test without dimensions @eval @test $f($x, $qy_dimensions) == $f($x, $y) @eval @test $f($qx_dimensions, $y) == $Q($f($x, $y), $dim) + elseif f in (:rem, :mod) + # Also do test without dimensions (need dimensionless) + qx_dimensionless = Q(x, D) + qy_dimensionless = Q(y, D) + @eval @test $f($x, $qy_dimensionless) == $Q($f($x, $y), $D) + @eval @test $f($qx_dimensionless, $y) == $Q($f($x, $y), $D) + @eval @test_throws DimensionError $f($qx_dimensions, $y) + @eval @test_throws DimensionError $f($x, $qy_dimensions) + if f == :rem + # Can also do other rounding modes + for r in (:RoundFromZero, :RoundNearest, :RoundUp, :RoundDown) + @eval @test $f($qx_dimensions, $qy_dimensions, $r) == $Q($f($x, $y, $r), $dim) + end + end end end end From 50e3f2d57db15b5c7bb906c672c8b4330a5c62a7 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 23 Nov 2023 17:40:52 +0000 Subject: [PATCH 37/39] Refactor disambiguities --- src/disambiguities.jl | 22 +++++++++------------- test/unittests.jl | 10 +++++----- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/disambiguities.jl b/src/disambiguities.jl index 6eb75bd9..86f6a01c 100644 --- a/src/disambiguities.jl +++ b/src/disambiguities.jl @@ -47,24 +47,12 @@ end # Assorted calls found by Aqua: ################################################ ################################################################################ -function Complex(q::AbstractRealQuantity) - @assert iszero(dimension(q)) "$(typeof(q)): $(q) has dimensions! Use `ustrip` instead." - return Complex(ustrip(q)) -end -function Complex{T}(q::AbstractRealQuantity) where {T<:Real} - @assert iszero(dimension(q)) "$(typeof(q)): $(q) has dimensions! Use `ustrip` instead." - return Complex{T}(ustrip(q)) -end -function Bool(q::AbstractRealQuantity) - @assert iszero(dimension(q)) "$(typeof(q)): $(q) has dimensions! Use `ustrip` instead." - return Bool(ustrip(q)) -end for type in (Signed, Float64, Float32, Rational), op in (:flipsign, :copysign) @eval function Base.$(op)(x::$type, y::AbstractRealQuantity) return $(op)(x, ustrip(y)) end end -for type in (Complex, Complex{Bool}) +for type in (:(Complex), :(Complex{Bool})) @eval begin function Base.:*(l::$type, r::AbstractRealQuantity) new_quantity(typeof(r), l * ustrip(r), dimension(r)) @@ -72,8 +60,16 @@ for type in (Complex, Complex{Bool}) function Base.:*(l::AbstractRealQuantity, r::$type) new_quantity(typeof(l), ustrip(l) * r, dimension(l)) end + function $type(q::AbstractRealQuantity) + @assert iszero(dimension(q)) "$(typeof(q)): $(q) has dimensions! Use `ustrip` instead." + return $type(ustrip(q)) + end end end +function Bool(q::AbstractRealQuantity) + @assert iszero(dimension(q)) "$(typeof(q)): $(q) has dimensions! Use `ustrip` instead." + return Bool(ustrip(q)) +end function Base.:/(l::Complex, r::AbstractRealQuantity) new_quantity(typeof(r), l / ustrip(r), inv(dimension(r))) end diff --git a/test/unittests.jl b/test/unittests.jl index b159f3b9..3dbf933f 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -863,8 +863,8 @@ end x = Q(1.0u"1") @test Bool(x) == true @test Bool(ustrip(x)) == true - @test Bool(Q(0.0u"m")) == false - @test Bool(ustrip(Q(0.0u"m"))) == false + @test Bool(Q(0.0u"1")) == false + @test Bool(ustrip(Q(0.0u"1"))) == false x = Q(1.0u"m") @test_throws AssertionError Bool(x) end @@ -1315,14 +1315,14 @@ end # Also do test without dimensions (need dimensionless) qx_dimensionless = Q(x, D) qy_dimensionless = Q(y, D) - @eval @test $f($x, $qy_dimensionless) == $Q($f($x, $y), $D) - @eval @test $f($qx_dimensionless, $y) == $Q($f($x, $y), $D) + @eval @test $f($x, $qy_dimensionless) ≈ $Q($f($x, $y), $D) + @eval @test $f($qx_dimensionless, $y) ≈ $Q($f($x, $y), $D) @eval @test_throws DimensionError $f($qx_dimensions, $y) @eval @test_throws DimensionError $f($x, $qy_dimensions) if f == :rem # Can also do other rounding modes for r in (:RoundFromZero, :RoundNearest, :RoundUp, :RoundDown) - @eval @test $f($qx_dimensions, $qy_dimensions, $r) == $Q($f($x, $y, $r), $dim) + @eval @test $f($qx_dimensions, $qy_dimensions, $r) ≈ $Q($f($x, $y, $r), $dim) end end end From 58e6bf3dbe757151350658d38be5c594f1e89d3e Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 23 Nov 2023 17:54:41 +0000 Subject: [PATCH 38/39] Fix complex ambiguity --- src/disambiguities.jl | 14 ++++++++------ test/unittests.jl | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/disambiguities.jl b/src/disambiguities.jl index 86f6a01c..49a3cf2b 100644 --- a/src/disambiguities.jl +++ b/src/disambiguities.jl @@ -60,15 +60,17 @@ for type in (:(Complex), :(Complex{Bool})) function Base.:*(l::AbstractRealQuantity, r::$type) new_quantity(typeof(l), ustrip(l) * r, dimension(l)) end - function $type(q::AbstractRealQuantity) - @assert iszero(dimension(q)) "$(typeof(q)): $(q) has dimensions! Use `ustrip` instead." - return $type(ustrip(q)) - end end end -function Bool(q::AbstractRealQuantity) +function Complex{T}(q::AbstractRealQuantity) where {T<:Real} @assert iszero(dimension(q)) "$(typeof(q)): $(q) has dimensions! Use `ustrip` instead." - return Bool(ustrip(q)) + return Complex{T}(ustrip(q)) +end +for type in (:Bool, :Complex) + @eval function $type(q::AbstractRealQuantity) + @assert iszero(dimension(q)) "$(typeof(q)): $(q) has dimensions! Use `ustrip` instead." + return $type(ustrip(q)) + end end function Base.:/(l::Complex, r::AbstractRealQuantity) new_quantity(typeof(r), l / ustrip(r), inv(dimension(r))) diff --git a/test/unittests.jl b/test/unittests.jl index 3dbf933f..5626d385 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -848,7 +848,7 @@ end @test typeof(out) == with_type_parameters(promote_quantity_on_value(Q, ComplexF64), Complex{Float64}, DEFAULT_DIM_TYPE) @test ustrip(out) ≈ 0.5 ^ (1 + 2im) - for CT in (Complex, Complex{Float32}) + for CT in (Complex, Complex{Bool}) x = Q(1.0) @test CT(x) == CT(1.0) @test typeof(CT(x)) <: CT From 0609463b8f186380a92223cc86f7c289f7e31acf Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Thu, 23 Nov 2023 18:04:10 +0000 Subject: [PATCH 39/39] Avoid `rem` tests on earlier Julia versions --- test/unittests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unittests.jl b/test/unittests.jl index 5626d385..e952ac3a 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -1319,7 +1319,7 @@ end @eval @test $f($qx_dimensionless, $y) ≈ $Q($f($x, $y), $D) @eval @test_throws DimensionError $f($qx_dimensions, $y) @eval @test_throws DimensionError $f($x, $qy_dimensions) - if f == :rem + if f == :rem && VERSION >= v"1.9" # Can also do other rounding modes for r in (:RoundFromZero, :RoundNearest, :RoundUp, :RoundDown) @eval @test $f($qx_dimensions, $qy_dimensions, $r) ≈ $Q($f($x, $y, $r), $dim)