Advanced Examples
Solving Differential Equations
There has been some attempts at passing units through differential equations solvers, and while FlexUnits can do this it comes with certain drawbacks:
- Passing units through ODE solvers does take a performance hit (often 2x the time)
- Passing units through ODE solvers isn't rigorously tested by their developers and regressions can happen
- If done improperly, it's difficult to troubleshoot errors
It also turns out that passing units through an ODE solver is often unnecessary. These solvers already produce correct results as long as the units are coherent (that is, they don't require scaling between dimensions). Unit/dimension errors often show up in setting up the ODE equations/functions, so it makes sense to restrict unit validation activity to this space.
In the section below, we introduce a strategy that can validate units inside differential equations, the benefits of this approach include:
- It solves the system at essentially zero added cost
- It introduces no added complexity, using the ODE solvers as the developers intended (and test for )
- It validates units in a way that is easy to interpret
This strategy involves defining new objects that behave like a user-defined object with units inside the differential equation system, but behave like numerical vectors inside the differential equation solver. This can be done by defining new objects with clear dimensional representations and subtype it to QuantFieldVector abstract class defined in FlexUnits
The problem definition
Let us consider an application where we are using the drag force equation to model a falling object with velocity v and position h. This equation also requires us to consider the following parameters:
Cdthe drag force coefficientAthe reference areaρthe density of the fluid the object is falling throughmthe mass of the objectggravitational acceleration
First, we import necessary packages and define our object as QuantFieldVectors contain state values with static dimensions attached to them. The @D_str macro makes easy work of assigning static dimensions in an intuitive manner.
using FlexUnits, .UnitRegistry
using OrdinaryDiffEqTsit5
using OrdinaryDiffEqRosenbrock
using StaticArrays
using Plots
using BenchmarkTools
using LinearAlgebra
import FlexUnits: QuantFieldVector, DimsMod
import OrdinaryDiffEqTsit5.SciMLBase.FullSpecialize
@kwdef struct FallingObjectState{T} <: QuantFieldVector{2,T}
v :: Quantity{T, D"m/s"}
h :: Quantity{T, D"m"}
end
@kwdef struct FallingObjectProps{T} <: QuantFieldVector{5,T}
Cd :: Quantity{T, D""}
A :: Quantity{T, D"m^2"}
ρ :: Quantity{T, D"kg/m^3"}
m :: Quantity{T, D"kg"}
g :: Quantity{T, D"m/s^2"}
end We can write out the equations as we would normally express them; when returning the value, we need to modify the dimensions by 1/s for the derivative. This is done using the DimsMod{D"1/s"} object constructor which modifies a QuantFieldArray object by a static dimension type D"1/s" to produce a time derivative. The constructor takes two arguments:
- The original object (
FallingObjectState) - A
NamedTuplewith fields identical to the original object, and values which are quantities having the modified dimensions
function acceleration(u0::AbstractVector, p::FallingObjectProps, t)
u = FallingObjectState(u0)
#Drag force
fd = -sign(u.v)*0.5*p.ρ*u.v^2*p.Cd*p.A
#Drag force effect on state, DimsMod validates the units of the derivative
dv = (fd/p.m - p.g)
dh = u.v
#Validate the units using DimsMod and return the original type (as required by the ODE solver)
du = DimsMod{D"1/s"}(FallingObjectState, (v=dv, h=dh))
return convert(typeof(u0), du)
endThe trick with defining the system this way is that calling FallingObjectState() on a unitless vector will apply units to it, and getting indices from it will give you a unitless scalar in SI base units.
julia> fos = FallingObjectState([1.0,1.0])
2-element FallingObjectState{Float64} with indices SOneTo(2):
1
1
julia> fos.v
1.0 m/s
julia> SVector(fos)
2-element SVector{2, Float64} with indices SOneTo(2):
1.0
1.0Solving the problem
Defining the problem in this manner will give you internal unit verification of your equations (where you need it most) at zero runtime cost regardless as to whether or not you're using an implicit or explicit solver. You can simply solve the system the way you would normally do it
# =============================================================================================================
println("\nExplicit Solution")
# =============================================================================================================
u0 = FallingObjectState{Float64}(v=0.0u"m/s", h=100u"m")
p = FallingObjectProps{Float64}(Cd=1.0, A=0.1u"m^2", ρ=1.0u"kg/m^3", m=50u"kg", g=9.81u"m/s^2")
abstol = FallingObjectState{Float64}(v=1e-6u"m/s", h=1e-6u"m")
reltol = SA[1e-6, 1e-6]
tspan = dstrip.((0.0u"min", 0.25u"min")) #Time span must be in seconds, dstrip takes care of this
prob = ODEProblem{false, FullSpecialize}(acceleration, u0, tspan, p, abstol=abstol, reltol=reltol)
sol = solve(prob, Tsit5())
plt = plot(sol.t, [dstrip(u.v) for u in sol.u], label="explicit") #Each element in sol.u is a QuantFieldVector
# =============================================================================================================
println("\nImplicit Solution")
# =============================================================================================================
u0 = FallingObjectState{Float64}(v=0.0u"m/s", h=100u"m")
p = FallingObjectProps{Float64}(Cd=1.0, A=0.1u"m^2", ρ=1.0u"kg/m^3", m=50u"kg", g=9.81u"m/s^2")
tspan = dstrip.((0.0u"min", 0.25u"min"))
prob = ODEProblem{false, FullSpecialize}(acceleration, u0, tspan, p, abstol=abstol, reltol=reltol)
sol = solve(prob, Rodas5P())
plt = plot!(plt, sol.t, [dstrip(u.v) for u in sol.u], label="implicit") #Each element in sol.u is a QuantFieldVector