Toy Examples with Code
1. Solving a Chemistry Homework Problem
On your chemistry homework, you are faced with the following problem on the photoelectric effect[1]:
In a photoelectric effect experiment, electrons are ejected from a titanium surface (work function $\Phi = 4.33\mathrm{eV}$) following irradition with UV light. The energy of the incident UV light is $7.2 \cdot 10^{-19} \mathrm{J}$ per photon. Calculate the wavelength of the ejected electrons, in nanometers.
Let's solve this problem with DynamicQuantities.jl
!
julia> using DynamicQuantities
julia> using DynamicQuantities.Constants: h, m_e
julia> Φ = 4.33u"Constants.eV" # work function
6.93742482522e-19 m² kg s⁻²
julia> E = 7.2e-19u"J" # incident energy
7.2e-19 m² kg s⁻²
julia> p = sqrt(2 * m_e * (E - Φ)) # momentum of ejected electrons
2.1871890716439906e-25 m kg s⁻¹
julia> λ = h / p # wavelength of ejected electrons
3.029491247878056e-9 m
julia> λ |> us"nm" # return answer in nanometers (equivalent to `uconvert(us"nm", λ)`)
3.0294912478780556 nm
Since units are automatically propagated, we can verify the dimension of our answer and all intermediates. Also, using DynamicQuantities.Constants
, we were able to obtain the (dimensionful!) values of all necessary constants without typing them ourselves.
2. Projectile motion
Let's solve a simple projectile motion problem. First load the DynamicQuantities
module:
using DynamicQuantities
Set up initial conditions as quantities:
# Can explicitly import units:
using DynamicQuantities: km, m, s, min
y0 = 10km
v0 = 250m/s
θ = deg2rad(60)
g = 9.81m/s^2
Next, we use trig functions to calculate x and y components of initial velocity. vx0
is the x component and vy0
is the y component:
vx0 = v0 * cos(θ)
vy0 = v0 * sin(θ)
Next, let's create a time vector from 0 seconds to 1.3 minutes. Note that these are the same dimension (time), so it's fine to treat them as dimensionally equivalent!
t = range(0s, 1.3min, length=100)
Next, use kinematic equations to calculate x and y as a function of time. x(t)
is the x position at time t, and y(t)
is the y position:
x(t) = vx0*t
y(t) = vy0*t - 0.5*g*t^2 + y0
These are functions, so let's evaluate them:
x_si = x.(t)
y_si = y.(t)
These are regular vectors of quantities with Dimensions
for physical dimensions.
Next, let's plot the trajectory. First convert to km and strip units:
x_km = ustrip.(x_si .|> us"km")
y_km = ustrip.(y_si .|> us"km")
Now, we plot:
plot(x_km, y_km, label="Trajectory", xlabel="x [km]", ylabel="y [km]")
3. Using dimensional angles
Say that we wish to track angles as a unit, rather than assume the SI convention that 1 rad = 1
. We can do this by creating a new struct to track dimensions:
using DynamicQuantities
struct AngleDimensions{R} <: AbstractDimensions{R}
length::R
mass::R
time::R
current::R
temperature::R
luminosity::R
amount::R
angle::R
end
Simply by inheriting from AbstractDimensions
, we get all the constructors and operations as defined on Dimensions
:
julia> x = Quantity(1.0, AngleDimensions(length=1, angle=-1))
1.0 m angle⁻¹
However, perhaps we want to set the default angle
dimension as rad
. We can do this by defining a method for dimension_name
:
import DynamicQuantities: DynamicQuantities as DQ
function DQ.dimension_name(::AngleDimensions, k::Symbol)
default_dimensions = (
length = "m",
mass = "kg",
time = "s",
current = "A",
temperature = "K",
luminosity = "cd",
amount = "mol",
angle = "rad",
)
return get(default_dimensions, k, string(k))
end
This gives us the following behavior:
julia> x = Quantity(1.0, AngleDimensions(length=1, angle=-1))
1.0 m rad⁻¹
Next, say that we are working with existing quantities defined using standard Dimensions
. We want to promote these to our new AngleDimensions
type.
For this, we define two functions: promote_rule
and a constructor for AngleDimensions
from regular Dimensions
:
function Base.promote_rule(::Type{AngleDimensions{R1}}, ::Type{Dimensions{R2}}) where {R1,R2}
return AngleDimensions{promote_type(R1, R2)}
end
function Base.convert(::Type{Quantity{T,AngleDimensions{R}}}, q::Quantity{<:Any,<:Dimensions}) where {T,Din,R}
val = ustrip(q)
d = dimension(q)
return Quantity(
T(val),
AngleDimensions{R}(;
d.length, d.mass, d.time, d.current, d.temperature, d.luminosity, d.amount, angle=zero(R)
)
)
end
This means that whenever a Quantity{<:Any,<:Dimensions}
interacts with a Quantity{<:Any,<:AngleDimensions}
, the result will be a Quantity{<:Any,<:AngleDimensions}
, and we will initialize the angle dimension to 0. (Code not given for SymbolicDimensions
; you will probably want to treat the angles in symbolic units explicitly, so that us"rad"
is correctly tracked.)
Let's define a constant for rad
:
julia> const rad = Quantity(1.0, AngleDimensions(angle = 1))
1.0 rad
and use it in a calculation:
julia> x = 2rad
2.0 rad
julia> y = 10u"min"
600.0 s
julia> angular_velocity = x / y
0.0033333333333333335 s⁻¹ rad
which as we can see, automatically promotes to AngleDimensions
.
However, note the following: If existing code uses rad
as a unit without tracking it with AngleDimensions
, you will need to explicitly add the missing dimensions. For this reason, if you decide to take this approach to tracking units, you probably want to use AngleDimensions
throughout your codebase, rather than mixing them.
4. Assorted examples
This section demonstrates miscellaneous examples of using DynamicQuantities.jl
.
Conversion
Convert a quantity to have a new type for the value:
quantity = 1.5u"m"
convert_q = Quantity{Float32}(quantity)
println("Converted Quantity to Float32: ", convert_q)
Array basics
Create a QuantityArray
(an array of quantities with the same dimension) by passing an array and a single quantity:
x = QuantityArray(randn(32), u"km/s")
or, by passing an array of individual quantities:
y = randn(32)
y_q = QuantityArray(y .* u"m * cd / s")
We can take advantage of this being <:AbstractArray
:
println("Sum x: ", sum(x))
We can also do things like setting a particular element:
y_q[5] = Quantity(5, length=1, luminosity=1, time=-1)
println("5th element of y_q: ", y_q[5])
We can get back the original array with ustrip
:
println("Stripped y_q: ", ustrip(y_q))
This QuantityArray
is useful for broadcasting:
f_square(v) = v^2 * 1.5 - v^2
println("Applying function to y_q: ", sum(f_square.(y_q)))
Fill
We can also make QuantityArray
using fill
:
filled_q = fill(u"m/s", 10)
println("Filled QuantityArray: ", filled_q)
fill
works for 0 dimensional QuantityArray
s as well:
empty_q = fill(u"m/s", ())
println("0 dimensional QuantityArray: ", empty_q)
Similar
Likewise, we can create a QuantityArray
with the same properties as another QuantityArray
:
qa = QuantityArray(rand(3, 4), u"m")
new_qa = similar(qa)
println("Similar qa: ", new_qa)
Promotion
Promotion rules are defined for QuantityArray
s:
qarr1 = QuantityArray(randn(32), convert(Dimensions{Rational{Int32}}, dimension(u"km/s")))
qarr2 = QuantityArray(randn(Float16, 32), convert(Dimensions{Rational{Int64}}, dimension(u"km/s")))
See what type they promote to:
println("Promoted type: ", typeof(promote(qarr1, qarr2)))
Array Concatenation
Likewise, we can take advantage of array concatenation, which will ensure we have the same dimensions:
qarr1 = QuantityArray(randn(3) .* u"km/s")
qarr2 = QuantityArray(randn(3) .* u"km/s")
Concatenate them:
concat_qarr = hcat(qarr1, qarr2)
println("Concatenated QuantityArray: ", concat_qarr)
Symbolic Units
We can use arbitrary AbstractQuantity
and AbstractDimensions
in a QuantityArray
, including SymbolicDimensions
:
z_ar = randn(32)
z = QuantityArray(z_ar, us"Constants.M_sun * km/s")
Expand to standard units:
z_expanded = uexpand(z)
println("Expanded z: ", z_expanded)
GenericQuantity Construction
In addition to Quantity
, we can also use GenericQuantity
:
x = GenericQuantity(1.5)
y = GenericQuantity(0.2u"km")
println(y)
This GenericQuantity
is subtyped to Any
, rather than Number
, and thus can also store custom non-scalar types.
For example, we can work with Coords
, and wrap it in a single GenericQuantity
type:
struct Coords
x::Float64
y::Float64
end
# Define arithmetic operations on Coords
Base.:+(a::Coords, b::Coords) = Coords(a.x + b.x, a.y + b.y)
Base.:-(a::Coords, b::Coords) = Coords(a.x - b.x, a.y - b.y)
Base.:*(a::Coords, b::Number) = Coords(a.x * b, a.y * b)
Base.:*(a::Number, b::Coords) = Coords(a * b.x, a * b.y)
Base.:/(a::Coords, b::Number) = Coords(a.x / b, a.y / b)
We can then build a GenericQuantity
out of this:
coord1 = GenericQuantity(Coords(0.3, 0.9), length=1)
coord2 = GenericQuantity(Coords(0.2, -0.1), length=1)
and perform operations on these:
coord1 + coord2 |> us"cm"
# (Coords(50.0, 80.0)) cm
The nice part about this is it only stores a single Dimensions (or SymbolicDimensions
) for the entire struct!
GenericQuantity and Quantity Promotion
When we combine a GenericQuantity
and a Quantity
, the result is another GenericQuantity
:
x = GenericQuantity(1.5f0)
y = Quantity(1.5, length=1)
println("Promoted type of x and y: ", typeof(x * y))
Custom Dimensions
We can create custom dimensions by subtyping to AbstractDimensions
:
struct MyDimensions{R} <: AbstractDimensions{R}
cookie::R
milk::R
end
Many constructors and functions are defined on AbstractDimensions
, so this can be used out-of-the-box. We can then use this in a Quantity
, and all operations will work as expected:
x = Quantity(1.5, MyDimensions(cookie=1, milk=-1))
y = Quantity(2.0, MyDimensions(milk=1))
x * y
which gives us 3.0 cookie
computed from a rate of 1.5 cookie milk⁻¹
multiplied by 2.0 milk
. Likewise, we can use these in a QuantityArray
:
x_qa = QuantityArray(randn(32), MyDimensions(cookie=1, milk=-1))
x_qa .^ 2
Custom Quantities
We can also create custom dimensions by subtyping to either AbstractQuantity
(for <:Number
) or AbstractGenericQuantity
(for <:Any
):
struct MyQuantity{T,D} <: AbstractQuantity{T,D}
value::T
dimensions::D
end
Since AbstractQuantity <: Number
, this will also be a number. Keep in mind that you must call these fields value
and dimensions
for ustrip(...)
and dimension(...)
to work. Otherwise, simply redefine those.
We can use this custom quantity just like we would use Quantity
:
q1 = MyQuantity(1.2, Dimensions(length=-2))
# prints as `1.2 m⁻²`
q2 = MyQuantity(1.5, MyDimensions(cookie=1))
# prints as `1.5 cookie`
Including mathematical operations:
q2 ^ 2
# `2.25 cookie²`
The main reason you would use a custom quantity is if you want to change built-in behavior, or maybe have special methods for different types of quantities.
Note that you can declare a method on AbstractQuantity
, or AbstractGenericQuantity
to allow their respective inputs.
Note: In general, you should probably specialize on UnionAbstractQuantity
which is the union of these two abstract quantities, as well as any other future abstract quantity types, such as the planned AbstractRealQuantity
.
function my_func(x::UnionAbstractQuantity{T,D}) where {T,D}
# value has type T and dimensions has type D
return x / ustrip(x)
end