Skip to content

Commit bd2c7e9

Browse files
docs: [AI] add docs on building precompilation-friendly components
1 parent e4a26b3 commit bd2c7e9

2 files changed

Lines changed: 175 additions & 0 deletions

File tree

docs/pages.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ pages = [
5454
"basics/Debugging.md",
5555
"basics/DependencyGraphs.md",
5656
"basics/Precompilation.md",
57+
"basics/PrecompileComponents.md",
5758
"basics/FAQ.md",
5859
],
5960
"comparison.md",
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# Building Precompilation-Friendly Components
2+
3+
How components are _defined_ has a significant impact on how well ModelingToolkit precompiles.
4+
The core principle is **type stability**: the `System` constructor and the functions it calls
5+
can only precompile specialized, fast code paths when the types of all arguments are fully
6+
known at compile time. Type instability causes the compiler to fall back to dynamic dispatch,
7+
generating generic `Any`-typed code that is slow and hard to compile.
8+
9+
## Build `vars` and `params` as `SymbolicT[]` vectors using `push!`
10+
11+
The macro forms `@variables` and `@parameters` return heterogeneous types. For example, a scalar
12+
variable `x(t)` is a `Num`, while an array variable `y(t)[1:2]` is an `Arr{Num,1}`. Collecting
13+
them with a literal array expression like `[x, y]` or `SymbolicT[x, y]` produces a `Vector{Any}`
14+
or triggers a specialized `getindex` method for each unique combination of argument types.
15+
16+
Instead, declare the collection as `SymbolicT[]` and add elements one at a time with `push!`.
17+
`SymbolicT` is a public API type alias from Symbolics.jl that wraps both `Num` and `Arr` uniformly:
18+
19+
```julia
20+
using Symbolics: SymbolicT
21+
22+
@variables begin
23+
x(t_nounits)
24+
y(t_nounits)
25+
end
26+
vars = SymbolicT[]
27+
push!(vars, x)
28+
push!(vars, y)
29+
30+
@parameters begin
31+
α = α
32+
β = β
33+
end
34+
params = SymbolicT[]
35+
push!(params, α)
36+
push!(params, β)
37+
```
38+
39+
This guarantees that `vars` and `params` are `Vector{SymbolicT}` — a concrete, fully-typed
40+
collection — so the `System` constructor receives an argument of a known type and can compile a
41+
specialized, cacheable method for it.
42+
43+
## Build `initial_conditions` and `guesses` as `Dict{SymbolicT, SymbolicT}`
44+
45+
Passing initial conditions or guesses inline as keyword arguments (e.g.
46+
`initial_conditions = [x => 1.0, y => ones(2)]`) creates a
47+
`Vector{Any}`, and causes type-instabilities in the `System` constructor.
48+
49+
Instead, build an explicit `Dict{SymbolicT, SymbolicT}` and `push!` pairs into it:
50+
51+
```julia
52+
initial_conditions = Dict{SymbolicT, SymbolicT}()
53+
push!(initial_conditions, x => 3.1)
54+
push!(initial_conditions, y => 1)
55+
56+
guesses = Dict{SymbolicT, SymbolicT}()
57+
# Similar
58+
```
59+
60+
An equivalent formulation is:
61+
62+
```julia
63+
initial_conditions = Dict{SymbolicT, SymbolicT}()
64+
initial_conditions[x] = 3.1
65+
initial_conditions[y] = 1
66+
```
67+
68+
This avoids the problematic `Dict{SymbolicT, SymbolicT}(pairs...)` constructor, which loops
69+
over the `pairs` tuple and can cause issues when each element in `pairs` is a different type.
70+
71+
## Build the equations vector as `Equation[]`
72+
73+
Equations should be collected into an `Equation[]` vector and populated with `push!`:
74+
75+
```julia
76+
eqs = Equation[]
77+
push!(eqs, D_nounits(x) ~ α * x - β * x * y)
78+
push!(eqs, D_nounits(y) ~ -δ * y + γ * x * y)
79+
```
80+
81+
The main intention here is to ensure that `eqs` is a `Vector{Equation}`. The same may be
82+
achieved using the array literal syntax:
83+
84+
```julia
85+
eqs = Equation[
86+
D_nounits(x) ~ α * x - β * x * y,
87+
D_nounits(y) ~ -δ * y + γ * x * y,
88+
]
89+
```
90+
91+
Though this does compile a unique method for `Base.vect` (or similar methods) depending on
92+
the number of values in the array literal.
93+
94+
## Ensure the list of subsystems is a `Vector{System}`
95+
96+
Pass the subsystems list as an explicit `System[]` rather than omitting it or passing an empty
97+
array (`[]`). This removes ambiguity about the element type:
98+
99+
```julia
100+
return System(eqs, t_nounits, vars, params;
101+
systems = System[], initial_conditions, guesses, name)
102+
```
103+
104+
## Complete example
105+
106+
Putting it all together, here is a component definition that follows all of the guidelines above
107+
and is designed to hit ModelingToolkit's well-compiled code paths:
108+
109+
```julia
110+
using ModelingToolkit
111+
using ModelingToolkit: SymbolicT
112+
113+
@component function LotkaVolterra(;
114+
name, α = 1.3, β = 0.9, γ = 0.8, δ = 1.8)
115+
@parameters begin
116+
α = α
117+
β = β
118+
γ = γ
119+
δ = δ
120+
end
121+
params = SymbolicT[]
122+
push!(params, α)
123+
push!(params, β)
124+
push!(params, γ)
125+
push!(params, δ)
126+
127+
@variables begin
128+
x(t_nounits)
129+
y(t_nounits)
130+
end
131+
vars = SymbolicT[]
132+
push!(vars, x)
133+
push!(vars, y)
134+
135+
initial_conditions = Dict{SymbolicT, SymbolicT}()
136+
push!(initial_conditions, x => 3.1)
137+
push!(initial_conditions, y => 1.5)
138+
139+
guesses = Dict{SymbolicT, SymbolicT}()
140+
141+
eqs = Equation[]
142+
push!(eqs, D_nounits(x) ~ α * x - β * x * y)
143+
push!(eqs, D_nounits(y) ~ -δ * y + γ * x * y)
144+
145+
return System(eqs, t_nounits, vars, params;
146+
systems = System[], initial_conditions, guesses, name)
147+
end
148+
```
149+
150+
## Use PrecompileTools.jl
151+
152+
Exercising all/many of the component constructors will help ensure they are precompiled
153+
appropriately.
154+
155+
## Diagnosing type instabilities
156+
157+
If you want to verify that your component definition is type-stable, [Cthulhu.jl](https://github.com/JuliaDebug/Cthulhu.jl)
158+
is the most reliable tool. Use `@descend` on the component constructor and look for red
159+
(runtime-dispatch) calls. In Cthulhu, press `T` to switch to Typed IR, `o` to enable
160+
optimizations, and `w` to highlight unstable code — this view is harder to read but gives the
161+
most accurate picture of what the compiler actually infers. The Julia flags `--trace-compile`
162+
and `--trace-compile-timing` are also useful for identifying methods that were not
163+
precompiled or were invalidated at load time.
164+
165+
!!! note "Julia 1.12 and precompilation"
166+
The `System` constructor, `complete`, and `mtkcompile` are specifically optimized to precompile
167+
as much as possible on Julia 1.12 and later. While the improvements also benefit earlier Julia
168+
versions, 1.12's compiler infrastructure (in particular, changes to how inference handles mutual
169+
recursion) allows significantly more of the compiled code to be cached. If precompilation
170+
performance is a priority, testing on Julia 1.12 will give the most informative results.
171+
172+
!!! note
173+
The discussion in [ModelingToolkit#4270](https://github.com/SciML/ModelingToolkit.jl/issues/4270)
174+
can also be of use here.

0 commit comments

Comments
 (0)