Skip to content

Commit dfdc3ab

Browse files
authored
Merge pull request #171 from SymbolicML/MilesCranmer/issue170
Add `ustrip(unit, quantity)`
2 parents d369989 + 605f21d commit dfdc3ab

File tree

5 files changed

+147
-1
lines changed

5 files changed

+147
-1
lines changed

README.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,12 +163,25 @@ julia> ulength(x)
163163
```
164164

165165
Finally, you can strip units with `ustrip`:
166-
166+
167167
```julia
168168
julia> ustrip(x)
169169
0.2
170170
```
171171

172+
You can also convert a quantity to a desired unit and *then* strip the units
173+
using a two-argument version of `ustrip`:
174+
175+
```julia
176+
julia> ustrip(u"km", 1000u"m")
177+
1.0
178+
179+
julia> ustrip(u"minute", 60u"s")
180+
1.0
181+
```
182+
183+
This is equivalent to `ustrip(quantity / unit)` but performs dimension checks first.
184+
172185
### Constants
173186

174187
There are a variety of physical constants accessible
@@ -298,6 +311,16 @@ julia> freezing = 32ua"degF"
298311
273.15 K
299312
```
300313

314+
These are regular `Quantity{Float64,Dimensions{...}}` objects, meaning that you
315+
can use them in the same way as regular quantities, including taking differences.
316+
317+
To convert back, you can use the two-argument `ustrip` with the particular affine unit:
318+
319+
```julia
320+
julia> ustrip(ua"degC", 295.15u"K")
321+
22.0
322+
```
323+
301324
### Arrays
302325

303326
For working with an array of quantities that have the same dimensions,
@@ -439,3 +462,4 @@ julia> @btime f($q8);
439462
julia> @btime f($q32);
440463
1.883 μs (4 allocations: 39.12 KiB)
441464
```
465+

src/affine_dimensions.jl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,23 @@ function aff_uparse(s::AbstractString)
114114
ex = AffineUnits.map_to_scope(Meta.parse(s))
115115
return eval(ex)::AffineUnit{DEFAULT_DIM_BASE_TYPE}
116116
end
117+
118+
"""
119+
ustrip(unit::AffineUnit, q::UnionAbstractQuantity)
120+
121+
Convert a quantity `q` to the numerical value in terms of affine units specified by `unit`,
122+
then strip the units. This allows getting the numerical value in terms of degrees Celsius or Fahrenheit.
123+
124+
# Examples
125+
```julia
126+
julia> ustrip(ua"degC", 27ua"degC")
127+
27.0
128+
129+
julia> ustrip(ua"degF", 300u"K")
130+
80.33000000000003
131+
```
132+
"""
133+
function ustrip(unit::AffineUnit, q::UnionAbstractQuantity)
134+
unit.basedim == dimension(q) || throw(DimensionError(unit, q))
135+
return (ustrip(q) - unit.offset) / unit.scale
136+
end

src/arrays.jl

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,33 @@ end
108108
)
109109
end
110110

111+
@inline function promote_except_value(q1::Q1, q2::QA2) where {
112+
T1,D1,T2,D2,Q1<:UnionAbstractQuantity{T1,D1},Q2,
113+
QA2<:QuantityArray{T2,N2,D2,Q2} where N2,
114+
}
115+
if D1 == D2 && constructorof(Q1) == constructorof(Q2)
116+
return (q1, q2)
117+
end
118+
Q = promote_type(Q1, Q2)
119+
D = promote_type(D1, D2)
120+
121+
# Create quantity with the properly promoted dimension type
122+
q1_converted = convert(with_type_parameters(Q, T1, D), q1)
123+
# Create a promoted QuantityArray
124+
d2 = convert(with_type_parameters(Q, T2, D), new_quantity(Q, one(T2), dimension(q2)))
125+
q2_converted = QuantityArray(ustrip(q2), d2)
126+
127+
return (q1_converted, q2_converted)
128+
end
129+
130+
@inline function promote_except_value(q1::QA1, q2::Q2) where {
131+
T1,D1,T2,D2,Q1,Q2<:UnionAbstractQuantity{T2,D2},
132+
QA1<:QuantityArray{T1,N1,D1,Q1} where N1,
133+
}
134+
q2_converted, q1_converted = promote_except_value(q2, q1)
135+
return (q1_converted, q2_converted)
136+
end
137+
111138
function Base.convert(::Type{QA}, A::QA) where {QA<:QuantityArray}
112139
return A
113140
end
@@ -125,6 +152,11 @@ end
125152

126153
@inline ustrip(A::QuantityArray) = A.value
127154
@inline ustrip(A::AbstractArray{<:UnionAbstractQuantity}) = ustrip.(A)
155+
@inline function ustrip(unit::UnionAbstractQuantity, q::QuantityArray)
156+
unit, q = promote_except_value(unit, q)
157+
dimension(unit) == dimension(q) || throw(DimensionError(unit, q))
158+
return ustrip(q) ./ ustrip(unit)
159+
end
128160
@inline dimension(A::QuantityArray) = A.dimensions
129161

130162
array_type(::Type{<:QuantityArray{T,N,D,Q,V}}) where {T,N,D,Q,V} = V

src/utils.jl

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,32 @@ Remove the units from a quantity.
388388
ustrip(::AbstractDimensions) = error("Cannot remove units from an `AbstractDimensions` object.")
389389
@inline ustrip(q) = q
390390

391+
"""
392+
ustrip(unit::UnionAbstractQuantity, q::UnionAbstractQuantity)
393+
394+
Convert quantity `q` to the units specified by `unit`, then strip the units.
395+
This is equivalent to `ustrip(q / unit)`, but also verifies the dimensions are compatible.
396+
397+
# Examples
398+
```julia
399+
julia> ustrip(u"km", 1000u"m")
400+
1.0
401+
402+
julia> ustrip(u"s", 1u"minute")
403+
60.0
404+
405+
julia> ustrip(u"km", [1000u"m", 2000u"m"])
406+
2-element Vector{Float64}:
407+
1.0
408+
2.0
409+
```
410+
"""
411+
@inline function ustrip(unit::UnionAbstractQuantity, q::UnionAbstractQuantity)
412+
unit, q = promote_except_value(unit, q)
413+
dimension(unit) == dimension(q) || throw(DimensionError(unit, q))
414+
return ustrip(q) / ustrip(unit)
415+
end
416+
391417
"""
392418
dimension(q::AbstractQuantity)
393419
dimension(q::AbstractGenericQuantity)

test/unittests.jl

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,6 +1017,41 @@ end
10171017
end
10181018
end
10191019

1020+
@testset "ustrip with unit conversion" begin
1021+
# Test Dimension/Dimension conversions
1022+
# Basic
1023+
@test ustrip(u"km", 1000u"m") == 1.0
1024+
@test ustrip(u"mm", 1000u"m") == 1000000.0
1025+
@test ustrip(u"s", 1u"minute") == 60.0
1026+
@test ustrip(u"minute", 60u"s") == 1.0
1027+
1028+
# Arrays
1029+
m_qarray = QuantityArray([1000.0, 2000.0], u"m")
1030+
@test ustrip(u"km", m_qarray) == [1.0, 2.0]
1031+
@test ustrip(u"mm", m_qarray) == [1000000.0, 2000000.0]
1032+
1033+
# Mixed dimensions
1034+
@test_throws DimensionError ustrip(u"m", u"K")
1035+
@test_throws DimensionError ustrip(u"K", u"m")
1036+
@test_throws DimensionError ustrip(u"K", u"m")
1037+
1038+
# Test with SymbolicDimensions
1039+
@test ustrip(u"km", us"m") == 0.001
1040+
@test ustrip(us"km", u"m") == 0.001
1041+
@test ustrip(us"mm", u"km") == 1000000.0
1042+
@test ustrip(us"km", 500us"km") == 500.0
1043+
1044+
# But, if no promotion, we still check dimensions symbolically!
1045+
@test_throws DimensionError ustrip(us"km", us"m")
1046+
@test_throws DimensionError ustrip(us"km", us"m")
1047+
1048+
# SymbolicDimensions arrays
1049+
m_sym_qarray = QuantityArray([1000us"km", 2000us"km"])
1050+
@test ustrip(u"m", m_sym_qarray) == [1000000.0, 2000000.0]
1051+
@test ustrip(us"km", m_sym_qarray) == [1000.0, 2000.0]
1052+
@test_throws DimensionError ustrip(us"cm", m_sym_qarray)
1053+
end
1054+
10201055
@testset "Test missing" begin
10211056
x = 1.0u"m"
10221057
@test round(Union{Int32,Missing}, FixedRational{Int32,100}(1)) isa Int32
@@ -2054,6 +2089,15 @@ end
20542089
@test sprint(show, °C) == "°C"
20552090

20562091
@test sprint(show, °F) == "°F"
2092+
2093+
# Test the two-argument ustrip method with AffineUnit
2094+
@test ustrip(ua"degC", 273.15u"K") 0.0
2095+
@test ustrip(ua"degC", 300.15u"K") 27.0
2096+
@test ustrip(ua"degF", 273.15u"K") 32.0
2097+
@test ustrip(ua"degF", 300.15u"K") 80.6
2098+
@test ustrip(ua"degC", 22ua"degC") 22.0
2099+
@test ustrip(ua"degF", 22ua"degC") 71.6
2100+
@test_throws DimensionError ustrip(ua"degC", 1.0u"m")
20572101
end
20582102

20592103
@testset "Test div" begin

0 commit comments

Comments
 (0)