Skip to content

Conversation

raphael-roemer
Copy link
Collaborator

We would like to add functionality for Rate dependent forcing functionality and easy system creation of non-autonomous systems

Copy link
Collaborator

@reykboerner reykboerner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for working on this!

I very much like the idea of starting with the general mathematical formulation \dot x = f(x(t), lambda(t)), and have made a suggestion of how this could be reflected more in the way a user would define a RateSystem.

One thing I am unsure about is whether we just need a constructor function RateSystem that returns either a CoupledODEs or CoupledSDEs, or whether we need an additional type in order to pass the necessary information about the rate system to the methods we want to add.

Maybe we can brainstorm the methods we would like to add here and think about whether they work with a CoupledODEs or whether we need more.

src/RateSys.jl Outdated
# t_i autonomous t_ni non-autonomous t_nf autonomous t_f
# | | | |

using DynamicalSystems
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be sufficient to use DynamicalSystemsBase.

src/RateSys.jl Outdated

# we write

function RateSyst(tni,tnf,f,λ,p_λ,initvals)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, these are quite many positional arguments, which makes it less user-friendly. What do you think of reorganising this in the following structure:

RateSystem(ds::ContinuousTimeDynamicalSystem, rate_protocol::function)

where ds can be a CoupledODEs or a CoupledSDEs and rate_protocol is your λ function that determines how the parameters change over time. This function could have the structure

rate_protocol(u, p_lambda, t; t_start=0.0, t_end=Inf, kwargs...)

i.e. it would contain all the input arguments needed to define the rate forcing function. Behind the scenes, this would then create and return a new CoupledODEs or CoupledSDEs with the explicit time dependence specified by rate_protocol.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The naming here is just a suggestion, but generally I think we should give more descriptive names than just lambda or tnf (clarity over brevity!)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds great, thanks Reyk! However, rate_protocol should not depend on u, in my opinion.

@oameye
Copy link
Member

oameye commented Jan 1, 2025

@reykboerner
Copy link
Collaborator

Hey, what's the status on this @raphael-roemer @ryandeeley ?
If you continue working on this, don't forget to merge the main branch into the RateSystem branch first, since the main branch has changed in the meantime.

@ryandeeley
Copy link
Collaborator

ryandeeley commented Mar 16, 2025

Hey, what's the status on this @raphael-roemer @ryandeeley ? If you continue working on this, don't forget to merge the main branch into the RateSystem branch first, since the main branch has changed in the meantime.

Hello, Raphael and I met again last week to pick this up again. We agree that it would be nice to pass some used-defined auto_sys::CoupledODEs and \lambda::Function - which respectively describe 1) the reference system in an autonomous form, and 2) the time-dependent evolution of the system parameters - to a function RateSystem() that will return a modified nonauto_sys::CoupledSDEs describing the associated nonautonomous version of the system. We also agree that it'd be nice if RateSystem() contains the fewest positional arguments as possible. I've had a crack at writing this generally, and applying to an example case. The user has to provide

  • the base system auto_sys::CoupledSDEs
  • the function \lambda::Function that describes the time-dependent evolution of the system parameters.

We are proposing that \lambda() should return all system parameters at each time instance, not just those that are explicitly time-dependent, to avoid the (ambiguous) choice of which parameters in the parameter vector of the original system need to be overwritten. Instead, the idea is that the function RateSystem() - which has auto_sys::CoupledODEs and \lambda::Function as positional arguments - creates a modified time-dependent drift function by evaluating the drift function auto_sys.integ.f(u,p,t) at p=\lambda(t) at each time instance. Using the useful function remake(), one then creates an ODEProblem which is identical to the corresponding ODEProblem of auto_sys::CoupledODEs in all components except for the drift function - which becomes the modified time-dependent drift function. The parameter vector and the initial time of the ODEProblem also change in this new construction. This ODEProblem is then converted back to a CoupledODEs, which is ultimately what RateSystem() then returns.

As I've written it, the positional arguments of RateSystem() are

  • auto_sys::CoupledODEs
  • \lambda::Function
  • r::Float64
  • t0::Float64

The first two arguments are hopefully clear. The argument r describes a scalar control parameter specifying the scaled rate of change of the parameters - because ultimately we are considering ODEs of the form dxₜ/dt = f(xₜ,λ(rt)). The argument t0 is the initial time that the returned CoupledODEs takes - which is fairly important in this nonautonomous context.

One keyword argument of RateSystem() is p_lambda::Vector=Float64[] which (in a meta way) contains the parameters for the evolution of the time-dependent system parameters. For instance, the minimal/maximal value of the parameter in a typical ramping scenario. I also find it most logical to have t_start::Float64=-Inf and t_end::Float64=Inf as keyword arguments of RateSystem(). These entries allow for a quick modification of the user-provided \lambda()::Function in order to "clip" the period of non-autonomous parameter evolution - specifically, the modified parameter function will assume the same values \lambda(t) in the interval t\in[t_start,t_end] but for t < t_start or t > t_end assumes \lambda(t_start) and \lambda(t_end) respectively. Note that this clipping of the \lambda()::Function occurs independently of the rate value r prescribed: the value of r has the effect of squeezing/stretching this particular clipped parameter evolution.

Note that there are two non-user-level functions modified_drift() and rate_protocol() that RateSystem() calls in order to construct the modified time-dependent drift function.

I tested this proposed RateSystem() on the paradigmatic one-dimensional model for studying R-tipping, and it seems to return the solutions one expects (in reference to the behaviour before / after the critical r-value for R-tipping with the particular time-dependent parameter evolution considered - in this case this is r = 4/3, for R-tipping of the trajectory that in backward-time t->-\infty limits to the attractor x = -1 of the "past" autonomous system). I can also see whether this works nicely for a two-dimensional system.

I've attached a .txtof the .jl file where the function RateSystem() is written up and the tests are made, so let me know what you think of this whenever you get time!

RateSystemDraft.txt

@reykboerner
Copy link
Collaborator

Hey @ryandeeley, thanks for the update! Could you push your proposed changes to this branch? This is what git is designed for, and pushing here will automatically run the tests. (The idea is that this draft pull request is continuously developed until we are happy to merge it with the main branch)

@ryandeeley
Copy link
Collaborator

Hey @ryandeeley, thanks for the update! Could you push your proposed changes to this branch? This is what git is designed for, and pushing here will automatically run the tests. (The idea is that this draft pull request is continuously developed until we are happy to merge it with the main branch)

Yes, we can do that, although I only wrote this yesterday and haven't explicitly discussed it with Raphael yet, who is also drafting some RateSystem() function. We plan to compare each of our proposals, and I believe we're meeting again tomorrow, so we could push something after then.

@ryandeeley
Copy link
Collaborator

Hi @raphael-roemer - I thought some more about this yesterday evening and I think I'm in a position to have a go at updating the RateSystem.jl file and pushing. I think I'll start this now, and I'm happy to share what I'm doing live via a call if you'd be interested. Otherwise, of course the source code once it's updated should more or less explain the changes that I propose.

@ryandeeley
Copy link
Collaborator

ryandeeley commented Aug 25, 2025

So I've just pushed some changes to the RateSystem.jl file, which answer to most of the things we discussed yesterday. In particular I've added t_pstart and t_pend fields to the RateConfig, and the dp-field has the effect as spelled out yesterday. The t_ramp_length has remained a field of RateConfig, although there is a correspondence between t_ramp_length and what was originally r via r = (t_pend-t_pstart)/t_ramp_length or equivalently t_ramp_length = (t_pend-t_pstart)/r [plus the necessary shiftings of time, because the nonautonomous dynamics is now programmed to always take place within t \in (t_start, t_start + t_ramp_length). If one chooses t_start = -t_ramp_length/2 and t_pend = -t_pstart, this shifting of time simplifies and is very natural.].

Please let me know your thoughts on the RateSystem.jl file!

I haven't yet altered any test files in correspondence with this yet, so that's still to do...

- p_parameters: the vector of parameters which are associated with p
- t_pstart: the parameter values of the past limit system are given by p(p_parameteters,t_pstart)
- t_pend: the parameter values of the future limit system are given by p(p_parameters,t_pend)
- t_start: the explicit value of time at which the nonautonomous dynamics starts
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, it seems redundant (and a bit confusing) to have all these different time inputs (t_pstart, t_start, ...). I am worried that we are making things too complicated.

To me, the most straight forward would be: you define a forcing function p(t) and a tuple (p_start, p_end). For all t, the parameters are then given by:

  • p(t_start) for $t \leq t_{start}$
  • p(t) for $t_{start} &lt; t &lt; t_{end}$
  • p(t_end) for $t \geq t_{end}$

This ensures the forcing is continuous. Its derivative might not be, but okay. If you have a tanh forcing on [t_start, t_end]and want the past and future limits to be closer to the 'true' limits of the tanh, just widen the interval [t_start, t_end].

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P.S. I am thinking that one applies the scaling (dp and rate) to the forcing first and then evaluates the start and end values of that rescaled forcing function using the specified t_start and t_end.

Copy link
Collaborator

@ryandeeley ryandeeley Aug 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would disagree that it's redundant and will try to dispel any possible confusion. One needs p, p_parameters, t_pstart and t_pend to define a function with piecewise constant tail ends that the R-tipping experiments are taken with respect to (see p_piecewise_scaled, here the dp simply scales the difference in parameter values across [t_pstart,t_pend]) . You need to fix a t_pstart and t_pend to fix the past and future limit systems. These have to be finite, because [t_pstart,t_pend] is rescaled into the finite interval [t_start,t_start+t_ramp_length]; this is the rescaling that was once associated with r (stretching / squeezing). If as you propose one simply transforms a rescaled forcing function into a function with piecewise constant tail ends from fixed t = t_start and t = t_end each time, then this is going to change the past and future limit systems depending on the rescaling (and could do so drastically if you consider a very shallow rate).

The parameter t_start in the current RateConfig struct is in my opinion unnecessary, since we could always fix t_start = -t_ramp_length/2 and have that the nonautonomous dynamics occurs in a symmetric window around t = 0.

Note also that for this formulation, it is less useful to think about r, and more useful to think about t_ramp_length (because these notions are only equivalent when the function has piecewise constant tail ends). Note that with the exception of dp, the code and system construction here is precisely the same as it was to before, except r has been replaced with t_ramp_length (thereby, restricting to cases where the time-dependent dynamics occurs over a finite time-interval, but since this is numerical, this restriction is not problematic).

@reykboerner
Copy link
Collaborator

Regarding the normalization of the forcing function: what is your solution for the case that $p(t_{start}) = p(t_{end})$ as @ryandeeley pointed out?

My opinion is: it's not necessary to normalize. What is now called dp can still be a scaling, and if you define your function p(t) such that it varies from 0 to 1, then dp corresponds to the ramping magnitude.

@Datseris
Copy link
Member

why are you not making dp just be an input to the user provided function so that the user explicitly takes care of what dp means?

@ryandeeley
Copy link
Collaborator

ryandeeley commented Aug 27, 2025

Regarding the normalization of the forcing function: what is your solution for the case that p ( t s t a r t ) = p ( t e n d ) as @ryandeeley pointed out?

In such cases no scaling is performed to those parameters - see the p_piecewise_scaled function. Since we have restricted to monotonic functions, here this only applies to fixed (time-independent) parameters.

My opinion is: it's not necessary to normalize. What is now called dp can still be a scaling, and if you define your function p(t) such that it varies from 0 to 1, then dp corresponds to the ramping magnitude.

I agree that the normalisation is specific here, and to reiterate it assumes monotonicity. The problem with simply scaling the whole function, i.e. via c p(t,...), is that this will also scale the value associated with the past limit system unless p(0) starts at 0, but why would we want to restrict to such cases? This would require one to incorporate the true starting value of the parameter somewhere else into the code, which might be an option but it doesn't sound so straight forward or at least would require more thinking... The problem also with this scaling is that it would change the values of the fixed time-independent parameters (which is again problematic and seemingly not the intention).

The way it works right now is admittedly a specific case but it captures the idea that dp scales the difference in parameter values of the time-dependent parameters over the interval of time-dependent dynamics.

Perhaps there is no simple universal and helpful interpretation of "scaling" the amplitude of the parameter functions, which I think has been part of the issue in unanimously understanding this so far... Maybe George's idea to leave the interpretation of the dp scaling to the user is the best compromise...

@ryandeeley
Copy link
Collaborator

ryandeeley commented Aug 27, 2025

why are you not making dp just be an input to the user provided function so that the user explicitly takes care of what dp means?

This is an option. I believe Raphael and I attempted to write this explicitly because it gives a clear interpretation of what dp does (which wasn't clear to me, at least, beforehand), and minimises the work required by the user (the rescaling of the amplitude "trick" is fine if you only have to work it out and write it down once, but if you have to repeat it every time I would find that time-consuming...). Making dp an input to the p-function would generalise its possible interpretation (a good thing...), but I'm wondering whether we can write useful functionality if we don't know have a clear understanding on what exactly dp is doing... Is the following what you would suggest?

function unscaled_ramping(p,t,dp,t_pstart,t_pend)
      # p is a function of t and dp
      if t<= t_pstart 
              return p(t_tpstart,dp)
      elseif t < t_pend
              return p(t,dp)
      else
              return p(t_pend,dp)
      end
end

This would then later gets stretched / squeezed from [t_pstart,t_pend] into [t_start,t_start+t_ramp_length].

Note that we already have the p_parameters function for unspecified parameter tweaking, but I suppose it is useful to isolate dp such that it is easily accessible...

Of course, the additional inclusion of dp alongside t_ramp_length means that evaluating things like critical_rate will be different for each dp, or more generally one will have to consider the "critical pairing" dp and t_ramp_length.

@ryandeeley
Copy link
Collaborator

Generally, I am finding this rather time-consuming and I think it would be more helpful to wrap this up and move on to writing more functionality for the RateConfig struct. The extra dp parameter which we are trying to add only has a clear interpretation for specific cases, which are bound to not please everybody... Perhaps George's suggestion to leave this part up to the user is the best compromise for now then...

@Datseris
Copy link
Member

In my experience the most difficult, most time consuming, and most important part of a scientific software is designing a good interface. So I think better get this one clean. Looking at the PR right now it isn't particularly clean, with some reduntant fields and documentation scattered throughout different functions. The functionality we discussed in the videocall is also missing, particularly the generic case of mapping parameter indices to time dependent functions [1 => p(t), 2 => g(t)]. There are also two files in the source code one with a time interval one without, which one should I be looking at?

@Datseris
Copy link
Member

I think we need one more video call; for me at least it is difficult to track the discussion here across different line-comments and different source code files.

@ryandeeley
Copy link
Collaborator

ryandeeley commented Aug 27, 2025

The functionality we discussed in the videocall is also missing, particularly the generic case of mapping parameter indices to time dependent functions [1 => p(t), 2 => g(t)].

I'm not sure I understand this...

There are also two files in the source code one with a time interval one without, which one should I be looking at?

I'm not sure about the RateSystemTimeInterval.jl file, I think it's possibly a copy of what we had before. @raphael-roemer can you confirm? The file where we have tried to implement the changes discussed last Friday is RateSystem.jl as far as I am aware.

@raphael-roemer
Copy link
Collaborator Author

There are also two files in the source code one with a time interval one without, which one should I be looking at?

I'm not sure about the RateSystemTimeInterval.jl file, I think it's possibly a copy of what we had before. @raphael-roemer can you confirm? The file where we have tried to implement the changes discussed last Friday is RateSystem.jl as far as I am aware.

Exactly!

Comment on lines +16 to +18
#modifiedtruscottbrindleywithdimensions!, modifiedtruscottbrindleywithdimensions
#originaltruscottbrindley!, originaltruscottbrindley
#rampedoriginaltruscottbrindley!, rampedoriginaltruscottbrindley
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these are commented out here, how come?

Fields
==============

- p: monotonic function which describes the time-dependent parametric forcing
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a function but what are the inputs and what are the outputs?

==============

- p: monotonic function which describes the time-dependent parametric forcing
- p_parameters: the vector of parameters which are associated with p
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does this have to be a vector? what is this in general? still not convinced why it should exist.

Comment on lines 29 to 33
- t_pstart = -100
- t_pend = 100
- p_parameters = []
- t_start = -t_ramp_length/2
- dp = 1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think these defaults make sense. Unless there is a singular unambiguous value, like 0 for starting time, you should not have defaults and request the user to provide them.
p_parameters should be nothing by default as with the rest of DynamicalSYstems.jl


q̃ = q(time_shift)

return dynamic_rule(ds)(u, q̃, t)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you do for in-place systems here?


time_shift = ((t_pend-t_pstart)/t_ramp_length)*(t-t_start)+t_pstart # such that [t_start,t_start+t_ramp_length] is shifted into [t_pstart,t_pend]

q̃ = q(time_shift)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure what this is. But importantly, something happens you that you don't discolse in the documentation. The function p the user provides must coincide with the parameter container of the original dynamical system. Is this really desirable?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants