Skip to content

Commit 701ac0e

Browse files
committed
Clean up independent variable change implementation
1 parent 98125b7 commit 701ac0e

File tree

3 files changed

+102
-101
lines changed

3 files changed

+102
-101
lines changed

docs/src/tutorials/change_independent_variable.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ M1 = ODESystem([
2626
], t; defaults = [
2727
y => 0.0
2828
], initialization_eqs = [
29-
#x ~ 0.0, # TODO: handle?
29+
#x ~ 0.0, # TODO: handle? # hide
3030
D(x) ~ D(y) # equal initial horizontal and vertical velocity (45 °)
3131
], name = :M) |> complete
3232
M1s = structural_simplify(M1)

src/systems/diffeqs/basic_transformations.jl

+61-85
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ function liouville_transform(sys::AbstractODESystem; kwargs...)
5151
end
5252

5353
"""
54-
function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummies = false, simplify = true, verbose = false, kwargs...)
54+
function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummies = false, simplify = true, fold = false, kwargs...)
5555
5656
Transform the independent variable (e.g. ``t``) of the ODE system `sys` to a dependent variable `iv` (e.g. ``f(t)``).
5757
An equation in `sys` must define the rate of change of the new independent variable (e.g. ``df(t)/dt``).
@@ -64,7 +64,7 @@ Keyword arguments
6464
=================
6565
If `dummies`, derivatives of the new independent variable are expressed through dummy equations; otherwise they are explicitly inserted into the equations.
6666
If `simplify`, these dummy expressions are simplified and often give a tidier transformation.
67-
If `verbose`, the function prints intermediate transformations of equations to aid debugging.
67+
If `fold`, internal substitutions will evaluate numerical expressions.
6868
Any additional keyword arguments `kwargs...` are forwarded to the constructor that rebuilds the system.
6969
7070
Usage before structural simplification
@@ -89,118 +89,94 @@ julia> unknowns(M′)
8989
y(x)
9090
```
9191
"""
92-
function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummies = false, simplify = true, verbose = false, kwargs...)
92+
function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummies = false, simplify = true, fold = false, kwargs...)
93+
iv2_of_iv1 = unwrap(iv) # e.g. u(t)
94+
iv1 = get_iv(sys) # e.g. t
95+
9396
if !iscomplete(sys)
94-
error("Cannot change independent variable of incomplete system $(nameof(sys))")
97+
error("System $(nameof(sys)) is incomplete. Complete it first!")
9598
elseif isscheduled(sys)
96-
error("Cannot change independent variable of structurally simplified system $(nameof(sys))")
99+
error("System $(nameof(sys)) is structurally simplified. Change independent variable before structural simplification!")
97100
elseif !isempty(get_systems(sys))
98-
error("Cannot change independent variable of hierarchical system $(nameof(sys)). Flatten it first.") # TODO: implement
101+
error("System $(nameof(sys)) is hierarchical. Flatten it first!") # TODO: implement and allow?
102+
elseif !iscall(iv2_of_iv1) || !isequal(only(arguments(iv2_of_iv1)), iv1)
103+
error("Variable $iv is not a function of the independent variable $iv1 of the system $(nameof(sys))!")
99104
end
100105

101-
iv = unwrap(iv)
102-
iv1 = get_iv(sys) # e.g. t
103-
if !iscall(iv) || !isequal(only(arguments(iv)), iv1)
104-
error("New independent variable $iv is not a function of the independent variable $iv1 of the system $(nameof(sys))")
105-
elseif !isautonomous(sys) && isempty(findall(eq -> isequal(eq.lhs, iv1), eqs))
106-
error("System $(nameof(sys)) is autonomous in $iv1. An equation of the form $iv1 ~ F($iv) must be provided.")
106+
iv1name = nameof(iv1) # e.g. :t
107+
iv2name = nameof(operation(iv2_of_iv1)) # e.g. :u
108+
iv2, = @independent_variables $iv2name # e.g. u
109+
iv1_of_iv2, = @variables $iv1name(iv2) # inverse in case sys is autonomous; e.g. t(u)
110+
D1 = Differential(iv1) # e.g. d/d(t)
111+
D2 = Differential(iv2_of_iv1) # e.g. d/d(u(t))
112+
113+
# 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
114+
function chain_rule(ex)
115+
vars = get_variables(ex)
116+
for var_of_iv1 in vars # loop through e.g. f(t)
117+
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))
118+
varname = nameof(operation(var_of_iv1)) # e.g. :f
119+
var_of_iv2, = @variables $varname(iv2_of_iv1) # e.g. f(u(t))
120+
ex = substitute(ex, var_of_iv1 => var_of_iv2; fold) # e.g. f(t) -> f(u(t))
121+
end
122+
end
123+
ex = expand_derivatives(ex, simplify) # expand chain rule, e.g. (d/dt)(f(u(t)))) -> df(u(t))/du(t) * du(t)/dt
124+
return ex
107125
end
108126

109-
iv2func = iv # e.g. a(t)
110-
iv2name = nameof(operation(iv))
111-
iv2, = @independent_variables $iv2name # e.g. a
112-
D1 = Differential(iv1)
113-
114-
iv1name = nameof(iv1) # e.g. t
115-
iv1func, = @variables $iv1name(iv2) # e.g. t(a)
116-
117-
eqs = [get_eqs(sys); eqs] # copies system equations to avoid modifying original system
118-
119-
# 1) Find and compute all necessary expressions for e.g. df/dt, d²f/dt², ...
120-
# 1.1) Find the 1st order derivative of the new independent variable (e.g. da(t)/dt = ...), ...
121-
div2_div1_idxs = findall(eq -> isequal(eq.lhs, D1(iv2func)), eqs) # index of e.g. da/dt = ...
122-
if length(div2_div1_idxs) != 1
123-
error("Exactly one equation for $D1($iv2func) was not specified.")
127+
# 2) Find e.g. du/dt in equations, then calculate e.g. d²u/dt², ...
128+
eqs = [get_eqs(sys); eqs] # all equations (system-defined + user-provided) we may use
129+
idxs = findall(eq -> isequal(eq.lhs, D1(iv2_of_iv1)), eqs)
130+
if length(idxs) != 1
131+
error("Exactly one equation for $D1($iv2_of_iv1) was not specified!")
124132
end
125-
div2_div1_eq = popat!(eqs, only(div2_div1_idxs)) # get and remove e.g. df/dt = ... (may be added back later)
126-
div2_div1 = div2_div1_eq.rhs
127-
if isequal(div2_div1, 0)
128-
error("Cannot change independent variable from $iv1 to $iv2 with singular transformation $div2_div1_eq.")
133+
div2_of_iv1_eq = popat!(eqs, only(idxs)) # get and remove e.g. du/dt = ... (may be added back later as a dummy)
134+
div2_of_iv1 = chain_rule(div2_of_iv1_eq.rhs)
135+
if isequal(div2_of_iv1, 0) # e.g. du/dt ~ 0
136+
error("Independent variable transformation $(div2_of_iv1_eq) is singular!")
129137
end
130-
# 1.2) ... then compute the 2nd order derivative of the new independent variable
131-
div1_div2 = 1 / div2_div1 # TODO: URL reference for clarity
132-
ddiv2_ddiv1 = expand_derivatives(-Differential(iv2func)(div1_div2) / div1_div2^3, simplify) # e.g. https://math.stackexchange.com/questions/249253/second-derivative-of-the-inverse-function # TODO: higher orders # TODO: pass simplify here
133-
# 1.3) # TODO: handle higher orders (3+) derivatives ...
138+
ddiv2_of_iv1 = chain_rule(D1(div2_of_iv1)) # TODO: implement higher orders (order >= 3) derivatives with a loop
134139

135-
# 2) If requested, insert extra dummy equations for e.g. df/dt, d²f/dt², ...
140+
# 3) If requested, insert extra dummy equations for e.g. du/dt, d²u/dt², ...
136141
# Otherwise, replace all these derivatives by their explicit expressions
137142
if dummies
138-
div2name = Symbol(iv2name, :_t) # TODO: not always t
139-
div2, = @variables $div2name(iv2) # e.g. a_t(a)
140-
ddiv2name = Symbol(iv2name, :_tt) # TODO: not always t
141-
ddiv2, = @variables $ddiv2name(iv2) # e.g. a_tt(a)
142-
eqs = [eqs; [div2 ~ div2_div1, ddiv2 ~ ddiv2_ddiv1]] # add dummy equations
143-
derivsubs = [D1(D1(iv2func)) => ddiv2, D1(iv2func) => div2] # order is crucial!
143+
div2name = Symbol(iv2name, :_, iv1name) # e.g. :u_t # TODO: customize
144+
ddiv2name = Symbol(div2name, iv1name) # e.g. :u_tt # TODO: customize
145+
div2, ddiv2 = @variables $div2name(iv2) $ddiv2name(iv2) # e.g. u_t(u), u_tt(u)
146+
eqs = [eqs; [div2 ~ div2_of_iv1, ddiv2 ~ ddiv2_of_iv1]] # add dummy equations
144147
else
145-
derivsubs = [D1(D1(iv2func)) => ddiv2_ddiv1, D1(iv2func) => div2_div1] # order is crucial!
146-
end
147-
derivsubs = [derivsubs; [iv2func => iv2, iv1 => iv1func]]
148-
149-
if verbose
150-
# Explain what we just did
151-
println("Order 1 (found): $div2_div1_eq")
152-
println("Order 2 (computed): $(D1(div2_div1_eq.lhs) ~ ddiv2_ddiv1)")
153-
println()
154-
println("Substitutions will be made in this order:")
155-
for (n, sub) in enumerate(derivsubs)
156-
println("$n: $(sub[1]) => $(sub[2])")
157-
end
158-
println()
148+
div2 = div2_of_iv1
149+
ddiv2 = ddiv2_of_iv1
159150
end
160151

161-
# 3) Define a transformation function that performs the change of variable on any expression/equation
152+
# 4) Transform everything from old to new independent variable, e.g. t -> u.
153+
# Substitution order matters! Must begin with highest order to get D(D(u(t))) -> u_tt(u).
154+
# If we had started with the lowest order, we would get D(D(u(t))) -> D(u_t(u)) -> 0!
155+
iv1_to_iv2_subs = [ # a vector ensures substitution order
156+
D1(D1(iv2_of_iv1)) => ddiv2 # order 2, e.g. D(D(u(t))) -> u_tt(u)
157+
D1(iv2_of_iv1) => div2 # order 1, e.g. D(u(t)) -> u_t(u)
158+
iv2_of_iv1 => iv2 # order 0, e.g. u(t) -> u
159+
iv1 => iv1_of_iv2 # in case sys was autonomous, e.g. t -> t(u)
160+
]
162161
function transform(ex)
163-
verbose && println("Step 0: ", ex)
164-
165-
# Step 1: substitute f(t₁) => f(t₂(t₁)) in all variables in the expression
166-
vars = Symbolics.get_variables(ex)
167-
for var1 in vars
168-
if Symbolics.iscall(var1) && !isequal(var1, iv2func) # && isequal(only(arguments(var1)), iv1) # skip e.g. constants
169-
name = nameof(operation(var1))
170-
var2, = @variables $name(iv2func)
171-
ex = substitute(ex, var1 => var2; fold = false)
172-
end
173-
end
174-
verbose && println("Step 1: ", ex)
175-
176-
# Step 2: expand out all chain rule derivatives
177-
ex = expand_derivatives(ex) # expand out with chain rule to get d(iv2)/d(iv1)
178-
verbose && println("Step 2: ", ex)
179-
180-
# Step 3: substitute d²f/dt², df/dt, ... (to dummy variables or explicit expressions, depending on dummies)
181-
for sub in derivsubs
182-
ex = substitute(ex, sub)
162+
ex = chain_rule(ex)
163+
for sub in iv1_to_iv2_subs
164+
ex = substitute(ex, sub; fold)
183165
end
184-
verbose && println("Step 3: ", ex)
185-
verbose && println()
186-
187166
return ex
188167
end
189-
190-
# 4) Transform all fields
191-
eqs = map(transform, eqs)
168+
eqs = map(transform, eqs) # we derived and added equations to eqs; they are not in get_eqs(sys)!
192169
observed = map(transform, get_observed(sys))
193170
initialization_eqs = map(transform, get_initialization_eqs(sys))
194171
parameter_dependencies = map(transform, get_parameter_dependencies(sys))
195172
defaults = Dict(transform(var) => transform(val) for (var, val) in get_defaults(sys))
196173
guesses = Dict(transform(var) => transform(val) for (var, val) in get_guesses(sys))
197174
assertions = Dict(transform(condition) => msg for (condition, msg) in get_assertions(sys))
198-
# TODO: handle subsystems
199175

200176
# 5) Recreate system with transformed fields
201177
return typeof(sys)(
202178
eqs, iv2;
203179
observed, initialization_eqs, parameter_dependencies, defaults, guesses, assertions,
204180
name = nameof(sys), description = description(sys), kwargs...
205-
) |> complete # original system had to be complete
181+
) |> complete # input system must be complete, so complete the output system
206182
end

test/basic_transformations.jl

+40-15
Original file line numberDiff line numberDiff line change
@@ -59,34 +59,44 @@ end
5959
end
6060

6161
@testset "Change independent variable (Friedmann equation)" begin
62+
@independent_variables t
6263
D = Differential(t)
63-
@variables a(t) (t) ρr(t) ρm(t) ρΛ(t) ρ(t) P(t) ϕ(t)
64-
@parameters Ωr0 Ωm0 ΩΛ0
64+
@variables a(t) (t) Ω(t) ϕ(t)
6565
eqs = [
66-
ρr ~ 3/(8*Num(π)) * Ωr0 / a^4
67-
ρm ~ 3/(8*Num(π)) * Ωm0 / a^3
68-
ρΛ ~ 3/(8*Num(π)) * ΩΛ0
69-
ρ ~ ρr + ρm + ρΛ
70-
~ (8*Num(π)/3*ρ*a^4)
66+
Ω ~ 123
7167
D(a) ~
68+
~ (Ω) * a^2
7269
D(D(ϕ)) ~ -3*D(a)/a*D(ϕ)
7370
]
7471
M1 = ODESystem(eqs, t; name = :M) |> complete
7572

7673
# Apply in two steps, where derivatives are defined at each step: first t -> a, then a -> b
77-
M2 = change_independent_variable(M1, M1.a) |> complete #, D(b) ~ D(a)/a; verbose = true)
74+
M2 = change_independent_variable(M1, M1.a; dummies = true)
75+
@independent_variables a
76+
@variables (a) Ω(a) ϕ(a) a_t(a) a_tt(a)
77+
Da = Differential(a)
78+
@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
82+
a_t ~# 1st order dummy equation
83+
a_tt ~ Da(ȧ) * a_t # 2nd order dummy equation
84+
])
85+
7886
@variables b(M2.a)
7987
M3 = change_independent_variable(M2, b, [Differential(M2.a)(b) ~ exp(-b), M2.a ~ exp(b)])
88+
89+
M1 = structural_simplify(M1)
8090
M2 = structural_simplify(M2; allow_symbolic = true)
8191
M3 = structural_simplify(M3; allow_symbolic = true)
82-
@test length(unknowns(M2)) == 2 && length(unknowns(M3)) == 2
92+
@test length(unknowns(M3)) == length(unknowns(M2)) == length(unknowns(M1)) - 1
8393
end
8494

8595
@testset "Change independent variable (simple)" begin
8696
@variables x(t)
87-
Mt = ODESystem([D(x) ~ 2*x], t; name = :M) |> complete # TODO: avoid complete. can avoid it if passing defined $variable directly to change_independent_variable
97+
Mt = ODESystem([D(x) ~ 2*x], t; name = :M) |> complete
8898
Mx = change_independent_variable(Mt, Mt.x; dummies = true)
89-
@test (@variables x x_t(x) x_tt(x); Set(equations(Mx)) == Set([x_t ~ 2x, x_tt ~ 4x]))
99+
@test (@variables x x_t(x) x_tt(x); Set(equations(Mx)) == Set([x_t ~ 2*x, x_tt ~ 2*x_t]))
90100
end
91101

92102
@testset "Change independent variable (free fall)" begin
@@ -101,10 +111,25 @@ end
101111
@test all(isapprox.(sol[Mx.y], sol[Mx.x - g*(Mx.x/v)^2/2]; atol = 1e-10)) # compare to analytical solution (x(t) = v*t, y(t) = v*t - g*t^2/2)
102112
end
103113

104-
@testset "Change independent variable (autonomous system)" begin
105-
M = ODESystem([D(x) ~ t], t; name = :M) |> complete # non-autonomous
106-
@test_throws "t ~ F(x(t)) must be provided" change_independent_variable(M, M.x)
107-
@test_nowarn change_independent_variable(M, M.x, [t ~ 2*x])
114+
@testset "Change independent variable (crazy analytical example)" begin
115+
@independent_variables t
116+
D = Differential(t)
117+
@variables x(t) y(t)
118+
M1 = ODESystem([ # crazy non-autonomous non-linear 2nd order ODE
119+
D(D(y)) ~ D(x)^2 + D(y^3) |> expand_derivatives # expand D(y^3) # TODO: make this test 3rd order
120+
D(x) ~ x^4 + y^5 + t^6
121+
], t; name = :M) |> complete
122+
M2 = change_independent_variable(M1, M1.x; dummies = true)
123+
124+
# Compare to pen-and-paper result
125+
@independent_variables x
126+
Dx = Differential(x)
127+
@variables x_t(x) x_tt(x) y(x) t(x)
128+
@test Set(equations(M2)) == Set([
129+
x_t^2*(Dx^2)(y) + x_tt*Dx(y) ~ x_t^2 + 3*y^2*Dx(y)*x_t # from D(D(y))
130+
x_t ~ x^4 + y^5 + t^6 # 1st order dummy equation
131+
x_tt ~ 4*x^3*x_t + 5*y^4*Dx(y)*x_t + 6*t^5 # 2nd order dummy equation
132+
])
108133
end
109134

110135
@testset "Change independent variable (errors)" begin

0 commit comments

Comments
 (0)