Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ SpecialFunctions = "0.7, 0.8, 0.9, 0.10, 1.0, 2"
StaticArrays = "0.10, 0.11, 0.12, 1.0"
SymbolicIndexingInterface = "0.3.31"
SymbolicUtils = "3.7"
Symbolics = "6.15.2"
Symbolics = "6.15.4"
URIs = "1"
UnPack = "0.1, 1.0"
Unitful = "1.1"
Expand Down
22 changes: 19 additions & 3 deletions docs/src/examples/perturbation.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,57 @@ In the [Mixed Symbolic-Numeric Perturbation Theory tutorial](https://symbolics.j
## Free fall in a varying gravitational field

Our first ODE example is a well-known physics problem: what is the altitude $x(t)$ of an object (say, a ball or a rocket) thrown vertically with initial velocity $ẋ(0)$ from the surface of a planet with mass $M$ and radius $R$? According to Newton's second law and law of gravity, it is the solution of the ODE

```math
ẍ = -\frac{GM}{(R+x)^2} = -\frac{GM}{R^2} \frac{1}{\left(1+ϵ\frac{x}{R}\right)^2}.
```

In the last equality, we introduced a perturbative expansion parameter $ϵ$. When $ϵ=1$, we recover the original problem. When $ϵ=0$, the problem reduces to the trivial problem $ẍ = -g$ with constant gravitational acceleration $g = GM/R^2$ and solution $x(t) = x(0) + ẋ(0) t - \frac{1}{2} g t^2$. This is a good setup for perturbation theory.

To make the problem dimensionless, we redefine $x \leftarrow x / R$ and $t \leftarrow t / \sqrt{R^3/GM}$. Then the ODE becomes

```@example perturbation
using ModelingToolkit
using ModelingToolkit: t_nounits as t, D_nounits as D
@variables ϵ x(t)
eq = D(D(x)) ~ -(1 + ϵ*x)^(-2)
eq = D(D(x)) ~ -(1 + ϵ * x)^(-2)
```

Next, expand $x(t)$ in a series up to second order in $ϵ$:

```@example perturbation
using Symbolics
@variables y(t)[0:2] # coefficients
x_series = series(y, ϵ)
```

Insert this into the equation and collect perturbed equations to each order:

```@example perturbation
eq_pert = substitute(eq, x => x_series)
eqs_pert = taylor_coeff(eq_pert, ϵ, 0:2)
```

!!! note

The 0-th order equation can be solved analytically, but ModelingToolkit does currently not feature automatic analytical solution of ODEs, so we proceed with solving it numerically.
These are the ODEs we want to solve. Now construct an `ODESystem`, which automatically inserts dummy derivatives for the velocities:
These are the ODEs we want to solve. Now construct an `ODESystem`, which automatically inserts dummy derivatives for the velocities:

```@example perturbation
@mtkbuild sys = ODESystem(eqs_pert, t)
```

To solve the `ODESystem`, we generate an `ODEProblem` with initial conditions $x(0) = 0$, and $ẋ(0) = 1$, and solve it:

```@example perturbation
using DifferentialEquations
u0 = Dict([unknowns(sys) .=> 0.0; D(y[0]) => 1.0]) # nonzero initial velocity
prob = ODEProblem(sys, u0, (0.0, 3.0))
sol = solve(prob)
```

This is the solution for the coefficients in the series for $x(t)$ and their derivatives. Finally, we calculate the solution to the original problem by summing the series for different $ϵ$:

```@example perturbation
using Plots
p = plot()
Expand All @@ -53,6 +64,7 @@ for ϵᵢ in 0.0:0.1:1.0
end
p
```

This makes sense: for larger $ϵ$, gravity weakens with altitude, and the trajectory goes higher for a fixed initial velocity.

An advantage of the perturbative method is that we run the ODE solver only once and calculate trajectories for several $ϵ$ for free. Had we solved the full unperturbed ODE directly, we would need to do repeat it for every $ϵ$.
Expand All @@ -62,19 +74,23 @@ An advantage of the perturbative method is that we run the ODE solver only once
Our second example applies perturbation theory to nonlinear oscillators -- a very important class of problems. As we will see, perturbation theory has difficulty providing a good solution to this problem, but the process is nevertheless instructive. This example closely follows chapter 7.6 of *Nonlinear Dynamics and Chaos* by Steven Strogatz.

The goal is to solve the ODE

```@example perturbation
eq = D(D(x)) + 2*ϵ*D(x) + x ~ 0
eq = D(D(x)) + 2 * ϵ * D(x) + x ~ 0
```

with initial conditions $x(0) = 0$ and $ẋ(0) = 1$. With $ϵ = 0$, the problem reduces to the simple linear harmonic oscillator with the exact solution $x(t) = \sin(t)$.

We follow the same steps as in the previous example to construct the `ODESystem`:

```@example perturbation
eq_pert = substitute(eq, x => x_series)
eqs_pert = taylor_coeff(eq_pert, ϵ, 0:2)
@mtkbuild sys = ODESystem(eqs_pert, t)
```

We solve and plot it as in the previous example, and compare the solution with $ϵ=0.1$ to the exact solution $x(t, ϵ) = e^{-ϵ t} \sin(\sqrt{(1-ϵ^2)}\,t) / \sqrt{1-ϵ^2}$ of the unperturbed equation:

```@example perturbation
u0 = Dict([unknowns(sys) .=> 0.0; D(y[0]) => 1.0]) # nonzero initial velocity
prob = ODEProblem(sys, u0, (0.0, 50.0))
Expand Down
4 changes: 2 additions & 2 deletions src/systems/abstractsystem.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3001,8 +3001,8 @@ By default, the resulting system inherits `sys`'s name and description.
See also [`compose`](@ref).
"""
function extend(sys::AbstractSystem, basesys::AbstractSystem;
name::Symbol = nameof(sys), description = description(sys),
gui_metadata = get_gui_metadata(sys))
name::Symbol = nameof(sys), description = description(sys),
gui_metadata = get_gui_metadata(sys))
T = SciMLBase.parameterless_type(basesys)
ivs = independent_variables(basesys)
if !(sys isa T)
Expand Down
127 changes: 121 additions & 6 deletions src/systems/problem_utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,25 @@ function add_parameter_dependencies!(sys::AbstractSystem, varmap::AbstractDict)
add_observed_equations!(varmap, parameter_dependencies(sys))
end

struct UnexpectedSymbolicValueInVarmap <: Exception
sym::Any
val::Any
end

function Base.showerror(io::IO, err::UnexpectedSymbolicValueInVarmap)
println(io,
"""
Found symbolic value $(err.val) for variable $(err.sym). You may be missing an \
initial condition or have cyclic initial conditions. If this is intended, pass \
`symbolic_u0 = true`. In case the initial conditions are not cyclic but \
require more substitutions to resolve, increase `substitution_limit`. To report \
cycles in initial conditions of unknowns/parameters, pass \
`warn_cyclic_dependency = true`. If the cycles are still not reported, you \
may need to pass a larger value for `circular_dependency_max_cycle_length` \
or `circular_dependency_max_cycles`.
""")
end

"""
$(TYPEDSIGNATURES)

Expand Down Expand Up @@ -278,6 +297,12 @@ function better_varmap_to_vars(varmap::AbstractDict, vars::Vector;
isempty(missing_vars) || throw(MissingVariablesError(missing_vars))
end
vals = map(x -> varmap[x], vars)
if !allow_symbolic
for (sym, val) in zip(vars, vals)
symbolic_type(val) == NotSymbolic() && continue
throw(UnexpectedSymbolicValueInVarmap(sym, val))
end
end

if container_type <: Union{AbstractDict, Tuple, Nothing, SciMLBase.NullParameters}
container_type = Array
Expand All @@ -298,17 +323,68 @@ function better_varmap_to_vars(varmap::AbstractDict, vars::Vector;
end
end

"""
$(TYPEDSIGNATURES)

Check if any of the substitution rules in `varmap` lead to cycles involving
variables in `vars`. Return a vector of vectors containing all the variables
in each cycle.

Keyword arguments:
- `max_cycle_length`: The maximum length (number of variables) of detected cycles.
- `max_cycles`: The maximum number of cycles to report.
"""
function check_substitution_cycles(
varmap::AbstractDict, vars; max_cycle_length = length(varmap), max_cycles = 10)
# ordered set so that `vars` are the first `k` in the list
allvars = OrderedSet{Any}(vars)
union!(allvars, keys(varmap))
allvars = collect(allvars)
var_to_idx = Dict(allvars .=> eachindex(allvars))
graph = SimpleDiGraph(length(allvars))

buffer = Set()
for (k, v) in varmap
kidx = var_to_idx[k]
if symbolic_type(v) != NotSymbolic()
vars!(buffer, v)
for var in buffer
haskey(var_to_idx, var) || continue
add_edge!(graph, kidx, var_to_idx[var])
end
elseif v isa AbstractArray
for val in v
vars!(buffer, val)
end
for var in buffer
haskey(var_to_idx, var) || continue
add_edge!(graph, kidx, var_to_idx[var])
end
end
empty!(buffer)
end

# detect at most 100 cycles involving at most `length(varmap)` vertices
cycles = Graphs.simplecycles_limited_length(graph, max_cycle_length, max_cycles)
# only count those which contain variables in `vars`
filter!(Base.Fix1(any, <=(length(vars))), cycles)

map(cycles) do cycle
map(Base.Fix1(getindex, allvars), cycle)
end
end

"""
$(TYPEDSIGNATURES)

Performs symbolic substitution on the values in `varmap` for the keys in `vars`, using
`varmap` itself as the set of substitution rules. If an entry in `vars` is not a key
in `varmap`, it is ignored.
"""
function evaluate_varmap!(varmap::AbstractDict, vars)
function evaluate_varmap!(varmap::AbstractDict, vars; limit = 100)
for k in vars
haskey(varmap, k) || continue
varmap[k] = fixpoint_sub(varmap[k], varmap)
varmap[k] = fixpoint_sub(varmap[k], varmap; maxiters = limit)
end
end

Expand Down Expand Up @@ -407,6 +483,14 @@ Keyword arguments:
length of `u0` vector for consistency. If `false`, do not check with equations. This is
forwarded to `check_eqs_u0`
- `symbolic_u0` allows the returned `u0` to be an array of symbolics.
- `warn_cyclic_dependency`: Whether to emit a warning listing out cycles in initial
conditions provided for unknowns and parameters.
- `circular_dependency_max_cycle_length`: Maximum length of cycle to check for.
Only applicable if `warn_cyclic_dependency == true`.
- `circular_dependency_max_cycles`: Maximum number of cycles to check for.
Only applicable if `warn_cyclic_dependency == true`.
- `substitution_limit`: The number times to substitute initial conditions into each
other to attempt to arrive at a numeric value.

All other keyword arguments are passed as-is to `constructor`.
"""
Expand All @@ -416,7 +500,11 @@ function process_SciMLProblem(
warn_initialize_determined = true, initialization_eqs = [],
eval_expression = false, eval_module = @__MODULE__, fully_determined = false,
check_initialization_units = false, tofloat = true, use_union = false,
u0_constructor = identity, du0map = nothing, check_length = true, symbolic_u0 = false, kwargs...)
u0_constructor = identity, du0map = nothing, check_length = true,
symbolic_u0 = false, warn_cyclic_dependency = false,
circular_dependency_max_cycle_length = length(all_symbols(sys)),
circular_dependency_max_cycles = 10,
substitution_limit = 100, kwargs...)
dvs = unknowns(sys)
ps = parameters(sys)
iv = has_iv(sys) ? get_iv(sys) : nothing
Expand Down Expand Up @@ -466,7 +554,8 @@ function process_SciMLProblem(
initializeprob = ModelingToolkit.InitializationProblem(
sys, t, u0map, pmap; guesses, warn_initialize_determined,
initialization_eqs, eval_expression, eval_module, fully_determined,
check_units = check_initialization_units)
warn_cyclic_dependency, check_units = check_initialization_units,
circular_dependency_max_cycle_length, circular_dependency_max_cycles)
initializeprobmap = getu(initializeprob, unknowns(sys))

punknowns = [p
Expand Down Expand Up @@ -503,7 +592,20 @@ function process_SciMLProblem(
add_observed!(sys, op)
add_parameter_dependencies!(sys, op)

evaluate_varmap!(op, dvs)
if warn_cyclic_dependency
cycles = check_substitution_cycles(
op, dvs; max_cycle_length = circular_dependency_max_cycle_length,
max_cycles = circular_dependency_max_cycles)
if !isempty(cycles)
buffer = IOBuffer()
for cycle in cycles
println(buffer, cycle)
end
msg = String(take!(buffer))
@warn "Cycles in unknowns:\n$msg"
end
end
evaluate_varmap!(op, dvs; limit = substitution_limit)

u0 = better_varmap_to_vars(
op, dvs; tofloat = true, use_union = false,
Expand All @@ -515,7 +617,20 @@ function process_SciMLProblem(

check_eqs_u0(eqs, dvs, u0; check_length, kwargs...)

evaluate_varmap!(op, ps)
if warn_cyclic_dependency
cycles = check_substitution_cycles(
op, ps; max_cycle_length = circular_dependency_max_cycle_length,
max_cycles = circular_dependency_max_cycles)
if !isempty(cycles)
buffer = IOBuffer()
for cycle in cycles
println(buffer, cycle)
end
msg = String(take!(buffer))
@warn "Cycles in parameters:\n$msg"
end
end
evaluate_varmap!(op, ps; limit = substitution_limit)
if is_split(sys)
p = MTKParameters(sys, op)
else
Expand Down
25 changes: 25 additions & 0 deletions test/initial_values.jl
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,28 @@ end
pmap = [c1 => 5.0, c2 => 1.0, c3 => 1.2]
oprob = ODEProblem(osys, u0map, (0.0, 10.0), pmap)
end

@testset "Cyclic dependency checking and substitution limits" begin
@variables x(t) y(t)
@mtkbuild sys = ODESystem(
[D(x) ~ x, D(y) ~ y], t; initialization_eqs = [x ~ 2y + 3, y ~ 2x],
guesses = [x => 2y, y => 2x])
@test_warn ["Cycle", "unknowns", "x", "y"] try
ODEProblem(sys, [], (0.0, 1.0), warn_cyclic_dependency = true)
catch
end
@test_throws ModelingToolkit.UnexpectedSymbolicValueInVarmap ODEProblem(
sys, [x => 2y + 1, y => 2x], (0.0, 1.0); build_initializeprob = false)

@parameters p q
@mtkbuild sys = ODESystem(
[D(x) ~ x * p, D(y) ~ y * q], t; guesses = [p => 1.0, q => 2.0])
# "unknowns" because they are initialization unknowns
@test_warn ["Cycle", "unknowns", "p", "q"] try
ODEProblem(sys, [x => 1, y => 2], (0.0, 1.0),
[p => 2q, q => 3p]; warn_cyclic_dependency = true)
catch
end
@test_throws ModelingToolkit.UnexpectedSymbolicValueInVarmap ODEProblem(
sys, [x => 1, y => 2], (0.0, 1.0), [p => 2q, q => 3p])
end
Loading