Skip to content

Commit 6ac1bee

Browse files
authored
Merge pull request #17 from SymbolicML/dimension-errors
Throw `DimensionError` rather than using a `valid` field in `Quantity`
2 parents fb19037 + b1092e0 commit 6ac1bee

File tree

6 files changed

+42
-61
lines changed

6 files changed

+42
-61
lines changed

README.md

+2-4
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

src/DynamicQuantities.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module DynamicQuantities
22

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

66
include("types.jl")

src/math.jl

+20-20
Original file line numberDiff line numberDiff line change
@@ -1,35 +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

2222
_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), l.valid)
23+
_pow(l::Quantity{T,R}, r::R) where {T,R} = Quantity(l.value^convert(T, r), _pow(l.dimensions, r))
2424
Base.:^(l::Dimensions{R}, r::Number) where {R} = _pow(l, tryrationalize(R, r))
2525
Base.:^(l::Quantity{T,R}, r::Number) where {T,R} = _pow(l, tryrationalize(R, r))
2626

2727
Base.inv(d::Dimensions) = @map_dimensions(-, d)
28-
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))
2929

3030
Base.sqrt(d::Dimensions{R}) where {R} = d^inv(convert(R, 2))
31-
Base.sqrt(q::Quantity) = Quantity(sqrt(q.value), sqrt(q.dimensions), q.valid)
31+
Base.sqrt(q::Quantity) = Quantity(sqrt(q.value), sqrt(q.dimensions))
3232
Base.cbrt(d::Dimensions{R}) where {R} = d^inv(convert(R, 3))
33-
Base.cbrt(q::Quantity) = Quantity(cbrt(q.value), cbrt(q.dimensions), q.valid)
33+
Base.cbrt(q::Quantity) = Quantity(cbrt(q.value), cbrt(q.dimensions))
3434

35-
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

+11-12
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,9 @@ const SYNONYM_MAPPING = NamedTuple(DIMENSION_NAMES .=> DIMENSION_SYNONYMS)
5757
Quantity{T}
5858
5959
Physical quantity with value `value` of type `T` and dimensions `dimensions`.
60-
The `valid` field is used to indicate whether the quantity is valid or not
61-
(e.g., due to dimensional error). For example, the velocity of an object
62-
with mass 1 kg and velocity 2 m/s is `Quantity(2, mass=1, length=1, time=-1)`.
63-
You should access these fields with `ustrip(q)`, `dimensions(q)`, and `valid(q)`.
60+
For example, the velocity of an object with mass 1 kg and velocity
61+
2 m/s is `Quantity(2, mass=1, length=1, time=-1)`.
62+
You should access these fields with `ustrip(q)`, and `dimensions(q)`.
6463
You can access specific dimensions with `ulength(q)`, `umass(q)`, `utime(q)`,
6564
`ucurrent(q)`, `utemperature(q)`, `uluminosity(q)`, and `uamount(q)`.
6665
@@ -71,17 +70,17 @@ including `*`, `+`, `-`, `/`, `^`, `sqrt`, and `cbrt`.
7170
7271
- `value::T`: value of the quantity of some type `T`
7372
- `dimensions::Dimensions`: dimensions of the quantity
74-
- `valid::Bool`: whether the quantity is valid or not
7573
"""
7674
struct Quantity{T, R}
7775
value::T
7876
dimensions::Dimensions{R}
79-
valid::Bool
8077

81-
Quantity(x; kws...) = new{typeof(x), DEFAULT_DIM_TYPE}(x, Dimensions(; kws...), true)
82-
Quantity(x, valid::Bool; kws...) = new{typeof(x), DEFAULT_DIM_TYPE}(x, Dimensions(; kws...), valid)
83-
Quantity(x, ::Type{_R}, valid::Bool; kws...) where {_R} = new{typeof(x), _R}(x, Dimensions(_R; kws...), valid)
84-
Quantity(x, ::Type{_R}; kws...) where {_R} = new{typeof(x), _R}(x, Dimensions(_R; kws...), true)
85-
Quantity(x, d::Dimensions{_R}) where {_R} = new{typeof(x), _R}(x, d, true)
86-
Quantity(x, d::Dimensions{_R}, valid::Bool) where {_R} = new{typeof(x), _R}(x, d, valid)
78+
Quantity(x; kws...) = new{typeof(x), DEFAULT_DIM_TYPE}(x, Dimensions(; kws...))
79+
Quantity(x, ::Type{_R}; kws...) where {_R} = new{typeof(x), _R}(x, Dimensions(_R; kws...))
80+
Quantity(x, d::Dimensions{_R}) where {_R} = new{typeof(x), _R}(x, d)
81+
end
82+
83+
struct DimensionError{Q1,Q2} <: Exception
84+
q1::Q1
85+
q2::Q2
8786
end

src/utils.jl

+5-12
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ end
2626
Base.float(q::Quantity{T}) where {T<:AbstractFloat} = convert(T, q)
2727
Base.convert(::Type{T}, q::Quantity) where {T<:Real} =
2828
let
29-
@assert q.valid "Quantity $(q) is invalid!"
3029
@assert iszero(q.dimensions) "Quantity $(q) has dimensions! Use `ustrip` instead."
3130
return convert(T, q.value)
3231
end
@@ -37,8 +36,8 @@ Base.iszero(d::Dimensions) = @all_dimensions(iszero, d)
3736
Base.iszero(q::Quantity) = iszero(q.value)
3837
Base.getindex(d::Dimensions, k::Symbol) = getfield(d, k)
3938
Base.:(==)(l::Dimensions, r::Dimensions) = @all_dimensions(==, l, r)
40-
Base.:(==)(l::Quantity, r::Quantity) = l.value == r.value && l.dimensions == r.dimensions && l.valid == r.valid
41-
Base.isapprox(l::Quantity, r::Quantity; kws...) = isapprox(l.value, r.value; kws...) && l.dimensions == r.dimensions && l.valid == r.valid
39+
Base.:(==)(l::Quantity, r::Quantity) = l.value == r.value && l.dimensions == r.dimensions
40+
Base.isapprox(l::Quantity, r::Quantity; kws...) = isapprox(l.value, r.value; kws...) && l.dimensions == r.dimensions
4241
Base.length(::Dimensions) = 1
4342
Base.length(::Quantity) = 1
4443
Base.iterate(d::Dimensions) = (d, nothing)
@@ -68,7 +67,7 @@ Base.show(io::IO, d::Dimensions) =
6867
s = replace(s, r"\s*$" => "")
6968
print(io, s)
7069
end
71-
Base.show(io::IO, q::Quantity) = q.valid ? print(io, q.value, " ", q.dimensions) : print(io, "INVALID")
70+
Base.show(io::IO, q::Quantity) = print(io, q.value, " ", q.dimensions)
7271

7372
string_rational(x) = isinteger(x) ? string(round(Int, x)) : string(x)
7473
pretty_print_exponent(io::IO, x) = print(io, " ", to_superscript(string_rational(x)))
@@ -84,6 +83,8 @@ tryrationalize(::Type{R}, x::R) where {R} = x
8483
tryrationalize(::Type{R}, x::Union{Rational,Integer}) where {R} = convert(R, x)
8584
tryrationalize(::Type{R}, x) where {R} = isinteger(x) ? convert(R, round(Int, x)) : convert(R, rationalize(Int, x))
8685

86+
Base.showerror(io::IO, e::DimensionError) = print(io, "DimensionError: ", e.q1, " and ", e.q2, " have different dimensions")
87+
8788
"""
8889
ustrip(q::Quantity)
8990
@@ -100,14 +101,6 @@ Get the dimensions of a quantity, returning a `Dimensions` object.
100101
dimension(q::Quantity) = q.dimensions
101102
dimension(::Number) = Dimensions()
102103

103-
"""
104-
valid(q::Quantity)
105-
106-
Check if a quantity is valid. If invalid, dimensional analysis
107-
during a previous calculation failed (such as adding mass and velocity).
108-
"""
109-
valid(q::Quantity) = q.valid
110-
111104
"""
112105
ulength(q::Quantity)
113106

test/unittests.jl

+3-12
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ using Test
1313
@test typeof(x).parameters[2] == R
1414
@test ulength(x) == R(1 // 1)
1515
@test umass(x) == R(5 // 2)
16-
@test valid(x)
1716
@test ustrip(x) T(0.2)
1817
@test dimension(x) == Dimensions(R, length=1, mass=5 // 2)
1918
if R == DEFAULT_DIM_TYPE
@@ -28,30 +27,26 @@ using Test
2827
@test ulength(y) == R(2 // 1)
2928
@test umass(y) == (5 // 1)
3029
@test ustrip(y) T(0.04)
31-
@test valid(y)
3230

3331
y = x + x
3432

3533
@test ulength(y) == R(1 // 1)
3634
@test umass(y) == R(5 // 2)
3735
@test ustrip(y) T(0.4)
38-
@test valid(y)
3936

40-
y = x^2 + x
41-
42-
@test !valid(y)
4337
if R <: Rational
4438
@test string(x) == "0.2 𝐋 ¹ 𝐌 ⁵ᐟ²"
4539
@test string(inv(x)) == "5.0 𝐋 ⁻¹ 𝐌 ⁻⁵ᐟ²"
4640
end
47-
@test string(y) == "INVALID"
41+
42+
@test_throws DimensionError x^2 + x
43+
4844

4945
y = inv(x)
5046

5147
@test ulength(y) == R(-1 // 1)
5248
@test umass(y) == R(-5 // 2)
5349
@test ustrip(y) R(5)
54-
@test valid(y)
5550

5651
y = x - x
5752

@@ -68,10 +63,6 @@ using Test
6863

6964
@test y x
7065

71-
y = Quantity(T(2 // 10), R, false, length=1, mass=5 // 2)
72-
73-
@test !(y x)
74-
7566
y = Quantity(T(2 // 10), R, length=1, mass=6 // 2)
7667

7768
@test !(y x)

0 commit comments

Comments
 (0)