home >> Low level routines for simulation >> pseudo code/code >> 2nd order dynsysts
last update: may 2006

2nd order dynsysts

2nd order continuous linear low-pass filter CF2L_0

Equations:
dv/dt= - k1*v+ k0*( u- x)
dx/dt= v
Parameter: k1, k0.

Tuning parameters;{ ω0, ζ } or { ωR, ζ } or { Settling Time, ζ }
Instead of fiddling with k1 and k0, tuning parameters that are more distinctly related to the wanted behavior of the dynsyst are proposed.
The core tuning parameters are the natural ( un-damped) frequency ( in radian/sec, not in Hertz) ω0 (read omega zero) and relative damping coefficient ζ (read zeta)
To put it simply:
-for a given ζ, ω0 sets the responsivenes of the dynsyst, ie how fast its output reaches the input ; "responsiveness" increases with ω0.
-for a given ω0 , ζ sets the damping of the dynsyst( 0< ζ ); damping increases with ζ.
.If ζ< 0, the filter is unstable ( diverging).
.If ζ= 0, the filter produces steady oscillations.
.If 0< ζ≤ 1, the filter produces more or less damped oscillations ( though they are hard to see for ζ> 0.7).
.If ζ≥ 1 ,no oscillation occur; The filter is "over-damped": it is equal to two first order filters placed in a row ( the output of the first being the input of the second). The parameters k_1 and k_2 of these first order filters are such as:
ω0= sqrt(k_1*k_2) and ζ= 0.5*(k_1 +k_2)/sqrt(k_1*k_2).
Usual values for ζ are between 0.5 and 0.7 [see Fig N2LL0].

Actually, ζ has also an influence on responsiveness. I consider a better set of tuning parameters for damped-oscillations dynsysts ( ie with 0< ζ≤ 1) to be { ωR, ζ } with ω0= ωR/sqrt(1-ζ2) because ωR is the actual frequency of the oscillations [see Fig N2LLR].

N2LL_Om0 N2LL_OmR

N2LL_OmS

Another convenient set of parameters is { Settling Time, ζ } .
The Settling Time ST is the time after which abs( u-x) stays under Delta=5% of input when input is a step [see Fig N2LLS].
Due to oscillations for ζ< 1, exact computation of Settling Time ST is complex, but one can use the following very rough approximation, with Delta=0.05,
for ζ<=0.707 , ω0= -log(Delta)/(ζ*ST).
for ζ>0.707 , ω0= -2*log(Delta)*ζ/ST.
I personally use my function "calcOmeg0FromSetTime()" hereunder, which gives a better approximation.
For exact computation, see Helper function(s).


float calcOmeg0FromSetTime( float SettlingTimeS_i, float DampCoeff_i)
{
if( DampCoeff_i< .8)
return 3.0f/ (SettlingTimeS_i* DampCoeff_i);
else if( DampCoeff_i< 1.937f)
{
float a= 4.7* exp(1.43940364*log(DampCoeff_i));
return a/ SettlingTimeS_i;
}
else
return 6.0f* DampCoeff_i/ SettlingTimeS_i;
}


The filter parameters are then computed as:
k0= ω0* ω0 ;
k1= 2* ζ* ω0 ;
Note:
k0= k1*k1 /( 4 * ζ2) ie k1= 2* ζ * sqrt(k0)

On a unit step input ( u(t< 0)=0, u(t≥ 0)= U0 with U0 constant≠ 0), k0 being set,
. ζ= k1 = 0 results in steady oscillations
. 0< ζ< 1 results in amortized oscillations around u:
x= UO*(1- exp(-ζ*ω0*t)*( cos(ωR*t)+sin(ωR*t)*ζ/sqrt(1-ζ2))).
Maxs and mins are reached for ti*ω0= i*π/sqrt(1-ζ2), i=0,1,2,.. where
abs(1-x/U0)= exp(-ζ*ω0*ti).
. ζ≥ 1, ie k1≥ 2* sqrt( k0) results in over-damped dynsyst:
.. ζ= 1: x= UO*(1- exp(-ω0*t)(1+ω0*t))
.. ζ> 1: x= UO*(1- ( λ1*exp(-t2*ω0) - λ2*exp(-t1*ω0))*.5/sqrt(ζ2- 1)) where λ1= ζ+ sqrt( ζ2-1) and λ2= ζ- sqrt( ζ2-1)

Numerical dynsysts

With the idea to keep the simple ( I think) tuning parameters setting process of the continuous dynsyst and to have a numerical dynsyst behaving like its continuous counterpart, I introduce K1 and K0 defined as
.K1= -2* exp( - T*ω0*ζ) cos ( T* ω0* sqrt(1-ζ2)) and K0= exp( - 2 * T * ω0* x) for damped oscillations dynsysts,
.K1= -( exp(-T*ω1)+ exp(-T*ω2)) and K0= exp(-T*ω1)* exp(-T*ω2) for over-damped dynsysts having two time constants τ1= 1/ω1 and τ2=1/ω2.
To reduce clutter, I use parameters A=1+K0+K1, B=2+K1 and C= B-A=1- K0

Besides, instead of using variables "finite difference" D= Xn- Xn-1 as usual, I use whenever possible the rate of increase of output ( pseuso-speed) as V= (Xn- Xn-1)/T and rate of increase of pseudo-speed (pseudo-acceleration)=(Vn- Vn-1)/T.
This puts forward variables that have a practical meaning to me, helping when I want to introduce non-linearities (eg: bounds) and solve the synchronisation/initialisation issues whenever I want to switch between dynsysts.

For damped-oscillations dynsysts, sampling imposes the constraint: ω0< π/T; degeneracy actually occurs below this bound, but the worst does not show until it is crossed.


2nd order numerical linear filters

2nd order numerical linear filter N2LL_0Ns


Procedure:
Vn= Vn-1- B* Vn-1+ (A/T)*(Un- Xn-1).
Xn= Xn-1+ T* Vn-1.
Parameters : A=(1+K0+K1), B==2+K1 ( C= 1+ K0).


Behavior , assuming X= 0 and V=0 for n<0...
On a step input ( U(n< 0)=0, U(n≥ 0)= U0 with U0 constant≠ 0)
It takes 1 step for the output to react to an input change.
On a ramp input ( U(n< 0)=0, U(n≥ 0)= USpeed*n*T)
In steady state: Vn= USpeed; Un- Xn= USpeed* T*(B/A-1)= USpeed* T*C/A.

N2LL_IS100 N2LL_IR100

2nd order numerical linear filter N2LL_1Ns

Procedure:
Vn= Vn-1- C* Vn-1+ (A/T)*(Un- Xn-1)
Xn= Xn-1+ T* Vn ie integrating on Vn instead of Vn-1
Parameters : A=(1+K0+K1), C= 1+ K0.

N2LL_1Ns is actually just one step ahead of N1LL0_Ns: with input= Un-1 instead of Un and C= B-A, the behaviour is the same as N1LL0_Ns, at the cost of memorizing U for one step.

By setting A=C=1 one can get a "no-effect" filter.

A convenient feature is that A=1+K1+K0 can be computed for a given ζ when C= 1-K0 is fixed ( K1 can be expressed as a function of K0 and ζ see 2nd order stationary linear system). A drawback is that N2LL_1Ns does not behave like expected ( low-pass) for ω0 too close to π/T, whereas N1LL0_Ns still does its best[ see Fig N1LL0_Nss_a & N1LL1_Nss_a].

N2LL0_Nss_a N2LL1_Nss_a

2nd order numerical non-linear dynsyst, bounded speed and acceleration N2NL


Given an ideal acceleration IdealAcc, the most obvious implementation is
AccTmp= minmax( IdealAcc, -ACCBOUND, ACCBOUND);
SpeedTmp= Vn-1+ T * AccTmp;
Vn=minmax( SpeedTmp, -SPEEDBOUND, SPEEDBOUND);
Xn= Xn-1+ T* Vn;
A problem arises if SPEEDBOUND is variable and decreases:
The bound on speed may generate a decrease from Vn-1 to Vn which would implicitely generate an ( negative) acceleration outside the required acceleration bounds.
The solution is to bound an intermediate speed input and not the ouput speed:

IdealSpeedTmp= Vn-1+T* IdealAcc;
SpeedTmp= minmax( IdealSpeedTmp, -SPEEDBOUND, SPEEDBOUND);
IdealAccTmp= (SpeedTmp- Vn-1)/T;
AccTmp= minmax( IdealAccTmp, -ACCBOUND, ACCBOUND);
Vn= Vn-1+ T* AccTmp;
Xn= Xn-1+ T* Vn;

Besides, the second solution allows for the addition of a perturbation - at AccTmp level- the effect of which will not be bounded.

If the input data is not an IdealAcc but an IdealSpeed, an IdealAcc can still be computed as: IdealAcc= (IdealSpeed-Vn-1)/T.
If the input data is an IdealX, an IdealSpeed can be computed as: IdealSpeed= (IdealX-Xn-1)/T.
To get a "pure" speed and acceleration bounder, calculate IdealAcc as:
IdealAcc= ((IdealX-Xn-1)/T-Vn-1)/T, which results in Xn= IdealX whenever SPEEDBOUND and ACCBOUND are not in effect.
One might want to have different acceleration bounds for speed increases and speed decreases.
Speed changing sign must in that case taken care of.

Another problem is that the output curve for a step input shows oscillations due to the fact that the process does not anticipate , ie it does not reduce speed before input= output.
A way to improve that is to limit the speed to the value allowing output to reach input at a max deceleration= ACCBOUND.[see Fig N2NLs]
The drawback is then that the ouput lags behind the input when the input speed is not zero.[see Fig N2NLr]
To activate this feature , uncomment the "//#define IMPL_PARABSPEEDBOUND" line.
The following implementation most probably will not not work for angles ( and asymetrical acceleration bounds are irrelevant): I designed it with a simulation of a mobile speed control in mind.

N2NL_IS100 N2NL_IR100


//code needs optimization!
float boundRateNAccAsym( float CurrTimeStep_i, float XInput_i)
{
/*
ForwardAccMax, ForwardDecelMax, BackwardAccMax ,BackwardDecelMax: all >=0
ForwardSpeedBound >0;
BackwardSpeedBound <0;
*/
const K_Epsilonf= .00001f;
float Delta_a;
float Rate_a;
float Acc_a;
float SpeedSgn_a;

Label_1:;

Delta_a= XInput_i- x;
SpeedSgn_a= Rate> 0? 1: (Rate< 0? -1:(Delta_a< 0? -1: 1));

Rate_a= Delta_a/ CurrTimeStep_i;

float RatePar_a;
float DecelMax_a;

//uncomment the following for alternative behaviour
//#define IMPL_PARABSPEEDBOUND

#ifdef IMPL_PARABSPEEDBOUND
//compute max speed to avoid overshoots and bound Rate_a accordingly
if( SpeedSgn_a> 0) DecelMax_a= ForwardDecelMax;
else DecelMax_a= BackwardDecelMax;
RatePar_a= -0.5* DecelMax_a* CurrTimeStep_i+ sqrt( 2* DecelMax_a*fabs(Delta_a));
if( RatePar_a> 0)
{
if( Rate_a> RatePar_a) Rate_a= RatePar_a;
else if( Rate_a< -RatePar_a) Rate_a= -RatePar_a;
}
#endif

//standard speed bound
if( Rate_a> ForwardSpeedBound) Rate_a= ForwardSpeedBound;
else if( Rate_a< BackwardSpeedBound) Rate_a= BackwardSpeedBound;

// acceleration bound
Acc_a= (Rate_a- Rate)/ CurrTimeStep_i;
if( SpeedSgn_a< 0)
{
if( Acc_a> BackwardDecelMax) Acc_a= BackwardDecelMax;
else if( Acc_a< -BackwardAccMax) Acc_a= -BackwardAccMax;
}
else
{
if( Acc_a> ForwardAccMax) Acc_a= ForwardAccMax;
else if( Acc_a< -ForwardDecelMax) Acc_a= -ForwardDecelMax;
}
Rate_a= Rate+ CurrTimeStep_i* Acc_a;
// handle speed sign change with a lerp based on time
if( (Rate_a> K_Epsilonf && Rate< -K_Epsilonf)||( (Rate_a< -K_Epsilonf && Rate> K_Epsilonf)))
{
CurrTimeStep_i= CurrTimeStep_i- Rate/ Acc_a;
Rate= 0;
goto Label_1;/*yes, I know...so I'll take the usual easy way out: re-writing the code without goto is left as an exercise to the reader.*/
}
//update
Rate= Rate_a;
x= x+ CurrTimeStep_i* Rate_a;
return x;
}

Numerical dynsysts insuring output= input in steady state on a ramp input

These dynsysts, due to the wanted behavior stated above, have the drawback that, though they are stable, they can show a non negligible transient overshoot ( say about 25% for a relative damping in the range of 0.5, 0.7) on a step input.
This overshoot should not be mistaken for a stability issue.
Some added non-linear features may incidentally reduce this "side effect".
I do not provide solutions based on the Proportional-Integral-Derivative ( without the Derivative actually) structure because the following solution is better and can be understood without knowlegde of PID design.

2nd order numerical (linear) estimator N2KL, no error on ramp input

This dynsyst is actually an estimator using the structure (only the structure, not the Kalman gains )of a Kalman filter for
a model assuming constant increase of the variable X; the model is then:
Vn= Vn-1 constant rate of increase
Xn= Xn-1+ T* Vn-1
- the measure is U, and is provided for each step
The parameters A and C are not the Kalman gains but are constants instead , computed as for the other dynsysts here above; The main advantage of using the Kalman framework is that by choosing the right model ( here constant increase), one gets easily the dynsyst that will provide a null error on the corresponding input ( here a ramp).

Procedure:
Prediction: Ve= Vn-1; Xe= Xn-1+ T*Vn-1.
Correction: Vn= Ve+ (A/T)* ( Un- Xe); Xn= Xe+ C* ( Un- Xe).


Behavior, assuming X=0 and E= 0 for n<0
On a step input ( U(n< 0)=0, U(n≥ 0)= U0 with U0 constant≠ 0) [see Fig N2KLs]
At t=n=0: V0= (A/T) U0; X0= C* U0
On a ramp input ( U(n< 0)=0, U(n≥ 0)= USpeed*n*T) [see Fig N2KLr]
At t=n=0: V0= 0 ; X0= 0
At n=1: V1= A*USpeed ; X= C*T*USpeed
In steady state: Vn= USpeed; Un- Xn= 0 The error on a ramp input is zero.
If used as intended, ie as an estimator, the system provides estimates for Xe and Ve of the mobile the actual (angular/-) position of wich is the input U. In particular, Xe and Ve must be initialized with the best guess on the mobile (angular/-) position and speed when a new mobile start to be tracked.
If tweaked and used ( as in N2KN_AS below ) as a simulation of a tracking device, Xe and Ve loose their meaning as best estimates of the target, but are then the tracker variables, and must be initialized with these variables.

N2KL_IS100 N2KL_IR100

2nd order numerical non linear dynsyst, no error on a ramp input, bounded speed and acceleration N2KN_AS

This is a tweak of N2KL no longer used as an estimator, by changing variables so as to show ouput rate and acceleration to be able to bound them.
I also introduce a state, in order to be able to reset the dynsyst when the information that the input variation (Un- Un-1)/T should not be interpreted as a speed is available, corresponding to State=0 ( eg: changing target for a dynsyst applied to a view angle).
If this information is available, the dynsyst should be initialized with State= 0.
If not, the dynsyst should be set with State= 1 forever; it may then show a noticeable overshoot on a big step input.

The next enhancement would be to introduce asymetry as in boundRateNAccAsym, but with ForwardAccMax= BackwardAccMax, ForwardDecelMax= BackwardDecelMax and BackwardSpeedBound=-ForwardSpeedBound.

Depending on the values of A, C, ACCBOUND and SPEEDBOUND, the bound on acceleration may generates noticeable overshoots, especially with dynsysts with high ω0.
The bound on acceleration must be understood as a bound on the rate of increase/decrease of the speed variable.

N2KN_AS_IS100 N2KN_AS_IR100



//A= 1+ K0+ K1;
//C= 1-K0;
//float x, v, PrevInput, State;// globals
//at init time: v=..., Input=x=...,State= 0;
float N2KN_AS( float CurrTimeStep_i, float CurrentInput_i)
{
float Delta_a= PrevInput- x;
float Vc_a;

if( State!= 1)
{
Input= CurrentInput_i;
State= 1;
}

if( CurrentInput_i== PrevInput)
{
Delta_a= wrapHalfPrd(Delta_a) ;
Vc_a= (A/C)* Delta_a/ CurrTimeStep_i;
}
else
{
float DeltaNew_a= CurrentInput_i- x;
DeltaNew_a= wrapHalfPrd(DeltaNew_a) ;
Delta_a= wrapHalfPrd(Delta_a) ;
Vc_a= ( ((A/C)-1)* Delta_a+ DeltaNew_a)/ CurrTimeStep_i;
//or? TO DO test on angles
//float DeltaCbn_a= wrapHalfPrd((A/C)-1)* Delta_a+ DeltaNew_a);
//Vc_a= DeltaCbn_a/ CurrTimeStep_i;
}

float V1_a= v+ C* ( Vc_a- v);
V1_a= minmax( V1_a, -SPEEDBOUND, SPEEDBOUND);
float Acc_a= (V1_a- v)/ CurrTimeStep_i;
Acc_a= minmax( Acc_a, -ACCBOUND, ACCBOUND);
v= v+ CurrTimeStep_i* Acc_a;

PrevInput= CurrentInput_i;
x= x+ CurrTimeStep_i* v;
x= wrapPrd(x) ;
return x;
}

Helper function(s)

Parameters computation

calcF2CoreParams( ) computes K0 and K1. Then A=1+K0+K1, B=2+K1 and C= B-A=1- K0.


int calcF2CoreParams( float T_i, float Omega0_i, float DampCoeff_i
, float & k1_o, float & k0_o)
{
// this function computes the core parameters for a 2nd order numerical filter
// T_i: expected Timestep
DBGASSERT( 0< DampCoeff_i);
float K_a= exp( - T_i* Omega0_i* DampCoeff_i);
k0_o= K_a* K_a;
k1_o= -2* K_a;
float OmegaR_a= Omega0_i* sqrt(fabs(1.0f- DampCoeff_i*DampCoeff_i));
if( DampCoeff_i<= 1)
{
//DBGASSERT( Omega0_a< (M_PI/ T_i));
k1_o*= cos( T_i* OmegaR_a);
}
else
{
k1_o*= cosh( T_i* OmegaR_a);
}
return 0;
}

exact 2orderfilter settling time commputation


static float calcSTOmgInf1( float DampCoeff_a, float ProportionalError_a);
static float calcSTOmgSup1( float DampCoeff_a, float ProportionalError_a);
//--------------------------------------------------------------
float calcSTOmeg0FromSetTime( float DampCoeff_i, float ProportionalError_i)
{
/* this function return the exact value of SettlingTime*Omeg0 for a continuous 2nd order low-pass filter where SettlingTime is the time after
which abs(input-ouput)< (ProportionalError_i* input) when input is a step.*/
if( DampCoeff_i> 1.0f)
return calcSTOmgSTSup1( DampCoeff_i, ProportionalError_i);
else if( DampCoeff_i< 1.0f)
return calcSTOmgSTInf1( DampCoeff_i, ProportionalError_i);
else //DampCoeff_i== 1.0f
return 4.744f;
}
//------------------------------------------------
float calcSTOmgInf1( float damp_a, float prop_a)
{
float x0, x;
float y0, y;
float s= sqrt( 1.f-damp_a*damp_a);
int k=0;
while( exp(-k*M_PI*damp_a/s)> prop_a) { ++k;}
x0= (k-1)*M_PI/s;
y0= exp(-x0*damp_a)*((k&1)?1:-1);
if( y0< 0) prop_a=-prop_a;
y0= y0- prop_a;
x= (k*M_PI-atan(s/damp_a))/s;
y=- prop_a;
while( fabs(y)>0.00001)
{
x=x0-y0*(x-x0)/(y-y0);
y= exp(-x*damp_a)*( cos(x*s)+sin(x*s)*damp_a/s)- prop_a;
float xPrev= x;
}
return x;
}
//------------------------------------------------
float calcSTOmgSup1( float damp_a, float prop_a)
{
float x0, x;
float y0, y;
float s= sqrt( damp_a*damp_a-1.f);
float a= prop_a*2*s;
x= -2*log(prop_a)*damp_a;
y= (damp_a+s)*exp(-x*(damp_a-s))-(damp_a-s)*exp(-x*(damp_a+s))-a;
x0= x;
y0= y;
x= 3/(2*damp_a);
while( fabs(y)>.00001)
{
y= (damp_a+s)*exp(-x*(damp_a-s))-(damp_a-s)*exp(-x*(damp_a+s))-a;
float xPrev= x;
x=x0-y0*(x-x0)/(y-y0);
x0=xPrev;
y0=y;
}
return x0;
}