├── LICENSE.txt ├── README.md ├── pidctrl.go └── pidctrl_test.go /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Felix Geisendörfer (felix@debuggable.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pidctrl 2 | 3 | A [PIDController][1] implementation in [Go][2]. 4 | 5 | ## API Documentation 6 | 7 | http://godoc.org/github.com/felixge/pidctrl 8 | 9 | ## TODO 10 | 11 | There are several [modified PID algorithms][3], these could be be implemented 12 | via flags / additional methods. 13 | 14 | ## License 15 | 16 | MIT License, see LICENSE.txt file. 17 | 18 | [1]: http://en.wikipedia.org/wiki/PID_controller 19 | [2]: http://golang.org/ 20 | [3]: http://en.wikipedia.org/wiki/PID_controller#Modifications_to_the_PID_algorithm 21 | -------------------------------------------------------------------------------- /pidctrl.go: -------------------------------------------------------------------------------- 1 | // Packege pidctrl implements a PID controller. 2 | // 3 | // see http://en.wikipedia.org/wiki/PID_controller 4 | package pidctrl 5 | 6 | import ( 7 | "fmt" 8 | "math" 9 | "time" 10 | ) 11 | 12 | type MinMaxError struct { 13 | min, max float64 14 | } 15 | 16 | func (e MinMaxError) Error() string { 17 | return fmt.Sprintf("min: %v is greater than max: %v", e.min, e.max) 18 | } 19 | 20 | // NewPIDController returns a new PIDController using the given gain values. 21 | func NewPIDController(p, i, d float64) *PIDController { 22 | return &PIDController{p: p, i: i, d: d, outMin: math.Inf(-1), outMax: math.Inf(0)} 23 | } 24 | 25 | // PIDController implements a PID controller. 26 | type PIDController struct { 27 | p float64 // proportional gain 28 | i float64 // integral gain 29 | d float64 // derrivate gain 30 | setpoint float64 // current setpoint 31 | prevValue float64 // last process value 32 | integral float64 // integral sum 33 | lastUpdate time.Time // time of last update 34 | outMin float64 // Output Min 35 | outMax float64 // Output Max 36 | } 37 | 38 | // Set changes the setpoint of the controller. 39 | func (c *PIDController) Set(setpoint float64) *PIDController { 40 | c.setpoint = setpoint 41 | return c 42 | } 43 | 44 | // Get returns the setpoint of the controller. 45 | func (c *PIDController) Get() float64 { 46 | return c.setpoint 47 | } 48 | 49 | // SetPID changes the P, I, and D constants 50 | func (c *PIDController) SetPID(p, i, d float64) *PIDController { 51 | c.p = p 52 | c.i = i 53 | c.d = d 54 | return c 55 | } 56 | 57 | // PID returns the P, I, and D constants 58 | func (c *PIDController) PID() (p, i, d float64) { 59 | return c.p, c.i, c.d 60 | } 61 | 62 | // SetOutputLimits sets the min and max output values 63 | func (c *PIDController) SetOutputLimits(min, max float64) *PIDController { 64 | if min > max { 65 | panic(MinMaxError{min, max}) 66 | } 67 | c.outMin = min 68 | c.outMax = max 69 | 70 | if c.integral > c.outMax { 71 | c.integral = c.outMax 72 | } else if c.integral < c.outMin { 73 | c.integral = c.outMin 74 | } 75 | return c 76 | } 77 | 78 | // OutputLimits returns the min and max output values 79 | func (c *PIDController) OutputLimits() (min, max float64) { 80 | return c.outMin, c.outMax 81 | } 82 | 83 | // Update is identical to UpdateDuration, but automatically keeps track of the 84 | // durations between updates. 85 | func (c *PIDController) Update(value float64) float64 { 86 | var duration time.Duration 87 | if !c.lastUpdate.IsZero() { 88 | duration = time.Since(c.lastUpdate) 89 | } 90 | c.lastUpdate = time.Now() 91 | return c.UpdateDuration(value, duration) 92 | } 93 | 94 | // UpdateDuration updates the controller with the given value and duration since 95 | // the last update. It returns the new output. 96 | // 97 | // see http://en.wikipedia.org/wiki/PID_controller#Pseudocode 98 | func (c *PIDController) UpdateDuration(value float64, duration time.Duration) float64 { 99 | var ( 100 | dt = duration.Seconds() 101 | err = c.setpoint - value 102 | d float64 103 | ) 104 | c.integral += err * dt * c.i 105 | if c.integral > c.outMax { 106 | c.integral = c.outMax 107 | } else if c.integral < c.outMin { 108 | c.integral = c.outMin 109 | } 110 | if dt > 0 { 111 | d = -((value - c.prevValue) / dt) 112 | } 113 | c.prevValue = value 114 | output := (c.p * err) + c.integral + (c.d * d) 115 | 116 | if output > c.outMax { 117 | output = c.outMax 118 | } else if output < c.outMin { 119 | output = c.outMin 120 | } 121 | 122 | return output 123 | } 124 | -------------------------------------------------------------------------------- /pidctrl_test.go: -------------------------------------------------------------------------------- 1 | package pidctrl 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | var tests = []struct { 11 | p float64 12 | i float64 13 | d float64 14 | min float64 15 | max float64 16 | updates []*testUpdate 17 | }{ 18 | // p-only controller 19 | { 20 | p: 0.5, 21 | updates: []*testUpdate{ 22 | {setpoint: 10, input: 5, output: 2.5}, 23 | {input: 10, output: 0}, 24 | {input: 15, output: -2.5}, 25 | {input: 100, output: -45}, 26 | {setpoint: 1, input: 0, output: 0.5}, 27 | }, 28 | }, 29 | // i-only controller 30 | { 31 | i: 0.5, 32 | updates: []*testUpdate{ 33 | {setpoint: 10, input: 5, duration: time.Second, output: 2.5}, 34 | {input: 5, duration: time.Second, output: 5}, 35 | {input: 5, duration: time.Second, output: 7.5}, 36 | {input: 15, duration: time.Second, output: 5}, 37 | {input: 20, duration: time.Second, output: 0}, 38 | }, 39 | }, 40 | // d-only controller 41 | { 42 | d: 0.5, 43 | updates: []*testUpdate{ 44 | {setpoint: 10, input: 5, duration: time.Second, output: -2.5}, 45 | {input: 5, duration: time.Second, output: 0}, 46 | {input: 10, duration: time.Second, output: -2.5}, 47 | }, 48 | }, 49 | // pid controller 50 | { 51 | p: 0.5, 52 | i: 0.5, 53 | d: 0.5, 54 | updates: []*testUpdate{ 55 | {setpoint: 10, input: 5, duration: time.Second, output: 2.5}, 56 | {input: 10, duration: time.Second, output: 0}, 57 | {input: 15, duration: time.Second, output: -5}, 58 | {input: 100, duration: time.Second, output: -132.5}, 59 | {setpoint: 1, duration: time.Second, input: 0, output: 6}, 60 | }, 61 | }, 62 | // Thermostat example 63 | { 64 | p: 0.6, 65 | i: 1.2, 66 | d: 0.075, 67 | max: 1, // on or off 68 | updates: []*testUpdate{ 69 | {setpoint: 72, input: 50, duration: time.Second, output: 1}, 70 | {input: 51, duration: time.Second, output: 1}, 71 | {input: 55, duration: time.Second, output: 1}, 72 | {input: 60, duration: time.Second, output: 1}, 73 | {input: 75, duration: time.Second, output: 0}, 74 | {input: 76, duration: time.Second, output: 0}, 75 | {input: 74, duration: time.Second, output: 0}, 76 | {input: 72, duration: time.Second, output: 0.15}, 77 | {input: 71, duration: time.Second, output: 1}, 78 | }, 79 | }, 80 | // pd controller, demonstrating we need to prevent i windup before summing up output 81 | { 82 | p: 40.0, 83 | i: 0, 84 | d: 12.0, 85 | max: 255, min: 0, 86 | updates: []*testUpdate{ 87 | {setpoint: 90.0, input: 22.00, duration: time.Second, output: 255.00}, 88 | {input: 25.29, duration: time.Second, output: 255.00}, 89 | {input: 28.56, duration: time.Second, output: 255.00}, 90 | {input: 31.80, duration: time.Second, output: 255.00}, 91 | {input: 35.02, duration: time.Second, output: 255.00}, 92 | {input: 38.21, duration: time.Second, output: 255.00}, 93 | {input: 41.38, duration: time.Second, output: 255.00}, 94 | {input: 44.53, duration: time.Second, output: 255.00}, 95 | {input: 47.66, duration: time.Second, output: 255.00}, 96 | {input: 50.76, duration: time.Second, output: 255.00}, 97 | {input: 53.84, duration: time.Second, output: 255.00}, 98 | {input: 56.90, duration: time.Second, output: 255.00}, 99 | {input: 59.93, duration: time.Second, output: 255.00}, 100 | {input: 62.95, duration: time.Second, output: 255.00}, 101 | {input: 65.94, duration: time.Second, output: 255.00}, 102 | {input: 68.91, duration: time.Second, output: 255.00}, 103 | {input: 71.85, duration: time.Second, output: 255.00}, 104 | {input: 74.78, duration: time.Second, output: 255.00}, 105 | {input: 77.69, duration: time.Second, output: 255.00}, 106 | {input: 80.57, duration: time.Second, output: 255.00}, 107 | {input: 83.43, duration: time.Second, output: 228.48}, 108 | {input: 85.93, duration: time.Second, output: 132.80}, 109 | {input: 87.18, duration: time.Second, output: 97.80}, 110 | {input: 87.96, duration: time.Second, output: 72.24}, 111 | {input: 88.41, duration: time.Second, output: 58.20}, 112 | {input: 88.68, duration: time.Second, output: 49.56}, 113 | {input: 88.83, duration: time.Second, output: 45.00}, 114 | {input: 88.92, duration: time.Second, output: 42.12}, 115 | {input: 88.98, duration: time.Second, output: 40.08}, 116 | {input: 89.00, duration: time.Second, output: 39.76}, 117 | {input: 89.03, duration: time.Second, output: 38.44}, 118 | {input: 89.03, duration: time.Second, output: 38.80}, 119 | {input: 89.05, duration: time.Second, output: 37.76}, 120 | {input: 89.04, duration: time.Second, output: 38.52}, 121 | {input: 89.05, duration: time.Second, output: 37.88}, 122 | {input: 89.05, duration: time.Second, output: 38.00}, 123 | {input: 89.05, duration: time.Second, output: 38.00}, 124 | {input: 89.05, duration: time.Second, output: 38.00}, 125 | {input: 89.05, duration: time.Second, output: 38.00}, 126 | }, 127 | }, 128 | // panic test 129 | { 130 | p: 0.5, 131 | i: 0.5, 132 | d: 0.5, 133 | min: 100, // min and max are swapped 134 | max: 1, 135 | updates: []*testUpdate{}, 136 | }, 137 | } 138 | 139 | type testUpdate struct { 140 | setpoint float64 141 | input float64 142 | duration time.Duration 143 | output float64 144 | } 145 | 146 | func round(v float64, decimals int) float64 { 147 | var pow float64 = 1 148 | for i := 0; i < decimals; i++ { 149 | pow *= 10 150 | } 151 | return float64(int((v*pow)+0.5)) / pow 152 | } 153 | 154 | func (u *testUpdate) check(c *PIDController) error { 155 | if u.setpoint != 0 { 156 | c.Set(u.setpoint) 157 | } 158 | output := c.UpdateDuration(u.input, u.duration) 159 | if round(output, 63) != round(u.output, 63) { 160 | return fmt.Errorf("Bad output: %f != %f (%#v)", output, u.output, u) 161 | } 162 | return nil 163 | } 164 | 165 | func TestUpdate_p(t *testing.T) { 166 | defer func() { 167 | if r := recover(); (reflect.TypeOf(r)).Name() == "MinMaxError" { 168 | fmt.Println("Recovered Error:", r) 169 | } else { 170 | t.Error(r) 171 | } 172 | }() 173 | for i, test := range tests { 174 | t.Logf("-- test #%d", i+1) 175 | c := NewPIDController(test.p, test.i, test.d) 176 | if test.min != 0 || test.max != 0 { 177 | c.SetOutputLimits(test.min, test.max) 178 | } 179 | for _, u := range test.updates { 180 | if err := u.check(c); err != nil { 181 | t.Error(err) 182 | } 183 | } 184 | } 185 | } 186 | --------------------------------------------------------------------------------