Skip to content

Commit 636ad04

Browse files
committed
Change independent variable of hierarchical systems
1 parent 34a6a4e commit 636ad04

File tree

3 files changed

+83
-48
lines changed

3 files changed

+83
-48
lines changed

docs/src/tutorials/change_independent_variable.md

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ There are at least three ways of answering this:
4242
We will demonstrate the last method by changing the independent variable from $t$ to $x$.
4343
This transformation is well-defined for any non-zero horizontal velocity $v$.
4444
```@example changeivar
45-
M2 = change_independent_variable(M1, M1.x; dummies = true) |> complete
45+
M2 = change_independent_variable(M1, M1.x; dummies = true)
4646
@assert M2.x isa Num # hide
4747
M2s = structural_simplify(M2; allow_symbolic = true)
4848
```
@@ -56,29 +56,38 @@ sol = solve(prob, Tsit5())
5656
plot(sol; idxs = M2.y) # must index by M2.y = y(x); not M1.y = y(t)!
5757
```
5858

59-
## 2. Reduce stiffness by changing to a logarithmic time axis
59+
## 2. Alleviating stiffness by changing to logarithmic time
6060

6161
In cosmology, the [Friedmann equations](https://en.wikipedia.org/wiki/Friedmann_equations) describe the expansion of the universe.
6262
In terms of conformal time $t$, they can be written
6363
```@example changeivar
64-
@variables a(t) Ω(t) Ωr(t) Ωm(t) ΩΛ(t)
65-
M1 = ODESystem([
64+
@variables a(t) Ω(t)
65+
a = GlobalScope(a) # global var needed by all species
66+
function species(w; kw...)
67+
eqs = [D(Ω) ~ -3(1 + w) * D(a)/a * Ω]
68+
return ODESystem(eqs, t, [Ω], []; kw...)
69+
end
70+
@named r = species(1//3) # radiation
71+
@named m = species(0) # matter
72+
@named Λ = species(-1) # dark energy / cosmological constant
73+
eqs = [
74+
Ω ~ r.Ω + m.Ω + Λ.Ω # total energy density
6675
D(a) ~ √(Ω) * a^2
67-
Ω ~ Ωr + Ωm + ΩΛ
68-
D(Ωm) ~ -3 * D(a)/a * Ωm
69-
D(Ωr) ~ -4 * D(a)/a * Ωr
70-
D(ΩΛ) ~ 0
71-
], t; initialization_eqs = [
72-
ΩΛ + Ωr + Ωm ~ 1
73-
], name = :M) |> complete
76+
]
77+
initialization_eqs = [
78+
Λ.Ω + r.Ω + m.Ω ~ 1
79+
]
80+
M1 = ODESystem(eqs, t, [Ω, a], []; initialization_eqs, name = :M)
81+
M1 = compose(M1, r, m, Λ)
82+
M1 = complete(M1; flatten = false)
7483
M1s = structural_simplify(M1)
7584
```
7685
Of course, we can solve this ODE as it is:
7786
```@example changeivar
78-
prob = ODEProblem(M1s, [M1.a => 1.0, M1.Ωr => 5e-5, M1.Ωm => 0.3], (0.0, -3.3), [])
87+
prob = ODEProblem(M1s, [M1.a => 1.0, M1.r.Ω => 5e-5, M1.m.Ω => 0.3], (0.0, -3.3), [])
7988
sol = solve(prob, Tsit5(); reltol = 1e-5)
8089
@assert Symbol(sol.retcode) == :Unstable # surrounding text assumes this was unstable # hide
81-
plot(sol, idxs = [M1.a, M1.Ωr/M1.Ω, M1.Ωm/M1.Ω, M1.ΩΛ/M1.Ω])
90+
plot(sol, idxs = [M1.a, M1.r.Ω/M1.Ω, M1.m.Ω/M1.Ω, M1.Λ.Ω/M1.Ω])
8291
```
8392
The solver becomes unstable due to stiffness.
8493
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.
@@ -89,22 +98,22 @@ To do this, we will change the independent variable in two stages; from $t$ to $
8998
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.
9099
First, we transform from $t$ to $a$:
91100
```@example changeivar
92-
M2 = change_independent_variable(M1, M1.a; dummies = true) |> complete
101+
M2 = change_independent_variable(M1, M1.a; dummies = true)
93102
```
94103
Unlike the original, notice that this system is *non-autonomous* because the independent variable $a$ appears explicitly in the equations!
95104
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$:
96105
```@example changeivar
97106
a = M2.a
98107
Da = Differential(a)
99108
@variables b(a)
100-
M3 = change_independent_variable(M2, b, [Da(b) ~ exp(-b), a ~ exp(b)]) |> complete
109+
M3 = change_independent_variable(M2, b, [Da(b) ~ exp(-b), a ~ exp(b)])
101110
```
102111
We can now solve and plot the ODE in terms of $b$:
103112
```@example changeivar
104113
M3s = structural_simplify(M3; allow_symbolic = true)
105-
prob = ODEProblem(M3s, [M3.Ωr => 5e-5, M3.Ωm => 0.3], (0, -15), [])
114+
prob = ODEProblem(M3s, [M3.r.Ω => 5e-5, M3.m.Ω => 0.3], (0, -15), [])
106115
sol = solve(prob, Tsit5())
107116
@assert Symbol(sol.retcode) == :Success # surrounding text assumes the solution was successful # hide
108-
plot(sol, idxs = [M3.Ωr/M3.Ω, M3.Ωm/M3.Ω, M3.ΩΛ/M3.Ω])
117+
plot(sol, idxs = [M3.r.Ω/M3.Ω, M3.m.Ω/M3.Ω, M3.Λ.Ω/M3.Ω])
109118
```
110119
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

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ Usage with non-autonomous systems
7777
If `sys` is 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))``).
7878
Otherwise the transformed system will be underdetermined and cannot be structurally simplified without additional changes.
7979
80+
Usage with hierarchical systems
81+
===============================
82+
It is recommended that `iv` is a non-namespaced variable in `sys`.
83+
This means it can belong to the top-level system or be a variable in a subsystem declared with `GlobalScope`.
84+
8085
Example
8186
=======
8287
Consider a free fall with constant horizontal velocity.
@@ -102,8 +107,6 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummi
102107
error("System $(nameof(sys)) is incomplete. Complete it first!")
103108
elseif isscheduled(sys)
104109
error("System $(nameof(sys)) is structurally simplified. Change independent variable before structural simplification!")
105-
elseif !isempty(get_systems(sys))
106-
error("System $(nameof(sys)) is hierarchical. Flatten it first!") # TODO: implement and allow?
107110
elseif !iscall(iv2_of_iv1) || !isequal(only(arguments(iv2_of_iv1)), iv1)
108111
error("Variable $iv is not a function of the independent variable $iv1 of the system $(nameof(sys))!")
109112
end
@@ -112,25 +115,25 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummi
112115
iv2name = nameof(operation(iv2_of_iv1)) # e.g. :u
113116
iv2, = @independent_variables $iv2name # e.g. u
114117
iv1_of_iv2, = @variables $iv1name(iv2) # inverse in case sys is autonomous; e.g. t(u)
118+
iv1_of_iv2 = GlobalScope(iv1_of_iv2) # do not namespace old independent variable as new dependent variable
115119
D1 = Differential(iv1) # e.g. d/d(t)
116120
D2 = Differential(iv2_of_iv1) # e.g. d/d(u(t))
117121

118122
# 1) Utility that performs the chain rule on an expression, e.g. (d/dt)(f(t)) -> (d/dt)(f(u(t))) -> df(u(t))/du(t) * du(t)/dt
119123
function chain_rule(ex)
120124
vars = get_variables(ex)
121125
for var_of_iv1 in vars # loop through e.g. f(t)
122-
if iscall(var_of_iv1) && !isequal(var_of_iv1, iv2_of_iv1) # handle e.g. f(t) -> f(u(t)), but not u(t) -> u(u(t))
123-
varname = nameof(operation(var_of_iv1)) # e.g. :f
124-
var_of_iv2, = @variables $varname(iv2_of_iv1) # e.g. f(u(t))
125-
ex = substitute(ex, var_of_iv1 => var_of_iv2; fold) # e.g. f(t) -> f(u(t))
126+
if iscall(var_of_iv1) && isequal(only(arguments(var_of_iv1)), iv1) && !isequal(var_of_iv1, iv2_of_iv1) # handle e.g. f(t) -> f(u(t)), but not u(t) -> u(u(t))
127+
var_of_iv2 = substitute(var_of_iv1, iv1 => iv2_of_iv1) # e.g. f(t) -> f(u(t))
128+
ex = substitute(ex, var_of_iv1 => var_of_iv2)
126129
end
127130
end
128131
ex = expand_derivatives(ex, simplify) # expand chain rule, e.g. (d/dt)(f(u(t)))) -> df(u(t))/du(t) * du(t)/dt
129132
return ex
130133
end
131134

132135
# 2) Find e.g. du/dt in equations, then calculate e.g. d²u/dt², ...
133-
eqs = [get_eqs(sys); eqs] # all equations (system-defined + user-provided) we may use
136+
eqs = [eqs; get_eqs(sys)] # all equations (system-defined + user-provided) we may use
134137
idxs = findall(eq -> isequal(eq.lhs, D1(iv2_of_iv1)), eqs)
135138
if length(idxs) != 1
136139
error("Exactly one equation for $D1($iv2_of_iv1) was not specified!")
@@ -148,11 +151,14 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummi
148151
div2name = Symbol(iv2name, :_, iv1name) # e.g. :u_t # TODO: customize
149152
ddiv2name = Symbol(div2name, iv1name) # e.g. :u_tt # TODO: customize
150153
div2, ddiv2 = @variables $div2name(iv2) $ddiv2name(iv2) # e.g. u_t(u), u_tt(u)
151-
eqs = [eqs; [div2 ~ div2_of_iv1, ddiv2 ~ ddiv2_of_iv1]] # add dummy equations
154+
div2, ddiv2 = GlobalScope.([div2, ddiv2]) # do not namespace dummies in new system
155+
eqs = [[div2 ~ div2_of_iv1, ddiv2 ~ ddiv2_of_iv1]; eqs] # add dummy equations
156+
@set! sys.unknowns = [get_unknowns(sys); [div2, ddiv2]] # add dummy variables
152157
else
153158
div2 = div2_of_iv1
154159
ddiv2 = ddiv2_of_iv1
155160
end
161+
@set! sys.eqs = eqs # add extra equations we derived before starting transformation process
156162

157163
# 4) Transform everything from old to new independent variable, e.g. t -> u.
158164
# Substitution order matters! Must begin with highest order to get D(D(u(t))) -> u_tt(u).
@@ -170,18 +176,31 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummi
170176
end
171177
return ex
172178
end
173-
eqs = map(transform, eqs) # we derived and added equations to eqs; they are not in get_eqs(sys)!
174-
observed = map(transform, get_observed(sys))
175-
initialization_eqs = map(transform, get_initialization_eqs(sys))
176-
parameter_dependencies = map(transform, get_parameter_dependencies(sys))
177-
defaults = Dict(transform(var) => transform(val) for (var, val) in get_defaults(sys))
178-
guesses = Dict(transform(var) => transform(val) for (var, val) in get_guesses(sys))
179-
assertions = Dict(transform(condition) => msg for (condition, msg) in get_assertions(sys))
180-
181-
# 5) Recreate system with transformed fields
182-
return typeof(sys)(
183-
eqs, iv2;
184-
observed, initialization_eqs, parameter_dependencies, defaults, guesses, assertions,
185-
name = nameof(sys), description = description(sys), kwargs...
186-
) |> complete # input system must be complete, so complete the output system
179+
function transform(sys::AbstractODESystem)
180+
eqs = map(transform, get_eqs(sys))
181+
unknowns = map(transform, get_unknowns(sys))
182+
unknowns = filter(var -> !isequal(var, iv2), unknowns) # remove e.g. u
183+
ps = map(transform, get_ps(sys))
184+
ps = filter(!isinitial, ps) # remove Initial(...) # # TODO: shouldn't have to touch this
185+
observed = map(transform, get_observed(sys))
186+
initialization_eqs = map(transform, get_initialization_eqs(sys))
187+
parameter_dependencies = map(transform, get_parameter_dependencies(sys))
188+
defaults = Dict(transform(var) => transform(val) for (var, val) in get_defaults(sys))
189+
guesses = Dict(transform(var) => transform(val) for (var, val) in get_guesses(sys))
190+
assertions = Dict(transform(condition) => msg for (condition, msg) in get_assertions(sys))
191+
systems = get_systems(sys) # save before reconstructing system
192+
wascomplete = iscomplete(sys) # save before reconstructing system
193+
sys = typeof(sys)( # recreate system with transformed fields
194+
eqs, iv2, unknowns, ps; observed, initialization_eqs, parameter_dependencies, defaults, guesses,
195+
assertions, name = nameof(sys), description = description(sys), kwargs...
196+
)
197+
systems = map(transform, systems) # recurse through subsystems
198+
sys = compose(sys, systems) # rebuild hierarchical system
199+
if wascomplete
200+
wasflat = isempty(systems)
201+
sys = complete(sys; flatten = wasflat) # complete output if input was complete
202+
end
203+
return sys
204+
end
205+
return transform(sys)
187206
end

test/basic_transformations.jl

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,25 +62,34 @@ end
6262
@independent_variables t
6363
D = Differential(t)
6464
@variables a(t) (t) Ω(t) ϕ(t)
65+
a, ȧ = GlobalScope.([a, ȧ])
66+
species(w; kw...) = ODESystem([D(Ω) ~ -3(1 + w) * D(a)/a * Ω], t, [Ω], []; kw...)
67+
@named r = species(1//3)
68+
@named m = species(0)
69+
@named Λ = species(-1)
6570
eqs = [
66-
Ω ~ 123
71+
Ω ~ r.Ω + m.Ω + Λ.Ω
6772
D(a) ~
6873
~ (Ω) * a^2
6974
D(D(ϕ)) ~ -3*D(a)/a*D(ϕ)
7075
]
71-
M1 = ODESystem(eqs, t; name = :M) |> complete
76+
M1 = ODESystem(eqs, t, [Ω, a, ȧ, ϕ], []; name = :M)
77+
M1 = compose(M1, r, m, Λ)
78+
M1 = complete(M1; flatten = false)
7279

7380
# Apply in two steps, where derivatives are defined at each step: first t -> a, then a -> b
7481
M2 = change_independent_variable(M1, M1.a; dummies = true)
75-
@independent_variables a
76-
@variables (a) Ω(a) ϕ(a) a_t(a) a_tt(a)
82+
a, ȧ, Ω, Ωr, Ωm, ΩΛ, ϕ, a_t, a_tt = M2.a, M2.ȧ, M2.Ω, M2.r.Ω, M2.m.Ω, M2.Λ.Ω, M2.ϕ, M2.a_t, M2.a_tt
7783
Da = Differential(a)
7884
@test Set(equations(M2)) == Set([
79-
a_tt*Da(ϕ) + a_t^2*(Da^2)(ϕ) ~ -3*a_t^2/a*Da(ϕ)
80-
~ (Ω) * a^2
81-
Ω ~ 123
8285
a_t ~# 1st order dummy equation
8386
a_tt ~ Da(ȧ) * a_t # 2nd order dummy equation
87+
Ω ~ Ωr + Ωm + ΩΛ
88+
~ (Ω) * a^2
89+
a_tt*Da(ϕ) + a_t^2*(Da^2)(ϕ) ~ -3*a_t^2/a*Da(ϕ)
90+
a_t*Da(Ωr) ~ -4*Ωr*a_t/a
91+
a_t*Da(Ωm) ~ -3*Ωm*a_t/a
92+
a_t*Da(ΩΛ) ~ 0
8493
])
8594

8695
@variables b(M2.a)
@@ -143,6 +152,4 @@ end
143152
@test_throws "not specified" change_independent_variable(M, v)
144153
@test_throws "not a function of the independent variable" change_independent_variable(M, y)
145154
@test_throws "not a function of the independent variable" change_independent_variable(M, z)
146-
M = compose(M, M)
147-
@test_throws "hierarchical" change_independent_variable(M, M.x)
148155
end

0 commit comments

Comments
 (0)