Unitless is now completely superseded by
TypeUtils and only exists for
backward compatibility.
Unitless is a small Julia package to facilitate
coding with numbers whether they have units or not. The package provides
methods to strip units from numbers or numeric types, convert the numeric type
of quantities (not their units), determine appropriate numeric type to carry
computations mixing numbers with different types and/or units. These methods
make it easy to write code that works consistently for numbers with any units
(including none). The intention is that the Unitless package automatically
extends its exported methods when packages such as
Unitful are loaded.
The Unitless package exports a few methods:
-
unitless(x)yieldsxwithout its units, if any.xcan be a number or a numeric type. In the latter case,unitlessbehaves likebare_typedescribed below. -
bare_type(x)yields the bare numeric type ofx(a numeric value or type). If this method is not extended for a specific type, the fallback implementation yieldstypeof(one(x)). With more than one argument,bare_type(args...)yields the type resulting from promoting the bare numeric types ofargs.... With no argument,bare_type()yieldsUnitless.BareNumberthe union of bare numeric types that may be returned by this method. -
real_type(x)yields the bare real type ofx(a numeric value or type). If this method is not extended for a specific type, the fallback implementation yieldstypeof(one(real(x)). With more than one argument,real_type(args...)yields the type resulting from promoting the bare real types ofargs.... With no argument,real_type()yieldsRealthe super-type of types that may be returned by this method. -
floating_point_type(args...)yields a floating-point type appropriate to represent the bare real type ofargs.... With no argument,floating_point_type()yieldsAbstractFloatthe super-type of types that may be returned by this method. You may considerfloating_point_type(args...)as an equivalent to tofloat(real_type(args...)). -
convert_bare_type(T,x)converts the bare numeric type ofxto the bare numeric type ofTwhile preserving the units ofxif any. Argumentxmay be a number or a numeric type, while argumentTmust be a numeric type. Ifxis one ofmissing,nothing,undef, or the type of one of these singletons,xis returned. -
convert_real_type(T,x)converts the bare real type ofxto the bare real type ofTwhile preserving the units ofxif any. Argumentxmay be a number or a numeric type, while argumentTmust be a numeric type. Ifxis one ofmissing,nothing,undef, or the type of one of these singletons,xis returned. -
convert_floating_point_type(T,x)converts the bare real type ofxto the suitable floating-point type for typeTwhile preserving the units ofxif any. Argumentxmay be a number or a numeric type, while argumentTmust be a numeric type. Ifxis one ofmissing,nothing,undef, or the type of one of these singletons,xis returned. You may considerconvert_floating_point_type(T,x)as an equivalent to toconvert_real_type(float(real_type(T)),x).
The only difference between bare_type and real_type is how they treat
complex numbers. The former preserves the complex kind of its argument while
the latter always returns a real type. You may assume that real_type(x) = real(bare_type(x)). Conversely, convert_bare_type(T,x) yields a complex
result if T is complex and a real result if T is real whatever x, while
convert_real_type(T,x) yields a complex result if x is complex and a real
result if x is real, only the real part of T matters for
convert_real_type(T,x). See examples below.
The following examples illustrate the result of the methods provided by
Unitful, first with bare numbers and bare numeric types, then with
quantities:
julia> using Unitless
julia> map(unitless, (2.1, Float64, true, ComplexF32))
(2.1, Float64, true, ComplexF32)
julia> map(bare_type, (1, 3.14f0, true, 1//3, π, 1.0 - 2.0im))
(Int64, Float32, Bool, Rational{Int64}, Irrational{:π}, Complex{Float64})
julia> map(real_type, (1, 3.14f0, true, 1//3, π, 1.0 - 2.0im))
(Int64, Float32, Bool, Rational{Int64}, Irrational{:π}, Float64)
julia> map(x -> convert_bare_type(Float32, x), (2, 1 - 0im, 1//2, Bool, Complex{Float64}))
(2.0f0, 1.0f0, 0.5f0, Float32, Float32)
julia> map(x -> convert_real_type(Float32, x), (2, 1 - 0im, 1//2, Bool, Complex{Float64}))
(2.0f0, 1.0f0 + 0.0f0im, 0.5f0, Float32, ComplexF32)
julia> using Unitful
julia> map(unitless, (u"2.1GHz", typeof(u"2.1GHz")))
(2.1, Float64)
julia> map(bare_type, (u"3.2km/s", u"5GHz", typeof((0+1im)*u"Hz")))
(Float64, Int64, Complex{Int64})
julia> map(real_type, (u"3.2km/s", u"5GHz", typeof((0+1im)*u"Hz")))
(Float64, Int64, Int64)The following example shows a first attempt to use bare_type to implement
efficient in-place multiplication of an array (whose element may have units) by
a real factor (which must be unitless in this context):
function scale!(A::AbstractArray, α::Number)
alpha = convert_bare_type(eltype(A), α)
@inbounds @simd for i in eachindex(A)
A[i] *= alpha
end
return A
endAn improvement is to realize that when α is a real while the entries of A
are complexes, it is more efficient to multiply the entries of A by a
real-valued multiplier rather than by a complex one. Implementing this is as
simple as replacing convert_bare_type by convert_real_type to only convert
the bare real type of the multiplier while preserving its complex/real kind:
function scale!(A::AbstractArray, α::Number)
alpha = convert_real_type(eltype(A), α)
@inbounds @simd for i in eachindex(A)
A[i] *= alpha
end
return A
endThis latter version consistently and efficiently deals with α being real
while the entries of A are reals or complexes, and with α and the entries
of A being complexes. If α is a complex and the entries of A are reals,
the statement A[i] *= alpha will throw an InexactConversion if the
imaginary part of α is not zero. This check is probably optimized out of the
loop by Julia but, to handle this with guaranteed no loss of efficiency, the
code can be written as:
function scale!(A::AbstractArray, α::Union{Real,Complex})
alpha = if α isa Complex && bare_type(eltype(A)) isa Real
convert(real_type(eltype(A)), α)
else
convert_real_type(eltype(A), α)
end
@inbounds @simd for i in eachindex(A)
A[i] *= alpha
end
return A
endThe restriction α::Union{Real,Complex} accounts for the fact that in-place
multiplication imposes a unitless multiplier. Since the test leading to the
expression used for alpha is based on the types of the arguments, the branch
is eliminated at compile time and the type of alpha is known by the compiler.
The InexactConversion exception may then only be thrown by the call to
convert in the first branch of the test.
This seemingly very specific case was in fact the key point to allow for
packages such as LazyAlgebra or
LinearInterpolators to work
seamlessly on arrays whose entries may have units. The Unitless package was
created to cover this need as transparently as possible.
Unitless can be installed as any other official Julia packages. For example:
using Pkg
Pkg.add("Unitless")