Operators for Large-scale 3D simulations
In this documentation so far, we have talked about creating systems of ordinary differential equations in ModelingToolkit and then converting them to systems of partial differential equations to perform 1-, 2-, or 3-dimensional simulations. However, currently this does not work for large scale simulations.
While this ModelingToolkit functionality is being built, we have a different solution based on the Operator
type in this package. Using this system, we still define systems of ODEs to define behavior in a single grid cell, and we also have Operator
processes that define behavior between grid cells.
ODE System
As an example, let's first define a system of ODEs:
using EarthSciMLBase
using ModelingToolkit, DifferentialEquations
using SciMLOperators, Plots
using DomainSets
using ModelingToolkit: t_nounits, D_nounits
t = t_nounits
D = D_nounits
@parameters y lon = 0.0 lat = 0.0 lev = 1.0 α = 10.0
@constants p = 1.0
@variables(
u(t) = 1.0, v(t) = 1.0, x(t) = 1.0, y(t) = 1.0, windspeed(t) = 1.0
)
eqs = [D(u) ~ -α * √abs(v) + lon,
D(v) ~ -α * √abs(u) + lat + 1e-14 * lev,
windspeed ~ lat + lon + lev,
]
sys = ODESystem(eqs, t; name=:sys)
\[ \begin{align} \frac{\mathrm{d} u\left( t \right)}{\mathrm{d}t} &= \mathtt{lon} - \sqrt{\left|v\left( t \right)\right|} \alpha \\ \frac{\mathrm{d} v\left( t \right)}{\mathrm{d}t} &= \mathtt{lat} + 1 \cdot 10^{-14} \mathtt{lev} - \sqrt{\left|u\left( t \right)\right|} \alpha \\ \mathtt{windspeed}\left( t \right) &= \mathtt{lat} + \mathtt{lev} + \mathtt{lon} \end{align} \]
The equations above don't really have any physical meaning, but they include two state variables, some parameters, and a constant. There is also a variable windspeed
which is "observed" based on the parameters, rather than being a state variable, which will be important later.
Operator
Next, we define an operator. To do so, first we create a new type that is a subtype of Operator
:
struct ExampleOp <: Operator
end
Next, we need to define a method of EarthSciMLBase.get_scimlop
for our operator. This method will be called to get a SciMLOperators.AbstractSciMLOperator
that will be used conjunction with the ModelingToolkit system above to integrate the simulation forward in time.
function EarthSciMLBase.get_scimlop(op::ExampleOp, csys::CoupledSystem, mtk_sys, domain::DomainInfo, u0, p)
α, trans1, trans2, trans3 = EarthSciMLBase.get_needed_vars(op, csys, mtk_sys, domain)
obs_f! = ModelingToolkit.build_explicit_observed_function(mtk_sys,
[α, trans1, trans2, trans3], checkbounds=false, return_inplace=true)[2]
setp! = EarthSciMLBase.coord_setter(mtk_sys, domain)
obscache = zeros(EarthSciMLBase.dtype(domain), 4)
grd = EarthSciMLBase.grid(domain)
function run(du, u, p, t)
u = reshape(u, size(u0)...)
du = reshape(du, size(u0)...)
II = CartesianIndices(size(u)[2:end])
for ix ∈ 1:size(u, 1)
for I in II
# Demonstrate coordinate transforms and observed values
uu = view(u, :, I)
setp!(p, I)
obs_f!(obscache, u, p, t)
t1, t2, t3, fv = obscache
# Set derivative value.
du[ix, I] = (t1 + t2 + t3) * fv
end
end
nothing
end
FunctionOperator(run, u0[:], p=p)
end
nothing
The function above also doesn't have any physical meaning, but it demonstrates some functionality of the Operator
"s
". First, it retrieves a function to get the current value of an observed variable in our ODE system using the obs_functions
argement, and it demonstrates how to call the resulting function to get that value. It also demonstrates how to get coordinate transforms using the coordinate_transform_functions
argument. Coordinate transforms are discussed in more detail in the documentation for the DomainInfo
type.
We also need to define a method of EarthSciMLBase.get_needed_vars
, which will return which variables are needed by the operator.
function EarthSciMLBase.get_needed_vars(::ExampleOp, csys, mtk_sys, domain::DomainInfo)
ts = EarthSciMLBase.partialderivative_transform_vars(mtk_sys, domain)
return [mtk_sys.sys₊windspeed, ts...]
end
nothing
Domain
Once we have an ODE system and an operator, the final component we need is a domain to run the simulation on. Defining a domain is covered in more depth in the documentation for the DomainInfo
type, but for now we'll just define a simple domain:
t_min = 0.0
lon_min, lon_max = -π, π
lat_min, lat_max = -0.45π, 0.45π
t_max = 11.5
indepdomain = t ∈ Interval(t_min, t_max)
partialdomains = [lon ∈ Interval(lon_min, lon_max),
lat ∈ Interval(lat_min, lat_max),
lev ∈ Interval(1, 3)]
domain = DomainInfo(
partialderivatives_δxyδlonlat,
constIC(16.0, indepdomain), constBC(16.0, partialdomains...);
grid_spacing = [0.1π, 0.1π, 1])
Note that our domain includes a coordinate transform to convert from degrees latitude and longitude to meters. Our domain specification also includes grid spacing the the lon
, lat
, and lev
coordinates, which we set as 0.1π, 0.1π, and 1, respectively.
Initial and boundary conditions are not fully implemented for this case, so regardless of the conditions you specify, the initial conditions will be the default values of the variables in the ODE system, and the boundary conditions will be zero.
Coupling and Running the Simulation
Next, initialize our operator, giving the the windspeed
observed variable, and we can couple our ODESystem, Operator, and Domain together into a single model:
op = ExampleOp()
csys = couple(sys, op, domain)
CoupledSystem containing 1 system(s), 1 operator(s), and 0 callback(s).
Finally, we can choose a EarthSciMLBase.SolverStrategy
and run the simulation. We choose the SolverStrangThreads
strategy, which needs us to specify an ODE solver from the options available in DifferentialEquations.jl for both the MTK system. We choose the Tsit5
solver. Then we create an ODEProblem which can be used to run the simulation. Finally, we solve the problem using the solve function. At this point we need to choose a solver for the Operator part of the system, and we choose the Euler
solver. We also choose a splitting time step of 1.0 seconds, which we pass both to our SolverStrangThreads
strategy and to the solve
function.
dt = 1.0 # Splitting time step
st = SolverStrangThreads(Tsit5(), 1.0)
prob = ODEProblem(csys, st)
sol = solve(prob, Euler(); dt=1.0)
After the simulation finishes, we can plot the result:
anim = @animate for i ∈ 1:length(sol.u)
u = reshape(sol.u[i], :, size(domain)...)
plot(
heatmap(u[1, :, :, 1]),
heatmap(u[1, :, :, 1]),
)
end
gif(anim, fps = 15)