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 };