Skip to content

Commit 3d99928

Browse files
committed
Merge branch 'main' into unitful-integration
2 parents 6bed265 + 9498314 commit 3d99928

File tree

13 files changed

+259
-227
lines changed

13 files changed

+259
-227
lines changed

Project.toml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
11
name = "DynamicQuantities"
22
uuid = "06fc5a27-2a28-4c7c-a15d-362465fb6821"
33
authors = ["MilesCranmer <miles.cranmer@gmail.com> and contributors"]
4-
version = "0.1.0"
4+
version = "0.2.0"
55

66
[deps]
7-
Ratios = "c84ed2f1-dad5-54f0-aa8e-dbefe2724439"
87
Requires = "ae029012-a4dd-5104-9daa-d747884805df"
98
SaferIntegers = "88634af6-177f-5301-88b8-7819386cfa38"
109
Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"
1110

1211

1312

1413
[compat]
15-
Ratios = "0.4"
1614
Requires = "1"
17-
SaferIntegers = "3"
1815
Unitful = "1"
1916
julia = "1.6"
2017

2118
[extras]
19+
Ratios = "c84ed2f1-dad5-54f0-aa8e-dbefe2724439"
20+
SaferIntegers = "88634af6-177f-5301-88b8-7819386cfa38"
2221
SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f"
2322
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
2423
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
2524

2625
[targets]
27-
test = ["Test", "SafeTestsets","Documenter"]
26+
test = ["Test", "Ratios", "SaferIntegers", "SafeTestsets", "Documenter"]
27+

README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,18 +94,16 @@ julia> x ^ 1.5
9494

9595
Each of these values has the same type, thus obviating the need for type inference at runtime.
9696

97-
Furthermore, we can do dimensional analysis automatically:
97+
Furthermore, we can do dimensional analysis by detecting `DimensionError`:
9898

9999
```julia
100100
julia> x + 3 * x
101101
1.2 𝐋 ¹ᐟ² 𝐌 ¹
102102

103103
julia> x + y
104-
INVALID
104+
ERROR: DimensionError: 0.3 𝐋 ¹ᐟ² 𝐌 ¹ and 10.2 𝐌 ² 𝐓 ⁻² have different dimensions
105105
```
106106

107-
We can see the second one has `valid(quantity) == false`. This doesn't throw an error by default, as it allows for stable return values.
108-
109107
The dimensions of a `Quantity` can be accessed either with `dimension(quantity)` for the entire `Dimensions` object:
110108

111109
```julia

benchmark/benchmarks.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ end
3434
SUITE["with_quantity"] = let s = BenchmarkGroup()
3535
f5(x, y) = x / y
3636
s["/y"] = @benchmarkable $f5(x, y) setup = (x = default(); y = default()) evals = 1000
37-
f6(x, y) = x^y
38-
s["^y"] = @benchmarkable $f6(x, y) setup = (x = default(); y = default() / dimension(default())) evals = 1000
37+
f6(x, y) = x + y
38+
s["+y"] = @benchmarkable $f6(x, y) setup = (x = default(); y = x + rand() * x) evals = 1000
3939
s
4040
end

coverage.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
using Coverage
22
# process '*.cov' files
33
coverage = process_folder() # defaults to src/; alternatively, supply the folder name as argument
4+
push!(coverage, process_folder("ext/")...)
45

56
LCOV.writefile("lcov.info", coverage)
67

78
# process '*.info' files
89
coverage = merge_coverage_counts(
910
coverage,
1011
filter!(
11-
let prefixes = (joinpath(pwd(), "src", ""),)
12+
let prefixes = (joinpath(pwd(), "src", ""), joinpath(pwd(), "ext", ""))
1213
c -> any(p -> startswith(c.filename, p), prefixes)
1314
end,
1415
LCOV.readfolder("test"),

ext/DynamicQuantitiesUnitfulExt.jl

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,22 @@ Base.convert(::Type{Unitful.Quantity}, x::DynamicQuantities.Quantity) =
2929
cumulator
3030
end
3131

32-
Base.convert(::Type{DynamicQuantities.Quantity}, x::Unitful.Quantity) =
32+
Base.convert(::Type{DynamicQuantities.Quantity}, x::Unitful.Quantity{T}) where {T} = convert(DynamicQuantities.Quantity{T,DynamicQuantities.DEFAULT_DIM_TYPE}, x)
33+
Base.convert(::Type{DynamicQuantities.Quantity{T,R}}, x::Unitful.Quantity) where {T,R} =
3334
let
3435
value = Unitful.ustrip(Unitful.upreferred(x))
35-
dimension = convert(DynamicQuantities.Dimensions, Unitful.dimension(x))
36-
return DynamicQuantities.Quantity(value, dimension)
36+
dimension = convert(DynamicQuantities.Dimensions{R}, Unitful.dimension(x))
37+
return DynamicQuantities.Quantity(convert(T, value), dimension)
3738
end
3839

39-
Base.convert(::Type{DynamicQuantities.Dimensions}, d::Unitful.Dimensions{D}) where {D} =
40+
Base.convert(::Type{DynamicQuantities.Dimensions}, d::Unitful.Dimensions) = convert(DynamicQuantities.Dimensions{DynamicQuantities.DEFAULT_DIM_TYPE}, d)
41+
Base.convert(::Type{DynamicQuantities.Dimensions{R}}, d::Unitful.Dimensions{D}) where {R,D} =
4042
let
41-
cumulator = DynamicQuantities.Dimensions()
43+
cumulator = DynamicQuantities.Dimensions{R}()
4244
for dim in D
4345
dim_symbol = _map_dim_name_to_dynamic_units(typeof(dim))
4446
dim_power = dim.power
45-
cumulator *= DynamicQuantities.Dimensions(; dim_symbol => dim_power)
47+
cumulator *= DynamicQuantities.Dimensions(R; dim_symbol => dim_power)
4648
end
4749
cumulator
4850
end

src/DynamicQuantities.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
module DynamicQuantities
22
import Unitful
33

4-
export Quantity, Dimensions, ustrip, dimension, valid
4+
export Quantity, Dimensions, DimensionError, ustrip, dimension, valid
55
export ulength, umass, utime, ucurrent, utemperature, uluminosity, uamount
66

77

88
export @q_str,@dynquantities, unitful,dynquantity
99

10-
import Ratios: SimpleRatio
10+
1111

1212
include("types.jl")
1313
include("utils.jl")

src/math.jl

Lines changed: 25 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,35 @@
11
Base.:*(l::Dimensions, r::Dimensions) = @map_dimensions(+, l, r)
2-
Base.:*(l::Quantity, r::Quantity) = Quantity(l.value * r.value, l.dimensions * r.dimensions, l.valid && r.valid)
3-
Base.:*(l::Quantity, r::Dimensions) = Quantity(l.value, l.dimensions * r, l.valid)
4-
Base.:*(l::Dimensions, r::Quantity) = Quantity(r.value, l * r.dimensions, r.valid)
5-
Base.:*(l::Quantity, r::Number) = Quantity(l.value * r, l.dimensions, l.valid)
6-
Base.:*(l::Number, r::Quantity) = Quantity(l * r.value, r.dimensions, r.valid)
7-
Base.:*(l::Dimensions, r::Number) = Quantity(r, l, true)
8-
Base.:*(l::Number, r::Dimensions) = Quantity(l, r, true)
2+
Base.:*(l::Quantity, r::Quantity) = Quantity(l.value * r.value, l.dimensions * r.dimensions)
3+
Base.:*(l::Quantity, r::Dimensions) = Quantity(l.value, l.dimensions * r)
4+
Base.:*(l::Dimensions, r::Quantity) = Quantity(r.value, l * r.dimensions)
5+
Base.:*(l::Quantity, r::Number) = Quantity(l.value * r, l.dimensions)
6+
Base.:*(l::Number, r::Quantity) = Quantity(l * r.value, r.dimensions)
7+
Base.:*(l::Dimensions, r::Number) = Quantity(r, l)
8+
Base.:*(l::Number, r::Dimensions) = Quantity(l, r)
99

1010
Base.:/(l::Dimensions, r::Dimensions) = @map_dimensions(-, l, r)
11-
Base.:/(l::Quantity, r::Quantity) = Quantity(l.value / r.value, l.dimensions / r.dimensions, l.valid && r.valid)
12-
Base.:/(l::Quantity, r::Dimensions) = Quantity(l.value, l.dimensions / r, l.valid)
13-
Base.:/(l::Dimensions, r::Quantity) = Quantity(inv(r.value), l / r.dimensions, r.valid)
14-
Base.:/(l::Quantity, r::Number) = Quantity(l.value / r, l.dimensions, l.valid)
11+
Base.:/(l::Quantity, r::Quantity) = Quantity(l.value / r.value, l.dimensions / r.dimensions)
12+
Base.:/(l::Quantity, r::Dimensions) = Quantity(l.value, l.dimensions / r)
13+
Base.:/(l::Dimensions, r::Quantity) = Quantity(inv(r.value), l / r.dimensions)
14+
Base.:/(l::Quantity, r::Number) = Quantity(l.value / r, l.dimensions)
1515
Base.:/(l::Number, r::Quantity) = l * inv(r)
16-
Base.:/(l::Dimensions, r::Number) = Quantity(inv(r), l, true)
17-
Base.:/(l::Number, r::Dimensions) = Quantity(l, inv(r), true)
16+
Base.:/(l::Dimensions, r::Number) = Quantity(inv(r), l)
17+
Base.:/(l::Number, r::Dimensions) = Quantity(l, inv(r))
1818

19-
Base.:+(l::Quantity, r::Quantity) = Quantity(l.value + r.value, l.dimensions, l.valid && r.valid && l.dimensions == r.dimensions)
20-
Base.:-(l::Quantity, r::Quantity) = Quantity(l.value - r.value, l.dimensions, l.valid && r.valid && l.dimensions == r.dimensions)
19+
Base.:+(l::Quantity, r::Quantity) = dimension(l) == dimension(r) ? Quantity(l.value + r.value, l.dimensions) : throw(DimensionError(l, r))
20+
Base.:-(l::Quantity, r::Quantity) = dimension(l) == dimension(r) ? Quantity(l.value - r.value, l.dimensions) : throw(DimensionError(l, r))
2121

22-
Base.:^(l::Quantity, r::Quantity) =
23-
let rr = tryrationalize(R, r.value)
24-
Quantity(l.value^rr, l.dimensions^rr, l.valid && r.valid && iszero(r.dimensions))
25-
end
26-
Base.:^(l::Dimensions, r::R) = @map_dimensions(Base.Fix1(*, r), l)
27-
Base.:^(l::Dimensions, r::Number) = l^tryrationalize(R, r)
28-
Base.:^(l::Quantity, r::Number) =
29-
let rr = tryrationalize(R, r)
30-
Quantity(l.value^rr, l.dimensions^rr, l.valid)
31-
end
22+
_pow(l::Dimensions{R}, r::R) where {R} = @map_dimensions(Base.Fix1(*, r), l)
23+
_pow(l::Quantity{T,R}, r::R) where {T,R} = Quantity(l.value^convert(T, r), _pow(l.dimensions, r))
24+
Base.:^(l::Dimensions{R}, r::Number) where {R} = _pow(l, tryrationalize(R, r))
25+
Base.:^(l::Quantity{T,R}, r::Number) where {T,R} = _pow(l, tryrationalize(R, r))
3226

3327
Base.inv(d::Dimensions) = @map_dimensions(-, d)
34-
Base.inv(q::Quantity) = Quantity(inv(q.value), inv(q.dimensions), q.valid)
28+
Base.inv(q::Quantity) = Quantity(inv(q.value), inv(q.dimensions))
3529

36-
Base.sqrt(d::Dimensions) = d^(1 // 2)
37-
Base.sqrt(q::Quantity) = Quantity(sqrt(q.value), sqrt(q.dimensions), q.valid)
38-
Base.cbrt(d::Dimensions) = d^(1 // 3)
39-
Base.cbrt(q::Quantity) = Quantity(cbrt(q.value), cbrt(q.dimensions), q.valid)
30+
Base.sqrt(d::Dimensions{R}) where {R} = d^inv(convert(R, 2))
31+
Base.sqrt(q::Quantity) = Quantity(sqrt(q.value), sqrt(q.dimensions))
32+
Base.cbrt(d::Dimensions{R}) where {R} = d^inv(convert(R, 3))
33+
Base.cbrt(q::Quantity) = Quantity(cbrt(q.value), cbrt(q.dimensions))
4034

41-
Base.abs(q::Quantity) = Quantity(abs(q.value), q.dimensions, q.valid)
35+
Base.abs(q::Quantity) = Quantity(abs(q.value), q.dimensions)

src/types.jl

Lines changed: 47 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
1-
import Ratios: SimpleRatio
2-
import SaferIntegers: SafeInt
3-
4-
const INT_TYPE = SafeInt
5-
const R = SimpleRatio{INT_TYPE}
6-
const ZERO = R(0)
7-
const DIMENSION_NAMES = (:length, :mass, :time, :current, :temperature, :luminosity, :amount)
8-
const DIMENSION_SYNONYMS = (:𝐋, :𝐌, :𝐓, :𝐈, :𝚯, :𝐉, :𝐍)
9-
const SYNONYM_MAPPING = NamedTuple(DIMENSION_NAMES .=> DIMENSION_SYNONYMS)
1+
const DEFAULT_DIM_TYPE = Rational{Int16}
2+
const DEFAULT_VALUE_TYPE = Float64
103

114
"""
125
Dimensions
@@ -17,15 +10,15 @@ example, the dimensions of velocity are `Dimensions(length=1, time=-1)`.
1710
1811
# Fields
1912
20-
- `length::Rational{Int}`: length dimension (i.e., meters^(length))
21-
- `mass::Rational{Int}`: mass dimension (i.e., kg^(mass))
22-
- `time::Rational{Int}`: time dimension (i.e., s^(time))
23-
- `current::Rational{Int}`: current dimension (i.e., A^(current))
24-
- `temperature::Rational{Int}`: temperature dimension (i.e., K^(temperature))
25-
- `luminosity::Rational{Int}`: luminosity dimension (i.e., cd^(luminosity))
26-
- `amount::Rational{Int}`: amount dimension (i.e., mol^(amount))
13+
- `length`: length dimension (i.e., meters^(length))
14+
- `mass`: mass dimension (i.e., kg^(mass))
15+
- `time`: time dimension (i.e., s^(time))
16+
- `current`: current dimension (i.e., A^(current))
17+
- `temperature`: temperature dimension (i.e., K^(temperature))
18+
- `luminosity`: luminosity dimension (i.e., cd^(luminosity))
19+
- `amount`: amount dimension (i.e., mol^(amount))
2720
"""
28-
struct Dimensions
21+
struct Dimensions{R <: Real}
2922
length::R
3023
mass::R
3124
time::R
@@ -34,27 +27,40 @@ struct Dimensions
3427
luminosity::R
3528
amount::R
3629

37-
Dimensions(length::R, mass::R, time::R, current::R, temperature::R, luminosity::R, amount::R) =
38-
new(length, mass, time, current, temperature, luminosity, amount)
39-
Dimensions(; kws...) = Dimensions(
40-
tryrationalize(R, get(kws, :length, ZERO)),
41-
tryrationalize(R, get(kws, :mass, ZERO)),
42-
tryrationalize(R, get(kws, :time, ZERO)),
43-
tryrationalize(R, get(kws, :current, ZERO)),
44-
tryrationalize(R, get(kws, :temperature, ZERO)),
45-
tryrationalize(R, get(kws, :luminosity, ZERO)),
46-
tryrationalize(R, get(kws, :amount, ZERO)),
30+
function Dimensions(length::_R,
31+
mass::_R,
32+
time::_R,
33+
current::_R,
34+
temperature::_R,
35+
luminosity::_R,
36+
amount::_R) where {_R<:Real}
37+
new{_R}(length, mass, time, current, temperature, luminosity, amount)
38+
end
39+
Dimensions(; kws...) = Dimensions(DEFAULT_DIM_TYPE; kws...)
40+
Dimensions(::Type{_R}; kws...) where {_R} = Dimensions(
41+
tryrationalize(_R, get(kws, :length, zero(_R))),
42+
tryrationalize(_R, get(kws, :mass, zero(_R))),
43+
tryrationalize(_R, get(kws, :time, zero(_R))),
44+
tryrationalize(_R, get(kws, :current, zero(_R))),
45+
tryrationalize(_R, get(kws, :temperature, zero(_R))),
46+
tryrationalize(_R, get(kws, :luminosity, zero(_R))),
47+
tryrationalize(_R, get(kws, :amount, zero(_R))),
4748
)
49+
Dimensions{_R}(; kws...) where {_R} = Dimensions(_R; kws...)
50+
Dimensions{_R}(args...) where {_R} = Dimensions(Base.Fix1(convert, _R).(args)...)
4851
end
4952

53+
const DIMENSION_NAMES = Base.fieldnames(Dimensions)
54+
const DIMENSION_SYNONYMS = (:𝐋, :𝐌, :𝐓, :𝐈, :𝚯, :𝐉, :𝐍)
55+
const SYNONYM_MAPPING = NamedTuple(DIMENSION_NAMES .=> DIMENSION_SYNONYMS)
56+
5057
"""
5158
Quantity{T}
5259
5360
Physical quantity with value `value` of type `T` and dimensions `dimensions`.
54-
The `valid` field is used to indicate whether the quantity is valid or not
55-
(e.g., due to dimensional error). For example, the velocity of an object
56-
with mass 1 kg and velocity 2 m/s is `Quantity(2, mass=1, length=1, time=-1)`.
57-
You should access these fields with `ustrip(q)`, `dimensions(q)`, and `valid(q)`.
61+
For example, the velocity of an object with mass 1 kg and velocity
62+
2 m/s is `Quantity(2, mass=1, length=1, time=-1)`.
63+
You should access these fields with `ustrip(q)`, and `dimensions(q)`.
5864
You can access specific dimensions with `ulength(q)`, `umass(q)`, `utime(q)`,
5965
`ucurrent(q)`, `utemperature(q)`, `uluminosity(q)`, and `uamount(q)`.
6066
@@ -65,15 +71,17 @@ including `*`, `+`, `-`, `/`, `^`, `sqrt`, and `cbrt`.
6571
6672
- `value::T`: value of the quantity of some type `T`
6773
- `dimensions::Dimensions`: dimensions of the quantity
68-
- `valid::Bool`: whether the quantity is valid or not
6974
"""
70-
struct Quantity{T}
75+
struct Quantity{T, R}
7176
value::T
72-
dimensions::Dimensions
73-
valid::Bool
77+
dimensions::Dimensions{R}
78+
79+
Quantity(x; kws...) = new{typeof(x), DEFAULT_DIM_TYPE}(x, Dimensions(; kws...))
80+
Quantity(x, ::Type{_R}; kws...) where {_R} = new{typeof(x), _R}(x, Dimensions(_R; kws...))
81+
Quantity(x, d::Dimensions{_R}) where {_R} = new{typeof(x), _R}(x, d)
82+
end
7483

75-
Quantity(x; kws...) = new{typeof(x)}(x, Dimensions(; kws...), true)
76-
Quantity(x, valid::Bool; kws...) = new{typeof(x)}(x, Dimensions(; kws...), valid)
77-
Quantity(x, d::Dimensions) = new{typeof(x)}(x, d, true)
78-
Quantity(x, d::Dimensions, valid::Bool) = new{typeof(x)}(x, d, valid)
84+
struct DimensionError{Q1,Q2} <: Exception
85+
q1::Q1
86+
q2::Q2
7987
end

0 commit comments

Comments
 (0)