mpmxyz 27 Posted April 24, 2015 Share Posted April 24, 2015 (edited) Introduction This program and its library make setting up PID controllers in Minecraft easy. Controllers are used whenever you want to maintain a certain speed, temperature etc. An example system in Minecraft would be a Big Reactors turbine: It takes an adjustable steam flow rate and has a speed that should be maintained at an optimal value of 1800 rpm. A turbine controller would therefore have the following inputs: 1. the current turbine speed 2. the target turbine speed (1800 rpm) and the following output: 1. steam flow rate (a value that would let the turbine reach and stay at 1800 rpm) This software uses a PID controller. It got its name after the three parts its output is calculated from. The output is the sum of: 1. P(proportional): This part is proportional to the error. (difference between target value and currently measured value) While it is directly minimizing the error, it alone often can't zero it completely. Too much of it can also lead to instability. (oscillations) 2. I(ntegral): This part is proportional to the sum of all errors accumulated over time. That makes it perfect to slowly remove the residual error left over by the proportional part, but it increases the tendency for overshooting. (or even oscillations) 3. D(erivative): This part is proportional to the change in error per time. It is therefore good to limit the speed of error changes. (preventing too fast error increases and slowing down decreases to avoid overshooting -> damping / increasing stability) But it is vulnerable to noisy measurements. (large changes divided by short durations -> big influence -> output becomes very noisy) The tricky part is determining the correct factors to have a responsive controller but no instabilities. (There is a small Tutorial in the next section.) If you are interested to learn more, here is a good introduction to controllers: Screenshots pid debug turbine.pid ID: turbine.pid Target: +1800 Current: +1800 Error: +0 change/s: +0 PID parts:+0 +914 +0 Output: +914 gpid Example / Tutorial (Big Reactors Turbine) Wikipedia knows a lot of ways on how to find the correct PID parameters. This one here is called "manual tuning". But before that we should create a basic controller file: --PREPARATION-- --Here we just get a reference to the turbine proxy. local component = require("component") turbine = component.br_turbine --INPUTS-- --#1: our measured input sensor = turbine.getRotorSpeed --#2: our target value / setpoint target = 1800 --OUTPUT-- --The actuator table is a description of the output of the controller. actuator = { --The setter is used as the output of the controller. set = turbine.setFluidFlowRateMax, --The getter is used to initialize the "I part" when (re-)starting the controller. --That ensures smooth operation even if you are tuning your controller. get = turbine.getFluidFlowRateMax, --the minimum and maximum values the controller output might reach --It also supports actuators without limits by setting the corresponding value to nil. min = 0, max = 2000, } --PID PARAMETERS-- factors = { --proportional factor p = 0, --integral factor i = 0, --derivative factor d = 0, } --OTHER OPTIONS-- --An update frequency of 4 Hz is fast enough but still very lag friendly. frequency = 4 Now that the preparations are done; let's start working on the PID parameters: First you should try some values for p. At first pick a random one, like 2: --PID PARAMETERS-- factors = { --proportional factor p = 2,--<== --integral factor i = 0, --derivative factor d = 0, } Save the changes, manually set the turbine flow to maintain the desired setting and run this command: pid run yourturbine.pid The first will load and execute your controller. If you run the command a second time it will override the running controller with a new one. That's how you update your controller when you changed anything. In the meantime your controller started doing its business. But nothing changed?! That's because the pid library is calculating an integral offset on startup to avoid any sudden control changes when starting the controller - even if you integral factor is 0! Since your controller doesn't want to change something on its own, you have to challenge him. Change the target value / setpoint: pid update yourturbine.pid target=900 This should give you a feeling on how fast your controller would react to changes. Since you want to make it react as fast as possible, you should increase your p value and test again. Note: Whenever you change factors, test your changes with different scenarios. (1800 -> 900, 900 -> 1800, 1800 -> 1900 or even some inductor on/off changes) You continue doing that until you observe an undesired behaviour: oscillations. With big reactor turbines these oscillations can be quite fast having periods of just a few ticks. (Real world machines would break soon under this conditions.) To keep some safety margin you should therefore keep the p value at half the value it needed to start oscillations. Now you can pick an integral factor. (e.g. 2) --PID PARAMETERS-- factors = { --proportional factor p = 40,--The turbine in my world started oscillations at p==80. --integral factor i = 2,--<== --derivative factor d = 0, } Now you can see that the turbine is finally reaching the target speed. To see what happens in the background you can run this command: pid debug yourturbine.pid It will show you a short debug screen: ID: turbine.pid --The id of the controller you are looking at Target: +1800 --the target value / setpoint Current: +1800 --the measured value Error: +0 --error: target value - measured value change/s: +0 --rate of error changes PID parts:+0 +914 +0 --proportional, integral and derivative parts Output: +914 --output: sum of parts Over time the integral part will change to the output that is required to maintain your setpoint and at the same time you will see how the other parts will become smaller and smaller. But depending on how well you guessed it might take a while. Increase the integral factor until you reach the point at which the controller starts to overshoot. Now you have several options: 1st: Continue by adjusting the D(erivative) factor which would be set to reduce the overshoot and the time it needs to reach the setpoint. Attention: It should be quite small. Else the controller might overreact to its own changes. 2nd: Reduce the integral factor a bit and finish here: A turbine controller doesn't necessarily need a derivative part to work "good enough". If you just want to quickly set up a reactor with a turbine, you can use the sample files available as another download. (You might want to tweak the factors because your setup might be different to my setup.) One thing you might note: The factors of the reactor controller are negative. This is because an increase in control rod level decreases the reactor output. Keep that in mind if you are designing a controller for a completely different system! In a newer update a program with a graphical user interface has been introduced: gpid Just run this command: gpid You can interact with the interface by using your mouse or by hitting the arrow keys and enter. Man Pages pid NAME pid - PID controllers for Minecraft SYNOPSIS pid <action> file or id [var=value or =var ...] [--args ...] pid debug [ids ...] DESCRIPTION Set up your PID controllers the easy way: Just create a controller file and run "pid run yourfile.pid". You controller is running in the background. The file name of a controller is used as an ID when none is given in the file. You can show and modify values using =var or var=value respectively: var meaning target tgt t target value frequency freq f update frequency in Hz p proportional factor i integral factor d derivative factor min minimum output value max maximum output value The actions "run" and "load" also accept the option "--args" followed by arguments that should be forwarded to the file being loaded. ACTIONS pid needs an action parameter telling it what it has to do. run loads and starts a PID load loads a PID but doesn't start it update updates only (to update PID vars) unload stops and unregisters a PID start (re-)starts a PID stop stops a PID debug shows a debug screen - the most important parameters are displayed for each given controller EXAMPLES pid run reactor.pid loads and starts the controller from file "reactor.pid" It's assigned the ID "reactor.pid" unless there is an overriding assignment in the file. pid load /pids/turbine.pid --args bada4648-3559-4784-b3c7-06c146d9dc3b loads the controller file "/pids/turbine.pid" The string argument "bada4648-3559-4784-b3c7-06c146d9dc3b" is used when loading the file. The controller is assigned the ID "turbine.pid" unless there is an overriding assignment in the file. pid start turbine.pid starts the controller "turbine.pid" pid update turbine.pid target=900 reduces the speed of the turbine to 900 rpm pid debug reactor.pid turbine.pid displays debug information of two controllers in one screen Each controller needs 8 lines on the screen. Debug information isn't displayed if there isn't enough space left. You can leave the debug screen by doing a soft interrupt. (Ctrl + C) pid stop turbine.pid stops the given controller pid unload reactor.pid stops and unregisters the controller gpid coming soon API You can use the library via require("pid"). Functions pid.new(controller:table, [id, enable:boolean, stopPrevious:boolean]) -> controller:table, previous:table, previousIsRunning:boolean Creates a pid controller by adding methods to the given controller table. For convenience it is also automaticly started unless enable is false. (default is true) Controllers can be registered globally using the id parameter. (or field; but the parameter takes priority) There can only be one controller with the same id. Existing controllers will be replaced. (and stopped if stopPrevious is true) pid.loadFile(file, enable, ...) -> controller:table, id loads a controller from a given source file The file is loaded with a custom environment which combines the normal environment with a controller table. Writing access is always redirected to the controller table. Reading access is first redirected to the controller and, if the value is nil, it is redirected to the normal environment. Additional parameters are forwarded when the main chunk of the file is called. pid.get(id) -> controller:table returns the controller registered with the given id pid.getID(controller:table) -> id gets the id the given PID controller is registered with pid.register(controller:table, [stopPrevious:boolean, id]) -> old pid:table, wasRunning:boolean registers a controller using either the id field as a key or the id parameter given to the function A controller can only be registered once and only one controller can be registered with a given id. If one tries to register a controller multiple times it is only registered with the last id. If one tries to register multiple controllers on the same id only the last controller stays. You can order the controller being previously registered with the same id to stop using the parameter "stop". pid.removeID(id, [stop:boolean]) -> old pid:table, wasRunning:boolean removes the controller with the given id from the registry You can also order the controller to stop using the parameter "stop". pid.remove(controller:table, [stop:boolean]) -> wasRunning:boolean removes the given controller from the registry You can also order the controller to stop using the parameter "stop". pid.registry() -> proxy:table returns a read only proxy of the registry Read only access ensures that the internal reverse registry stays updated. controller:doStep(dt:number) runs the controller calculation for the given time interval controller:forceOffset(newOffset:number) changes the internal offset of the controller (in case you feel the need for a manual override...) controller:start() starts the controller controller:isRunning() -> boolean returns true if the controller is already running controller:stop() stops the controller controller:isValid() -> true or false, errorText:string returns true if the controller seems to be valid returns false and an error message if the controller is invalid controller:assertValid() errors if the controller is not valid controller:getID() -> id see pid.getID controller:register([stopPrevious:boolean, id]) -> old pid:table, wasRunning:boolean see pid.register controller:remove([stop:boolean]) -> wasRunning:boolean see pid.remove Properties controller={ sensor=value, --a function returning the value being controlled target=value, --the value that the controlled value should reach actuator={ --The actuator is the thing that is 'working' on your system. set=function(number), --It is 'actuated' using this setter function. get=value or nil, --For better jump starting capabilities it is recommended to also add a getter function. min=value or nil, --Minimum and maximum values can also be set to define the range of control inputs to the actuator. max=value or nil, --The limit can also be one sided. (e.g. from 0 to infinity) }, factors={ --These are the factors that define the behaviour of the PID controller. It has even got its name from them. p=value, --P: proportional, factor applied to the error (current value - target value) is added directly acts like a spring, increases tendency to return to target, but might leave some residual error i=value, --I: integral, factor applied to the error before adding it to the offset value the offset value is added increases tendency to return to target and reduces residual error, but also adds some kind of inertia -> more prone to oscillations d=value, --D: derivative, factor applied to the change of error per second is added directly can be used to dampen instabilities caused by the other factors (needs smooth input values to work properly) }, --The sum of all parts is the controller output. frequency=number, --the update frequency in updates per second id = optional, --the id used to register the controller } When the controller is active, it is also updating a debug info table: controller.info = { p=number, --currently used P factor i=number, --currently used I factor d=number, --currently used D factor dt=number, --current time interval between update cycles value=number, --current sensor output target=number, --current setpoint error=target-value, --current error (defined as the given difference) lastError=number, --error of last cycle (used to calculate D term) controlMin=number, --lower limit of control output controlMax=number, --upper limit of control output rawP=p*currentError, --p component of sum rawI=old offset+i*dt*error, --i component of sum rawD=d*derror, --d component of sum rawSum=rawP+rawI+rawD, --sum of PID components (limits not applied) doffset=i*dt*error or 0,--change in offset (this cycle); can be forced to 0 when output value is on the limit offset=number, --new offset, after adding doffset derror=(error-lastError) / dt, --rate of change of error since last cycle output=p*error+d*derror+offset, --rawSum with output limits applied } Dependencies Software only OpenOS Hardware RAM: almost nothing on top of what OpenOS is already usingInstallation Simply download the tar archive and extract it into the root directory. All files should then be there where they should be.Download (last update: 07.11.15)github: program and library github: sample controllers Ingame: #program + library: wget 'https://github.com/mpmxyz/ocprograms/raw/master/tars/pid.tar' tar -xf pid.tar #sample controllers: wget 'https://github.com/mpmxyz/ocprograms/raw/master/tars/pids.tar' tar -xf pids.tar OR oppm install mpmpid Known Issues currently none Feel free to ask, if anything is unclear. Control Engineering has never been part of my curriculum; so if you know more and would like to share something - like an automatic tuning algorithm, feel free to do so. Edited April 13, 2017 by mpmxyz added oppm download option Quote Link to post Share on other sites
Slaughters 0 Posted August 9, 2015 Share Posted August 9, 2015 Where's Zhe screenshots? Quote Link to post Share on other sites
mpmxyz 27 Posted August 12, 2015 Author Share Posted August 12, 2015 This software is meant to run in the background and has no graphical interface. So there wouldn't be much I could show. (apart from the debug mode output, which is included in my post) Any ideas? PS: I wrote a detailed introduction on how to set up a turbine and included ready to use examples for reactors and turbines to make up for the missing GUI. Quote Link to post Share on other sites
mpmxyz 27 Posted November 7, 2015 Author Share Posted November 7, 2015 Update (07.11.15) The API has changed a bit. pid.remove now expects a controller instead of an id. Use pid.removeID for the old behaviour. The command line interface of pid has been changed, too. It now has the strict format of: pid <action> id or file [other stuff] (Note: actions are no longer written with "--" in front of them.) This change has been introduced to allow changing controller properties: pid run /pids/turbine.pid p=10 i=0.4 d=1 f=4 target=900 --args bada4648-3559-4784-b3c7-06c146d9dc3b There also is a new program: gpid It has no options or parameters but instead provides you a graphical user interface. Use your mouse or the arrow keys + enter to navigate. I'm going to write a more detailed description about this program. One thing that might need to be explained is that after pressing "Add New" you have to input the absolute path of the controller file and after that you have to write the arguments for the controller as lua code. Since this is part of a new feature you can leave it empty and just hit enter when you are testing the program with your own controllers. Quote Link to post Share on other sites