Skip to content

Create in-house units module #22

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 31 commits into from
Jun 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
5b55d2a
Add internal units module
MilesCranmer Jun 12, 2023
cbe5754
Export `uparse` and `u_str`
MilesCranmer Jun 12, 2023
12ffd98
Print SI units rather than dimension
MilesCranmer Jun 12, 2023
b35c7ac
Don't print power if == 1
MilesCranmer Jun 12, 2023
f68f87b
Additional conversion utilities
MilesCranmer Jun 12, 2023
1709941
Define additional time units
MilesCranmer Jun 12, 2023
221e8ef
Add unit tests for in-house unit module
MilesCranmer Jun 12, 2023
542076f
Clean up conversion utility
MilesCranmer Jun 12, 2023
416bf1a
Change hour to h
MilesCranmer Jun 12, 2023
5a53f65
Fix prefix of `Pa` unit
MilesCranmer Jun 12, 2023
025a825
Remove `wk` unit as rarely used
MilesCranmer Jun 12, 2023
f7869f4
Update README to parsing-based constructor
MilesCranmer Jun 12, 2023
92acc39
Add kPa unit
MilesCranmer Jun 12, 2023
4d56554
Fix u_str docstring
MilesCranmer Jun 12, 2023
7cb0143
Add more common but non-SI units
MilesCranmer Jun 12, 2023
7beb061
Add eV as unit
MilesCranmer Jun 12, 2023
45748b2
Clean up README
MilesCranmer Jun 12, 2023
037a275
Permit identity unit to be parsed
MilesCranmer Jun 12, 2023
28cdeb8
Use Rational{Int} as default type for units
MilesCranmer Jun 13, 2023
760ed42
Remove eV and test type stability across units
MilesCranmer Jun 13, 2023
22740ca
Fix behavior of ^ for integer power
MilesCranmer Jun 13, 2023
638066d
Fix type stability of units with promote(::Int, ::FixedRational)
MilesCranmer Jun 13, 2023
8d23d7b
Revert unit type back to Float64
MilesCranmer Jun 13, 2023
243017d
Test additional type stability
MilesCranmer Jun 13, 2023
e40c32c
Remove duplicate code in promotions
MilesCranmer Jun 13, 2023
3ea8992
Add docstrings to units
MilesCranmer Jun 14, 2023
755e497
Backquotes on variants in unit docstrings
MilesCranmer Jun 14, 2023
eb541ed
More documentation
MilesCranmer Jun 14, 2023
e74a296
Greatly expand docs
MilesCranmer Jun 14, 2023
7f93c4c
Improve docs on `FixedRational`
MilesCranmer Jun 14, 2023
37de814
Describe Float64 conversion explicitly
MilesCranmer Jun 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 65 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ This is done to allow for calculations where physical dimensions are not known a

- [Performance](#performance)
- [Usage](#usage)
- [Units](#units)
- [Types](#types)
- [Vectors](#vectors)

Expand All @@ -26,8 +25,8 @@ when the compiler cannot infer dimensions in a function:
```julia
julia> using BenchmarkTools, DynamicQuantities; import Unitful

julia> dyn_uni = Quantity(0.2, mass=1, length=0.5, amount=3)
0.2 𝐋 ¹ᐟ² 𝐌 ¹ 𝐍 ³
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³
Expand Down Expand Up @@ -64,56 +63,71 @@ to units and the compiler can optimize away units from the code.

## Usage

You can create a `Quantity` object with a value and keyword arguments for the powers of the physical dimensions
(`mass`, `length`, `time`, `current`, `temperature`, `luminosity`, `amount`):
You can create a `Quantity` object
by using the convenience macro `u"..."`:

```julia
julia> x = Quantity(0.3, mass=1, length=0.5)
0.3 𝐋 ¹ᐟ² 𝐌 ¹
julia> x = 0.3u"km/s"
300.0 m s⁻¹

julia> y = 42 * u"kg"
42.0 kg

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> y = Quantity(10.2, mass=2, time=-2)
10.2 𝐌 ² 𝐓 ⁻²
```julia
julia> x = Quantity(300.0, length=1, time=-1)
300.0 m s⁻¹
```

Elementary calculations with `+, -, *, /, ^, sqrt, cbrt` are supported:
Elementary calculations with `+, -, *, /, ^, sqrt, cbrt, abs` are supported:

```julia
julia> x * y
3.0599999999999996 𝐋 ¹ᐟ² 𝐌 ³ 𝐓 ⁻²
12600.0 m kg s⁻¹

julia> x / y
0.029411764705882353 𝐋 ¹ᐟ² 𝐌 ⁻¹ 𝐓 ²
7.142857142857143 m kg⁻¹ s⁻¹

julia> x ^ 3
0.027 𝐋 ³ᐟ² 𝐌 ³
2.7e7 m³ s⁻³

julia> x ^ -1
3.3333333333333335 𝐋 ⁻¹ᐟ² 𝐌 ⁻¹
0.0033333333333333335 m⁻¹ s

julia> sqrt(x)
0.5477225575051661 𝐋 ¹ᐟ⁴ 𝐌 ¹ᐟ²
17.320508075688775 m¹ᐟ² s⁻¹ᐟ²

julia> x ^ 1.5
0.1643167672515498 𝐋 ³ᐟ⁴ 𝐌 ³ᐟ²
5196.152422706632 m³ᐟ² s⁻³ᐟ²
```

Each of these values has the same type, thus obviating the need for type inference at runtime.
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
julia> x + 3 * x
1.2 𝐋 ¹ᐟ² 𝐌 ¹
1.2 m¹ᐟ² kg

julia> x + y
ERROR: DimensionError: 0.3 𝐋 ¹ᐟ² 𝐌 ¹ and 10.2 𝐌 ² 𝐓 ⁻² have different dimensions
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
julia> dimension(x)
𝐋 ¹ᐟ² 𝐌 ¹
m¹ᐟ² kg
```

or with `umass`, `ulength`, etc., for the various dimensions:
Expand All @@ -133,26 +147,28 @@ julia> ustrip(x)
0.2
```

## Units
### Unitful

DynamicQuantities works with quantities that are exclusively
represented by their SI base units. This gives us type stability
and greatly improves performance.

DynamicQuantities works with quantities which store physical dimensions and a value,
and does not directly provide a unit system.
However, performing calculations with physical dimensions
is actually equivalent to working with a standardized unit system.
Thus, you can use Unitful to parse units,
and then use the DynamicQuantities->Unitful extension for conversion:

```julia
julia> using Unitful: Unitful, @u_str
julia> using Unitful: Unitful, @u_str; import DynamicQuantities

julia> x = 0.5u"km/s"
0.5 km s⁻¹

julia> y = convert(DynamicQuantities.Quantity, x)
500.0 𝐋 ¹ 𝐓 ⁻¹
500.0 m s⁻¹

julia> y2 = y^2 * 0.3
75000.0 𝐋 ² 𝐓 ⁻²
75000.0 m² s⁻²

julia> x2 = convert(Unitful.Quantity, y2)
75000.0 m² s⁻²
Expand All @@ -163,24 +179,31 @@ true

## Types

Both the `Quantity`'s values and dimensions are of arbitrary type.
Both a `Quantity`'s values and dimensions are of arbitrary type.
By default, dimensions are stored as a `DynamicQuantities.FixedRational{Int32,C}`
object, which represents a rational number
with a fixed denominator `C`. This is much faster than `Rational`.

```julia
julia> typeof(Quantity(0.5, mass=1))
julia> typeof(0.5u"kg")
Quantity{Float64, FixedRational{Int32, 25200}
```

You can change the type of the value field by initializing with a value
of the desired type.
explicitly of the desired type.

```julia
julia> typeof(Quantity(Float16(0.5), mass=1, length=1))
Quantity{Float16, FixedRational{Int32, 25200}}
```

or by conversion:

```julia
julia> typeof(convert(Quantity{Float16}, 0.5u"m/s"))
Quantity{Float16, DynamicQuantities.FixedRational{Int32, 25200}}
```

For many applications, `FixedRational{Int8,6}` will suffice,
and can be faster as it means the entire `Dimensions`
struct will fit into 64 bits.
Expand Down Expand Up @@ -213,23 +236,23 @@ There is not a separate class for vectors, but you can create units
like so:

```julia
julia> randn(5) .* Dimensions(mass=2/5, length=2)
5-element Vector{Quantity{Float64, FixedRational{Int32, 25200}}}:
-0.6450221578668845 𝐋 ² 𝐌 ²ᐟ⁵
0.4024829670050946 𝐋 ² 𝐌 ²ᐟ⁵
0.21478863605789672 𝐋 ² 𝐌 ²ᐟ⁵
0.0719774550969669 𝐋 ² 𝐌 ²ᐟ⁵
-1.4231241943420674 𝐋 ² 𝐌 ²ᐟ⁵
julia> randn(5) .* u"m/s"
5-element Vector{Quantity{Float64, DynamicQuantities.FixedRational{Int32, 25200}}}:
1.1762086954956399 m s⁻¹
1.320811324040591 m s⁻¹
0.6519033652437799 m s⁻¹
0.7424822374423569 m s⁻¹
0.33536928068133726 m s⁻¹
```

Because it is type stable, you can have mixed units in a vector too:

```julia
julia> v = [Quantity(randn(), mass=rand(0:5), length=rand(0:5)) for _=1:5]
5-element Vector{Quantity{Float64, FixedRational{Int32, 25200}}}:
2.2054411324716865 𝐌 ³
-0.01603602425887379 𝐋 ⁴ 𝐌 ³
1.4388184352393647
2.382303019892503 𝐋 ² 𝐌 ¹
0.6071392594021706 𝐋 ⁴ 𝐌 ⁴
5-element Vector{Quantity{Float64, DynamicQuantities.FixedRational{Int32, 25200}}}:
0.4309293892461158 kg⁵
1.415520139801276
1.2179414706524276 m³ kg⁴
-0.18804207255117408 m³ kg⁵
0.52123911329638 m³ kg²
```
1 change: 0 additions & 1 deletion docs/Project.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
[deps]
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
DynamicQuantities = "06fc5a27-2a28-4c7c-a15d-362465fb6821"
3 changes: 2 additions & 1 deletion docs/make.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using DynamicQuantities
import DynamicQuantities.Units
using Documenter

DocMeta.setdocmeta!(DynamicQuantities, :DocTestSetup, :(using DynamicQuantities); recursive=true)
Expand Down Expand Up @@ -26,7 +27,7 @@ open(dirname(@__FILE__) * "/src/index.md", "w") do io
end

makedocs(;
modules=[DynamicQuantities],
modules=[DynamicQuantities, DynamicQuantities.Units],
authors="MilesCranmer <miles.cranmer@gmail.com> and contributors",
repo="https://github.com/SymbolicML/DynamicQuantities.jl/blob/{commit}{path}#{line}",
sitename="DynamicQuantities.jl",
Expand Down
90 changes: 83 additions & 7 deletions docs/src/api.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,91 @@
```@meta
CurrentModule = DynamicQuantities
# Usage

## Types

```@docs
Quantity
Dimensions
```

## Utilities

The two main general utilities for working
with quantities are `ustrip` and `dimension`:

```@docs
ustrip
dimension
```

# API Reference
### Accessing dimensions

API Reference for [DynamicQuantities](https://github.com/SymbolicML/DynamicQuantities.jl).
Utility functions to extract specific dimensions are as follows:

```@index
```@docs
ulength
umass
utime
ucurrent
utemperature
uluminosity
uamount
```

```@autodocs
Modules = [DynamicQuantities]
Order = [:type, :function]
```
Pages = ["utils.jl"]
Filter = t -> !(t in [ustrip, dimension, ulength, umass, utime, ucurrent, utemperature, uluminosity, uamount])
```

## Units

The two main functions for working with units are `uparse` and `u_str`:

```@docs
@u_str
uparse
```

### Available units

The base SI units are as follows.
Instead of calling directly, it is recommended to access them via
the `@u_str` macro, which evaluates the expression
in a namespace with all the units available.

```@docs
Units.m
Units.g
Units.s
Units.A
Units.K
Units.cd
Units.mol
```

Several derived SI units are available as well:

```@docs
Units.Hz
Units.N
Units.Pa
Units.J
Units.W
Units.C
Units.V
Units.F
Units.Ω
Units.T
Units.L
Units.bar
```

## Internals

### FixedRational

```@docs
DynamicQuantities.FixedRational
DynamicQuantities.denom
```

3 changes: 3 additions & 0 deletions src/DynamicQuantities.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ module DynamicQuantities

export Quantity, Dimensions, DimensionError, ustrip, dimension, valid
export ulength, umass, utime, ucurrent, utemperature, uluminosity, uamount
export uparse, @u_str

include("fixed_rational.jl")
include("types.jl")
include("utils.jl")
include("math.jl")
include("units.jl")

import Requires: @init, @require
import .Units: uparse, @u_str

if !isdefined(Base, :get_extension)
@init @require Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" include("../ext/DynamicQuantitiesUnitfulExt.jl")
Expand Down
11 changes: 10 additions & 1 deletion src/fixed_rational.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
A rational number with a fixed denominator. Significantly
faster than `Rational{T}`, as it never needs to compute
the `gcd` apart from when printing.
Access the denominator with `denom(F)` (which converts to `T`).

# Fields

- `num`: numerator of type `T`. The denominator is fixed to the type parameter `den`.
"""
struct FixedRational{T<:Integer,den} <: Real
num::T
Expand Down Expand Up @@ -35,14 +40,18 @@ Base.inv(x::F) where {F<:FixedRational} = unsafe_fixed_rational(widemul(denom(F)

Base.:(==)(x::F, y::F) where {F<:FixedRational} = x.num == y.num
Base.iszero(x::FixedRational) = iszero(x.num)
Base.isone(x::F) where {F<:FixedRational} = x.num == denom(F)
Base.isinteger(x::F) where {F<:FixedRational} = iszero(x.num % denom(F))
Base.convert(::Type{F}, x::Integer) where {F<:FixedRational} = unsafe_fixed_rational(x * denom(F), eltype(F), val_denom(F))
Base.convert(::Type{F}, x::Rational) where {F<:FixedRational} = F(x)
Base.convert(::Type{Rational{R}}, x::F) where {R,F<:FixedRational} = Rational{R}(x.num, denom(F))
Base.convert(::Type{Rational}, x::F) where {F<:FixedRational} = Rational{eltype(F)}(x.num, denom(F))
Base.convert(::Type{AF}, x::F) where {AF<:AbstractFloat,F<:FixedRational} = convert(AF, x.num) / convert(AF, denom(F))
Base.round(::Type{T}, x::F) where {T,F<:FixedRational} = div(convert(T, x.num), convert(T, denom(F)), RoundNearest)
Base.promote(x::Integer, y::F) where {F<:FixedRational} = (F(x), y)
Base.promote(x::F, y::Integer) where {F<:FixedRational} = reverse(promote(y, x))
Base.promote(x, y::F) where {F<:FixedRational} = promote(x, convert(Rational, y))
Base.promote(x::F, y) where {F<:FixedRational} = promote(convert(Rational, x), y)
Base.promote(x::F, y) where {F<:FixedRational} = reverse(promote(y, x))
Base.show(io::IO, x::F) where {F<:FixedRational} = show(io, convert(Rational, x))
Base.zero(::Type{F}) where {F<:FixedRational} = unsafe_fixed_rational(0, eltype(F), val_denom(F))

Expand Down
9 changes: 6 additions & 3 deletions src/math.jl
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@ Base.:/(l::Number, r::Dimensions) = Quantity(l, inv(r))
Base.:+(l::Quantity, r::Quantity) = dimension(l) == dimension(r) ? Quantity(l.value + r.value, l.dimensions) : throw(DimensionError(l, r))
Base.:-(l::Quantity, r::Quantity) = dimension(l) == dimension(r) ? Quantity(l.value - r.value, l.dimensions) : throw(DimensionError(l, r))

_pow(l::Dimensions{R}, r::R) where {R} = @map_dimensions(Base.Fix1(*, r), l)
_pow(l::Quantity{T,R}, r::R) where {T,R} = Quantity(l.value^convert(T, r), _pow(l.dimensions, r))
_pow(l::Dimensions, r) = @map_dimensions(Base.Fix1(*, r), l)
_pow(l::Quantity{T}, r) where {T} = Quantity(l.value^r, _pow(l.dimensions, r))
_pow_as_T(l::Quantity{T}, r) where {T} = Quantity(l.value^convert(T, r), _pow(l.dimensions, r))
Base.:^(l::Dimensions{R}, r::Integer) where {R} = _pow(l, r)
Base.:^(l::Dimensions{R}, r::Number) where {R} = _pow(l, tryrationalize(R, r))
Base.:^(l::Quantity{T,R}, r::Number) where {T,R} = _pow(l, tryrationalize(R, r))
Base.:^(l::Quantity{T,R}, r::Integer) where {T,R} = _pow(l, r)
Base.:^(l::Quantity{T,R}, r::Number) where {T,R} = _pow_as_T(l, tryrationalize(R, r))

Base.inv(d::Dimensions) = @map_dimensions(-, d)
Base.inv(q::Quantity) = Quantity(inv(q.value), inv(q.dimensions))
Expand Down
Loading