Skip to content

Commit 98125b7

Browse files
committed
Reorder things and make universal function that transforms all ODESystem fields
1 parent 792739e commit 98125b7

File tree

3 files changed

+77
-53
lines changed

3 files changed

+77
-53
lines changed

docs/src/tutorials/change_independent_variable.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ D = Differential(t)
2323
@parameters g = 9.81 v # gravitational acceleration and constant horizontal velocity
2424
M1 = ODESystem([
2525
D(D(y)) ~ -g, D(x) ~ v # constant horizontal velocity
26-
], t; initialization_eqs = [
26+
], t; defaults = [
27+
y => 0.0
28+
], initialization_eqs = [
2729
#x ~ 0.0, # TODO: handle?
28-
y ~ 0.0, D(x) ~ D(y) # equal initial horizontal and vertical velocity (45 °)
30+
D(x) ~ D(y) # equal initial horizontal and vertical velocity (45 °)
2931
], name = :M) |> complete
3032
M1s = structural_simplify(M1)
3133
```

src/systems/diffeqs/basic_transformations.jl

+71-49
Original file line numberDiff line numberDiff line change
@@ -110,20 +110,59 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummi
110110
iv2name = nameof(operation(iv))
111111
iv2, = @independent_variables $iv2name # e.g. a
112112
D1 = Differential(iv1)
113-
D2 = Differential(iv2)
114113

115114
iv1name = nameof(iv1) # e.g. t
116115
iv1func, = @variables $iv1name(iv2) # e.g. t(a)
117116

118-
div2name = Symbol(iv2name, :_t)
119-
div2, = @variables $div2name(iv2) # e.g. a_t(a)
117+
eqs = [get_eqs(sys); eqs] # copies system equations to avoid modifying original system
120118

121-
ddiv2name = Symbol(iv2name, :_tt)
122-
ddiv2, = @variables $ddiv2name(iv2) # e.g. a_tt(a)
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.")
124+
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.")
129+
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 ...
134+
135+
# 2) If requested, insert extra dummy equations for e.g. df/dt, d²f/dt², ...
136+
# Otherwise, replace all these derivatives by their explicit expressions
137+
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!
144+
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()
159+
end
123160

161+
# 3) Define a transformation function that performs the change of variable on any expression/equation
124162
function transform(ex)
125-
# 1) Substitute f(t₁) => f(t₂(t₁)) in all variables
126-
verbose && println("1. ", ex)
163+
verbose && println("Step 0: ", ex)
164+
165+
# Step 1: substitute f(t₁) => f(t₂(t₁)) in all variables in the expression
127166
vars = Symbolics.get_variables(ex)
128167
for var1 in vars
129168
if Symbolics.iscall(var1) && !isequal(var1, iv2func) # && isequal(only(arguments(var1)), iv1) # skip e.g. constants
@@ -132,53 +171,36 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummi
132171
ex = substitute(ex, var1 => var2; fold = false)
133172
end
134173
end
174+
verbose && println("Step 1: ", ex)
135175

136-
# 2) Substitute in dummy variables for dⁿt₂/dt₁ⁿ
137-
verbose && println("2. ", ex)
176+
# Step 2: expand out all chain rule derivatives
138177
ex = expand_derivatives(ex) # expand out with chain rule to get d(iv2)/d(iv1)
139-
verbose && println("3. ", ex)
140-
ex = substitute(ex, D1(D1(iv2func)) => ddiv2) # order 2 # TODO: more orders
141-
ex = substitute(ex, D1(iv2func) => div2) # order 1; e.g. D(a(t)) => a_t(t)
142-
ex = substitute(ex, iv2func => iv2) # order 0; make iv2 independent
143-
verbose && println("4. ", ex)
144-
ex = substitute(ex, iv1 => iv1func) # if autonomous
145-
verbose && println("5. ", ex)
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)
183+
end
184+
verbose && println("Step 3: ", ex)
185+
verbose && println()
186+
146187
return ex
147188
end
148189

149-
eqs = [get_eqs(sys); eqs]
190+
# 4) Transform all fields
150191
eqs = map(transform, eqs)
151-
192+
observed = map(transform, get_observed(sys))
152193
initialization_eqs = map(transform, get_initialization_eqs(sys))
153-
154-
div2_div1_idxs = findall(eq -> isequal(eq.lhs, div2), eqs)
155-
if length(div2_div1_idxs) == 0
156-
error("No equation for $D1($iv2func) was specified.")
157-
elseif length(div2_div1_idxs) >= 2
158-
error("Multiple equations for $D1($iv2func) were specified.")
159-
end
160-
div2_div1 = eqs[only(div2_div1_idxs)].rhs
161-
if isequal(div2_div1, 0)
162-
error("Cannot change independent variable from $iv1 to $iv2 with singular transformation $(div2 ~ div2_div1).")
163-
end
164-
165-
# 3) Add equations for dummy variables
166-
div1_div2 = 1 / div2_div1
167-
ddiv1_ddiv2 = expand_derivatives(-D2(div1_div2) / div1_div2^3)
168-
if simplify
169-
ddiv1_ddiv2 = Symbolics.simplify(ddiv1_ddiv2)
170-
end
171-
push!(eqs, ddiv2 ~ ddiv1_ddiv2) # e.g. https://math.stackexchange.com/questions/249253/second-derivative-of-the-inverse-function # TODO: higher orders
172-
173-
# 4) If requested, instead remove and insert dummy equations
174-
if !dummies # TODO: make dummies = false work with more fields!
175-
dummyidxs = findall(eq -> isequal(eq.lhs, div2) || isequal(eq.lhs, ddiv2), eqs)
176-
dummyeqs = splice!(eqs, dummyidxs) # return and remove dummy equations
177-
dummysubs = Dict(eq.lhs => eq.rhs for eq in dummyeqs)
178-
eqs = substitute.(eqs, Ref(dummysubs)) # don't iterate over dummysubs
179-
end
180-
181-
# 5) Recreate system with new equations
182-
sys2 = typeof(sys)(eqs, iv2; initialization_eqs, name = nameof(sys), description = description(sys), kwargs...)
183-
return sys2
194+
parameter_dependencies = map(transform, get_parameter_dependencies(sys))
195+
defaults = Dict(transform(var) => transform(val) for (var, val) in get_defaults(sys))
196+
guesses = Dict(transform(var) => transform(val) for (var, val) in get_guesses(sys))
197+
assertions = Dict(transform(condition) => msg for (condition, msg) in get_assertions(sys))
198+
# TODO: handle subsystems
199+
200+
# 5) Recreate system with transformed fields
201+
return typeof(sys)(
202+
eqs, iv2;
203+
observed, initialization_eqs, parameter_dependencies, defaults, guesses, assertions,
204+
name = nameof(sys), description = description(sys), kwargs...
205+
) |> complete # original system had to be complete
184206
end

test/basic_transformations.jl

+2-2
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,8 @@ end
114114
M = complete(M)
115115
@test_throws "singular" change_independent_variable(M, M.x)
116116
@test_throws "structurally simplified" change_independent_variable(structural_simplify(M), y)
117-
@test_throws "No equation" change_independent_variable(M, w)
118-
@test_throws "No equation" change_independent_variable(M, v)
117+
@test_throws "not specified" change_independent_variable(M, w)
118+
@test_throws "not specified" change_independent_variable(M, v)
119119
@test_throws "not a function of the independent variable" change_independent_variable(M, y)
120120
@test_throws "not a function of the independent variable" change_independent_variable(M, z)
121121
M = compose(M, M)

0 commit comments

Comments
 (0)