Happy to share DynamicQuantities.jl v0.7.0!

The package has had some major changes over the last few versions, so I think it deserved a new post.

**DynamicQuantities defines a simple statically-typed Quantity type for storing physical units.**

Physical dimensions are stored as a *value*, as opposed to a parametric type, as in Unitful.jl. This is done to allow for calculations where physical dimensions are not known at compile time.

Changes since the last post (described more below)

- Performance improvements (via better compiler inlining)
- Units, and unit parsing via the
`@u_str`

macro - Physical constants, and constant parsing via the
`Constants.*`

prefix (also in`@u_str`

) `SymbolicDimensions`

for working with symbolic units and constants- Standard
`Dimensions`

will eagerly convert to SI units; this avoids it. - Symbolic unit/constant parsing via the
`@us_str`

macro.

- Standard
`QuantityArray <: AbstractArray`

for efficiently storing arrays of quantities that have the same units- Extensions for ScientificTypes.jl, LinearAlgebra.jl, as well as an extension to convert to/from Unitful.jl quantities
`AbstractQuantity`

and`AbstractDimensions`

for extending behavior or defining custom spaces of physical dimensions

Thanks to contributions from Gaurav Arya @odow @Oscar_Smith @j-fu, and suggestions/feedback from @non-Jedi @ChrisRackauckas @mcabbott and many others in the last thread and on GitHub!

## Performance

DynamicQuantities can greatly outperform Unitful when the compiler cannot infer dimensions in a function:

```
julia> using BenchmarkTools, DynamicQuantities; import Unitful
julia> dyn_uni = 0.2u"m^0.5 * kg * mol^3"
0.2 mĀ¹įĀ² kg molĀ³
julia> unitful = convert(Unitful.Quantity, dyn_uni)
0.2 kg mĀ¹įĀ² molĀ³
julia> f(x, i) = x ^ i * 0.3;
julia> @btime f($dyn_uni, 1);
2.750 ns (0 allocations: 0 bytes)
julia> @btime f($unitful, 1);
2.718 Ī¼s (30 allocations: 1.34 KiB)
```

**Note the Ī¼ and n: this is a 1000x speedup.** Here, the DynamicQuantities quantity object allows the compiler to build a function that is type stable, while the Unitful quantity object, which stores its dimensions in the type, requires type inference at runtime.

However, if the dimensions in your function *can* be inferred by the compiler, then you can get better speeds with Unitful:

```
julia> g(x) = x ^ 2 * 0.3;
julia> @btime g($dyn_uni);
1.875 ns (0 allocations: 0 bytes)
julia> @btime g($unitful);
1.500 ns (0 allocations: 0 bytes)
```

While both of these are type stable, because Unitful parametrizes the type on the dimensions, functions can specialize to units and the compiler can optimize away units from the code.

**Aside**: The compiler seems to be pretty good at inlining things especially with the recent update (thanks to Gaurav Arya for helping get this through!), so this performance gap seems to have shrunk for even statically typed calculations. This above calculation used to be 5x in favor of Unitful. However, this depends on compiler constant propagation so it is calculation dependent how big this gap would be.

## Types

At the heart of the package is just two immutable structs:

```
struct Dimensions{R<:Real} <: AbstractDimensions{R}
length::R
mass::R
time::R
current::R
temperature::R
luminosity::R
amount::R
end
struct Quantity{T,D<:AbstractDimensions} <: AbstractQuantity{T,D}
value::T
dimensions::D
end
```

The `R`

type here is typically a rational-like number (the default is an internal `FixedRational`

type ā a rational number with fixed denominator which gives much faster operations than `Rational`

).

Whatās nice about the abstract interface is you can just write a custom `AbstractDimensions`

type with the physical dimensions you want to use, and via the use of @oxinaboxās Tricks.jl, `static_fieldnames`

is used to compile all of the unit propagation methods at first call. So e.g., `struct MyDimensions{R} <: AbstractDimensions{R}; length::R; time::R; end`

would work out of the box! (`Quantity(0.3, MyDimensions, length=1, mass=-1)`

would then be `0.3 m/s`

, and you could calculate away)

## Generic usage

You can create a `Quantity`

object by using the convenience macro `u"..."`

:

```
julia> x = 0.3u"km/s"
300.0 m sā»Ā¹
julia> room_temp = 100u"kPa"
100000.0 mā»Ā¹ kg sā»Ā²
```

This supports a wide range of SI base and derived units, with common prefixes.

You can also construct values explicitly with the `Quantity`

type, with a value and keyword arguments for the powers of the physical dimensions (`mass`

, `length`

, `time`

, `current`

, `temperature`

, `luminosity`

, `amount`

):

```
julia> x = Quantity(300.0, length=1, time=-1)
300.0 m sā»Ā¹
```

Elementary calculations with `+, -, *, /, sqrt, cbrt, abs`

are supported, and `^`

will use `rationalize`

to get a reasonable power from exponentiation:

```
julia> x * y
12600.0 m kg sā»Ā¹
julia> x / y
7.142857142857143 m kgā»Ā¹ sā»Ā¹
julia> x ^ 3
2.7e7 mĀ³ sā»Ā³
julia> x ^ -1
0.0033333333333333335 mā»Ā¹ s
julia> sqrt(x)
17.320508075688775 mĀ¹įĀ² sā»Ā¹įĀ²
julia> x ^ 1.5
5196.152422706632 mĀ³įĀ² sā»Ā³įĀ²
```

Each of these values has the same type, which means we donāt need to perform type inference at runtime.

Furthermore, we can do dimensional analysis by detecting `DimensionError`

:

```
julia> x + 3 * x
1.2 mĀ¹įĀ² kg
julia> x + y
ERROR: DimensionError: 0.3 mĀ¹įĀ² kg and 10.2 kgĀ² sā»Ā² have incompatible dimensions
```

The dimensions of a `Quantity`

can be accessed either with `dimension(quantity)`

for the entire `Dimensions`

object:

```
julia> dimension(x)
mĀ¹įĀ² kg
```

or with `umass`

, `ulength`

, etc., for the various dimensions:

```
julia> umass(x)
1//1
julia> ulength(x)
1//2
```

Finally, you can strip units with `ustrip`

:

```
julia> ustrip(x)
0.2
```

### Constants

There are a variety of physical constants accessible

via the `Constants`

submodule:

```
julia> Constants.c
2.99792458e8 m sā»Ā¹
```

These can also be used inside the `u"..."`

macro:

```
julia> u"Constants.c * Hz"
2.99792458e8 m sā»Ā²
```

For the full list, see the docs.

### Symbolic Units

You can also choose to not eagerly convert to SI base units, instead leaving the units as the user had written them. For example:

```
julia> q = 100us"cm * kPa"
100.0 cm kPa
julia> q^2
10000.0 cmĀ² kPaĀ²
```

You can convert to regular SI base units with `expand_units`

:

```
julia> expand_units(q^2)
1.0e6 kgĀ² sā»ā“
```

This also works with constants:

```
julia> x = us"Constants.c * Hz"
1.0 Hz c
julia> x^2
1.0 HzĀ² cĀ²
julia> expand_units(x^2)
8.987551787368176e16 mĀ² sā»ā“
```

This dimensions type works a bit differently as it stores all the dimensions in a sparse vector (source for the curious). All unit calculations are performed as operations on this sparse vector (this is convenient for user interfaces, and still reasonably fast, but not as fast as the regular immutable `Dimensions`

).

### Arrays

For working with an array of quantities that have the same dimensions, you can use a `QuantityArray`

:

```
julia> ar = QuantityArray(rand(3), u"m/s")
3-element QuantityArray(::Vector{Float64}, ::Quantity{Float64, Dimensions{DynamicQuantities.FixedRational{Int32, 25200}}}):
0.2729202669351497 m sā»Ā¹
0.992546340360901 m sā»Ā¹
0.16863543422972482 m sā»Ā¹
```

This `QuantityArray`

is a subtype `<:AbstractArray{Quantity{Float64,Dimensions{...}},1}`

,

meaning that indexing a specific element will return a `Quantity`

:

```
julia> ar[2]
0.992546340360901 m sā»Ā¹
julia> ar[2] *= 2
1.985092680721802 m sā»Ā¹
julia> ar[2] += 0.5u"m/s"
2.485092680721802 m sā»Ā¹
```

Which performs dimension checks.

This has a custom broadcasting interface which allows the compiler to avoid redundant dimension calculations and dimensional analysis, relative to if you had simply used an array of quantities:

```
julia> f(v) = v^2 * 1.5;
julia> @btime $f.(xa) setup=(xa = randn(100000) .* u"km/s");
109.500 Ī¼s (2 allocations: 3.81 MiB)
julia> @btime $f.(qa) setup=(xa = randn(100000) .* u"km/s"; qa = QuantityArray(xa));
50.917 Ī¼s (3 allocations: 781.34 KiB)
```

So we can see the `QuantityArray`

version saves on both time and memory.

### Unitful

DynamicQuantities allows you to convert back and forth from Unitful.jl:

```
julia> import Unitful; import DynamicQuantities
julia> x = 0.5Unitful.u"km/s"
0.5 km sā»Ā¹
julia> y = convert(DynamicQuantities.Quantity, x)
500.0 m sā»Ā¹
julia> y2 = y^2 * 0.3
75000.0 mĀ² sā»Ā²
julia> x2 = convert(Unitful.Quantity, y2)
75000.0 mĀ² sā»Ā²
julia> x^2*0.3 == x2
true
```

### Other

One other thing to mention, which is actually part of the reason I started working on this package: SymbolicRegression.jl now supports dimensional constraints as part of equation discovery searches! This feature required the creation of this package, as expressions are runtime generated so Unitful.jl could not be used efficiently. Example here: Examples Ā· SymbolicRegression.jl