Fb1_Hopf AFDC Adaptive Hopf pseudo ugen with fast dynamical coupling


Part of: miSCellaneous


Inherits from: UGen


This is an adaptive variant of Hopf, which enhances the adaption mechanism of [1], see [2] (4.1.) for a description. The parameter naming follows the convention of [2].


v'(t) = (mu - (v(t)^2 + w(t)^2)) * v(t) - (theta(t) * w(t)) + p(t)

w'(t) = (mu - (v(t)^2 + w(t)^2)) * w(t) + (theta(t) * v(t)) 

tau * beta'(t) = beta0 - beta(t) + (kappa * p(t) * v(t))

tau * epsilon'(t) = epsilon0 - epsilon(t) + (kappa * f(t) * p(t))

theta'(t) = -eta * p(t) * w(t) / sqrt(v(t)^2 + w(t)^2)


with 


p(t) = (epsilon(t) * f(t)) - (beta(t) * v(t))


It returns a 5-channel signal. See Fb1_ODE help for general information about Fb1 ODE integrator UGens.


HISTORY AND CREDITS: Big credit to David Pirrò from IEM Graz for pointing me to the symplectic integration methods, which are essential for audifying ODEs, as they help to ensure numeric stability in the long run (e.g. to avoid drifts of oscillations that are mathematically expected to be regular). See the chapter on integration in his dissertation [4], pp 135-146. You might also want check David Pirròs optimized ODE compiler named Henri [5]. Big credit also to Nathaniel Virgo who brought up the buffering strategy used in Fb1, which is Fb1_ODE's working horse.  


WARNING: The usage of this class is – inherently – highly experimental. Be careful with amplitudes, as always with feedback it can become loud! Sudden blowups might result form the mathematical characteristics of the ODE systems or they might come from parameter changes on which ODEs can react extremely sensitive to, they can also stem from numerical accumulation effects. It is highly recommended to take precautionary measures, e.g. by limiting/distorting operators (tanh, clip, softclip, distort) with the compose option (See Fb1_ODE Ex.5) and/or external limiting and/or using MasterFX from the JITLibExtensions quark. 


NOTE: The convenience of direct definition of the ODE relation comes with the price of a large number of UGens involved. You might want to allow a higher number of UGens with the server option numWireBufs. For a nice workflow I'd recommended to take reduced blockSizes (e.g. 1, 2, 4, 8, 16) while experimenting as compile time is shorter, but once you have finished the design of a SynthDef it might pay going back to blocksize 32 or 64 for runtime efficiency, especially if many kr UGens are involved.  


See also: Fb1_ODE, Fb1_ODEdef, Fb1_ODEintdef, Fb1, Fb1_MSD, Fb1_SD, Fb1_Lorenz, Fb1_Hopf, Fb1_HopfA, Fb1_VanDerPol, Fb1_Duffing


References:


[1] Righetti, Ludovic; Buchli, Jonas; Ijspeert, Auke Jan (2009): 

"Adaptive Frequency Oscillators and Applications". 

The Open Cybernetics and Systemics Journal, 3, 64-69.

https://www.researchgate.net/publication/41666931_Adaptive_Frequency_Oscillators_and_Applications_Open_Access

Summary: https://biorob.epfl.ch/research/research-dynamical/page-36365-en-html/

[2] Nachstedt, Timo; Tetzlaff, Christian; Manoonpong, Poramate (2017): 

"Fast Dynamical Coupling Enhances Frequency Adaptation of Oscillators for Robotic Locomotion Control". 

Frontiers in Neurorobotics. Published online 2017 Mar 21

https://www.frontiersin.org/articles/10.3389/fnbot.2017.00014/full

[3] Trefethen, Lloyd N.; Birkisson Ásgeir; Driscoll, Tobin A. (2017): 

Exploring ODEs. SIAM - Society for Industrial and Applied Mathematics.

Free download from: https://people.maths.ox.ac.uk/trefethen/Exploring.pdf

[4] Pirrò, David (2017). Composing Interactions. Dissertation. 

Institute of Electronic Music and Acoustics, University of Music and Performing Arts Graz.

Free download from: https://pirro.mur.at/media/pirro_david_composing_interactions_print.pdf

[5] https://git.iem.at/davidpirro/henri


Creation / Class Methods


*new (f = 0, mu = 1, eta = 1, tau = 1, kappa = 1, tMul = 1, t0 = 0, y0 = #[1, 1, 0.01, 0.01, 1],

intType = \sym2, compose, composeArIn, dt0, argList0, init_intType = \sym8,

withOutScale = true, withDiffChannels = false, withTimeChannel = false, blockSize,

graphOrderType = 1, leakDC = true, leakCoef = 0.995)

Creates a new Fb1_HopfAFDC ar object.

f - Defaults to 0.

mu - Defaults to 1.

eta - Defaults to 1.

tau - Defaults to 1.

kappa - Defaults to 1.


tMul - Time multiplier, which determines velocity of proceeding in the dynamic system.

The default value 1 means that the step delta of time equals 1 / sample duration.

For getting audible oscillations you might, depending on the ODE definition, have to pass

much higher values. Can also be a kr / ar UGen and might also be negative.

t0 - Initial time. Expects Number. 

Defaults to 0.

y0 - Initial value of the ODE. 

Expects array of size 5.

Defaults to #[1, 1, 0.01, 0.01, 1].

intType - Integration type. 

Expects one of the Symbols, for which procedures are stored with Fb1_ODEintdef.

The use of symplectic procedures (with prefix 'sym') is highly recommended.

Defaults to \sym2. For more accurate integration you can try symplectic procedures of

higher order like \sym4, \sym8 etc. Families of integration procedures:

Symplectic: 

\sym2, \sym2_d, \sym4, \sym4_d, \sym6, \sym6_d, \sym8, \sym8_d, 

\sym12, \sym12_d, \sym16, \sym16_d, \sym32, \sym32_d, \sym64, \sym64_d

Euler: 

\eu, \eu_d, \eum, \eum_d, \eui, \eui_d

Prediction-Evaluation-Correction: 

\pec, \pece, \pecec, \pecece

Runge-Kutta: 

\rk3, \rk3_d, \rk3h, \rk3h_d, \rk4, \rk4_d

Adams-Bashforth: 

\ab2, \ab3, \ab4, \ab5, \ab6

Adams-Bashforth-Moulton: 

\abm21, \abm22, \abm32, \abm33, \abm43, \abm44, \abm54, \abm55, \abm65, \abm66

compose - Operator(s) / Function(s) to be applied to the system value on a per-sample base.

This of course blurs the numeric procedure but can be used for containing or in a creative way.

Can be an operator Symbol, a Function or an arbitrarily mixed SequenceableCollection thereof.

The Functions are in any case expected to take an array (the system state) as first argument.

If only one Function is given it must output an array of same (system) size.

Within a SequenceableCollection a Function must output a value of size 0.

Within a SequenceableCollection an operator Symbol is applied only to the corresponding component.

A Function can optionally take a second argument which is for ar UGens passed via composeArIn.

composeArIn - ar UGen or SequenceableCollection thereof or a SequenceableCollection that can contain both.

This is the way to use ar UGens within a Function passed to compose.

UGens are passed to the Function's second argument.

dt0 - First time delta in seconds to be used for the language-side calculation of 

first values of a multi-step intType procedure.

This will mostly be irrelevant as (single-step) symplectic procedures are to be preferred.

In case of a multi-step procedure a dt0 value will be derived from the default server's properties

(sample duration * tMul).

argList0 - Initial argList value(s) to be used for the language-side calculation of 

first values of a multi-step intType procedure.

This will mostly be irrelevant as (single-step) symplectic procedures are to be preferred.

If no UGens are passed to argList, values will be assumed from passed argList Numbers.

init_intType - Integration type for language-side calculation of first values of a 

multi-step intType procedure.

This will mostly be irrelevant as (single-step) symplectic procedures are to be preferred.

Defaults to \sym8.

withOutScale - Boolean. Determines if the Fb1_ODEdef's default scaling values for

integration and differential signals should be applied to the output.

Defaults to true.

WARNING: withOutScale does not implement a general safety net functionality, 

withOutScale's default value true does by no means say that output is limited. 

The default scaling values of predefined Fb1_ODEdefs are average assumptions and can, 

under different circumstances, always lead to high out levels. 

withDiffChannels - Boolean. Determines if channel(s) with differential value(s) should be returned.

This is only applicable with integration types with a sizeFactor == 2, which means that for every sample

the values of the differential are buffered. As by default this is not the case for all predefined 

numeric procedures, there exist variants with appendix '_d', e.g. \sym2_d.

Defaults to false.

blockSize - Integer, this should be the server blockSize. 

If no Integer is passed, the current default server's blockSize is assumed (in contrast to Fb1). 

So explicitely passing a blockSize should only be necessary in special cases,

e.g. when compiling before booting.

However for a pleasant workflow for ar usage it's recommended to use 

a reduced server blockSize in order to reduce SynthDef compile time.

withTimeChannel - Boolean. Determines if accumulated time is output in an additional channel. 

WARNING: with constant tMul it produces an ascending DC which is not affected by leakDC 

if this is set to true. Might especially be of interest if time is modulated.

Defaults to false.

graphOrderType - 0, 1 or 2. 

Determines if topological order of generated BufRd and BufWr instances

in the SynthDef graph is forced by additional UGens. 

Type 0: forced graph order is turned off.

Type 1 (default): graph order is forced by summation and <!.

Type 2: graph order is forced by <! operators only.

Default 1 is recommended, but with CPU-intense SynthDefs it might be worth trying it with the value 0. 

This saves a lot of UGens though there might be exceptional cases with different results.

Type 2 can shorten the SynthDef compilation time for certain graphs with a large number of UGens,

which can be lengthy with type 1.

However, CPU usage doesn't directly correspond to the number of UGens.

leakDC - Boolean. Determines if a LeakDC is applied to the output (except time channel).

Defaults to true.

leakCoef - Number, the leakDC coefficient. Defaults to 0.995.

 


*ar (f = 0, mu = 1, eta = 1, tau = 1, kappa = 1, tMul = 1, t0 = 0, y0 = #[1, 1, 0.01, 0.01, 1],

intType = \sym2, compose, composeArIn, dt0, argList0, init_intType = \sym8,

withOutScale = true, withDiffChannels = false, withTimeChannel = false, blockSize,

graphOrderType = 1, leakDC = true, leakCoef = 0.995)

Equivalent to *new.

*kr (f = 0, mu = 1, eta = 1, tau = 1, kappa = 1, tMul = 1, t0 = 0, y0 = #[1, 1, 0.01, 0.01, 1],

intType = \sym2, compose, composeArIn, dt0, argList0, init_intType = \sym8,

withOutScale = true, withDiffChannels = false, withTimeChannel = false, blockSize,

graphOrderType = 1, leakDC = true, leakCoef = 0.995)

Creates a new Fb1_HopfAFDC kr object.

f - Defaults to 0.

mu - Defaults to 1.

eta - Defaults to 1.

tau - Defaults to 1.

kappa - Defaults to 1.


tMul - Time multiplier, which determines velocity of proceeding in the dynamic system.

The default value 1 means that the step delta of time equals 1 / control duration.

For getting audible oscillations you might, depending on the ODE definition, have to pass

much higher values. Can also be a kr UGen and might also be negative.

t0 - Initial time. Expects Number. 

Defaults to 0.

y0 - Initial value of the ODE. 

Expects array of size 5.

Defaults to #[1, 1, 0.01, 0.01, 1].

intType - Integration type. 

Expects one of the Symbols, for which procedures are stored with Fb1_ODEintdef.

The use of symplectic procedures (with prefix 'sym') is highly recommended.

Defaults to \sym4. For more accurate integration you can try symplectic procedures of

higher order like \sym6, \sym8 etc. Families of integration procedures:

Symplectic: 

\sym2, \sym2_d, \sym4, \sym4_d, \sym6, \sym6_d, \sym8, \sym8_d, 

\sym12, \sym12_d, \sym16, \sym16_d, \sym32, \sym32_d, \sym64, \sym64_d

Euler: 

\eu, \eu_d, \eum, \eum_d, \eui, \eui_d

Prediction-Evaluation-Correction: 

\pec, \pece, \pecec, \pecece

Runge-Kutta: 

\rk3, \rk3_d, \rk3h, \rk3h_d, \rk4, \rk4_d

Adams-Bashforth: 

\ab2, \ab3, \ab4, \ab5, \ab6

Adams-Bashforth-Moulton: 

\abm21, \abm22, \abm32, \abm33, \abm43, \abm44, \abm54, \abm55, \abm65, \abm66

compose - Operator(s) / Function(s) to be applied to the system value on a per-control-block base.

This of course blurs the numeric procedure but can be used for containing or in a creative way.

Can be an operator Symbol, a Function or an arbitrarily mixed SequenceableCollection thereof.

The Functions are in any case expected to take an array (the system state) as first argument.

If only one Function is given it must output an array of same (system) size.

Within a SequenceableCollection a Function must output a value of size 0.

Within a SequenceableCollection an operator Symbol is applied only to the corresponding component.

dt0 - First time delta in seconds to be used for the language-side calculation of 

first values of a multi-step intType procedure.

This will mostly be irrelevant as (single-step) symplectic procedures are to be preferred.

In case of a multi-step procedure a dt0 value will be assumed from the default server's properties

(control duration * tMul).

argList0 - Initial argList value(s) to be used for the language-side calculation of 

first values of a multi-step intType procedure.

This will mostly be irrelevant as (single-step) symplectic procedures are to be preferred.

If no UGens are passed to argList, values will be assumed from passed argList Numbers.

init_intType - Integration type for language-side calculation of first values of a 

multi-step intType procedure.

This will mostly be irrelevant as (single-step) symplectic procedures are to be preferred.

Defaults to \sym8.

withOutScale - Boolean. Determines if the Fb1_ODEdef's default scaling values for

integration and differential signals should be applied to the output.

Defaults to true.

WARNING: withOutScale does not implement a general safety net functionality, 

withOutScale's default value true does by no means say that output is limited. 

The default scaling values of predefined Fb1_ODEdefs are average assumptions and can, 

under different circumstances, always lead to high out levels. 

withDiffChannels - Boolean. Determines if channel(s) with differential value(s) should be returned.

This is only applicable with integration types with a sizeFactor == 2, which means that for every sample

the values of the differential are buffered. As by default this is not the case for all predefined 

numeric procedures, there exist variants with appendix '_d', e.g. \sym2_d.

Defaults to false.

withTimeChannel - Boolean. Determines if accumulated time is output in an additional channel. 

WARNING: with constant tMul it produces an ascending DC which is not affected by leakDC 

if this is set to true. Might especially be of interest if time is modulated.

Defaults to false.

graphOrderType - 0, 1 or 2. 

Determines if topological order of generated BufRd and BufWr instances

in the SynthDef graph is forced by additional UGens. 

Type 0: forced graph order is turned off.

Type 1 (default): graph order is forced by summation and <!.

Type 2: graph order is forced by <! operators only.

Default 1 is recommended, but with CPU-intense SynthDefs it might be worth trying it with the value 0. 

This saves a lot of UGens though there might be exceptional cases with different results.

Type 2 can shorten the SynthDef compilation time for certain graphs with a large number of UGens,

which can be lengthy with type 1.

However, CPU usage doesn't directly correspond to the number of UGens.

leakDC - Boolean. Determines if a LeakDC is applied to the output (except time channel).

Defaults to true.

leakCoef - Number, the leakDC coefficient. Defaults to 0.995.




Examples


// reboot with reduced blockSize


(

s = Server.local;

Server.default = s;

s.options.blockSize = 8;

s.reboot;

)


// In the source code of Fb1_ODEdef.sc the corresponding Fb1_ODEdef looks like this:

// the Function brackets within the array are not absolutely necessary, 

// but compile process is faster.

// beta0 and epsilon0 are taken over as components of indices 2 and 3 from y0


// don't evaluate, already stored


Fb1_ODEdef(\HopfAFDC, { |t, y, f = 0, mu = 1,

eta = 1, tau = 1, kappa = 1, beta0 = 0.01, epsilon0 = 0.01|

var p, u, v;

u = y[0] * y[0] + (y[1] * y[1]);

v = mu - u;

p = y[3] * f - (y[2] * y[0]);

[

{ v * y[0] - (y[4] * y[1]) + p },

{ v * y[1] + (y[4] * y[0]) },

{ beta0 - y[2] + (kappa * p * y[0]) / tau },

{ epsilon0 - y[3] + (kappa * p * f) / tau },

{ p.neg * y[1] * eta / sqrt(u) }

]

}, 0, [1, 1, 0.01, 0.01, 1], 0.3, 0.3);




// adaption to sine source (R) by Fb1_HopfAFDC (L)

// mouse left turns source down for the Hopf

// for comparison the source continues in the right channel

// move mouse to right to turn source on again


(

x = {

var ratio = Demand.ar(

Impulse.ar(3), 0,

Dseq([0, 2, 4, 5, 7, 9, 11, 12], inf)

).midiratio;

var hold = (MouseX.kr(0, 1) > 0.1).poll;

var src = SinOsc.ar(ratio * 200);

var sig = Fb1_HopfAFDC.ar(src * hold.lag(0.1), 0.2, 0.6, 1, 50, 150) *

EnvGen.ar(Env.asr(0.05));

[sig[0] * 0.7, src * 0.1]

}.play

)


x.release



// in contrast to Fb1_HopfA adaption to the Saw works quite well


(

x = {

var ratio = Demand.ar(

Impulse.ar(3), 0,

Dseq([0, 2, 4, 5, 7, 9, 11, 12], inf)

).midiratio;

var hold = (MouseX.kr(0, 1) > 0.1).poll;

var src = Saw.ar(ratio * 200);

var sig = Fb1_HopfAFDC.ar(src * hold.lag(0.1), 0.2, 0.6, 1, 50, 150) *

EnvGen.ar(Env.asr(0.05));

[sig[0] * 0.5, src * 0.04]

}.play

)


x.release



// it works still with faster tempo and bigger jumps

// however big jumps and/or the hold gate make the system instable,

// compose with clip helps


(

x = {

var ratio = Demand.ar(

Impulse.ar(7), 0,

Dseq([0, 2, 4, 5, 7, 9, 11, 12], inf) + Drand([0, 12, 24], inf)

).midiratio;

var hold = MouseX.kr(0, 1) > 0.1;

var src = Saw.ar(ratio * 200);

var tMul = 150;

var sig = Fb1_HopfAFDC.ar(src * hold.lag(0.1), 0.2, 0.6, 1, 50, tMul,

compose: { |x| x.clip2(100) }

) * EnvGen.ar(Env.asr(0.05));

[sig[0] * 0.5, src * 0.04]

}.play

)


x.release



// confused adaption by feeding with phase modulated signal


(

x = {

var ratio = Demand.ar(

Impulse.ar(3), 0,

Dseq([0, 2, 4, 5, 7, 9, 11, 12], inf)

).midiratio;

var src = SinOsc.ar(ratio * 200, SinOsc.ar(200).range(0.2, 1));

var sig = Fb1_HopfAFDC.ar(src, 1.5, 1, 1, 100, 200) *

EnvGen.ar(Env.asr(0.05));

sig[0] ! 2 * 0.3

}.play

)


x.release