Gradual parameter changes via callbacks

vh94
Wed, 19 Mar 2025 20:31:33 +0000
Modelling & Simulations

Problem

I’m trying to change the parameter of a dynamical system gradually.

In the documentation for the DifferentialEquations library there is an emphasis on abrupt / instantaneous change - when explaining callbacks ; i.e. something hits a wall; gets kicked, fast drug dosage kinetics…

However the systems I’m trying to model show gradual changes in parameters (mostly linear with time) at discrete timepoints.

While I could achieve such change using a ContinuousCallback, I’m suspecting that I should use the VectorContinuousCallback instead - however it has sadly no example yet.

Or maybe even use the DiffEqCallbacks.jl library instead?

Reproducable Example:

Gradual breaking of an harmonic oscillator, i.e. the damping force \omega gets linearly increased by 0.5 twice, at t = 25 and t = 75 each over a duration of 10 seconds.

using OrdinaryDiffEq, Plots, ModelingToolkit
# Example dynamical system:
harmonicoscillator(ddu,du,u,ω,t) =  ddu .= -ω^2 * u

# Initial Conditions, timescale, stepsize
x₀ = [0.0]; dx₀ = [π/2]; ω = .75
tspan = (0.0, 100.0); dt = 0.001

# Im using ModelingToolkit for my models mostly

@named sys = modelingtoolkitize(
  SecondOrderODEProblem(harmonicoscillator, dx₀, x₀, tspan, ω)
  )

prob = ODEProblem(complete(sys))

as previous mentioned I am looking to increasing the force ω gradually over 10 (seconds) twice;
once at t=25 and once at t=75

timeranges = [25.0:dt:35.0, 75.0:dt:85.0] # should i collect the vector here?
Δω = 0.5 # absolute change of force ω
duration = length(timeranges[1]) # duration in timesteps of the acceleration

# This is the expected force after two changes:
expected_force = ω + ( length(timeranges) * Δω ) # i.e. :  0.75 + (2*0.5) = 1.75

# This is the naive callback i coded:

# check if the current timestep falls in one of the timeranges:
condition(u, t, integrator) =  !any(t .∈ timeranges) |> Int # hit if: == zero

# [NOTE] modelingtoolkitize assigns symbols for parameters ie force ω is now α :
affect!(integrator) = integrator.ps[:α] += Δω / duration # or (Δω * dt) / 10
# how would i handle different durations here for example ?!

cb = ContinuousCallback(condition, affect!)

solving the system:
for the tstops argument collect the values of the ranges, vector them, splat and concatenate, I feel this is not how its supposed to go?!

sol = solve(prob, callback = cb, dt = dt, tstops = vcat(collect.(timeranges)...) )

plot(sol , idxs = 2); vline!(vcat(collect.(extrema.(timeranges))...))
prob.ps[:α] ≈ expected_force # true; 1.750000000000166

Question

How bad is this? how to use VectorContinuousCallback or DiffEqCallbacks.jl to achieve the same behaviour (ideally with cleaner code, and better performance).

If this is already answered, please feel free to just point me to the resource and close this problem.
Thanks!

4 posts - 3 participants