Refactor tune() to not return the complexity of the scene.
[WebKit-https.git] / PerformanceTests / Animometer / tests / resources / math.js
1 SimpleKalmanEstimator = Utilities.createClass(
2     function(processError, measurementError) {
3         this._initialized = false;
4
5         var error = .5 * (Math.sqrt(processError * processError + 4 * processError * measurementError) - processError);
6         this._gain = error / (error + measurementError);
7     }, {
8
9     sample: function(newMeasurement)
10     {
11         if (!this._initialized) {
12             this._initialized = true;
13             this._estimatedMeasurement = newMeasurement;
14             return;
15         }
16
17         this._estimatedMeasurement = this._estimatedMeasurement + this._gain * (newMeasurement - this._estimatedMeasurement);
18     },
19
20     get estimate()
21     {
22         return this._estimatedMeasurement;
23     }
24 });
25
26 PIDController = Utilities.createClass(
27     function(ysp)
28     {
29         this._ysp = ysp;
30         this._out = 0;
31
32         this._Kp = 0;
33         this._stage = PIDController.stages.WARMING;
34
35         this._eold = 0;
36         this._I = 0;
37     }, {
38
39     // Determines whether the current y is
40     //  before ysp => (below ysp if ysp > y0) || (above ysp if ysp < y0)
41     //  after ysp => (above ysp if ysp > y0) || (below ysp if ysp < y0)
42     _yPosition: function(y)
43     {
44         return (y < this._ysp) == (this._y0 < this._ysp)
45             ? PIDController.yPositions.BEFORE_SETPOINT
46             : PIDController.yPositions.AFTER_SETPOINT;
47     },
48
49     // Calculate the ultimate distance from y0 after time t. We want to move very
50     // slowly at the beginning to see how adding few items to the test can affect
51     // its output. The complexity of a single item might be big enough to keep the
52     // proportional gain very small but achieves the desired progress. But if y does
53     // not change significantly after adding few items, that means we need a much
54     // bigger gain. So we need to move over a cubic curve which increases very
55     // slowly with small t values but moves very fast with larger t values.
56     // The basic formula is: y = t^3
57     // Change the formula to reach y=1 after 1000 ms: y = (t/1000)^3
58     // Change the formula to reach y=(ysp - y0) after 1000 ms: y = (ysp - y0) * (t/1000)^3
59     _distanceUltimate: function(t)
60     {
61         return (this._ysp - this._y0) * Math.pow(t / 1000, 3);
62     },
63
64     // Calculates the distance of y relative to y0. It also ensures we do not return
65     // zero by returning a epsilon value in the same direction as ultimate distance.
66     _distance: function(y, du)
67     {
68         const epsilon = 0.0001;
69         var d  = y - this._y0;
70         return du < 0 ? Math.min(d, -epsilon) : Math.max(d, epsilon);
71     },
72
73     // Decides how much the proportional gain should be increased during the manual
74     // gain stage. We choose to use the ratio of the ultimate distance to the current
75     // distance as an indication of how much the system is responsive. We want
76     // to keep the increment under control so it does not cause the system instability
77     // So we choose to take the natural logarithm of this ratio.
78     _gainIncrement: function(t, y, e)
79     {
80         var du = this._distanceUltimate(t);
81         var d = this._distance(y, du);
82         return Math.log(du / d) * 0.1;
83     },
84
85     // Update the stage of the controller based on its current stage and the system output
86     _updateStage: function(y)
87     {
88         var yPosition = this._yPosition(y);
89
90         switch (this._stage) {
91         case PIDController.stages.WARMING:
92             if (yPosition == PIDController.yPositions.AFTER_SETPOINT)
93                 this._stage = PIDController.stages.OVERSHOOT;
94             break;
95
96         case PIDController.stages.OVERSHOOT:
97             if (yPosition == PIDController.yPositions.BEFORE_SETPOINT)
98                 this._stage = PIDController.stages.UNDERSHOOT;
99             break;
100
101         case PIDController.stages.UNDERSHOOT:
102             if (yPosition == PIDController.yPositions.AFTER_SETPOINT)
103                 this._stage = PIDController.stages.SATURATE;
104             break;
105         }
106     },
107
108     // Manual tuning is used before calculating the PID controller gains.
109     _tuneP: function(e)
110     {
111         // The output is the proportional term only.
112         return this._Kp * e;
113     },
114
115     // PID tuning function. Kp, Ti and Td were already calculated
116     _tunePID: function(h, y, e)
117     {
118         // Proportional term.
119         var P = this._Kp * e;
120
121         // Integral term is the area under the curve starting from the beginning
122         // till the current time.
123         this._I += (this._Kp / this._Ti) * ((e + this._eold) / 2) * h;
124
125         // Derivative term is the slope of the curve at the current time.
126         var D = (this._Kp * this._Td) * (e - this._eold) / h;
127
128         // The ouput is a PID function.
129        return P + this._I + D;
130     },
131
132     // Apply different strategies for the tuning based on the stage of the controller.
133     _tune: function(t, h, y, e)
134     {
135         switch (this._stage) {
136         case PIDController.stages.WARMING:
137             // This is the first stage of the Zieglerâ\80Nichols method. It increments
138             // the proportional gain till the system output passes the set-point value.
139             if (typeof this._y0 == "undefined") {
140                 // This is the first time a tuning value is required. We want the test
141                 // to add only one item. So we need to return -1 which forces us to
142                 // choose the initial value of Kp to be = -1 / e
143                 this._y0 = y;
144                 this._Kp = -1 / e;
145             } else {
146                 // Keep incrementing the Kp as long as we have not reached the
147                 // set-point yet
148                 this._Kp += this._gainIncrement(t, y, e);
149             }
150
151             return this._tuneP(e);
152
153         case PIDController.stages.OVERSHOOT:
154             // This is the second stage of the Zieglerâ\80Nichols method. It measures the
155             // oscillation period.
156             if (typeof this._t0 == "undefined") {
157                 // t is the time of the begining of the first overshot
158                 this._t0 = t;
159                 this._Kp /= 2;
160             }
161
162             return this._tuneP(e);
163
164         case PIDController.stages.UNDERSHOOT:
165             // This is the end of the Zieglerâ\80Nichols method. We need to calculate the
166             // integral and derivative periods.
167             if (typeof this._Ti == "undefined") {
168                 // t is the time of the end of the first overshot
169                 var Tu = t - this._t0;
170
171                 // Calculate the system parameters from Kp and Tu assuming
172                 // a "some overshoot" control type. See:
173                 // https://en.wikipedia.org/wiki/Ziegler%E2%80%93Nichols_method
174                 this._Ti = Tu / 2;
175                 this._Td = Tu / 3;
176                 this._Kp = 0.33 * this._Kp;
177
178                 // Calculate the tracking time.
179                 this._Tt = Math.sqrt(this._Ti * this._Td);
180             }
181
182             return this._tunePID(h, y, e);
183
184         case PIDController.stages.SATURATE:
185             return this._tunePID(h, y, e);
186         }
187
188         return 0;
189     },
190
191     // Ensures the system does not fluctuates.
192     _saturate: function(v, e)
193     {
194         var u = v;
195
196         switch (this._stage) {
197         case PIDController.stages.OVERSHOOT:
198         case PIDController.stages.UNDERSHOOT:
199             // Calculate the min-max values of the saturation actuator.
200             if (typeof this._min == "undefined")
201                 this._min = this._max = this._out;
202             else {
203                 this._min = Math.min(this._min, this._out);
204                 this._max = Math.max(this._max, this._out);
205             }
206             break;
207
208         case PIDController.stages.SATURATE:
209             const limitPercentage = 0.90;
210             var min = this._min > 0 ? Math.min(this._min, this._max * limitPercentage) : this._min;
211             var max = this._max < 0 ? Math.max(this._max, this._min * limitPercentage) : this._max;
212             var out = this._out + u;
213
214             // Clip the controller output to the min-max values
215             out = Math.max(Math.min(max, out), min);
216             u = out - this._out;
217
218             // Apply the back-calculation and tracking
219             if (u != v)
220                 u += (this._Kp * this._Tt / this._Ti) * e;
221             break;
222         }
223
224         this._out += u;
225         return u;
226     },
227
228     // Called from the benchmark to tune its test. It uses Ziegler-Nichols method
229     // to calculate the controller parameters. It then returns a PID tuning value.
230     tune: function(t, h, y)
231     {
232         this._updateStage(y);
233
234         // Current error.
235         var e = this._ysp - y;
236         var v = this._tune(t, h, y, e);
237
238         // Save e for the next call.
239         this._eold = e;
240
241         // Apply back-calculation and tracking to avoid integrator windup
242         return this._saturate(v, e);
243     }
244 });
245
246 Utilities.extendObject(PIDController, {
247     // This enum will be used to tell whether the system output (or the controller input)
248     // is moving towards the set-point or away from it.
249     yPositions: {
250         BEFORE_SETPOINT: 0,
251         AFTER_SETPOINT: 1
252     },
253
254     // The Ziegler-Nichols method for is used tuning the PID controller. The workflow of
255     // the tuning is split into four stages. The first two stages determine the values
256     // of the PID controller gains. During these two stages we return the proportional
257     // term only. The third stage is used to determine the min-max values of the
258     // saturation actuator. In the last stage back-calculation and tracking are applied
259     // to avoid integrator windup. During the last two stages, we return a PID control
260     // value.
261     stages: {
262         WARMING: 0,         // Increase the value of the Kp until the system output reaches ysp.
263         OVERSHOOT: 1,       // Measure the oscillation period and the overshoot value
264         UNDERSHOOT: 2,      // Return PID value and measure the undershoot value
265         SATURATE: 3         // Return PID value and apply back-calculation and tracking.
266     }
267 });