Skip to content

Give Dimensions a type parameter. #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ authors = ["MilesCranmer <[email protected]> and contributors"]
version = "0.1.0"

[deps]
Ratios = "c84ed2f1-dad5-54f0-aa8e-dbefe2724439"
Requires = "ae029012-a4dd-5104-9daa-d747884805df"
SaferIntegers = "88634af6-177f-5301-88b8-7819386cfa38"

[weakdeps]
Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"
Expand All @@ -15,16 +13,16 @@ Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"
DynamicQuantitiesUnitfulExt = "Unitful"

[compat]
Ratios = "0.4"
Requires = "1"
SaferIntegers = "3"
Unitful = "1"
julia = "1.6"

[extras]
Ratios = "c84ed2f1-dad5-54f0-aa8e-dbefe2724439"
SaferIntegers = "88634af6-177f-5301-88b8-7819386cfa38"
SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"

[targets]
test = ["Test", "SafeTestsets", "Unitful"]
test = ["Test", "Ratios", "SaferIntegers", "SafeTestsets", "Unitful"]
4 changes: 2 additions & 2 deletions benchmark/benchmarks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ end
SUITE["with_quantity"] = let s = BenchmarkGroup()
f5(x, y) = x / y
s["/y"] = @benchmarkable $f5(x, y) setup = (x = default(); y = default()) evals = 1000
f6(x, y) = x^y
s["^y"] = @benchmarkable $f6(x, y) setup = (x = default(); y = default() / dimension(default())) evals = 1000
f6(x, y) = x + y
s["+y"] = @benchmarkable $f6(x, y) setup = (x = default(); y = x + rand() * x) evals = 1000
s
end
14 changes: 8 additions & 6 deletions ext/DynamicQuantitiesUnitfulExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,22 @@ Base.convert(::Type{Unitful.Quantity}, x::DynamicQuantities.Quantity) =
cumulator
end

Base.convert(::Type{DynamicQuantities.Quantity}, x::Unitful.Quantity) =
Base.convert(::Type{DynamicQuantities.Quantity}, x::Unitful.Quantity{T}) where {T} = convert(DynamicQuantities.Quantity{T,DynamicQuantities.DEFAULT_DIM_TYPE}, x)
Base.convert(::Type{DynamicQuantities.Quantity{T,R}}, x::Unitful.Quantity) where {T,R} =
let
value = Unitful.ustrip(Unitful.upreferred(x))
dimension = convert(DynamicQuantities.Dimensions, Unitful.dimension(x))
return DynamicQuantities.Quantity(value, dimension)
dimension = convert(DynamicQuantities.Dimensions{R}, Unitful.dimension(x))
return DynamicQuantities.Quantity(convert(T, value), dimension)
end

Base.convert(::Type{DynamicQuantities.Dimensions}, d::Unitful.Dimensions{D}) where {D} =
Base.convert(::Type{DynamicQuantities.Dimensions}, d::Unitful.Dimensions) = convert(DynamicQuantities.Dimensions{DynamicQuantities.DEFAULT_DIM_TYPE}, d)
Base.convert(::Type{DynamicQuantities.Dimensions{R}}, d::Unitful.Dimensions{D}) where {R,D} =
let
cumulator = DynamicQuantities.Dimensions()
cumulator = DynamicQuantities.Dimensions{R}()
for dim in D
dim_symbol = _map_dim_name_to_dynamic_units(typeof(dim))
dim_power = dim.power
cumulator *= DynamicQuantities.Dimensions(; dim_symbol => dim_power)
cumulator *= DynamicQuantities.Dimensions(R; dim_symbol => dim_power)
end
cumulator
end
Expand Down
2 changes: 0 additions & 2 deletions src/DynamicQuantities.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ module DynamicQuantities
export Quantity, Dimensions, ustrip, dimension, valid
export ulength, umass, utime, ucurrent, utemperature, uluminosity, uamount

import Ratios: SimpleRatio

include("types.jl")
include("utils.jl")
include("math.jl")
Expand Down
18 changes: 6 additions & 12 deletions src/math.jl
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,17 @@ Base.:/(l::Number, r::Dimensions) = Quantity(l, inv(r), true)
Base.:+(l::Quantity, r::Quantity) = Quantity(l.value + r.value, l.dimensions, l.valid && r.valid && l.dimensions == r.dimensions)
Base.:-(l::Quantity, r::Quantity) = Quantity(l.value - r.value, l.dimensions, l.valid && r.valid && l.dimensions == r.dimensions)

Base.:^(l::Quantity, r::Quantity) =
let rr = tryrationalize(R, r.value)
Quantity(l.value^rr, l.dimensions^rr, l.valid && r.valid && iszero(r.dimensions))
end
Base.:^(l::Dimensions, r::R) = @map_dimensions(Base.Fix1(*, r), l)
Base.:^(l::Dimensions, r::Number) = l^tryrationalize(R, r)
Base.:^(l::Quantity, r::Number) =
let rr = tryrationalize(R, r)
Quantity(l.value^rr, l.dimensions^rr, l.valid)
end
_pow(l::Dimensions{R}, r::R) where {R} = @map_dimensions(Base.Fix1(*, r), l)
_pow(l::Quantity{T,R}, r::R) where {T,R} = Quantity(l.value^convert(T, r), _pow(l.dimensions, r), l.valid)
Base.:^(l::Dimensions{R}, r::Number) where {R} = _pow(l, tryrationalize(R, r))
Base.:^(l::Quantity{T,R}, r::Number) where {T,R} = _pow(l, tryrationalize(R, r))

Base.inv(d::Dimensions) = @map_dimensions(-, d)
Base.inv(q::Quantity) = Quantity(inv(q.value), inv(q.dimensions), q.valid)

Base.sqrt(d::Dimensions) = d^(1 // 2)
Base.sqrt(d::Dimensions{R}) where {R} = d^inv(convert(R, 2))
Base.sqrt(q::Quantity) = Quantity(sqrt(q.value), sqrt(q.dimensions), q.valid)
Base.cbrt(d::Dimensions) = d^(1 // 3)
Base.cbrt(d::Dimensions{R}) where {R} = d^inv(convert(R, 3))
Base.cbrt(q::Quantity) = Quantity(cbrt(q.value), cbrt(q.dimensions), q.valid)

Base.abs(q::Quantity) = Quantity(abs(q.value), q.dimensions, q.valid)
74 changes: 41 additions & 33 deletions src/types.jl
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
import Ratios: SimpleRatio
import SaferIntegers: SafeInt

const INT_TYPE = SafeInt
const R = SimpleRatio{INT_TYPE}
const ZERO = R(0)
const DIMENSION_NAMES = (:length, :mass, :time, :current, :temperature, :luminosity, :amount)
const DIMENSION_SYNONYMS = (:𝐋, :𝐌, :𝐓, :𝐈, :𝚯, :𝐉, :𝐍)
const SYNONYM_MAPPING = NamedTuple(DIMENSION_NAMES .=> DIMENSION_SYNONYMS)
const DEFAULT_DIM_TYPE = Rational{Int16}

"""
Dimensions
Expand All @@ -17,15 +9,15 @@ example, the dimensions of velocity are `Dimensions(length=1, time=-1)`.

# Fields

- `length::Rational{Int}`: length dimension (i.e., meters^(length))
- `mass::Rational{Int}`: mass dimension (i.e., kg^(mass))
- `time::Rational{Int}`: time dimension (i.e., s^(time))
- `current::Rational{Int}`: current dimension (i.e., A^(current))
- `temperature::Rational{Int}`: temperature dimension (i.e., K^(temperature))
- `luminosity::Rational{Int}`: luminosity dimension (i.e., cd^(luminosity))
- `amount::Rational{Int}`: amount dimension (i.e., mol^(amount))
- `length`: length dimension (i.e., meters^(length))
- `mass`: mass dimension (i.e., kg^(mass))
- `time`: time dimension (i.e., s^(time))
- `current`: current dimension (i.e., A^(current))
- `temperature`: temperature dimension (i.e., K^(temperature))
- `luminosity`: luminosity dimension (i.e., cd^(luminosity))
- `amount`: amount dimension (i.e., mol^(amount))
"""
struct Dimensions
struct Dimensions{R <: Real}
length::R
mass::R
time::R
Expand All @@ -34,19 +26,33 @@ struct Dimensions
luminosity::R
amount::R

Dimensions(length::R, mass::R, time::R, current::R, temperature::R, luminosity::R, amount::R) =
new(length, mass, time, current, temperature, luminosity, amount)
Dimensions(; kws...) = Dimensions(
tryrationalize(R, get(kws, :length, ZERO)),
tryrationalize(R, get(kws, :mass, ZERO)),
tryrationalize(R, get(kws, :time, ZERO)),
tryrationalize(R, get(kws, :current, ZERO)),
tryrationalize(R, get(kws, :temperature, ZERO)),
tryrationalize(R, get(kws, :luminosity, ZERO)),
tryrationalize(R, get(kws, :amount, ZERO)),
function Dimensions(length::_R,
mass::_R,
time::_R,
current::_R,
temperature::_R,
luminosity::_R,
amount::_R) where {_R<:Real}
new{_R}(length, mass, time, current, temperature, luminosity, amount)
end
Dimensions(; kws...) = Dimensions(DEFAULT_DIM_TYPE; kws...)
Dimensions(::Type{_R}; kws...) where {_R} = Dimensions(
tryrationalize(_R, get(kws, :length, zero(_R))),
tryrationalize(_R, get(kws, :mass, zero(_R))),
tryrationalize(_R, get(kws, :time, zero(_R))),
tryrationalize(_R, get(kws, :current, zero(_R))),
tryrationalize(_R, get(kws, :temperature, zero(_R))),
tryrationalize(_R, get(kws, :luminosity, zero(_R))),
tryrationalize(_R, get(kws, :amount, zero(_R))),
)
Dimensions{_R}(; kws...) where {_R} = Dimensions(_R; kws...)
Dimensions{_R}(args...) where {_R} = Dimensions(Base.Fix1(convert, _R).(args)...)
end

const DIMENSION_NAMES = Base.fieldnames(Dimensions)
const DIMENSION_SYNONYMS = (:𝐋, :𝐌, :𝐓, :𝐈, :𝚯, :𝐉, :𝐍)
const SYNONYM_MAPPING = NamedTuple(DIMENSION_NAMES .=> DIMENSION_SYNONYMS)

"""
Quantity{T}

Expand All @@ -67,13 +73,15 @@ including `*`, `+`, `-`, `/`, `^`, `sqrt`, and `cbrt`.
- `dimensions::Dimensions`: dimensions of the quantity
- `valid::Bool`: whether the quantity is valid or not
"""
struct Quantity{T}
struct Quantity{T, R}
value::T
dimensions::Dimensions
dimensions::Dimensions{R}
valid::Bool

Quantity(x; kws...) = new{typeof(x)}(x, Dimensions(; kws...), true)
Quantity(x, valid::Bool; kws...) = new{typeof(x)}(x, Dimensions(; kws...), valid)
Quantity(x, d::Dimensions) = new{typeof(x)}(x, d, true)
Quantity(x, d::Dimensions, valid::Bool) = new{typeof(x)}(x, d, valid)
Quantity(x; kws...) = new{typeof(x), DEFAULT_DIM_TYPE}(x, Dimensions(; kws...), true)
Quantity(x, valid::Bool; kws...) = new{typeof(x), DEFAULT_DIM_TYPE}(x, Dimensions(; kws...), valid)
Quantity(x, ::Type{_R}, valid::Bool; kws...) where {_R} = new{typeof(x), _R}(x, Dimensions(_R; kws...), valid)
Quantity(x, ::Type{_R}; kws...) where {_R} = new{typeof(x), _R}(x, Dimensions(_R; kws...), true)
Quantity(x, d::Dimensions{_R}) where {_R} = new{typeof(x), _R}(x, d, true)
Quantity(x, d::Dimensions{_R}, valid::Bool) where {_R} = new{typeof(x), _R}(x, d, valid)
end
26 changes: 11 additions & 15 deletions src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,21 @@ Base.iszero(q::Quantity) = iszero(q.value)
Base.getindex(d::Dimensions, k::Symbol) = getfield(d, k)
Base.:(==)(l::Dimensions, r::Dimensions) = @all_dimensions(==, l, r)
Base.:(==)(l::Quantity, r::Quantity) = l.value == r.value && l.dimensions == r.dimensions && l.valid == r.valid
Base.:(≈)(l::Quantity, r::Quantity) = l.valuer.value && l.dimensions == r.dimensions && l.valid == r.valid
Base.isapprox(l::Quantity, r::Quantity; kws...) = isapprox(l.value, r.value; kws...) && l.dimensions == r.dimensions && l.valid == r.valid
Base.length(::Dimensions) = 1
Base.length(::Quantity) = 1
Base.iterate(d::Dimensions) = (d, nothing)
Base.iterate(::Dimensions, ::Nothing) = nothing
Base.iterate(q::Quantity) = (q, nothing)
Base.iterate(::Quantity, ::Nothing) = nothing
Base.zero(::Type{Quantity{T,R}}) where {T,R} = Quantity(zero(T), R)
Base.one(::Type{Quantity{T,R}}) where {T,R} = Quantity(one(T), R)
Base.zero(::Type{Quantity{T}}) where {T} = Quantity(zero(T))
Base.one(::Type{Quantity{T}}) where {T} = Quantity(one(T))
Base.zero(::Type{Quantity}) = Quantity(zero(DEFAULT_DIM_TYPE))
Base.one(::Type{Quantity}) = Quantity(one(DEFAULT_DIM_TYPE))
Base.one(::Type{Dimensions}) = Dimensions()
Base.one(::Type{Dimensions{R}}) where {R} = Dimensions{R}()

Base.show(io::IO, d::Dimensions) =
let tmp_io = IOBuffer()
Expand All @@ -65,12 +70,8 @@ Base.show(io::IO, d::Dimensions) =
end
Base.show(io::IO, q::Quantity) = q.valid ? print(io, q.value, " ", q.dimensions) : print(io, "INVALID")

string_rational(x::Rational) = isinteger(x) ? string(x.num) : string(x)
string_rational(x::SimpleRatio) = string_rational(x.num // x.den)
pretty_print_exponent(io::IO, x::R) =
let
print(io, " ", to_superscript(string_rational(x)))
end
string_rational(x) = isinteger(x) ? string(round(Int, x)) : string(x)
pretty_print_exponent(io::IO, x) = print(io, " ", to_superscript(string_rational(x)))
const SUPERSCRIPT_MAPPING = ['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹']
const INTCHARS = ['0' + i for i = 0:9]
to_superscript(s::AbstractString) = join(
Expand All @@ -79,14 +80,9 @@ to_superscript(s::AbstractString) = join(
end
)

tryrationalize(::Type{RI}, x::RI) where {RI} = x
tryrationalize(::Type{RI}, x::Rational) where {RI} = RI(x)
tryrationalize(::Type{RI}, x::Integer) where {RI} = RI(x)
tryrationalize(::Type{RI}, x) where {RI} = simple_ratio_rationalize(RI, x)
simple_ratio_rationalize(::Type{RI}, x) where {RI} =
let int_type = RI.parameters[1]
isinteger(x) ? RI(round(int_type, x)) : RI(rationalize(int_type, x))
end
tryrationalize(::Type{R}, x::R) where {R} = x
tryrationalize(::Type{R}, x::Union{Rational,Integer}) where {R} = convert(R, x)
tryrationalize(::Type{R}, x) where {R} = isinteger(x) ? convert(R, round(Int, x)) : convert(R, rationalize(Int, x))

"""
ustrip(q::Quantity)
Expand Down
11 changes: 8 additions & 3 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
using SafeTestsets
import Ratios: SimpleRatio

@safetestset "Unit tests" begin
include("unittests.jl")
@static if !hasmethod(round, Tuple{Int, SimpleRatio{Int}})
@eval Base.round(T, x::SimpleRatio) = round(T, x.num // x.den)
end

@safetestset "Unitful.jl integration tests" begin
include("unitful.jl")
include("test_unitful.jl")
end

@safetestset "Unit tests" begin
include("unittests.jl")
end
28 changes: 28 additions & 0 deletions test/test_unitful.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import DynamicQuantities
import Unitful
import Unitful: @u_str
import Ratios: SimpleRatio
import SaferIntegers: SafeInt16
using Test

# Try to work with different preferred units:
Unitful.preferunits(u"km")

risapprox(x::Unitful.Quantity, y::Unitful.Quantity; kws...) =
let (xfloat, yfloat) = (Unitful.ustrip ∘ Unitful.upreferred).((x, y))
return isapprox(xfloat, yfloat; kws...)
end

factor_for_preferred_units = 1e-3

for T in [Float16, Float32, Float64], R in [Rational{Int16}, Rational{Int32}, SimpleRatio{Int}, SimpleRatio{SafeInt16}]
x = DynamicQuantities.Quantity(T(0.2*factor_for_preferred_units), R, length=1, amount=2, current=-1 // 2, luminosity=2 // 5)
x_unitful = T(0.2)u"m*mol^2*A^(-1//2)*cd^(2//5)"

@test risapprox(convert(Unitful.Quantity, x), x_unitful; atol=1e-6)
@test typeof(convert(DynamicQuantities.Quantity, convert(Unitful.Quantity, x))) <: DynamicQuantities.Quantity{T,DynamicQuantities.DEFAULT_DIM_TYPE}
@test isapprox(convert(DynamicQuantities.Quantity, convert(Unitful.Quantity, x)), x; atol=1e-6)

@test isapprox(convert(DynamicQuantities.Quantity{T,R}, x_unitful), x; atol=1e-6)
@test risapprox(convert(Unitful.Quantity, convert(DynamicQuantities.Quantity{T,R}, x_unitful)), Unitful.upreferred(x_unitful); atol=1e-6)
end
10 changes: 0 additions & 10 deletions test/unitful.jl

This file was deleted.

Loading