Jump to content
  • Sky
  • Blueberry
  • Slate
  • Blackcurrant
  • Watermelon
  • Strawberry
  • Orange
  • Banana
  • Apple
  • Emerald
  • Chocolate
  • Charcoal
mpmxyz

pid: PID controllers for your reactor (library included)

Recommended Posts

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

gpid.png

 

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 using

Installation
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 by mpmxyz
added oppm download option
Link to post
Share on other sites

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.

Link to post
Share on other sites

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.

gpid.png

Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...

×
×
  • Create New...

Important Information

By using this site, you agree to our Terms of Use and Privacy Policy.