1

I am working on a homework problem. I'm trying to simulate a PID control in Python with Scipy's integrate.solve_ivp() function.

My method is to run the PID code within the right-hand-side of the function, using global variables and appending them to a global matrix at the end of each timestep, like so:

solution = integrate.solve_ivp(rhs, tspan, init, t_eval=teval)

Here is my code:

def rhs(dt, init):

    global old_time, omega0dot, rhs_t, omega0dotmat
    timestep = dt - old_time
    old_time = dt

    # UNPACK INITIAL
    x = init[0]
    y = init[1]
    z = init[2]
    xdot = init[3]
    ydot = init[4]
    zdot = init[5]
    alpha = init[6]
    beta = init[7]
    gamma = init[8]
    alphadot = init[9]
    betadot = init[10]
    gammadot = init[11]

    # SOLVE EQUATIONS
    (xddot, yddot, zddot, alphaddot, betaddot, gammaddot) = dynamics(k_d, k_m, x, y, z, xdot, ydot, zdot, alpha, beta, gamma, alphadot, betadot, gammadot, omega0dot)

    # CONTROL SYSTEMS
    z_des = 10

    err_z = z_des - z

    zPID = (1*err_z) + hover

    omega0dot = zPID

    rhs_t.append(dt)
    omega0dotmat.append(omega0dot)

    return [xdot, ydot, zdot, xddot, yddot, zddot, alphadot, betadot, gammadot, alphaddot, betaddot, gammaddot]

The global variables are initialized outside this function. You might notice that I am specifically trying to simulate a quadcopter, where the linear and angular motion of the quadrotor are dependent on omega0dot, which represents rotor velocity and which I am trying to control with PID.

My difficulty is with the timestep of integrate.solve_ivp(). Both the integral and derivative part of the PID control rely on the timestep, but the solve_ivp() function has a variable time step and seems to even go backwards in time sometimes, and sometimes makes no timestep (i.e. dt <= 0).

I was wondering if there was a better way to go about this PID control, or if maybe I'm interpreting the dt term in solve_ivp() wrong.

Jonas
  • 121,568
  • 97
  • 310
  • 388
squeegene
  • 467
  • 1
  • 5
  • 18
  • As an aside, I tried fixing the timesteps according to the answers in this thread (https://stackoverflow.com/questions/54494770/how-to-set-fixed-step-size-with-scipy-integrate) but I still get 0 timesteps occassionally. – squeegene Nov 12 '19 at 00:50
  • This is a bad idea. The methods of `solve_ivp` have all adaptive time stepping, and each single step contains multiple evaluations of the derivative function at points that are close to, but not on the solution curve. It would be better to just solve the equations without any global variables and then construct the desired arrays from the solution samples. – Lutz Lehmann Nov 12 '19 at 18:45
  • Unfortunately I cannot solve the equations after the fact because the rotor velocity, and thus the altitude and attitude of the quadcopter, are each dependent on calculations done at the previous timestep. Am I wrong in this regard? It would be like simulating a system without PID and then calculating PID after the fact - but the simulation obviously has to take into account the PID when it is first running because the PID will change the system – squeegene Nov 13 '19 at 06:08
  • This is not visible in the code, the extra values are used to fill some array, but are not used to influence the dynamic. – Lutz Lehmann Nov 13 '19 at 06:18

1 Answers1

2

Let's look at a more simple system, the ubiquitous spring with dampening

    y'' + c*y' + k*y = u(t)

where u(t) could represent the force exerted by an electro-magnet (which immediately suggests ways to make the system more complicated by introducing a more realistic relation of voltage and magnetic field).

Now in a PID controller we have the error to a reference output e = yr - y and

    u(t) = kD*e'(t) + kP*e(t) + kI*integral(e(t))

To treat this with an ODE solver we immediately see that the state needs to be extended with a new component E(t) where E'(t)=e(t). The next difficulty is to implement the derivative of an expression that is not necessarily differentiable. This is achievable by avoiding to differentiate this expression at all by using a non-standard first-order implementation (where the standard would be to use [y,y',E] as state).

Essentially, collect all derived expressions in the formula in their integrated form as

    v(t)=y'(t)+c*y-kD*e(t). 

Then going back to the derivative one gets the first order system

    v'(t) = y''(t) + c*y'(t) - kD*e'(t) 
          = kP*e(t) + kI*E(t) - k*y(t)

    y'(t) = v(t) - c*y(t) + kD*e(t)
    E'(t) = e(t)

This now allows to implement the controlled system as ODE system without tricks involving a global memory or similar

def odePID(t,u,params):
    c,k,kD,kP,kI = params
    y,v,E = u
    e = yr(t)-y
    return [ v-c*y+kD*e, kP*e+kI*E-k*y, e ]

You should be able to use similar transformations of the first order system in your more complex model.

Lutz Lehmann
  • 25,219
  • 2
  • 22
  • 51
  • Wow! I'm surprised you came back to this after 6 months!! It's nice to find some closure as I believe this method would work – squeegene Apr 03 '20 at 15:48
  • 1
    @squeegene : The question was edited and thus appeared in the "active" list. Most search results only look at the Laplace picture and block diagrams and never do get to tricks like this. It had an "unfinished" feel to it. – Lutz Lehmann Apr 03 '20 at 16:09
  • @LutzLehmann Let me dig this out again. How would one use this transformation if there’s no second derivative in the model, e.g trying to control just velocity y’ = u. In this case, v(t) does not contain y’ (only y) and thus rearranging for y’ in the last step fails - so there is no equation to determine the D part. – Max Apr 14 '22 at 20:12
  • @Max : With `u` still a PID expression? The substitution has now to be "algebraic", as the orders of the highest derivative left and right are the same. So `v=y-kD*e`, the state variable is `v` and `y=v+kD*e` has to be reconstructed from it, inside the ODE function and after integration. – Lutz Lehmann Apr 14 '22 at 21:07
  • @LutzLehmann First of all thanks for replying to this old topic. Yes using PID well knowing that PI might be better suited here. Your last line is where I was and what I do not understand. You say *now* v(t) is state. In the first version v(t) is also state. Which then leads to the issue of needing to specify v’(t) RHS containing e’ which I can’t figure out – Max Apr 15 '22 at 06:36
  • @Max : But `v` is selected in a way that it contains all the derivative terms, so that `v'=k_P*e + k_I*E`, with still `E'=e`, where `e=y-y_r=v+k_D*e-y_r` so that `e=(v-y_r)/(1-k_D)`. This becomes more complicated if the observed error is a function of the position difference, `e=h(y-y_r)`, as then the determination of `e` from `v` requires solving a non-linear equation. – Lutz Lehmann Apr 15 '22 at 06:44
  • Great, thanks, I will give this a try next week! – Max Apr 16 '22 at 08:30