Skip to content

Commit 4636099

Browse files
Merge branch 'master' into sde_throws
2 parents 239fe99 + 3ff798b commit 4636099

28 files changed

+748
-304
lines changed

Project.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "ModelingToolkit"
22
uuid = "961ee093-0014-501f-94e3-6117800e7a78"
33
authors = ["Yingbo Ma <[email protected]>", "Chris Rackauckas <[email protected]> and contributors"]
4-
version = "9.19.0"
4+
version = "9.23.0"
55

66
[deps]
77
AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c"
@@ -108,8 +108,8 @@ SparseArrays = "1"
108108
SpecialFunctions = "0.7, 0.8, 0.9, 0.10, 1.0, 2"
109109
StaticArrays = "0.10, 0.11, 0.12, 1.0"
110110
SymbolicIndexingInterface = "0.3.12"
111-
SymbolicUtils = "2"
112-
Symbolics = "5.30.1"
111+
SymbolicUtils = "2.1"
112+
Symbolics = "5.32"
113113
URIs = "1"
114114
UnPack = "0.1, 1.0"
115115
Unitful = "1.1"

docs/pages.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ pages = [
3131
"basics/MTKModel_Connector.md",
3232
"basics/Validation.md",
3333
"basics/DependencyGraphs.md",
34+
"basics/Precompilation.md",
3435
"basics/FAQ.md"],
3536
"System Types" => Any["systems/ODESystem.md",
3637
"systems/SDESystem.md",

docs/src/basics/Precompilation.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Working with Precompilation and Binary Building
2+
3+
## tl;dr, I just want precompilation to work
4+
5+
The tl;dr is, if you want to make precompilation work then instead of
6+
7+
```julia
8+
ODEProblem(sys, u0, tspan, p)
9+
```
10+
11+
use:
12+
13+
```julia
14+
ODEProblem(sys, u0, tspan, p, eval_module = @__MODULE__, eval_expression = true)
15+
```
16+
17+
As a full example, here's an example of a module that would precompile effectively:
18+
19+
```julia
20+
module PrecompilationMWE
21+
using ModelingToolkit
22+
23+
@variables x(ModelingToolkit.t_nounits)
24+
@named sys = ODESystem([ModelingToolkit.D_nounits(x) ~ -x + 1], ModelingToolkit.t_nounits)
25+
prob = ODEProblem(structural_simplify(sys), [x => 30.0], (0, 100), [],
26+
eval_expression = true, eval_module = @__MODULE__)
27+
28+
end
29+
```
30+
31+
If you use that in your package's code then 99% of the time that's the right answer to get
32+
precompilation working.
33+
34+
## I'm doing something fancier and need a bit more of an explanation
35+
36+
Oh you dapper soul, time for the bigger explanation. Julia's `eval` function evaluates a
37+
function into a module at a specified world-age. If you evaluate a function within a function
38+
and try to call it from within that same function, you will hit a world-age error. This looks like:
39+
40+
```julia
41+
function worldageerror()
42+
f = eval(:((x) -> 2x))
43+
f(2)
44+
end
45+
```
46+
47+
```
48+
julia> worldageerror()
49+
ERROR: MethodError: no method matching (::var"#5#6")(::Int64)
50+
51+
Closest candidates are:
52+
(::var"#5#6")(::Any) (method too new to be called from this world context.)
53+
@ Main REPL[12]:2
54+
```
55+
56+
This is done for many reasons, in particular if the code that is called within a function could change
57+
at any time, then Julia functions could not ever properly optimize because the meaning of any function
58+
or dispatch could always change and you would lose performance by guarding against that. For a full
59+
discussion of world-age, see [this paper](https://arxiv.org/abs/2010.07516).
60+
61+
However, this would be greatly inhibiting to standard ModelingToolkit usage because then something as
62+
simple as building an ODEProblem in a function and then using it would get a world age error:
63+
64+
```julia
65+
function wouldworldage()
66+
prob = ODEProblem(sys, [], (0.0, 1.0))
67+
sol = solve(prob)
68+
end
69+
```
70+
71+
The reason is because `prob.f` would be constructed via `eval`, and thus `prob.f` could not be called
72+
in the function, which means that no solve could ever work in the same function that generated the
73+
problem. That does mean that:
74+
75+
```julia
76+
function wouldworldage()
77+
prob = ODEProblem(sys, [], (0.0, 1.0))
78+
end
79+
sol = solve(prob)
80+
```
81+
82+
is fine, or putting
83+
84+
```julia
85+
prob = ODEProblem(sys, [], (0.0, 1.0))
86+
sol = solve(prob)
87+
```
88+
89+
at the top level of a module is perfectly fine too. They just cannot happen in the same function.
90+
91+
This would be a major limitation to ModelingToolkit, and thus we developed
92+
[RuntimeGeneratedFunctions](https://github.com/SciML/RuntimeGeneratedFunctions.jl) to get around
93+
this limitation. It will not be described beyond that, it is dark art and should not be investigated.
94+
But it does the job. But that does mean that it plays... oddly with Julia's compilation.
95+
96+
There are ways to force RuntimeGeneratedFunctions to perform their evaluation and caching within
97+
a given module, but that is not recommended because it does not play nicely with Julia v1.9's
98+
introduction of package images for binary caching.
99+
100+
Thus when trying to make things work with precompilation, we recommend using `eval`. This is
101+
done by simply adding `eval_expression=true` to the problem constructor. However, this is not
102+
a silver bullet because the moment you start using eval, all potential world-age restrictions
103+
apply, and thus it is recommended this is simply used for evaluating at the top level of modules
104+
for the purpose of precompilation and ensuring binaries of your MTK functions are built correctly.
105+
106+
However, there is one caveat that `eval` in Julia works depending on the module that it is given.
107+
If you have `MyPackage` that you are precompiling into, or say you are using `juliac` or PackageCompiler
108+
or some other static ahead-of-time (AOT) Julia compiler, then you don't want to accidentally `eval`
109+
that function to live in ModelingToolkit and instead want to make sure it is `eval`'d to live in `MyPackage`
110+
(since otherwise it will not cache into the binary). ModelingToolkit cannot know that in advance, and thus
111+
you have to pass in the module you wish for the functions to "live" in. This is done via the `eval_module`
112+
argument.
113+
114+
Hence `ODEProblem(sys, u0, tspan, p, eval_module=@__MODULE__, eval_expression=true)` will work if you
115+
are running this expression in the scope of the module you wish to be precompiling. However, if you are
116+
attempting to AOT compile a different module, this means that `eval_module` needs to be appropriately
117+
chosen. And, because `eval_expression=true`, all caveats of world-age apply.

src/discretedomain.jl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ struct SampleTime <: Operator
44
SampleTime() = SymbolicUtils.term(SampleTime, type = Real)
55
end
66
SymbolicUtils.promote_symtype(::Type{<:SampleTime}, t...) = Real
7+
Base.nameof(::SampleTime) = :SampleTime
8+
SymbolicUtils.isbinop(::SampleTime) = false
79

810
# Shift
911

@@ -32,6 +34,9 @@ struct Shift <: Operator
3234
end
3335
Shift(steps::Int) = new(nothing, steps)
3436
normalize_to_differential(s::Shift) = Differential(s.t)^s.steps
37+
Base.nameof(::Shift) = :Shift
38+
SymbolicUtils.isbinop(::Shift) = false
39+
3540
function (D::Shift)(x, allow_zero = false)
3641
!allow_zero && D.steps == 0 && return x
3742
Term{symtype(x)}(D, Any[x])
@@ -108,6 +113,8 @@ Sample(x) = Sample()(x)
108113
(D::Sample)(x) = Term{symtype(x)}(D, Any[x])
109114
(D::Sample)(x::Num) = Num(D(value(x)))
110115
SymbolicUtils.promote_symtype(::Sample, x) = x
116+
Base.nameof(::Sample) = :Sample
117+
SymbolicUtils.isbinop(::Sample) = false
111118

112119
Base.show(io::IO, D::Sample) = print(io, "Sample(", D.clock, ")")
113120

@@ -137,6 +144,8 @@ end
137144
(D::Hold)(x) = Term{symtype(x)}(D, Any[x])
138145
(D::Hold)(x::Num) = Num(D(value(x)))
139146
SymbolicUtils.promote_symtype(::Hold, x) = x
147+
Base.nameof(::Hold) = :Hold
148+
SymbolicUtils.isbinop(::Hold) = false
140149

141150
Hold(x) = Hold()(x)
142151

src/inputoutput.jl

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,8 @@ function generate_control_function(sys::AbstractODESystem, inputs = unbound_inpu
195195
disturbance_inputs = disturbances(sys);
196196
implicit_dae = false,
197197
simplify = false,
198+
eval_expression = false,
199+
eval_module = @__MODULE__,
198200
kwargs...)
199201
isempty(inputs) && @warn("No unbound inputs were found in system.")
200202

@@ -240,7 +242,8 @@ function generate_control_function(sys::AbstractODESystem, inputs = unbound_inpu
240242
end
241243
process = get_postprocess_fbody(sys)
242244
f = build_function(rhss, args...; postprocess_fbody = process,
243-
expression = Val{false}, kwargs...)
245+
expression = Val{true}, kwargs...)
246+
f = eval_or_rgf.(f; eval_expression, eval_module)
244247
(; f, dvs, ps, io_sys = sys)
245248
end
246249

@@ -395,7 +398,7 @@ model_outputs = [model.inertia1.w, model.inertia2.w, model.inertia1.phi, model.i
395398
396399
`f_oop` will have an extra state corresponding to the integrator in the disturbance model. This state will not be affected by any input, but will affect the dynamics from where it enters, in this case it will affect additively from `model.torque.tau.u`.
397400
"""
398-
function add_input_disturbance(sys, dist::DisturbanceModel, inputs = nothing)
401+
function add_input_disturbance(sys, dist::DisturbanceModel, inputs = nothing; kwargs...)
399402
t = get_iv(sys)
400403
@variables d(t)=0 [disturbance = true]
401404
@variables u(t)=0 [input = true] # New system input
@@ -418,6 +421,6 @@ function add_input_disturbance(sys, dist::DisturbanceModel, inputs = nothing)
418421
augmented_sys = extend(augmented_sys, sys)
419422

420423
(f_oop, f_ip), dvs, p = generate_control_function(augmented_sys, all_inputs,
421-
[d])
424+
[d]; kwargs...)
422425
(f_oop, f_ip), augmented_sys, dvs, p
423426
end

0 commit comments

Comments
 (0)