Skip to content

Commit 98f8b7a

Browse files
combine high level usage and finish up round 1
1 parent 1b233ce commit 98f8b7a

File tree

4 files changed

+173
-100
lines changed

4 files changed

+173
-100
lines changed

README.md

Lines changed: 27 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -23,103 +23,51 @@ the documentation which contains the un-released features.
2323

2424
## High Level Example
2525

26-
First we define some variables. In a differential equation
27-
system, we need to differentiate between our (dependent) variables
28-
and parameters. Therefore we label them as follows:
26+
Let's define the Lorenz equations for numerically solving with DifferentialEquations.jl,
27+
but tell the symbolic system to automatically generate code for efficiently
28+
handling the sparse Jacobian.
2929

3030
```julia
3131
using ModelingToolkit
3232

33-
# Define some variables
3433
@parameters t σ ρ β
3534
@variables x(t) y(t) z(t)
3635
@derivatives D'~t
37-
```
38-
39-
Then we build the system:
4036

41-
```julia
4237
eqs = [D(x) ~ σ*(y-x),
4338
D(y) ~ x*-z)-y,
4439
D(z) ~ x*y - β*z]
45-
```
4640

47-
Each operation builds an `Operation` type, and thus `eqs` is an array of
48-
`Operation` and `Variable`s. This holds a tree of the full system that can be
49-
analyzed by other programs. We can turn this into a `ODESystem` via:
41+
sys = ODESystem(eqs)
5042

51-
```julia
52-
de = ODESystem(eqs, t, [x,y,z], [σ,ρ,β])
53-
```
43+
u0 = [x => 1.0,
44+
y => 0.0,
45+
z => 0.0]
5446

55-
where we tell it the variable types and ordering in the first version, or let it
56-
automatically determine the variable types in the second version.
57-
This can then generate the function. For example, we can see the
58-
generated code via:
47+
p ==> 28.0,
48+
ρ => 10.0,
49+
β => 8/3]
5950

60-
```julia
61-
myode_oop = generate_function(de,linenumbers=false)[1] # first one is the out-of-place function
62-
63-
#=
64-
:((u, p, t)->begin
65-
if u isa Array
66-
return @inbounds(begin
67-
let (x, y, z, σ, ρ, β) = (u[1], u[2], u[3], p[1], p[2], p[3])
68-
[σ * (y - x), x * (ρ - z) - y, x * y - β * z]
69-
end
70-
end)
71-
else
72-
X = @inbounds(begin
73-
let (x, y, z, σ, ρ, β) = (u[1], u[2], u[3], p[1], p[2], p[3])
74-
(σ * (y - x), x * (ρ - z) - y, x * y - β * z)
75-
end
76-
end)
77-
end
78-
T = promote_type(map(typeof, X)...)
79-
map(T, X)
80-
construct = if u isa ModelingToolkit.StaticArrays.StaticArray
81-
ModelingToolkit.StaticArrays.similar_type(typeof(u), eltype(X))
82-
else
83-
x->begin
84-
convert(typeof(u), x)
85-
end
86-
end
87-
construct(X)
88-
end)
89-
=#
90-
91-
myode_iip = generate_function(de,linenumbers=false)[2] # second one is the in-place function
92-
93-
#=
94-
:((var"##MTIIPVar#793", u, p, t)->begin
95-
@inbounds begin
96-
@inbounds begin
97-
let (x, y, z, σ, ρ, β) = (u[1], u[2], u[3], p[1], p[2], p[3])
98-
var"##MTIIPVar#793"[1] = σ * (y - x)
99-
var"##MTIIPVar#793"[2] = x * (ρ - z) - y
100-
var"##MTIIPVar#793"[3] = x * y - β * z
101-
end
102-
end
103-
end
104-
nothing
105-
end)
106-
=#
51+
tspan = (0.0,100.0)
52+
prob = ODEProblem(sys,u0,tspan,p,jac=true,sparse=true)
10753
```
10854

109-
or directly get the generated ODE function via:
55+
This automatically will have generated fast sparse Jacobian functions, making
56+
it more optimized than directly building a function. In addition, we can then
57+
use ModelingToolkit to compose multiple ODE subsystems. Let's define two
58+
interacting Lorenz equations:
11059

11160
```julia
112-
f = ODEFunction(de, [x,y,z], [σ,ρ,β])
113-
```
61+
lorenz1 = ODESystem(eqs,name=:lorenz1)
62+
lorenz2 = ODESystem(eqs,name=:lorenz2)
11463

115-
Here already you can see some advantages of the ModelingToolkit.jl compilation system. As an
116-
IR to target, this output can compile to multiple different forms, including ones specific
117-
to static arrays and in-place functions. Forms which automatically parallelize the calculations
118-
based on internal cost models are a work-in-progress as well. This means DSLs built on top of
119-
this as a model compiler can write domain-specific languages without having to write complex
120-
optimized Julia function compilers.
121-
122-
## Tutorial
64+
@variables α
65+
@parameters γ
66+
connections = [0 ~ lorenz1.x + lorenz2.y + sin*γ)]
67+
connected = ODESystem(connections,[α],[γ],systems=[lorenz1,lorenz2])
68+
```
12369

124-
For an introductory tutorial to using ModelingToolkit.jl, please checkout
125-
[ModelingToolkit.jl, An IR and Compiler for Scientific Models](https://tutorials.juliadiffeq.org/html/ode_extras/01-ModelingToolkit.html).
70+
which is now a differential-algebraic equation (DAE) of 7 variables which has
71+
two independent Lorenz systems and an algebraic equation that determines `α`
72+
such that an implicit constraint holds. We can then define the resulting
73+
`ODEProblem` and send it over to DifferentialEquations.jl.

docs/src/highlevel.md

Lines changed: 138 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,19 +51,41 @@ p = [σ => 10.0
5151
ρ => 28.0
5252
β => 8/3]
5353
tspan = (0.0,100.0)
54-
prob = ODEProblem(sys,u0,tspan,p;jac=true)
54+
prob = ODEProblem(sys,u0,tspan,p;jac=true,sparse=true)
5555
```
5656

5757
Note that the additional `jac=true` tells the system to symbolically generate
58-
an optimized Jacobian function to enhance the differential equation solvers.
58+
an optimized Jacobian function to enhance the differential equation solvers,
59+
and `sparse` tells it to build the ODEProblem with all of the enhancements
60+
setup for sparse Jacobians.
5961

60-
### Example 2: Building a Component-Based ODEProblem with Sparse Jacobians
62+
### Example 2: Building a Component-Based ODEProblem
63+
64+
In addition, we can then use ModelingToolkit to compose multiple ODE subsystems.
65+
Let's define two interacting Lorenz equations:
66+
67+
```julia
68+
lorenz1 = ODESystem(eqs,name=:lorenz1)
69+
lorenz2 = ODESystem(eqs,name=:lorenz2)
70+
71+
@variables α
72+
@parameters γ
73+
connections = [0 ~ lorenz1.x + lorenz2.y + sin*γ)]
74+
connected = ODESystem(connections,[α],[γ],systems=[lorenz1,lorenz2])
75+
```
76+
77+
which is now a differential-algebraic equation (DAE) of 7 variables which has
78+
two independent Lorenz systems and an algebraic equation that determines `α`
79+
such that an implicit constraint holds. We can then define the resulting
80+
`ODEProblem` and send it over to DifferentialEquations.jl.
6181

6282
### Example 3: Building Nonlinear Systems to Solve with NLsolve.jl
6383

64-
We can also build nonlinear systems. Let's say we wanted to solve for the steady
65-
state of the previous ODE. This is the nonlinear system defined by where the
66-
derivatives are zero. We use (unknown) variables for our nonlinear system.
84+
In this example we will go one step deeper and showcase the direct function
85+
generation capabilities in ModelingToolkit.jl to build nonlinear systems.
86+
Let's say we wanted to solve for the steady state of the previous ODE. This is
87+
the nonlinear system defined by where the derivatives are zero. We use (unknown)
88+
variables for our nonlinear system.
6789

6890
```julia
6991
using ModelingToolkit
@@ -150,6 +172,7 @@ which gives:
150172
Now we can call `nlsolve` by enclosing our parameters into the functions:
151173

152174
```julia
175+
using NLsolve
153176
nlsolve((out, x) -> f(out, x, params), (out, x) -> j!(out, x, params), ones(3))
154177
```
155178

@@ -174,7 +197,7 @@ Base.:~(::Expression, ::Expression)
174197

175198
## Additional High Level Explanations and Tips
176199

177-
## The Auto-Detecting System Constructors
200+
### The Auto-Detecting System Constructors
178201

179202
For the high level interface, the system constructors such as `ODESystem` have
180203
high level constructors which just take in the required equations and automatically
@@ -186,6 +209,114 @@ ODESystem(eqs)
186209
NonlinearSystem(eqs)
187210
```
188211

212+
### Direct Tracing
213+
214+
Because the ModelingToolkit `Expression` types obey Julia-semantics, one can
215+
directly transform existing Julia functions into ModelingToolkit symbolic
216+
representations of the function by simply inputting the symbolic values into
217+
the function and using what is returned. For example, let's take the following
218+
numerical PDE discretization:
219+
220+
```julia
221+
using ModelingToolkit, LinearAlgebra, SparseArrays
222+
223+
# Define the constants for the PDE
224+
const α₂ = 1.0
225+
const α₃ = 1.0
226+
const β₁ = 1.0
227+
const β₂ = 1.0
228+
const β₃ = 1.0
229+
const r₁ = 1.0
230+
const r₂ = 1.0
231+
const _DD = 100.0
232+
const γ₁ = 0.1
233+
const γ₂ = 0.1
234+
const γ₃ = 0.1
235+
const N = 8
236+
const X = reshape([i for i in 1:N for j in 1:N],N,N)
237+
const Y = reshape([j for i in 1:N for j in 1:N],N,N)
238+
const α₁ = 1.0.*(X.>=4*N/5)
239+
240+
const Mx = Tridiagonal([1.0 for i in 1:N-1],[-2.0 for i in 1:N],[1.0 for i in 1:N-1])
241+
const My = copy(Mx)
242+
Mx[2,1] = 2.0
243+
Mx[end-1,end] = 2.0
244+
My[1,2] = 2.0
245+
My[end,end-1] = 2.0
246+
247+
# Define the discretized PDE as an ODE function
248+
function f!(du,u,p,t)
249+
A = @view u[:,:,1]
250+
B = @view u[:,:,2]
251+
C = @view u[:,:,3]
252+
dA = @view du[:,:,1]
253+
dB = @view du[:,:,2]
254+
dC = @view du[:,:,3]
255+
mul!(MyA,My,A)
256+
mul!(AMx,A,Mx)
257+
@. DA = _DD*(MyA + AMx)
258+
@. dA = DA + α₁ - β₁*A - r₁*A*B + r₂*C
259+
@. dB = α₂ - β₂*B - r₁*A*B + r₂*C
260+
@. dC = α₃ - β₃*C + r₁*A*B - r₂*C
261+
end
262+
```
263+
264+
We can then define the corresponding arrays as ModelingToolkit variables:
265+
266+
```julia
267+
# Define the initial condition as normal arrays
268+
@variables du[1:N,1:N,1:3] u[1:N,1:N,1:3] MyA[1:N,1:N] AMx[1:N,1:N] DA[1:N,1:N]
269+
f!(du,u,nothing,0.0)
270+
```
271+
272+
The output, here the in-place modified `du`, is a symbolic representation of
273+
each output of the function. We can then utilize this in the ModelingToolkit
274+
functionality. For example, let's compute the sparse Jacobian function and
275+
compile a fast multithreaded version:
276+
277+
```julia
278+
jac = sparse(ModelingToolkit.jacobian(vec(du),vec(u),simplify=false))
279+
multithreadedjac = eval(ModelingToolkit.build_function(vec(jac),u,multithread=true)[2])
280+
```
281+
282+
### modelingtoolkitize
283+
284+
For some `DEProblem` types, automatic tracing functionality is already included
285+
via the `modelingtoolkitize` function. Take for example the Robertson ODE
286+
defined as an `ODEProblem` for DifferentialEquations.jl:
287+
288+
```julia
289+
using DifferentialEquations
290+
function rober(du,u,p,t)
291+
y₁,y₂,y₃ = u
292+
k₁,k₂,k₃ = p
293+
du[1] = -k₁*y₁+k₃*y₂*y₃
294+
du[2] = k₁*y₁-k₂*y₂^2-k₃*y₂*y₃
295+
du[3] = k₂*y₂^2
296+
nothing
297+
end
298+
prob = ODEProblem(rober,[1.0,0.0,0.0],(0.0,1e5),(0.04,3e7,1e4))
299+
```
300+
301+
If we want to get a symbolic representation, we can simply call `modelingtoolkitize`
302+
on the `prob` which will return an `ODESystem`:
303+
304+
```julia
305+
sys = modelingtoolkitize(prob)
306+
```
307+
308+
Using this, we can symbolically build the Jacobian and then rebuild the ODEProblem:
309+
310+
```julia
311+
jac = eval(ModelingToolkit.generate_jacobian(de...)[2])
312+
f = ODEFunction(rober, jac=jac)
313+
prob_jac = ODEProblem(f,[1.0,0.0,0.0],(0.0,1e5),(0.04,3e7,1e4))
314+
```
315+
316+
```@docs
317+
modelingtoolkitize
318+
```
319+
189320
### Intermediate Calculations
190321

191322
The system building functions can handle intermediate calculations by simply

docs/src/index.md

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,18 @@ generate but also easy for programs to manipulate.
1212

1313
## Package Overview
1414

15-
ModelingToolkit has 4 layers:
15+
ModelingToolkit has 3 layers:
1616

17-
1. The high level. This level has syntactic sugar for easily generating
18-
ModelingToolkit models. It can be used directly like a DSL for advanced
19-
users who want flexibility, and for easily generating DSLs.
20-
2. The tracing level. This level allows for easily generating ModelingToolkit
21-
models directly from Julia code by tracing existing functions and turning
22-
them into ModelingToolkit IR and `AbstractSystem`s for symbolic manipulation.
23-
3. The `AbstractSystem` level. This is the level where content-dependent functionality
17+
1. The model definition level. This is a high level of syntactic sugar for
18+
easily generating ModelingToolkit models. It can be used directly like a DSL
19+
for advanced users who want a lot of flexibility in a modeling language.
20+
Additionally, automatic tracing functionality allows for easily generating
21+
ModelingToolkit models directly from Julia code.
22+
2. The `AbstractSystem` level. This is the level where content-dependent functionality
2423
is added, where models such an ordinary differential equation are represented.
2524
At the system level there are *transformations* which take one system to
2625
another, and *targets* which output code for numerical solvers.
27-
4. The IR level, also referred to as the direct level. At this level, one
26+
3. The IR level, also referred to as the direct level. At this level, one
2827
directly acts on arrays of `Equation`, `Operation` and `Variable` types to
2928
generate functions.
3029

docs/src/tracing.md

Lines changed: 0 additions & 5 deletions
This file was deleted.

0 commit comments

Comments
 (0)