|
| 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