1 /** 2 Copyright: Copyright (c) 2020, Joakim Brännström. All rights reserved. 3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0) 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 6 A PID regulator and low pass filter. 7 8 # Theory 9 10 Velocity PID controller from the Laplace transform 11 U(s) / E(s) = Kp(1 + 1/(Ti*s) + Td*s) 12 to time domain with backward-difference 13 u(t) = u(t-1) + Kp(e(t) - e(t-1) + e(t)*Ts/Ti + (e(t) - 2e(t-1) + e(t-2))*Td/Ts) 14 15 This is the second form of the classical PID equation. 16 It is called noninteractive or parallel or ideal or ISA. 17 18 Source: http://straightlinecontrol.com/pid_algorithms.html 19 20 ## SECOND FORM OF THE PID ALGORITHM 21 22 The second form of the algorithm is called "noninteracting, or "parallel" or 23 "ideal" or "ISA" . I understand one manufacturer refers to this as 24 "interacting", which serves to illustrate that terms by themselves may not 25 tell you what the algorithm is. This form is used in most textbooks, I 26 understand. I think it is unfortunate that textbooks do not at least 27 recognize the different forms. Most if not all books written for industry 28 users rather than students recognize at least the first two forms. The basic 29 difference between the first and second forms is in the way derivative is 30 handled. If the derivative term is set to zero, then the two algorithms are 31 identical. Since derivative is not used very often (and shouldn't be used 32 very often) perhaps it is not important to focus on the difference. But it 33 is important to anyone using derivative, and people who use derivative 34 should know what they are doing. The parameters set in this form can be made 35 equivalent (except for the treatment of gain-limiting on derivative) to 36 those in the first form in this way: 37 38 Kc' = ((Ti +Td)/Ti))Kc, "effective" gain. 39 Ti' = Ti + Td, "effective" integral or reset time 40 Td' = TiTd/(Ti + Td), "effective" derivative time 41 42 These conversions are made by equating the coefficients of s. Conversions in 43 the reverse direction are: 44 45 Kc = FKc' 46 Ti = FTi' 47 Td = Td'/F 48 49 where 50 51 F =0.5 + sqrt(0.25 - Td'/Ti') 52 53 Typically Ti is set about 4 to 8 times Td, so the conversion factor is not 54 huge, but it is important to not loose sight of the correction. With this 55 algorithm it is possible to have very troublesome combinations of Ti' and 56 Td'. If Ti'<4Td' then the reset and derivative times, as differentiated from 57 settings, become complex numbers, which can confuse tuning. Don't slip into 58 these settings inadvertently! A very knowledgeable tuner may be able to take 59 advantage of that characteristic in very special cases, but it is not for 60 everyone, every day. Some companies advise to use the interacting form if 61 available, simply to avoid that potential pitfall. 62 63 This algorithm also has no provision for limiting high frequency gain from 64 derivative action, a virtually essential feature. In the first algorithm Kd 65 is typically fixed at 10, or if adjustable, should typically be set 66 somewhere in the range of 6 to 10. This desirable limiting of the derivative 67 component is sometimes accomplished in this second form by writing it as: 68 69 Kc'(1 + 1/Ti's + Td's)/(1 + Td's/Kd) 70 71 or 72 73 Kc'(1 + 1/Ti's + Td's/(1 + Td's/Kd)) 74 75 There are likely many variations on the theme. 76 77 The variables Kc', Ti' and Td' have been called "effective". In the Bode 78 plot, IF Ti'>4Td', THEN Kc' is the minimum frequency-dependent gain (Kc is a 79 frequency-independent gain). This is at a frequency which is midway between 80 the "corners" defined by Ti and Td, which is also midway between the 81 "effective " corners associated with Ti' and Td'. Ti' is always larger than 82 Ti and Td' is always smaller than Td, which recognizes the slight spreading 83 of the "effective" corners of the Bode plot as they approach each other. 84 85 This algorithm is also called the "ISA" algorithm. The ISA has no 86 association with this algorithm. Apparently this attribution got started 87 when someone working on the Fieldbus thought it would become "THE" 88 algorithm. It didn't. Or hasn't. ANSI/ISA-S51.1-1979 (Rev. 1993) is a 89 standard on Process Instrumentation Terminology. While this is a standard on 90 terminology, not algorithms, it uses the first form of the algorithm for 91 examples and in its Bode plot for a PID controller. Another term used to 92 identify this algorithm is "ideal". Think of this word as one to identify 93 the algorithm, not describe it. It is true that it can do everything the 94 first form can do, and more, provided the gain for derivative is handled 95 appropriately. But settings which produce complex roots should be used only 96 by the very knowledgeable. 97 98 ## THE FIRST FORM OF THE PID ALGORITHM 99 100 This first form is called "series" or "interacting" or "analog" or 101 "classical". The variables are: 102 103 Kc = controller gain = 100/proportional band 104 Ti = Integral or reset time = 1/reset rate in repeats/time 105 Td = derivative time 106 Kd = derivative gain 107 108 Early pneumatic controllers were probably designed more to meet mechanical 109 and patent constraints than by a zeal to achieve a certain algorithm. Later 110 pneumatic controllers tended to have an algorithm close to this first form. 111 Electronic controllers of major vendors tended to use this algorithm. It is 112 what process industry control users were used to at the time. If you are 113 unsure what algorithm is being used for the controller you are tuning, find 114 out what it is before you start to tune. 115 116 I did not follow closely the evolution of algorithms as digital controllers 117 were introduced. It is my understanding that most major vendors of digital 118 controllers provide this algorithm as basic, and many provide the second 119 form as well. Also, many provide several variations (I'm told Allen-Bradley 120 has 10, and that other manufacturers are adding variations continually). 121 122 The choice of the word interacting is interesting. At least one author says 123 that it is interacting in the time domain and noninteracting in the 124 frequency domain. Another author disagrees with this distinction. This 125 really becomes a discussion of what interacts with what. To be safe, think 126 of the word interacting as one to identify the algorithm, not to describe 127 it. 128 */ 129 module my.signal_theory.pid; 130 131 import core.time : dur, Duration; 132 133 @safe: 134 135 /** A discrete position PID controller. 136 * 137 * Implemented internally using floating point to simplify the code. Change to 138 * fixed point precision if so desired. 139 */ 140 struct PositionPid { 141 /** 142 * Params: 143 * Kp = the propotional gain constant 144 * Ki = the integrating gain constant 145 * Kd = the derivating gain constant 146 */ 147 this(double Kp, double Ki, double Kd) { 148 this.Kp = Kp; 149 this.Ki = Ki; 150 this.Kd = Kd; 151 } 152 153 /** Uses the input values to adjust the output. 154 * 155 * Params: 156 * pv = the measured value/Process Variable. 157 */ 158 void input(double pv) { 159 import std.algorithm : clamp; 160 161 // the error between the current output and desired 162 double e = sv - pv; 163 164 integral = (integral + e) * Ki; 165 integral = clamp(integral, integral_low, integral_high); 166 167 // derivative on measurement, eliminate spikes when sv changes. 168 double derivative = 0; 169 if (useDFilter) { 170 dFilter.input(e - prev_e); 171 derivative = Kd * dFilter.output; 172 } else { 173 derivative = Kd * (e - prev_e); 174 } 175 176 output_ = Kp * e + integral + derivative; 177 output_ = clamp(output_, output_low, output_high); 178 179 prev_e = e; 180 } 181 182 /// Returns: the calculated output gain. 183 double output() const { 184 return output_; 185 } 186 187 /** Set value. 188 * 189 * The regulator is trying to keep this value 190 */ 191 void setSv(double x) { 192 sv = x; 193 } 194 195 double getSv() const { 196 return sv; 197 } 198 199 /// Proportional gain. 200 void setKp(double x) { 201 Kp = x; 202 } 203 204 /// Integral gain. 205 void setKi(double x) { 206 Ki = x; 207 } 208 209 /// Derivative gain. 210 void setKd(double x) { 211 Kd = x; 212 } 213 214 void setKd(LowPassFilter lp) { 215 dFilter = lp; 216 useDFilter = true; 217 } 218 219 /// Clamps the integration to the range [low, high]. 220 void setIntegralClamp(double low, double high) 221 in (low < high) { 222 integral_low = low; 223 integral_high = high; 224 } 225 226 /// Clamps the PID control output to the range [low, high]. 227 void setOutputClamp(double low, double high) 228 in (low < high) { 229 output_low = low; 230 output_high = high; 231 } 232 233 /// Resets the controllers state to initial. 234 void reset() { 235 this = typeof(this).init; 236 } 237 238 import std.range : isOutputRange; 239 240 string toString() const { 241 import std.array : appender; 242 243 auto buf = appender!string; 244 toString(buf); 245 return buf.data; 246 } 247 248 void toString(Writer)(ref Writer w) const if (isOutputRange!(Writer, char)) { 249 import std.format : formattedWrite; 250 251 formattedWrite(w, "PositionPid(Kp:%s Ki:%s Kd:%s sv:%s integral:%s prev_e:%s output:%s", 252 Kp, Ki, Kd, sv, integral, prev_e, output); 253 } 254 255 private: 256 /// previous errors (t-1, t-2) 257 double prev_e = 0; 258 259 /// The output value 260 double output_ = 0; 261 262 /// The setpoint value the PID is trying to reach 263 double sv = 0; 264 265 /// Constaint gain. 266 double Kp = 0; 267 /// Integral gain. 268 double Ki = 0; 269 /// Derivative gain. 270 double Kd = 0; 271 272 /// Track error over time. 273 double integral = 0; 274 275 /// filter to use for the derivative 276 LowPassFilter dFilter; 277 bool useDFilter; 278 279 /// Clamp of the integral. 280 double integral_low = double.min_10_exp; 281 double integral_high = double.max_10_exp; 282 283 /// Clamp of the output. 284 double output_low = double.min_10_exp; 285 double output_high = double.max_10_exp; 286 }; 287 288 @("shall instantiate a PositionPid") 289 unittest { 290 import std.stdio : writefln, writeln; 291 import my.signal_theory.simulate; 292 293 Simulator sim; 294 295 const period = sim.period; 296 const double ticks = cast(double) 1.dur!"seconds" 297 .total!"nsecs" / cast(double) period.total!"nsecs"; 298 const clamp = period.total!"nsecs" / 2; 299 300 const double kp = 0.1; 301 const double ki = 0.125; 302 303 auto pid = PositionPid(kp, ki, 4); 304 pid.setIntegralClamp(-clamp, clamp); 305 pid.setOutputClamp(-clamp, clamp); 306 pid.setKd(LowPassFilter(1, 10)); 307 308 while (sim.currTime < 1000.dur!"msecs") { 309 sim.tick!("nsecs")(a => pid.input(a.total!"nsecs"), () => pid.output); 310 if (sim.updated) { 311 const diff = sim.targetTime - sim.wakeupTime; 312 //writefln!"time[%s] pv[%s] diff[%s] output:%0.2f"(sim.currTime, sim.pv, diff, pid.output); 313 //writeln(pid); 314 } 315 } 316 317 import std.math : abs; 318 319 assert(abs(sim.pv.total!"msecs") < 100); 320 assert(abs(pid.output) < 10000.0); 321 } 322 323 /// A first order low pass filter. 324 struct LowPassFilter { 325 /** 326 * Params: 327 * dt = time interval 328 * RC = time constant 329 */ 330 this(double dt, double RC) { 331 this.dt = dt; 332 alpha = dt / (RC + dt); 333 } 334 335 void input(double x) { 336 y = alpha * x + (1.0 - alpha) * y; 337 } 338 339 double output() { 340 return y; 341 } 342 343 private: 344 double dt = 1; 345 double alpha = 0; 346 double y = 0; 347 };