Class: DeviceControl::PIDController

Inherits:
Controller show all
Defined in:
lib/device_control.rb

Overview

A PIDController is a Controller that tracks its error over time in order to calculate:

Proportion (current error)
Integral   (accumulated error)
Derivative (error slope, last_error)

The sum of these terms is the output

Constant Summary collapse

HZ =
1000
TICK =
Rational(1) / HZ
ZN =

Ziegler-Nichols method for tuning PID gain knobs en.wikipedia.org/wiki/Ziegler%E2%80%93Nichols_method

{
  #           Kp     Ti    Td     Ki     Kd
  #     Var:  Ku     Tu    Tu    Ku/Tu  Ku*Tu
  'P'    => [1/2r],
  'PI'   => [9/20r, 4/5r,   nil, 27/50r],
  'PD'   => [ 4/5r,  nil,  1/8r,  nil, 1/10r],
  'PID'  => [ 3/5r, 1/2r,  1/8r, 6/5r, 3/40r],
  'PIR'  => [7/10r, 2/5r, 3/20r, 7/4r, 21/200r],
  # less overshoot than standard PID
  'some' => [ 1/3r, 1/2r,  1/3r, 2/3r, 1/11r],
  'none' => [ 1/5r, 1/2r,  1/3r, 2/5r, 2/30r],
}

Instance Attribute Summary collapse

Attributes inherited from Controller

#measure, #setpoint

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Updateable

#update

Constructor Details

#initialize(setpoint, dt: TICK, low_pass_ticks: 0) {|_self| ... } ⇒ PIDController

Returns a new instance of PIDController.

Yields:

  • (_self)

Yield Parameters:



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/device_control.rb', line 204

def initialize(setpoint, dt: TICK, low_pass_ticks: 0)
  super(setpoint)
  @dt = dt
  @error, @last_error, @sum_error = 0.0, 0.0, 0.0
  if low_pass_ticks > 0
    @mavg = MovingAverage.new(low_pass_ticks)
  else
    @mavg = nil
  end

  # gain / multipliers for PID; tunables
  @kp, @ki, @kd = 1.0, 1.0, 1.0

  # optional clamps for PID terms and output
  @p_range = (-Float::INFINITY..Float::INFINITY)
  @i_range = (-Float::INFINITY..Float::INFINITY)
  @d_range = (-Float::INFINITY..Float::INFINITY)
  @o_range = (-Float::INFINITY..Float::INFINITY)
  @e_range = (-Float::INFINITY..Float::INFINITY)

  yield self if block_given?
end

Instance Attribute Details

#d_rangeObject

Returns the value of attribute d_range.



198
199
200
# File 'lib/device_control.rb', line 198

def d_range
  @d_range
end

#dtObject

Returns the value of attribute dt.



198
199
200
# File 'lib/device_control.rb', line 198

def dt
  @dt
end

#e_rangeObject

Returns the value of attribute e_range.



198
199
200
# File 'lib/device_control.rb', line 198

def e_range
  @e_range
end

#errorObject

Returns the value of attribute error.



198
199
200
# File 'lib/device_control.rb', line 198

def error
  @error
end

#i_rangeObject

Returns the value of attribute i_range.



198
199
200
# File 'lib/device_control.rb', line 198

def i_range
  @i_range
end

#kdObject

Returns the value of attribute kd.



198
199
200
# File 'lib/device_control.rb', line 198

def kd
  @kd
end

#kiObject

Returns the value of attribute ki.



198
199
200
# File 'lib/device_control.rb', line 198

def ki
  @ki
end

#kpObject

Returns the value of attribute kp.



198
199
200
# File 'lib/device_control.rb', line 198

def kp
  @kp
end

#last_errorObject

Returns the value of attribute last_error.



198
199
200
# File 'lib/device_control.rb', line 198

def last_error
  @last_error
end

#low_pass_ticksObject

Returns the value of attribute low_pass_ticks.



198
199
200
# File 'lib/device_control.rb', line 198

def low_pass_ticks
  @low_pass_ticks
end

#mavgObject (readonly)

Returns the value of attribute mavg.



202
203
204
# File 'lib/device_control.rb', line 202

def mavg
  @mavg
end

#o_rangeObject

Returns the value of attribute o_range.



198
199
200
# File 'lib/device_control.rb', line 198

def o_range
  @o_range
end

#p_rangeObject

Returns the value of attribute p_range.



198
199
200
# File 'lib/device_control.rb', line 198

def p_range
  @p_range
end

#sum_errorObject

Returns the value of attribute sum_error.



198
199
200
# File 'lib/device_control.rb', line 198

def sum_error
  @sum_error
end

Class Method Details

.tune(type, ku, tu) ⇒ Object

ku = ultimate gain, tu = oscillation period output includes ti and td, which are not necessary typically kp, ki, and kd are used



187
188
189
190
191
192
193
194
195
196
# File 'lib/device_control.rb', line 187

def self.tune(type, ku, tu)
  record = ZN[type.downcase] || ZN[type.upcase] || ZN.fetch(type)
  kp, ti, td, ki, kd = *record
  kp *= ku if kp
  ti *= tu if ti
  td *= tu if td
  ki *= (ku / tu) if ki
  kd *= (ku * tu) if kd
  { kp: kp, ti: ti, td: td, ki: ki, kd: kd }
end

Instance Method Details

#derivativeObject



261
262
263
# File 'lib/device_control.rb', line 261

def derivative
  (@kd * (@error - @last_error) / @dt).clamp(@d_range.begin, @d_range.end)
end

#input=(val) ⇒ Object

update @error, @last_error, and @sum_error



228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/device_control.rb', line 228

def input=(val)
  @measure = val
  @last_error = @error
  @error = @setpoint - @measure
  # Incorporate @ki here for better behavior when @ki is updated
  # It's a good idea to clamp the accumulated error so that if we start
  #   way under setpoint, we don't accumulate so much error that we spend
  #   too much time overshooting to counteract it
  @sum_error =
    (@sum_error + @ki * @error * @dt).clamp(@e_range.begin, @e_range.end)
  # update mavg here to ensure only one update per PID input
  @mavg.input = self.derivative if @mavg
end

#integralObject

It may seem funny to clamp both @sum_error and the integral term, but

we may want different values for these clamps.  @e_range is just to
make sure we don't create a mountain to chew through.  @i_range gives
additional flexibility for balancing P I & D


257
258
259
# File 'lib/device_control.rb', line 257

def integral
  @sum_error.clamp(@i_range.begin, @i_range.end)
end

#outputObject



242
243
244
245
246
247
# File 'lib/device_control.rb', line 242

def output
  drv = @mavg ? @mavg.output : self.derivative
  (self.proportion +
   self.integral +
   drv).clamp(@o_range.begin, @o_range.end)
end

#proportionObject



249
250
251
# File 'lib/device_control.rb', line 249

def proportion
  (@kp * @error).clamp(@p_range.begin, @p_range.end)
end

#to_sObject



265
266
267
268
269
270
271
272
273
274
# File 'lib/device_control.rb', line 265

def to_s
  [super,
   format("Error: %+.3f\tLast: %+.3f\tSum: %+.3f",
          @error, @last_error, @sum_error),
   format(" Gain:\t%.3f\t%.3f\t%.3f",
          @kp, @ki, @kd),
   format("  PID:\t%+.3f\t%+.3f\t%+.3f\t= %.5f",
          self.proportion, self.integral, self.derivative, self.output),
  ].join("\n")
end