Skip to content

Commit 53e51d8

Browse files
Merge pull request #248 from SciML/docs
Big docs update
2 parents a00ec0b + 3643bdc commit 53e51d8

File tree

6 files changed

+322
-25
lines changed

6 files changed

+322
-25
lines changed

docs/Project.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
55
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
66
IncompleteLU = "40713840-3770-5561-ab4c-a76e7d0d7895"
77
LinearSolve = "7ed4a6bd-45f5-4d41-b270-4a48e9bafcae"
8+
ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78"
89
NonlinearSolve = "8913a72c-1f9b-4ce2-8d82-65094dcecaec"
910
NonlinearSolveMINPACK = "c100e077-885d-495a-a2ea-599e143bf69d"
1011
SciMLNLSolve = "e9a6253c-8580-4d32-9898-8661bb511710"

docs/pages.jl

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
# Put in a separate page so it can be used by SciMLDocs.jl
22

33
pages = ["index.md",
4-
"tutorials/getting_started.md",
5-
"Tutorials" => Any["tutorials/code_optimization.md",
6-
"tutorials/large_systems.md",
4+
"Getting Started with Nonlinear Rootfinding in Julia" => "tutorials/getting_started.md",
5+
"Tutorials" => Any[
6+
"Code Optimization for Small Nonlinear Systems" => "tutorials/code_optimization.md",
7+
"Handling Large Ill-Conditioned and Sparse Systems" => "tutorials/large_systems.md",
8+
"Symbolic System Definition and Acceleration via ModelingToolkit" => "tutorials/modelingtoolkit.md",
79
"tutorials/small_compile.md",
810
"tutorials/termination_conditions.md",
911
"tutorials/iterator_interface.md"],

docs/src/tutorials/code_optimization.md

Lines changed: 133 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# [Code Optimization for Solving Nonlinear Systems](@id code_optimization)
1+
# [Code Optimization for Small Nonlinear Systems in Julia](@id code_optimization)
22

3-
## Code Optimization in Julia
3+
## General Code Optimization in Julia
44

55
Before starting this tutorial, we recommend the reader to check out one of the
66
many tutorials for optimization Julia code. The following is an incomplete
@@ -29,30 +29,145 @@ Let's start with small systems.
2929

3030
## Optimizing Nonlinear Solver Code for Small Systems
3131

32-
```@example
33-
using NonlinearSolve, StaticArrays
32+
Take for example a prototypical small nonlinear solver code in its out-of-place form:
3433

35-
f(u, p) = u .* u .- p
36-
u0 = @SVector[1.0, 1.0]
34+
```@example small_opt
35+
using NonlinearSolve
36+
37+
function f(u, p)
38+
u .* u .- p
39+
end
40+
u0 = [1.0, 1.0]
3741
p = 2.0
38-
probN = NonlinearProblem(f, u0, p)
39-
sol = solve(probN, NewtonRaphson(), reltol = 1e-9)
42+
prob = NonlinearProblem(f, u0, p)
43+
sol = solve(prob, NewtonRaphson())
4044
```
4145

42-
## Using Jacobian Free Newton Krylov (JNFK) Methods
46+
We can use BenchmarkTools.jl to get more precise timings:
4347

44-
If we want to solve the first example, without constructing the entire Jacobian
48+
```@example small_opt
49+
using BenchmarkTools
50+
51+
@btime solve(prob, NewtonRaphson())
52+
```
4553

46-
```@example
47-
using NonlinearSolve, LinearSolve
54+
Note that this way of writing the function is a shorthand for:
4855

49-
function f!(res, u, p)
50-
@. res = u * u - p
56+
```@example small_opt
57+
function f(u, p)
58+
[u[1] * u[1] - p, u[2] * u[2] - p]
5159
end
52-
u0 = [1.0, 1.0]
60+
```
61+
62+
where the function `f` returns an array. This is a common pattern from things like MATLAB's `fzero`
63+
or SciPy's `scipy.optimize.fsolve`. However, by design it's very slow. I nthe benchmark you can see
64+
that there are many allocations. These allocations cause the memory allocator to take more time
65+
than the actual numerics itself, which is one of the reasons why codes from MATLAB and Python end up
66+
slow.
67+
68+
In order to avoid this issue, we can use a non-allocating "in-place" approach. Written out by hand,
69+
this looks like:
70+
71+
```@example small_opt
72+
function f(du, u, p)
73+
du[1] = u[1] * u[1] - p
74+
du[2] = u[2] * u[2] - p
75+
nothing
76+
end
77+
78+
prob = NonlinearProblem(f, u0, p)
79+
@btime sol = solve(prob, NewtonRaphson())
80+
```
81+
82+
Notice how much faster this already runs! We can make this code even simpler by using
83+
the `.=` in-place broadcasting.
84+
85+
```@example small_opt
86+
function f(du, u, p)
87+
du .= u .* u .- p
88+
end
89+
90+
@btime sol = solve(prob, NewtonRaphson())
91+
```
92+
93+
## Further Optimizations for Small Nonlinear Solves with Static Arrays and SimpleNonlinearSolve
94+
95+
Allocations are only expensive if they are “heap allocations”. For a more
96+
in-depth definition of heap allocations,
97+
[there are many sources online](http://net-informations.com/faq/net/stack-heap.htm).
98+
But a good working definition is that heap allocations are variable-sized slabs
99+
of memory which have to be pointed to, and this pointer indirection costs time.
100+
Additionally, the heap has to be managed, and the garbage controllers has to
101+
actively keep track of what's on the heap.
102+
103+
However, there's an alternative to heap allocations, known as stack allocations.
104+
The stack is statically-sized (known at compile time) and thus its accesses are
105+
quick. Additionally, the exact block of memory is known in advance by the
106+
compiler, and thus re-using the memory is cheap. This means that allocating on
107+
the stack has essentially no cost!
108+
109+
Arrays have to be heap allocated because their size (and thus the amount of
110+
memory they take up) is determined at runtime. But there are structures in
111+
Julia which are stack-allocated. `struct`s for example are stack-allocated
112+
“value-type”s. `Tuple`s are a stack-allocated collection. The most useful data
113+
structure for NonlinearSolve though is the `StaticArray` from the package
114+
[StaticArrays.jl](https://github.com/JuliaArrays/StaticArrays.jl). These arrays
115+
have their length determined at compile-time. They are created using macros
116+
attached to normal array expressions, for example:
117+
118+
```@example small_opt
119+
using StaticArrays
120+
A = SA[2.0, 3.0, 5.0]
121+
typeof(A) # SVector{3, Float64} (alias for SArray{Tuple{3}, Float64, 1, 3})
122+
```
123+
124+
Notice that the `3` after `SVector` gives the size of the `SVector`. It cannot
125+
be changed. Additionally, `SVector`s are immutable, so we have to create a new
126+
`SVector` to change values. But remember, we don't have to worry about
127+
allocations because this data structure is stack-allocated. `SArray`s have
128+
numerous extra optimizations as well: they have fast matrix multiplication,
129+
fast QR factorizations, etc. which directly make use of the information about
130+
the size of the array. Thus, when possible, they should be used.
131+
132+
Unfortunately, static arrays can only be used for sufficiently small arrays.
133+
After a certain size, they are forced to heap allocate after some instructions
134+
and their compile time balloons. Thus, static arrays shouldn't be used if your
135+
system has more than ~20 variables. Additionally, only the native Julia
136+
algorithms can fully utilize static arrays.
137+
138+
Let's ***optimize our nonlinear solve using static arrays***. Note that in this case, we
139+
want to use the out-of-place allocating form, but this time we want to output
140+
a static array. Doing it with broadcasting looks like:
141+
142+
```@example small_opt
143+
function f_SA(u, p)
144+
u .* u .- p
145+
end
146+
u0 = SA[1.0, 1.0]
53147
p = 2.0
54-
prob = NonlinearProblem(f!, u0, p)
148+
prob = NonlinearProblem(f_SA, u0, p)
149+
@btime solve(prob, NewtonRaphson())
150+
```
151+
152+
Note that only change here is that `u0` is made into a StaticArray! If we needed to write `f` out
153+
for a more complex nonlinear case, then we'd simply do the following:
154+
155+
```@example small_opt
156+
function f_SA(u, p)
157+
SA[u[1] * u[1] - p, u[2] * u[2] - p]
158+
end
55159
56-
linsolve = LinearSolve.KrylovJL_GMRES()
57-
sol = solve(prob, NewtonRaphson(; linsolve), reltol = 1e-9)
160+
@btime solve(prob, NewtonRaphson())
58161
```
162+
163+
However, notice that this did not give us a speedup but rather a slowdown. This is because many of the
164+
methods in NonlinearSolve.jl are designed to scale to larger problems, and thus aggressively do things
165+
like caching which is good for large problems but not good for these smaller problems and static arrays.
166+
In order to see the full benefit, we need to move to one of the methods from SimpleNonlinearSolve.jl
167+
which are designed for these small-scale static examples. Let's now use `SimpleNewtonRaphson`:
168+
169+
```@example small_opt
170+
@btime solve(prob, SimpleNewtonRaphson())
171+
```
172+
173+
And there we go, around 100ns from our starting point of almost 6μs!

docs/src/tutorials/large_systems.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
# [Solving Large Ill-Conditioned Nonlinear Systems with NonlinearSolve.jl](@id large_systems)
1+
# [Efficiently Solving Large Sparse Ill-Conditioned Nonlinear Systems in Julia](@id large_systems)
22

3-
This tutorial is for getting into the extra features of using NonlinearSolve.jl. Solving ill-conditioned nonlinear systems requires specializing the linear solver on properties of the Jacobian in order to cut down on the ``\mathcal{O}(n^3)`` linear solve and the ``\mathcal{O}(n^2)`` back-solves. This tutorial is designed to explain the advanced usage of NonlinearSolve.jl by solving the steady state stiff Brusselator partial differential equation (BRUSS) using NonlinearSolve.jl.
3+
This tutorial is for getting into the extra features of using NonlinearSolve.jl. Solving ill-conditioned nonlinear systems
4+
requires specializing the linear solver on properties of the Jacobian in order to cut down on the ``\mathcal{O}(n^3)``
5+
linear solve and the ``\mathcal{O}(n^2)`` back-solves. This tutorial is designed to explain the advanced usage of
6+
NonlinearSolve.jl by solving the steady state stiff Brusselator partial differential equation (BRUSS) using NonlinearSolve.jl.
47

58
## Definition of the Brusselator Equation
69

@@ -179,7 +182,9 @@ nothing # hide
179182

180183
Notice that this acceleration does not require the definition of a sparsity
181184
pattern, and can thus be an easier way to scale for large problems. For more
182-
information on linear solver choices, see the [linear solver documentation](https://docs.sciml.ai/DiffEqDocs/stable/features/linear_nonlinear/#linear_nonlinear). `linsolve` choices are any valid [LinearSolve.jl](https://linearsolve.sciml.ai/dev/) solver.
185+
information on linear solver choices, see the
186+
[linear solver documentation](https://docs.sciml.ai/DiffEqDocs/stable/features/linear_nonlinear/#linear_nonlinear).
187+
`linsolve` choices are any valid [LinearSolve.jl](https://linearsolve.sciml.ai/dev/) solver.
183188

184189
!!! note
185190

docs/src/tutorials/modelingtoolkit.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# [Symbolic Nonlinear System Definition and Acceleration via ModelingToolkit](@id modelingtoolkit)
2+
3+
[ModelingToolkit.jl](https://docs.sciml.ai/ModelingToolkit/dev/) is a symbolic-numeric modeling system
4+
for the Julia SciML ecosystem. It adds a high-level interactive interface for the numerical solvers
5+
which can make it easy to symbolically modify and generate equations to be solved. The basic form of
6+
using ModelingToolkit looks as follows:
7+
8+
```@example mtk
9+
using ModelingToolkit, NonlinearSolve
10+
11+
@variables x y z
12+
@parameters σ ρ β
13+
14+
# Define a nonlinear system
15+
eqs = [0 ~ σ * (y - x),
16+
0 ~ x * (ρ - z) - y,
17+
0 ~ x * y - β * z]
18+
@named ns = NonlinearSystem(eqs, [x, y, z], [σ, ρ, β])
19+
20+
u0 = [x => 1.0,
21+
y => 0.0,
22+
z => 0.0]
23+
24+
ps = [σ => 10.0
25+
ρ => 26.0
26+
β => 8 / 3]
27+
28+
prob = NonlinearProblem(ns, u0, ps)
29+
sol = solve(prob, NewtonRaphson())
30+
```
31+
32+
## Symbolic Derivations of Extra Functions
33+
34+
As a symbolic system, ModelingToolkit can be used to represent the equations and derive new forms. For example,
35+
let's look at the equations:
36+
37+
```@example mtk
38+
equations(ns)
39+
```
40+
41+
We can ask it what the Jacobian of our system is via `calculate_jacobian`:
42+
43+
```@example mtk
44+
calculate_jacobian(ns)
45+
```
46+
47+
We can tell MTK to generate a computable form of this analytical Jacobian via `jac = true` to help the solver
48+
use efficient forms:
49+
50+
```@example mtk
51+
prob = NonlinearProblem(ns, u0, ps, jac = true)
52+
sol = solve(prob, NewtonRaphson())
53+
```
54+
55+
## Symbolic Simplification of Nonlinear Systems via Tearing
56+
57+
One of the major reasons for using ModelingToolkit is to allow structural simplification of the systems. It's very
58+
easy to write down a mathematical model that, in theory, could be solved more simply. Let's take a look at a quick
59+
system:
60+
61+
```@example mtk
62+
@variables u1 u2 u3 u4 u5
63+
eqs = [
64+
0 ~ u1 - sin(u5),
65+
0 ~ u2 - cos(u1),
66+
0 ~ u3 - hypot(u1, u2),
67+
0 ~ u4 - hypot(u2, u3),
68+
0 ~ u5 - hypot(u4, u1),
69+
]
70+
@named sys = NonlinearSystem(eqs, [u1, u2, u3, u4, u5], [])
71+
```
72+
73+
If we run structural simplification, we receive the following form:
74+
75+
```@example mtk
76+
sys = structural_simplify(sys)
77+
```
78+
79+
```@example mtk
80+
equations(sys)
81+
```
82+
83+
How did it do this? Let's look at the `observed` to see the relationships that it found:
84+
85+
```@example mtk
86+
observed(sys)
87+
```
88+
89+
Using ModelingToolkit, we can build and solve the simplified system:
90+
91+
```@example mtk
92+
u0 = [u5 .=> 1.0]
93+
prob = NonlinearProblem(sys, u0)
94+
sol = solve(prob, NewtonRaphson())
95+
```
96+
97+
We can then use symbolic indexing to retrieve any variable:
98+
99+
```@example mtk
100+
sol[u1]
101+
```
102+
103+
```@example mtk
104+
sol[u2]
105+
```
106+
107+
```@example mtk
108+
sol[u3]
109+
```
110+
111+
```@example mtk
112+
sol[u4]
113+
```
114+
115+
```@example mtk
116+
sol[u5]
117+
```
118+
119+
## Component-Based and Acausal Modeling
120+
121+
If you're interested in building models in a component or block based form, such as seen in systems like Simulink or Modelica,
122+
take a deeper look at [ModelingToolkit.jl's documentation](https://docs.sciml.ai/ModelingToolkit/stable/) which goes into
123+
detail on such features.

docs/src/tutorials/small_compile.md

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,54 @@
11
# Faster Startup and and Static Compilation
22

3-
This is a stub article to be completed soon.
3+
In many instances one may want a very lightweight version of NonlinearSolve.jl. For this case there
4+
exists the solver package SimpleNonlinearSolve.jl. SimpleNonlinearSolve.jl solvers all satisfy the
5+
same interface as NonlinearSolve.jl, but they are designed to be simpler, lightweight, and thus
6+
have a faster startup time. Everything that can be done with NonlinearSolve.jl can be done with
7+
SimpleNonlinearSolve.jl. Thus for example, we can solve the core tutorial problem with just SimpleNonlinearSolve.jl
8+
as follows:
9+
10+
```@example simple
11+
using SimpleNonlinearSolve
12+
13+
f(u, p) = u .* u .- p
14+
u0 = [1.0, 1.0]
15+
p = 2.0
16+
prob = NonlinearProblem(f, u0, p)
17+
sol = solve(prob, SimpleNewtonRaphson())
18+
```
19+
20+
However, there are a few downsides to SimpleNonlinearSolve's `SimpleX` style algorithms to note:
21+
22+
1. SimpleNonlinearSolve.jl's methods are not hooked into the LinearSolve.jl system, and thus do not have
23+
the ability to specify linear solvers, use sparse matrices, preconditioners, and all of the other features
24+
which are required to scale for very large systems of equations.
25+
2. SimpleNonlinearSolve.jl's methods have less robust error handling and termination conditions, and thus
26+
these methods are missing some flexibility and give worse hints for debugging.
27+
3. SimpleNonlinearSolve.jl's methods are focused on out-of-place support. There is some in-place support,
28+
but it's designed for simple smaller systems and so some optimizations are missing.
29+
30+
However, the major upsides of SimpleNonlinearSolve.jl are:
31+
32+
1. The methods are optimized and non-allocating on StaticArrays
33+
2. The methods are minimal in compilation
34+
35+
As such, you can use the code as shown above to have very low startup with good methods, but for more scaling and debuggability
36+
we recommend the full NonlinearSolve.jl. But that said,
37+
38+
```@example simple
39+
using StaticArrays
40+
41+
u0 = SA[1.0, 1.0]
42+
p = 2.0
43+
prob = NonlinearProblem(f, u0, p)
44+
sol = solve(prob, SimpleNewtonRaphson())
45+
```
46+
47+
using StaticArrays.jl is also the fastest form for small equations, so if you know your system is small then SimpleNonlinearSolve.jl
48+
is not only sufficient but optimal.
49+
50+
## Static Compilation
51+
52+
Julia has tools for building small binaries via static compilation with [StaticCompiler.jl](https://github.com/tshort/StaticCompiler.jl).
53+
However, these tools are currently limited to type-stable non-allocating functions. That said, SimpleNonlinearSolve.jl's solvers are
54+
precisely the subset of NonlinearSolve.jl which are compatible with static compilation.

0 commit comments

Comments
 (0)