FlexUnits v0.5.0 is a major new release, aimed at supporting two features:
-
Unit simplification
-
Logarithmic units
Unit Simplification
What Unitful does
Unitful.jl propagates units directly. When performing a calculation, it records all the unit symbols and objects and produces a new unit type with all the symbols concatenated. This result is useful for small operations:
using Unitful
julia> r = 5u"V"/2u"A" #Ohms is better but this will do
2.5 V A^-1
However, for longer operations, the results can be unsightly and difficult to reason about, this is why upreferred exists, which gives a dimensional view
julia> p = (1.0u"kg/L" * 9.81u"m/s^2" * 25u"cm") #This is supposed to be a pressure
245.25 kg cm m L^-1 s^-2
julia> upreferred(p) #This is a bit better
2452.5 kg m^-1 s^-2
What FlexUnits does by default
FlexUnits.jl and DynamicQuantities.jl, by default will give you a dimensional view of the result (i.e. upreferred). This gives better results for long operations:
using FlexUnits, .UnitRegistry
julia> p = (1.0u"kg/L" * 9.81u"m/s^2" * 25u"cm")
2452.5 kg/(m s²)
But the results are a lot harder to understand for some units (electrical units are particularly challenging)
julia> r = 5u"V"/2u"A" #Electrical units are opaque
2.5 (m² kg)/(s³ A²)
FlexUnits simplify
FlexUnits now has a simplify function that performs a greedy search algorithm to minimize the number of symbols displayed. This approach is more likely yield understandable results than both alternatives (unit-propagation and dimensional-view).
julia> p = (1.0u"kg/L" * 9.81u"m/s^2" * 25u"cm") |> simplify #This is exactly what I wanted
2452.5 Pa
julia r = 5u"V"/2u"A" |> simplify #And so is this
2.5 Ω
Moreover, if you don’t like typing |> simplify all the time, you can set the display_simplified_units setting
display_simplified_units(true) #Show simplified results
julia> p = (1.0u"kg/L" * 9.81u"m/s^2" * 25u"cm")
2452.5 Pa
julia> r = 5u"V"/2u"A"
2.5 Ω
You can also change the units that the simplification algorithm uses in its search process. You can only have one unit per dimension, so if your unit clashes with an existing unit in that dimension, your unit will replace it.
set_preferred_unit(u"psi") #Simplified pressures are now in PSI
julia> p = (1.0u"kg/L" * 9.81u"m/s^2" * 25u"cm")
0.35570491213617295 psi
Gotchas with display_simplified_units(true)
Simplified units are not displayed by default mainly because WARNING, display_simplified_units(true) does not actually simplify the result. It only displays the result you would get if you did simplify it. Setting simplified units by default could give you unexpected answers for ustrip if your preferred units are not SI.
#Recall p was shown in PSI, but it's actually still in kg/(m s²)
julia ustrip(p)
2452.5
Setting display_simplified_units(true) can also produce errors if you’re displaying quantities and units that don’t subscribe to the default Dimensions object (or its static derivatives).
FlexUnits simplify performance
The greedy algorithm used by simplify does require significant resources to run, and is easily 10x more expensive than Unitful’s unit tracking. However, unlike Unitful’s tracking, simplify does not run automatically during calculations (neither at compile nor run time). Even when display_simplified_units(true) is called, the simplify algorithm only runs when displaying results; this isn’t very noticeable because display is rarely called repeatedly and it already has significant overhead anyway.
Logarithmic Units, the Simple Way
Handling logarithmic units such as decibels comes with some level of controversy. The question is whether a quantity such as 1 dB(W) is a logarithm of a power quantity or merely the logarithmic representation of a power quantity.
-
If
1 dB(W)is a logarithmic representation of a power quantity:1 dB(W) + 1 dB(W) = 4.0103 dB(W) -
If
1 dB(W)is a an actual logarithm of a power quantity:1 dB(W) + 1 dB = 2 dB(W²)
FlexUnits consistently adopts the philosophy of the second camp, where a decibel represents the logarithm of a quantity and supplies algebraic tools to manipulate logarithms of quantities (referred to as a LogQuant). Taking this consistent approach makes logarithmic units simpler in FlexUnits than Unitful, as the latter can take either philosophy depending on whether your decibel level is unitless or has units.
If you truly believe that 1 dB(W) + 1 dB(W) = 4.0103 dB(W), fear not! FlexUnits has you covered with the semiring notational convention with \oplus and \ominus symbols that apply the following operations
-
x ⊕ y = log(exp(x) + exp(y))
-
x ⊖ y = log(exp(x) - exp(y))
Since exp of a logarithmic quantity is a linear quantity, ⊕ ⊖ simply apply addition/subtraction to the linear quantities and convert the result back to log-space.
Producing logarithmic quantities
One way to produce a LogQuant is by taking a log of a quantity.
julia> q = log(2u"W")
log(2.0 (m² kg)/s³)
Another way is to multiply a number by a logarithmic unit. For example, dB is a LogScale object that can be imported to construct a logarithmic unit
import FlexUnits.dB
julia> q = 30dB(u"W")
log(1000.0000000000016 (m² kg)/s³)
As you can see here, 30 dB(W) is equivalent to 1000 W but it displayed as its logarithm. This helps reinforce how operations are performed based on logarithmic identities. While their logarithms are displayed (to emphasize this algebra), the actual numerical value stored is the logarithmic form
julia> ustrip(log(2u"W"))
0.6931471805599453
Operations on logarithmic quantities
The algebraic rules LogQuant are centered around logarithmic identities.
julia> log(4u"m") + log(4u"s") # log(x) + log(y) = log(x*y)
log(15.999999999999998 (m s))
julia> log(4u"m") - log(4u"s") # log(x) - log(y) = log(x/y)
log(1.0 m/s)
julia> 2log(4u"m") # nlog(x) = log(x^n)
log(15.999999999999998 m²)
We also make use of the ⊕ and ⊖ operators that, in this context, commonly refers to adding/subtracting linearized values and transforming back to log space. It’s not exported by default as this symbol could be used by other packages to mean something else.
import FlexUnits: ⊕, ⊖
julia> log(8u"m") ⊕ log(4u"m") #Observe that the linear addition happened
log(12.0 m)
julia> log(8u"m") ⊖ log(4u"m") #Observe that linear subtraction happened
log(3.9999999999999982 m)
Logarithmic quantities can be converted back to regular quantities using quantity, linquant, ubase or exp
julia> (linquant(log(4u"m")), quantity(log(4u"m")), exp(log(4u"m")), ubase(log(4u"m")))
(4.0 m, 4.0 m, 4.0 m, 4.0 m)
Simplification
Simplification on logarithmic units produces decibels by default
display_simplified_units(true)
julia> log(1u"W") + log(1u"W")
0.0 dB(W²)
julia> 1dB(u"W") + 1dB(u"W")
2.0 dB(W²)
You can change this setting to Nepers if you prefer
import FlexUnits.Np
set_preferred_logscale(Np)
julia> 1dB(u"W") + 1dB(u"W")
0.4605170185988092 Np(W²)
julia> 1Np(u"W") + 1Np(u"W")
2.0 Np(W²)
Registering logarithmic units
The default unit registry can only register affine units, which is also usually sufficient for working logarithmic units unless you need to parse strings to produce logarithmic units. In such cases, you will need to register logarithmic units with the LogUnitRegistry instead. This registry can hold both affine and logarithmic units, but uparse can introduce performance issues because the output is a Union. WARNING, because multiplying uparse outputs can produce a Quantity or a LogQuant, based on the string value, it’s recommended that you use explicit constructors like quantity or ubase to always produce linear quantities, or logquant or logubase always produce logarithmic units.
using FlexUnits, .LogUnitRegistry
import FlexUnits.dB
register_unit("dB_V" => dB(u"V"))
julia> 10uparse("dB_V")
log(10.000000000000002 (m² kg)/(s³ A))
julia> 10uparse("V")
10.0 (m² kg)/(s³ A)
julia> ubase(10, uparse("dB_V"))
10.000000000000002 (m² kg)/(s³ A)
julia> logubase(10, uparse("V"))
log(10.000000000000002 (m² kg)/(s³ A))
julia> 10u"dB_V"
log(10.000000000000002 (m² kg)/(s³ A))