|
| 1 | + |
| 2 | + |
| 3 | + |
| 4 | + |
| 5 | +[](https://symbolicml.org/DynamicQuantities.jl/dev/) |
| 6 | +[](https://github.com/SymbolicML/DynamicQuantities.jl/actions/workflows/CI.yml?query=branch%3Amain) |
| 7 | +[](https://coveralls.io/github/SymbolicML/DynamicQuantities.jl?branch=main) |
| 8 | + |
| 9 | + |
| 10 | + |
| 11 | +DynamicQuantities defines a simple statically-typed `Quantity` type for Julia. |
| 12 | +Physical dimensions are stored as a *value*, as opposed to a parametric type, as in [Unitful.jl](https://github.com/PainterQubits/Unitful.jl). |
| 13 | +This is done to allow for calculations where physical dimensions are not known at compile time. |
| 14 | + |
| 15 | +- [Performance](#performance) |
| 16 | +- [Usage](#usage) |
| 17 | +- [Types](#types) |
| 18 | +- [Vectors](#vectors) |
| 19 | + |
| 20 | +## Performance |
| 21 | + |
| 22 | +DynamicQuantities can greatly outperform Unitful |
| 23 | +when the compiler cannot infer dimensions in a function: |
| 24 | + |
| 25 | +```julia |
| 26 | +julia> using BenchmarkTools, DynamicQuantities; import Unitful |
| 27 | + |
| 28 | +julia> dyn_uni = 0.2u"m^0.5 * kg * mol^3" |
| 29 | +0.2 m¹ᐟ² kg mol³ |
| 30 | + |
| 31 | +julia> unitful = convert(Unitful.Quantity, dyn_uni) |
| 32 | +0.2 kg m¹ᐟ² mol³ |
| 33 | + |
| 34 | +julia> f(x, i) = x ^ i * 0.3; |
| 35 | + |
| 36 | +julia> @btime f($dyn_uni, 1); |
| 37 | + 8.759 ns (0 allocations: 0 bytes) |
| 38 | + |
| 39 | +julia> @btime f($unitful, 1); |
| 40 | + 30.083 μs (42 allocations: 1.91 KiB) |
| 41 | +``` |
| 42 | + |
| 43 | +**(Note the μ and n.)** |
| 44 | +Here, the DynamicQuantities quantity object allows the compiler to build a function that is type stable, |
| 45 | +while the Unitful quantity object, which stores its dimensions in the type, requires type inference at runtime. |
| 46 | + |
| 47 | +However, if the dimensions in your function *can* be inferred by the compiler, |
| 48 | +then you can get better speeds with Unitful: |
| 49 | + |
| 50 | +```julia |
| 51 | +julia> g(x) = x ^ 2 * 0.3; |
| 52 | + |
| 53 | +julia> @btime g($dyn_uni); |
| 54 | + 10.051 ns (0 allocations: 0 bytes) |
| 55 | + |
| 56 | +julia> @btime g($unitful); |
| 57 | + 2.000 ns (0 allocations: 0 bytes) |
| 58 | +``` |
| 59 | + |
| 60 | +While both of these are type stable, |
| 61 | +because Unitful parametrizes the type on the dimensions, functions can specialize |
| 62 | +to units and the compiler can optimize away units from the code. |
| 63 | + |
| 64 | +## Usage |
| 65 | + |
| 66 | +You can create a `Quantity` object |
| 67 | +by using the convenience macro `u"..."`: |
| 68 | + |
| 69 | +```julia |
| 70 | +julia> x = 0.3u"km/s" |
| 71 | +300.0 m s⁻¹ |
| 72 | + |
| 73 | +julia> y = 42 * u"kg" |
| 74 | +42.0 kg |
| 75 | + |
| 76 | +julia> room_temp = 100u"kPa" |
| 77 | +100000.0 m⁻¹ kg s⁻² |
| 78 | +``` |
| 79 | + |
| 80 | +This supports a wide range of SI base and derived units, with common |
| 81 | +prefixes. |
| 82 | + |
| 83 | +You can also construct values explicitly with the `Quantity` type, |
| 84 | +with a value and keyword arguments for the powers of the physical dimensions |
| 85 | +(`mass`, `length`, `time`, `current`, `temperature`, `luminosity`, `amount`): |
| 86 | + |
| 87 | +```julia |
| 88 | +julia> x = Quantity(300.0, length=1, time=-1) |
| 89 | +300.0 m s⁻¹ |
| 90 | +``` |
| 91 | + |
| 92 | +Elementary calculations with `+, -, *, /, ^, sqrt, cbrt, abs` are supported: |
| 93 | + |
| 94 | +```julia |
| 95 | +julia> x * y |
| 96 | +12600.0 m kg s⁻¹ |
| 97 | + |
| 98 | +julia> x / y |
| 99 | +7.142857142857143 m kg⁻¹ s⁻¹ |
| 100 | + |
| 101 | +julia> x ^ 3 |
| 102 | +2.7e7 m³ s⁻³ |
| 103 | + |
| 104 | +julia> x ^ -1 |
| 105 | +0.0033333333333333335 m⁻¹ s |
| 106 | + |
| 107 | +julia> sqrt(x) |
| 108 | +17.320508075688775 m¹ᐟ² s⁻¹ᐟ² |
| 109 | + |
| 110 | +julia> x ^ 1.5 |
| 111 | +5196.152422706632 m³ᐟ² s⁻³ᐟ² |
| 112 | +``` |
| 113 | + |
| 114 | +Each of these values has the same type, which means we don't need to perform type inference at runtime. |
| 115 | + |
| 116 | +Furthermore, we can do dimensional analysis by detecting `DimensionError`: |
| 117 | + |
| 118 | +```julia |
| 119 | +julia> x + 3 * x |
| 120 | +1.2 m¹ᐟ² kg |
| 121 | + |
| 122 | +julia> x + y |
| 123 | +ERROR: DimensionError: 0.3 m¹ᐟ² kg and 10.2 kg² s⁻² have incompatible dimensions |
| 124 | +``` |
| 125 | + |
| 126 | +The dimensions of a `Quantity` can be accessed either with `dimension(quantity)` for the entire `Dimensions` object: |
| 127 | + |
| 128 | +```julia |
| 129 | +julia> dimension(x) |
| 130 | +m¹ᐟ² kg |
| 131 | +``` |
| 132 | + |
| 133 | +or with `umass`, `ulength`, etc., for the various dimensions: |
| 134 | + |
| 135 | +```julia |
| 136 | +julia> umass(x) |
| 137 | +1//1 |
| 138 | + |
| 139 | +julia> ulength(x) |
| 140 | +1//2 |
| 141 | +``` |
| 142 | + |
| 143 | +Finally, you can strip units with `ustrip`: |
| 144 | + |
| 145 | +```julia |
| 146 | +julia> ustrip(x) |
| 147 | +0.2 |
| 148 | +``` |
| 149 | + |
| 150 | +### Unitful |
| 151 | + |
| 152 | +DynamicQuantities works with quantities that are exclusively |
| 153 | +represented by their SI base units. This gives us type stability |
| 154 | +and greatly improves performance. |
| 155 | + |
| 156 | +However, performing calculations with physical dimensions |
| 157 | +is actually equivalent to working with a standardized unit system. |
| 158 | +Thus, you can use Unitful to parse units, |
| 159 | +and then use the DynamicQuantities->Unitful extension for conversion: |
| 160 | + |
| 161 | +```julia |
| 162 | +julia> using Unitful: Unitful, @u_str; import DynamicQuantities |
| 163 | + |
| 164 | +julia> x = 0.5u"km/s" |
| 165 | +0.5 km s⁻¹ |
| 166 | + |
| 167 | +julia> y = convert(DynamicQuantities.Quantity, x) |
| 168 | +500.0 m s⁻¹ |
| 169 | + |
| 170 | +julia> y2 = y^2 * 0.3 |
| 171 | +75000.0 m² s⁻² |
| 172 | + |
| 173 | +julia> x2 = convert(Unitful.Quantity, y2) |
| 174 | +75000.0 m² s⁻² |
| 175 | + |
| 176 | +julia> x^2*0.3 == x2 |
| 177 | +true |
| 178 | +``` |
| 179 | + |
| 180 | +## Types |
| 181 | + |
| 182 | +Both a `Quantity`'s values and dimensions are of arbitrary type. |
| 183 | +By default, dimensions are stored as a `DynamicQuantities.FixedRational{Int32,C}` |
| 184 | +object, which represents a rational number |
| 185 | +with a fixed denominator `C`. This is much faster than `Rational`. |
| 186 | + |
| 187 | +```julia |
| 188 | +julia> typeof(0.5u"kg") |
| 189 | +Quantity{Float64, FixedRational{Int32, 25200} |
| 190 | +``` |
| 191 | +
|
| 192 | +You can change the type of the value field by initializing with a value |
| 193 | +explicitly of the desired type. |
| 194 | +
|
| 195 | +```julia |
| 196 | +julia> typeof(Quantity(Float16(0.5), mass=1, length=1)) |
| 197 | +Quantity{Float16, FixedRational{Int32, 25200}} |
| 198 | +``` |
| 199 | +
|
| 200 | +or by conversion: |
| 201 | +
|
| 202 | +```julia |
| 203 | +julia> typeof(convert(Quantity{Float16}, 0.5u"m/s")) |
| 204 | +Quantity{Float16, DynamicQuantities.FixedRational{Int32, 25200}} |
| 205 | +``` |
| 206 | +
|
| 207 | +For many applications, `FixedRational{Int8,6}` will suffice, |
| 208 | +and can be faster as it means the entire `Dimensions` |
| 209 | +struct will fit into 64 bits. |
| 210 | +You can change the type of the dimensions field by passing |
| 211 | +the type you wish to use as the second argument to `Quantity`: |
| 212 | +
|
| 213 | +```julia |
| 214 | +julia> using DynamicQuantities |
| 215 | + |
| 216 | +julia> R8 = DynamicQuantities.FixedRational{Int8,6}; |
| 217 | + |
| 218 | +julia> R32 = DynamicQuantities.FixedRational{Int32,2^4 * 3^2 * 5^2 * 7}; # Default |
| 219 | + |
| 220 | +julia> q8 = [Quantity(randn(), R8, length=rand(-2:2)) for i in 1:1000]; |
| 221 | + |
| 222 | +julia> q32 = [Quantity(randn(), R32, length=rand(-2:2)) for i in 1:1000]; |
| 223 | + |
| 224 | +julia> f(x) = @. x ^ 2 * 0.5; |
| 225 | + |
| 226 | +julia> @btime f($q8); |
| 227 | + 7.750 μs (1 allocation: 15.75 KiB) |
| 228 | + |
| 229 | +julia> @btime f($q32); |
| 230 | + 8.417 μs (2 allocations: 39.11 KiB) |
| 231 | +``` |
| 232 | +
|
| 233 | +## Vectors |
| 234 | +
|
| 235 | +There is not a separate class for vectors, but you can create units |
| 236 | +like so: |
| 237 | +
|
| 238 | +```julia |
| 239 | +julia> randn(5) .* u"m/s" |
| 240 | +5-element Vector{Quantity{Float64, DynamicQuantities.FixedRational{Int32, 25200}}}: |
| 241 | + 1.1762086954956399 m s⁻¹ |
| 242 | + 1.320811324040591 m s⁻¹ |
| 243 | + 0.6519033652437799 m s⁻¹ |
| 244 | + 0.7424822374423569 m s⁻¹ |
| 245 | + 0.33536928068133726 m s⁻¹ |
| 246 | +``` |
| 247 | +
|
| 248 | +Because it is type stable, you can have mixed units in a vector too: |
| 249 | +
|
| 250 | +```julia |
| 251 | +julia> v = [Quantity(randn(), mass=rand(0:5), length=rand(0:5)) for _=1:5] |
| 252 | +5-element Vector{Quantity{Float64, DynamicQuantities.FixedRational{Int32, 25200}}}: |
| 253 | + 0.4309293892461158 kg⁵ |
| 254 | + 1.415520139801276 |
| 255 | + 1.2179414706524276 m³ kg⁴ |
| 256 | + -0.18804207255117408 m³ kg⁵ |
| 257 | + 0.52123911329638 m³ kg² |
| 258 | +``` |
| 259 | +
|
0 commit comments