Skip to content

Commit 4c56324

Browse files
committed
Clean up and format
1 parent f2a1a5a commit 4c56324

File tree

3 files changed

+148
-99
lines changed

3 files changed

+148
-99
lines changed

docs/src/tutorials/change_independent_variable.md

+47-31
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,46 @@
22

33
Ordinary differential equations describe the rate of change of some dependent variables with respect to one independent variable.
44
For the modeler it is often most natural to write down the equations with a particular independent variable, say time $t$.
5-
In many cases there are good reasons for reparametrizing ODEs in terms of a different independent variable:
5+
However, in many cases there are good reasons for changing the independent variable:
66

7-
1. One may want $y(x)$ as a function of $x$ instead of $(x(t), y(t))$ as a function of $t$
8-
2. Some differential equations vary more nicely (e.g. less stiff or better behaved) with respect to one independent variable than another.
9-
3. It can reduce the number of equations that must be solved (e.g. $y(x)$ is one equation, while $(x(t), y(t))$ are two).
7+
1. One may want $y(x)$ as a function of $x$ instead of $(x(t), y(t))$ as a function of $t$
8+
2. Some differential equations vary more nicely (e.g. less stiff) with respect to one independent variable than another.
9+
3. It can reduce the number of equations that must be solved (e.g. $y(x)$ is one equation, while $(x(t), y(t))$ are two).
1010

1111
To manually change the independent variable of an ODE, one must rewrite all equations in terms of a new variable and transform differentials with the chain rule.
1212
This is mechanical and error-prone.
1313
ModelingToolkit provides the utility function [`change_independent_variable`](@ref) that automates this process.
1414

1515
## 1. Get one dependent variable as a function of another
1616

17-
Consider a projectile shot with some initial velocity in a gravitational field.
17+
Consider a projectile shot with some initial velocity in a vertical gravitational field with constant horizontal velocity.
18+
1819
```@example changeivar
1920
using ModelingToolkit
2021
@independent_variables t
2122
D = Differential(t)
2223
@variables x(t) y(t)
23-
@parameters g = 9.81 v # gravitational acceleration and constant horizontal velocity
24-
M1 = ODESystem([
25-
D(D(y)) ~ -g, D(x) ~ v # constant horizontal velocity
26-
], t; initialization_eqs = [
27-
D(x) ~ D(y) # equal initial horizontal and vertical velocity (45°)
28-
], name = :M)
24+
@parameters g=9.81 v # gravitational acceleration and horizontal velocity
25+
eqs = [D(D(y)) ~ -g, D(x) ~ v]
26+
initialization_eqs = [D(x) ~ D(y)] # 45° initial angle
27+
M1 = ODESystem(eqs, t; initialization_eqs, name = :M)
2928
M1s = structural_simplify(M1)
29+
@assert length(equations(M1s)) == 3 # hide
30+
M1s # hide
3031
```
32+
3133
This is the standard parametrization that arises naturally from kinematics and Newton's laws.
3234
It expresses the position $(x(t), y(t))$ as a function of time $t$.
3335
But suppose we want to determine whether the projectile hits a target 10 meters away.
3436
There are at least three ways of answering this:
35-
* Solve the ODE for $(x(t), y(t))$ and use a callback to terminate when $x$ reaches 10 meters, and evaluate $y$ at the final time.
36-
* Solve the ODE for $(x(t), y(t))$ and use root finding to find the time when $x$ reaches 10 meters, and evaluate $y$ at that time.
37-
* Solve the ODE for $y(x)$ and evaluate it at 10 meters.
37+
38+
- Solve the ODE for $(x(t), y(t))$ and use a callback to terminate when $x$ reaches 10 meters, and evaluate $y$ at the final time.
39+
- Solve the ODE for $(x(t), y(t))$ and use root finding to find the time when $x$ reaches 10 meters, and evaluate $y$ at that time.
40+
- Solve the ODE for $y(x)$ and evaluate it at 10 meters.
3841

3942
We will demonstrate the last method by changing the independent variable from $t$ to $x$.
40-
This transformation is well-defined for any non-zero horizontal velocity $v$.
43+
This transformation is well-defined for any non-zero horizontal velocity $v$, so $x$ and $t$ are one-to-one.
44+
4145
```@example changeivar
4246
M2 = change_independent_variable(M1, x)
4347
M2s = structural_simplify(M2; allow_symbolic = true)
@@ -46,17 +50,21 @@ M2s = structural_simplify(M2; allow_symbolic = true)
4650
@assert allequal([M2.x, M2s.x]) # hide
4751
@assert allequal([y, M1s.y]) # hide
4852
@assert allunique([M1.x, M1.y, M2.y, M2s.y]) # hide
53+
@assert length(equations(M2s)) == 2 # hide
4954
M2s # display this # hide
5055
```
56+
5157
The derivatives are now with respect to the new independent variable $x$, which can be accessed with `M2.x`.
5258

5359
!!! warn
60+
5461
At this point `x`, `M1.x`, `M1s.x`, `M2.x`, `M2s.x` are *three* different variables.
5562
Meanwhile `y`, `M1.y`, `M1s.y`, `M2.y` and `M2s.y` are *four* different variables.
5663
It can be instructive to inspect these yourself to see their subtle differences.
5764

5865
Notice how the number of equations has also decreased from three to two, as $\mathrm{d}x/\mathrm{d}t$ has been turned into an observed equation.
5966
It is straightforward to evolve the ODE for 10 meters and plot the resulting trajectory $y(x)$:
67+
6068
```@example changeivar
6169
using OrdinaryDiffEq, Plots
6270
prob = ODEProblem(M2s, [M2s.y => 0.0], [0.0, 10.0], [v => 8.0]) # throw 10 meters with x-velocity 8 m/s
@@ -65,67 +73,75 @@ plot(sol; idxs = M2.y) # must index by M2.y = y(x); not M1.y = y(t)!
6573
```
6674

6775
!!! tip "Usage tips"
76+
6877
Look up the documentation of [`change_independent_variable`](@ref) for tips on how to use it.
69-
78+
7079
For example, if you also need $t(x)$, you can tell it to add a differential equation for the old independent variable in terms of the new one using the [inverse function rule](https://en.wikipedia.org/wiki/Inverse_function_rule) (here $\mathrm{d}t/\mathrm{d}x = 1 / (\mathrm{d}x/\mathrm{d}t)$). If you know an analytical expression between the independent variables (here $t = x/v$), you can also pass it directly to the function to avoid the extra differential equation.
7180

7281
## 2. Alleviating stiffness by changing to logarithmic time
7382

7483
In cosmology, the [Friedmann equations](https://en.wikipedia.org/wiki/Friedmann_equations) describe the expansion of the universe.
7584
In terms of conformal time $t$, they can be written
85+
7686
```@example changeivar
7787
@variables a(t) Ω(t)
7888
a = GlobalScope(a) # global var needed by all species
7989
function species(w; kw...)
80-
eqs = [D(Ω) ~ -3(1 + w) * D(a)/a * Ω]
90+
eqs = [D(Ω) ~ -3(1 + w) * D(a) / a * Ω]
8191
return ODESystem(eqs, t, [Ω], []; kw...)
8292
end
83-
@named r = species(1//3) # radiation
93+
@named r = species(1 // 3) # radiation
8494
@named m = species(0) # matter
8595
@named Λ = species(-1) # dark energy / cosmological constant
86-
eqs = [
87-
Ω ~ r.Ω + m.Ω + Λ.Ω # total energy density
88-
D(a) ~ √(Ω) * a^2
89-
]
90-
initialization_eqs = [
91-
Λ.Ω + r.Ω + m.Ω ~ 1
92-
]
96+
eqs = [Ω ~ r.Ω + m.Ω + Λ.Ω, D(a) ~ √(Ω) * a^2]
97+
initialization_eqs = [Λ.Ω + r.Ω + m.Ω ~ 1]
9398
M1 = ODESystem(eqs, t, [Ω, a], []; initialization_eqs, name = :M)
9499
M1 = compose(M1, r, m, Λ)
95100
M1s = structural_simplify(M1)
96101
```
102+
97103
Of course, we can solve this ODE as it is:
104+
98105
```@example changeivar
99106
prob = ODEProblem(M1s, [M1s.a => 1.0, M1s.r.Ω => 5e-5, M1s.m.Ω => 0.3], (0.0, -3.3), [])
100107
sol = solve(prob, Tsit5(); reltol = 1e-5)
101108
@assert Symbol(sol.retcode) == :Unstable # surrounding text assumes this was unstable # hide
102-
plot(sol, idxs = [M1.a, M1.r.Ω/M1.Ω, M1.m.Ω/M1.Ω, M1.Λ.Ω/M1.Ω])
109+
plot(sol, idxs = [M1.a, M1.r.Ω / M1.Ω, M1.m.Ω / M1.Ω, M1.Λ.Ω / M1.Ω])
103110
```
104-
The solver becomes unstable due to stiffness.
111+
112+
But the solver becomes unstable due to stiffness.
105113
Also notice the interesting dynamics taking place towards the end of the integration (in the early universe), which gets compressed into a very small time interval.
106-
These ODEs would benefit from being defined with respect to a logarithmic "time" that better captures the evolution of the universe through *orders of magnitude* of time.
114+
These ODEs would benefit from being defined with respect to a logarithmic "time" that better captures the evolution of the universe through *orders of magnitude* of time, rather than linear time.
107115

108116
It is therefore common to write these ODEs in terms of $b = \ln a$.
109-
To do this, we will change the independent variable in two stages; from $t$ to $a$ to $b$.
110-
Notice that $\mathrm{d}a/\mathrm{d}t > 0$ provided that $\Omega > 0$, and $\mathrm{d}b/\mathrm{d}a > 0$, so the transformation is well-defined.
117+
To do this, we will change the independent variable in two stages; first from $t$ to $a$, and then from $a$ to $b$.
118+
Notice that $\mathrm{d}a/\mathrm{d}t > 0$ provided that $\Omega > 0$, and $\mathrm{d}b/\mathrm{d}a > 0$, so the transformation is well-defined since $t \leftrightarrow a \leftrightarrow b$ are one-to-one.
111119
First, we transform from $t$ to $a$:
120+
112121
```@example changeivar
113122
M2 = change_independent_variable(M1, M1.a)
123+
@assert !ModelingToolkit.isautonomous(M2) # hide
124+
M2 # hide
114125
```
126+
115127
Unlike the original, notice that this system is *non-autonomous* because the independent variable $a$ appears explicitly in the equations!
116128
This means that to change the independent variable from $a$ to $b$, we must provide not only the rate of change relation $db(a)/da = \exp(-b)$, but *also* the equation $a(b) = \exp(b)$ so $a$ can be eliminated in favor of $b$:
129+
117130
```@example changeivar
118131
a = M2.a
119132
Da = Differential(a)
120133
@variables b(a)
121134
M3 = change_independent_variable(M2, b, [Da(b) ~ exp(-b), a ~ exp(b)])
122135
```
136+
123137
We can now solve and plot the ODE in terms of $b$:
138+
124139
```@example changeivar
125140
M3s = structural_simplify(M3; allow_symbolic = true)
126141
prob = ODEProblem(M3s, [M3s.r.Ω => 5e-5, M3s.m.Ω => 0.3], (0, -15), [])
127142
sol = solve(prob, Tsit5())
128143
@assert Symbol(sol.retcode) == :Success # surrounding text assumes the solution was successful # hide
129-
plot(sol, idxs = [M3.r.Ω/M3.Ω, M3.m.Ω/M3.Ω, M3.Λ.Ω/M3.Ω])
144+
plot(sol, idxs = [M3.r.Ω / M3.Ω, M3.m.Ω / M3.Ω, M3.Λ.Ω / M3.Ω])
130145
```
146+
131147
Notice that the variables vary "more nicely" with respect to $b$ than $t$, making the solver happier and avoiding numerical problems.

src/systems/diffeqs/basic_transformations.jl

+30-19
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,17 @@ function liouville_transform(sys::AbstractODESystem; kwargs...)
4747
neweq = D(trJ) ~ trJ * -tr(calculate_jacobian(sys))
4848
neweqs = [equations(sys); neweq]
4949
vars = [unknowns(sys); trJ]
50-
ODESystem(neweqs, t, vars, parameters(sys); checks = false, name = nameof(sys), kwargs...)
50+
ODESystem(
51+
neweqs, t, vars, parameters(sys);
52+
checks = false, name = nameof(sys), kwargs...
53+
)
5154
end
5255

5356
"""
54-
change_independent_variable(sys::AbstractODESystem, iv, eqs = []; add_old_diff = false, simplify = true, fold = false, kwargs...)
57+
change_independent_variable(
58+
sys::AbstractODESystem, iv, eqs = [];
59+
add_old_diff = false, simplify = true, fold = false
60+
)
5561
5662
Transform the independent variable (e.g. ``t``) of the ODE system `sys` to a dependent variable `iv` (e.g. ``u(t)``).
5763
The transformation is well-defined when the mapping between the new and old independent variables are one-to-one.
@@ -68,13 +74,13 @@ Any extra equations `eqs` involving the new and old independent variables will b
6874
# Usage before structural simplification
6975
7076
The variable change must take place before structural simplification.
71-
Subsequently, consider passing `allow_symbolic = true` to `structural_simplify(sys)` to reduce the number of unknowns, with the understanding that the transformation is well-defined.
77+
In following calls to `structural_simplify`, consider passing `allow_symbolic = true` to avoid undesired constraint equations between between dummy variables.
7278
7379
# Usage with non-autonomous systems
7480
75-
If `sys` is non-autonomous (i.e. ``t`` appears explicitly in its equations), it is often desirable to also pass an algebraic equation relating the new and old independent variables (e.g. ``t = f(u(t))``).
76-
Otherwise the transformed system will be underdetermined and cannot be structurally simplified without additional changes.
77-
If an algebraic relation is not known, consider using `add_old_diff`.
81+
If `sys` is non-autonomous (i.e. ``t`` appears explicitly in its equations), consider passing an algebraic equation relating the new and old independent variables (e.g. ``t = f(u(t))``).
82+
Otherwise the transformed system can be underdetermined.
83+
If an algebraic relation is not known, consider using `add_old_diff` instead.
7884
7985
# Usage with hierarchical systems
8086
@@ -102,7 +108,10 @@ julia> unknowns(M)
102108
yˍx(x)
103109
```
104110
"""
105-
function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; add_old_diff = false, simplify = true, fold = false, kwargs...)
111+
function change_independent_variable(
112+
sys::AbstractODESystem, iv, eqs = [];
113+
add_old_diff = false, simplify = true, fold = false
114+
)
106115
iv2_of_iv1 = unwrap(iv) # e.g. u(t)
107116
iv1 = get_iv(sys) # e.g. t
108117

@@ -114,6 +123,7 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; add_o
114123
error("Variable $iv is not a function of the independent variable $iv1 of the system $(nameof(sys))!")
115124
end
116125

126+
# Set up intermediate and final variables for the transformation
117127
iv1name = nameof(iv1) # e.g. :t
118128
iv2name = nameof(operation(iv2_of_iv1)) # e.g. :u
119129
iv2, = @independent_variables $iv2name # e.g. u
@@ -127,23 +137,22 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; add_o
127137
if add_old_diff
128138
eqs = [eqs; Differential(iv2)(iv1_of_iv2) ~ 1 / div2_of_iv2] # e.g. dt(u)/du ~ 1 / uˍt(u) (https://en.wikipedia.org/wiki/Inverse_function_rule)
129139
end
130-
@set! sys.eqs = [get_eqs(sys); eqs] # add extra equations we derived before starting transformation process
131-
@set! sys.unknowns = [get_unknowns(sys); [iv1, div2_of_iv1]] # add new variables, will be transformed to e.g. t(u) and uˍt(u) # add dummy variables and old independent variable as a function of the new one
140+
@set! sys.eqs = [get_eqs(sys); eqs] # add extra equations we derived
141+
@set! sys.unknowns = [get_unknowns(sys); [iv1, div2_of_iv1]] # add new variables, will be transformed to e.g. t(u) and uˍt(u)
132142

133-
# Create a utility that performs the chain rule on an expression, followed by insertion of the new independent variable
134-
# e.g. (d/dt)(f(t)) -> (d/dt)(f(u(t))) -> df(u(t))/du(t) * du(t)/dt -> df(u)/du * uˍt(u)
135-
# Then use it to transform everything in the system!
143+
# Create a utility that performs the chain rule on an expression, followed by insertion of the new independent variable:
144+
# e.g. (d/dt)(f(t)) -> (d/dt)(f(u(t))) -> df(u(t))/du(t) * du(t)/dt -> df(u)/du * uˍt(u)
136145
function transform(ex)
137146
# 1) Replace the argument of every function; e.g. f(t) -> f(u(t))
138147
for var in vars(ex; op = Nothing) # loop over all variables in expression (op = Nothing prevents interpreting "D(f(t))" as one big variable)
139-
is_function_of_iv1 = iscall(var) && isequal(only(arguments(var)), iv1) # is the expression of the form f(t)?
148+
is_function_of_iv1 = iscall(var) && isequal(only(arguments(var)), iv1) # of the form f(t)?
140149
if is_function_of_iv1 && !isequal(var, iv2_of_iv1) # prevent e.g. u(t) -> u(u(t))
141150
var_of_iv1 = var # e.g. f(t)
142151
var_of_iv2_of_iv1 = substitute(var_of_iv1, iv1 => iv2_of_iv1) # e.g. f(u(t))
143152
ex = substitute(ex, var_of_iv1 => var_of_iv2_of_iv1; fold)
144153
end
145154
end
146-
# 2) Expand with chain rule until nothing changes anymore
155+
# 2) Repeatedly expand chain rule until nothing changes anymore
147156
orgex = nothing
148157
while !isequal(ex, orgex)
149158
orgex = ex # save original
@@ -156,22 +165,25 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; add_o
156165
return ex
157166
end
158167

168+
# Use the utility function to transform everything in the system!
159169
function transform(sys::AbstractODESystem)
160170
eqs = map(transform, get_eqs(sys))
161171
unknowns = map(transform, get_unknowns(sys))
162172
unknowns = filter(var -> !isequal(var, iv2), unknowns) # remove e.g. u
163173
ps = map(transform, get_ps(sys))
164-
ps = filter(!isinitial, ps) # remove Initial(...) # # TODO: shouldn't have to touch this
174+
ps = filter(!isinitial, ps) # remove Initial(...) # TODO: shouldn't have to touch this
165175
observed = map(transform, get_observed(sys))
166176
initialization_eqs = map(transform, get_initialization_eqs(sys))
167177
parameter_dependencies = map(transform, get_parameter_dependencies(sys))
168-
defaults = Dict(transform(var) => transform(val) for (var, val) in get_defaults(sys))
178+
defaults = Dict(transform(var) => transform(val)
179+
for (var, val) in get_defaults(sys))
169180
guesses = Dict(transform(var) => transform(val) for (var, val) in get_guesses(sys))
170-
assertions = Dict(transform(condition) => msg for (condition, msg) in get_assertions(sys))
181+
assertions = Dict(transform(ass) => msg for (ass, msg) in get_assertions(sys))
171182
systems = get_systems(sys) # save before reconstructing system
172183
wascomplete = iscomplete(sys) # save before reconstructing system
173184
sys = typeof(sys)( # recreate system with transformed fields
174-
eqs, iv2, unknowns, ps; observed, initialization_eqs, parameter_dependencies, defaults, guesses,
185+
eqs, iv2, unknowns, ps; observed, initialization_eqs,
186+
parameter_dependencies, defaults, guesses,
175187
assertions, name = nameof(sys), description = description(sys)
176188
)
177189
systems = map(transform, systems) # recurse through subsystems
@@ -182,6 +194,5 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; add_o
182194
end
183195
return sys
184196
end
185-
186197
return transform(sys)
187198
end

0 commit comments

Comments
 (0)