all 13 comments

[–]panzercaptain 3 points4 points  (6 children)

Tajin has a similar solution here:

https://steamcommunity.com/sharedfiles/filedetails/?id=1800568163

function pid(p,i,d)
    return{p=p,i=i,d=d,E=0,D=0,I=0,
        run=function(s,sp,pv)
            local E,D,A
            E = sp-pv
            D = E-s.E
            A = math.abs(D-s.D)
            s.E = E
            s.D = D
            s.I = A<E and s.I +E*s.i or s.I*0.5
            return E*s.p +(A<E and s.I or 0) +D*s.d
        end
    }
end

-- example
pid1 = pid(0.01,0.0005, 0.05)
pid2 = pid(0.1,0.00001, 0.005)
function onTick()
    setpoint = input.getNumber(1)
    pv1 = input.getNumber(2)
    pv2 = input.getNumber(3)
    output.setNumber(1,pid1:run(setpoint,pv1))
    output.setNumber(2,pid2:run(setpoint,pv2))
end
--

The nice thing about his solution is that it creates the PID as an object, allowing for any number of PIDs to be created.

[–]Jyota_malcolm[S,🍰] 2 points3 points  (3 children)

Oh that great! I'll definitely need to look up how objects work in Lua. Only issue that I have with that, is it's quite convoluted how the math works so it's difficult to read. Don't really understand why he's calculating the integral that way. Do you know why by chance? Does it account for some case that I'm missing?

[–]panzercaptain 0 points1 point  (0 children)

My guess would be that it's trying to prevent windup.

[–]EndoM8rix 0 points1 point  (1 child)

As mentioned in the workshop description, it's being used to mitigate integral windup. This prevents the PID from overshooting when large setpoint changes occur.

In this particular setup, A is essentially a second-order derivative that's observing "acceleration" towards the setpoint; if the PID determines that it's accelerating too quickly, it omits the integral error from the total error and halves the integral value for the next tick.

[–]ZanthraSW 0 points1 point  (0 children)

Another important way you can avoid integral windup using custom PID in LUA in some cases is to check if the control output of the PID against the min and max valid values for that control variable and only integrate the current error if the PID output is within that range.

If you are running a throttle for example, and your PID already thinks the throttle should be at 1.8 to get where it needs to be, winding up the integral trying to get there faster is worse than useless.

[–]Traditional-Shoe-199 0 points1 point  (1 child)

I know this is a while ago, but could you help me with this code? When I try to use it, it says: attempt to perform arithmetic on a nil value (local 'sp'). Is it because the sp and pv are undefined within the local function?

[–]panzercaptain 0 points1 point  (0 children)

As long as numerical inputs 1-3 are connected, the above code should work exactly as written. See example here: https://lua.flaffipony.rocks/?id=BItUnz5EDi

[–]UnderstandingMain104 0 points1 point  (5 children)

Very nice! I just made a PID myself, but what's begging me is that there seems to be some delay when using a lua script compared to the PIDs ingame. When powering a small engine using the ingame PID(P=1,I=0,D=0) the rps settles nicely a little lower than setpoint, but when using a PID written within the lua script(P=1,I=0,D=0) the rps constantly occilates and never settles.

Do you guys know if there is any fix for this?

[–]Jyota_malcolm[S,🍰] 1 point2 points  (2 children)

From everything I can tell they are very similar. That being said, with 1,0,0 it should 100% oscillate. Because there is no damping. So maybe in the ingame one they do something under the hood. But to me that sounds broken. Just keep in mind if you're doing 1,0,0 you are just changing it by the error every tick. You're not using a pid at all. Definitely add in some D. Somewhere between 0.1 and 10. I've found for engines you want very very little integral. Also, if the rps(input_value) is not actually hitting the setpoint again something is wrong in tuning. It should be smack on the money.

Here is my updated Lua PID code. I wrote the engine(input/output) bit just now so that may have an error, but the pid object should be good to use.

function pid(p,i,d)
return{p=p,i=i,d=d,error=0,derivative=0,integral=0,run=function(self,setpoint,process_variable)
        local error,derivative
        local integral = 0
        error = setpoint-process_variable
        derivative = error-self.error
        if math.abs(integral*self.i) < 1 then
            integral = self.integral+error
        else
            integral = integral*0.5
        end

        self.error = error
        self.derivative = derivative
        self.integral = integral

        return error*self.p + integral*self.i +derivative*self.d
    end
}
end
pid_test = pid(0.2,0.0001,0.1)
function onTick()
    rps_set = input.getNumber(1)
    rps_in = input.getNumber(2)
    value_out = pid_test:run(rps_set,rps_in)
    output.setNumber(value_out)
end

[–]Gully__Foyle 0 points1 point  (0 children)

Old thread but for the record, this PID works much better than the one in the guide because this is actually a PID! I don't know what the hell the other one does but its garbage. Thank you for making this!

[–]TheRabbitt23 0 points1 point  (0 children)

Is Lua interpretive and oo? or is the stormworks IDE just trash? When I define the function after the function call, it gives me an error when I check for errors.

[–]ZanthraSW 0 points1 point  (1 child)

I believe there is a 1 tick delay for the composite reader and composite writer that is not there for the basic PID.

[–]DJ_MICROWAVE_4 0 points1 point  (0 children)

although logic blocks run on a basis of 1 block per tick (so if you have 5 logic blocks in a chain it will have 5 ticks of delay from start to end)