Skip to content

Commit 40af19e

Browse files
committed
Fix the two piecewise-linear regression calculation
#66 The current implementation of the regression calculation has these flaws: 1. When processing (x[0], y[0]), L1 must be any line through (x0, y0) which meets L2 at a point (x’, y’) where x[0] < x' < x[1]. L1 has no error. 2. When processing (x[n - 2], y[n - 2]), L2 must be any line through (x[n - 1], y[n - 1]) which meets L1 at a point (x’, y’) where x[n - 2] < x' < x[n - 1]. L2 has no error. 3. The lambda calculation is incorrect. It includes a term called H which is equal to C - I. Looking at the algorithm of Kundu/Ubhaya, this should be just C. 4. lambda should to be used with calculating L1 and (1 - lambda) should to be used with calculating L2. Currently (1 - lambda) is used in calculating L1 and L2. 5. The current calculation has this condition if (t1 != t2) continue; This condition is almost always true even if t1 and t2 are essentiallyEqual.
1 parent f1c7edb commit 40af19e

File tree

1 file changed

+187
-147
lines changed

1 file changed

+187
-147
lines changed

MotionMark/resources/statistics.js

Lines changed: 187 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2015-2017 Apple Inc. All rights reserved.
2+
* Copyright (C) 2015-2024 Apple Inc. All rights reserved.
33
*
44
* Redistribution and use in source and binary forms, with or without
55
* modification, are permitted provided that the following conditions
@@ -181,39 +181,39 @@ Regression = Utilities.createClass(
181181
// All samples are analyzed. startIndex, endIndex are just stored for use by the caller.
182182
function(samples, startIndex, endIndex, options)
183183
{
184-
const desiredFrameLength = options.desiredFrameLength;
185-
var profile;
184+
this.startIndex = Math.min(startIndex, endIndex);
185+
this.endIndex = Math.max(startIndex, endIndex);
186186

187-
if (!options.preferredProfile || options.preferredProfile == Strings.json.profiles.slope) {
188-
profile = this._calculateRegression(samples, {
189-
shouldClip: true,
190-
s1: desiredFrameLength,
191-
t1: 0
192-
});
193-
this.profile = Strings.json.profiles.slope;
194-
} else if (options.preferredProfile == Strings.json.profiles.flat) {
195-
profile = this._calculateRegression(samples, {
196-
shouldClip: true,
197-
s1: desiredFrameLength,
187+
this.s1 = 0;
188+
this.t1 = 0;
189+
this.n1 = 0;
190+
this.e1 = Number.MAX_VALUE;
191+
192+
this.s2 = 0;
193+
this.t2 = 0;
194+
this.n2 = 0;
195+
this.e2 = Number.MAX_VALUE;
196+
197+
this.complexity = 0;
198+
199+
if (options.preferredProfile == Strings.json.profiles.flat) {
200+
this._calculateRegression(samples, {
201+
s1: options.desiredFrameLength,
198202
t1: 0,
199203
t2: 0
200204
});
201205
this.profile = Strings.json.profiles.flat;
206+
} else {
207+
this._calculateRegression(samples, {
208+
s1: options.desiredFrameLength,
209+
t1: 0
210+
});
211+
this.profile = Strings.json.profiles.slope;
202212
}
203213

204-
this.startIndex = Math.min(startIndex, endIndex);
205-
this.endIndex = Math.max(startIndex, endIndex);
206-
207-
this.complexity = profile.complexity;
208-
this.s1 = profile.s1;
209-
this.t1 = profile.t1;
210-
this.s2 = profile.s2;
211-
this.t2 = profile.t2;
212-
this.stdev1 = profile.stdev1;
213-
this.stdev2 = profile.stdev2;
214-
this.n1 = profile.n1;
215-
this.n2 = profile.n2;
216-
this.error = profile.error;
214+
this.stdev1 = Math.sqrt(this.e1 / this.n1);
215+
this.stdev2 = Math.sqrt(this.e2 / this.n2);
216+
this.error = this._error();
217217
}, {
218218

219219
valueAt: function(complexity)
@@ -223,6 +223,60 @@ Regression = Utilities.createClass(
223223
return this.s1 + this.t1 * complexity;
224224
},
225225

226+
_intersection: function(segment1, segment2)
227+
{
228+
return (segment1.s - segment2.s) / (segment2.t - segment1.t);
229+
},
230+
231+
_error: function() {
232+
return this.e1 + this.e2;
233+
},
234+
235+
_areEssentiallyEqual: function(n1, n2) {
236+
// Choose epsilon not too small to ensure the intersetion
237+
// of the two segements is not too far from sampled data.
238+
const epsilon = 0.0001;
239+
return Math.abs(n1 - n2) < epsilon;
240+
},
241+
242+
_setOptimal: function(segment1, segment2, x, xn, options) {
243+
if (segment1.e + segment2.e > this.e1 + this.e2)
244+
return false;
245+
246+
segment1.s = options.s1 !== undefined ? options.s1 : segment1.s;
247+
segment1.t = options.t1 !== undefined ? options.t1 : segment1.t;
248+
segment2.s = options.s2 !== undefined ? options.s2 : segment2.s;
249+
segment2.t = options.t2 !== undefined ? options.t2 : segment2.t;
250+
251+
// The score is the x coordinate of the intersection of segment1 and segment2.
252+
let complexity = this._intersection(segment1, segment2);
253+
254+
if (!this._areEssentiallyEqual(segment1.t, segment2.t)) {
255+
// If segment1 and segment2 are not parallel, then they have to meet
256+
// at complexity such that x <= complexity <= xn.
257+
if (!(complexity >= x && complexity <= xn))
258+
return false;
259+
} else {
260+
// If segment1 and segment2 are parallel, then they have to form one
261+
// single line.
262+
if (!this._areEssentiallyEqual(segment1.s, segment2.s))
263+
return false;
264+
}
265+
266+
this.s1 = segment1.s;
267+
this.t1 = segment1.t;
268+
this.n1 = segment1.n;
269+
this.e1 = segment1.e;
270+
271+
this.s2 = segment2.s;
272+
this.t2 = segment2.t;
273+
this.n2 = segment2.n;
274+
this.e2 = segment2.e;
275+
276+
this.complexity = complexity;
277+
return true;
278+
},
279+
226280
// A generic two-segment piecewise regression calculator. Based on Kundu/Ubhaya
227281
//
228282
// Minimize sum of (y - y')^2
@@ -239,153 +293,139 @@ Regression = Utilities.createClass(
239293
const complexityIndex = 0;
240294
const frameLengthIndex = 1;
241295

242-
if (samples.length == 1) {
243-
// Only one sample point; we can't calculate any regression.
244-
var x = samples[0][complexityIndex];
245-
return {
246-
complexity: x,
247-
s1: x,
248-
t1: 0,
249-
s2: x,
250-
t2: 0,
251-
error1: 0,
252-
error2: 0
253-
};
254-
}
255-
256296
// Sort by increasing complexity.
257-
var sortedSamples = samples.slice().sort((a, b) => a[complexityIndex] - b[complexityIndex]);
258-
259-
// x is expected to increase in complexity
260-
var lowComplexity = sortedSamples[0][complexityIndex];
261-
var highComplexity = sortedSamples[samples.length - 1][complexityIndex];
262-
263-
var a1 = 0, b1 = 0, c1 = 0, d1 = 0, h1 = 0, k1 = 0;
264-
var a2 = 0, b2 = 0, c2 = 0, d2 = 0, h2 = 0, k2 = 0;
265-
266-
for (var i = 0; i < sortedSamples.length; ++i) {
267-
var x = sortedSamples[i][complexityIndex];
268-
var y = sortedSamples[i][frameLengthIndex];
297+
let sortedSamples = samples.slice().sort((a, b) => a[complexityIndex] - b[complexityIndex]);
298+
299+
let a1 = 0, b1 = 0, c1 = 0, d1 = 0, h1 = 0, k1 = 0;
300+
let a2 = 0, b2 = 0, c2 = 0, d2 = 0, h2 = 0, k2 = 0;
301+
302+
for (let j = 0; j < sortedSamples.length; ++j) {
303+
let x = sortedSamples[j][complexityIndex];
304+
let y = sortedSamples[j][frameLengthIndex];
305+
269306
a2 += 1;
270307
b2 += x;
271308
c2 += x * x;
272309
d2 += y;
273-
h2 += y * x;
310+
h2 += x * y;
274311
k2 += y * y;
275312
}
276313

277-
var s1_best, t1_best, s2_best, t2_best, n1_best, n2_best, error1_best, error2_best, x_best, x_prime;
278-
279-
function setBest(s1, t1, error1, s2, t2, error2, splitIndex, x_prime, x)
280-
{
281-
s1_best = s1;
282-
t1_best = t1;
283-
error1_best = error1;
284-
s2_best = s2;
285-
t2_best = t2;
286-
error2_best = error2;
287-
// Number of samples included in the first segment, inclusive of splitIndex
288-
n1_best = splitIndex + 1;
289-
// Number of samples included in the second segment
290-
n2_best = samples.length - splitIndex - 1;
291-
if (!options.shouldClip || (x_prime >= lowComplexity && x_prime <= highComplexity))
292-
x_best = x_prime;
293-
else {
294-
// Discontinuous piecewise regression
295-
x_best = x;
296-
}
297-
}
314+
for (let j = 0; j < sortedSamples.length - 1; ++j) {
315+
let x = sortedSamples[j][complexityIndex];
316+
let y = sortedSamples[j][frameLengthIndex];
317+
let xx = x * x;
318+
let xy = x * y;
319+
let yy = y * y;
298320

299-
// Iterate from 0 to n - 2, inclusive
300-
for (var i = 0; i < sortedSamples.length - 1; ++i) {
301-
var x = sortedSamples[i][complexityIndex];
302-
var y = sortedSamples[i][frameLengthIndex];
303-
var xx = x * x;
304-
var yx = y * x;
305-
var yy = y * y;
306-
// a1, b1, etc. is sum from 0 to i, inclusive
307321
a1 += 1;
308322
b1 += x;
309323
c1 += xx;
310324
d1 += y;
311-
h1 += yx;
325+
h1 += xy;
312326
k1 += yy;
313-
// a2, b2, etc. is sum from i+1 to sortedSamples.length - 1, inclusive
327+
314328
a2 -= 1;
315329
b2 -= x;
316330
c2 -= xx;
317331
d2 -= y;
318-
h2 -= yx;
332+
h2 -= xy;
319333
k2 -= yy;
320334

321-
var A = c1*d1 - b1*h1;
322-
var B = a1*h1 - b1*d1;
323-
var C = a1*c1 - b1*b1;
324-
var D = c2*d2 - b2*h2;
325-
var E = a2*h2 - b2*d2;
326-
var F = a2*c2 - b2*b2;
327-
var s1 = options.s1 !== undefined ? options.s1 : (A / C);
328-
var t1 = options.t1 !== undefined ? options.t1 : (B / C);
329-
var s2 = options.s2 !== undefined ? options.s2 : (D / F);
330-
var t2 = options.t2 !== undefined ? options.t2 : (E / F);
331-
// Assumes that the two segments meet
332-
var x_prime = (s1 - s2) / (t2 - t1);
333-
334-
var error1 = (k1 + a1*s1*s1 + c1*t1*t1 - 2*d1*s1 - 2*h1*t1 + 2*b1*s1*t1) || Number.MAX_VALUE;
335-
var error2 = (k2 + a2*s2*s2 + c2*t2*t2 - 2*d2*s2 - 2*h2*t2 + 2*b2*s2*t2) || Number.MAX_VALUE;
336-
337-
if (i == 0) {
338-
setBest(s1, t1, error1, s2, t2, error2, i, x_prime, x);
339-
continue;
340-
}
335+
let A = (c1 * d1) - (b1 * h1);
336+
let B = (a1 * h1) - (b1 * d1);
337+
let C = (a1 * c1) - (b1 * b1);
338+
let D = (c2 * d2) - (b2 * h2);
339+
let E = (a2 * h2) - (b2 * d2);
340+
let F = (a2 * c2) - (b2 * b2);
341+
342+
let s1 = A / C;
343+
let t1 = B / C;
344+
let s2 = D / F;
345+
let t2 = E / F;
341346

342347
if (C == 0 || F == 0)
343348
continue;
344349

345-
// Projected point is not between this and the next sample
346-
var nextSampleComplexity = sortedSamples[i + 1][complexityIndex];
347-
if (x_prime > nextSampleComplexity || x_prime < x) {
348-
// Calculate lambda, which divides the weight of this sample between the two lines
349-
350-
// These values remove the influence of this sample
351-
var I = c1 - 2*b1*x + a1*xx;
352-
var H = C - I;
353-
var G = A + B*x - C*y;
354-
355-
var J = D + E*x - F*y;
356-
var K = c2 - 2*b2*x + a2*xx;
357-
358-
var lambda = (G*F + G*K - H*J) / (I*J + G*K);
359-
if (lambda > 0 && lambda < 1) {
360-
var lambda1 = 1 - lambda;
361-
s1 = options.s1 !== undefined ? options.s1 : ((A - lambda1*(-h1*x + d1*xx + c1*y - b1*yx)) / (C - lambda1*I));
362-
t1 = options.t1 !== undefined ? options.t1 : ((B - lambda1*(h1 - d1*x - b1*y + a1*yx)) / (C - lambda1*I));
363-
s2 = options.s2 !== undefined ? options.s2 : ((D + lambda1*(-h2*x + d2*xx + c2*y - b2*yx)) / (F + lambda1*K));
364-
t2 = options.t2 !== undefined ? options.t2 : ((E + lambda1*(h2 - d2*x - b2*y + a2*yx)) / (F + lambda1*K));
365-
x_prime = (s1 - s2) / (t2 - t1);
366-
367-
error1 = ((k1 + a1*s1*s1 + c1*t1*t1 - 2*d1*s1 - 2*h1*t1 + 2*b1*s1*t1) - lambda1 * Math.pow(y - (s1 + t1*x), 2)) || Number.MAX_VALUE;
368-
error2 = ((k2 + a2*s2*s2 + c2*t2*t2 - 2*d2*s2 - 2*h2*t2 + 2*b2*s2*t2) + lambda1 * Math.pow(y - (s2 + t2*x), 2)) || Number.MAX_VALUE;
369-
} else if (t1 != t2)
370-
continue;
350+
let segment1;
351+
let segment2;
352+
let xp = (j == 0) ? 0 : sortedSamples[j - 1][complexityIndex];
353+
354+
if (j == 0) {
355+
// Let segment1 be any line through (x[0], y[0]) which meets segment2 at
356+
// a point (x’, y’) where x[0] < x' < x[1]. segment1 has no error.
357+
let xMid = (x + sortedSamples[j + 1][complexityIndex]) / 2;
358+
let yMid = s2 + t2 * xMid;
359+
let tMid = (yMid - y) / (xMid - x);
360+
segment1 = {
361+
s: y - tMid * x,
362+
t: tMid,
363+
n: 1,
364+
e: 0
365+
};
366+
} else {
367+
segment1 = {
368+
s: s1,
369+
t: t1,
370+
n: j + 1,
371+
e: k1 + (a1 * s1 * s1) + (c1 * t1 * t1) - (2 * d1 * s1) - (2 * h1 * t1) + (2 * b1 * s1 * t1)
372+
};
371373
}
372374

373-
if (error1 + error2 < error1_best + error2_best)
374-
setBest(s1, t1, error1, s2, t2, error2, i, x_prime, x);
375-
}
375+
if (j == sortedSamples.length - 2) {
376+
// Let segment2 be any line through (x[n - 1], y[n - 1]) which meets segment1
377+
// at a point (x’, y’) where x[n - 2] < x' < x[n - 1]. segment2 has no error.
378+
let xMid = (x + sortedSamples[j + 1][complexityIndex]) / 2;
379+
let yMid = s1 + t1 * xMid;
380+
let tMid = (yMid - sortedSamples[j + 1][frameLengthIndex]) / (xMid - sortedSamples[j + 1][complexityIndex]);
381+
segment2 = {
382+
s: y - tMid * x,
383+
t: tMid,
384+
n: 1,
385+
e: 0
386+
};
387+
} else {
388+
segment2 = {
389+
s: s2,
390+
t: t2,
391+
n: sortedSamples.length - (j + 1),
392+
e: k2 + (a2 * s2 * s2) + (c2 * t2 * t2) - (2 * d2 * s2) - (2 * h2 * t2) + (2 * b2 * s2 * t2)
393+
};
394+
}
376395

377-
return {
378-
complexity: x_best,
379-
s1: s1_best,
380-
t1: t1_best,
381-
stdev1: Math.sqrt(error1_best / n1_best),
382-
s2: s2_best,
383-
t2: t2_best,
384-
stdev2: Math.sqrt(error2_best / n2_best),
385-
error: error1_best + error2_best,
386-
n1: n1_best,
387-
n2: n2_best
388-
};
396+
if (this._setOptimal(segment1, segment2, x, sortedSamples[j + 1][complexityIndex], options))
397+
continue
398+
399+
// These values remove the influence of this sample
400+
let G = A + B * x - C * y;
401+
let J = D + E * x - F * y;
402+
403+
let I = c1 - 2 * b1 * x + a1 * xx;
404+
let K = c2 - 2 * b2 * x + a2 * xx;
405+
406+
// Calculate lambda, which divides the weight of this sample between the two lines
407+
let lambda = (G * F + G * K - J * C) / (I * J + G * K);
408+
if (!(lambda > 0 && lambda < 1))
409+
continue;
410+
411+
let lambda1 = 1 - lambda;
412+
413+
segment1 = {
414+
s: (A + lambda * (-h1 * x + d1 * xx + c1 * y - b1 * xy)) / (C - lambda * I),
415+
t: (B + lambda * (h1 - d1 * x - b1 * y + a1 * xy)) / (C - lambda * I),
416+
n: j + 1,
417+
e: (k1 + a1 * s1 * s1 + c1 * t1 * t1 - 2 * d1 * s1 - 2 * h1 * t1 + 2 * b1 * s1 * t1) - lambda * Math.pow(y - (s1 + t1 * x), 2)
418+
};
419+
420+
segment2 = {
421+
s: (D + lambda1 * (-h2 * x + d2 * xx + c2 * y - b2 * xy)) / (F + lambda1 * K),
422+
t: (E + lambda1 * (h2 - d2 * x - b2 * y + a2 * xy)) / (F + lambda1 * K),
423+
n: sortedSamples.length - (j + 1),
424+
e: (k2 + a2 * s2 * s2 + c2 * t2 * t2 - 2 * d2 * s2 - 2 * h2 * t2 + 2 * b2 * s2 * t2) + lambda1 * Math.pow(y - (s2 + t2 * x), 2)
425+
};
426+
427+
this._setOptimal(segment1, segment2, x, sortedSamples[j + 1][complexityIndex], options);
428+
}
389429
}
390430
});
391431

0 commit comments

Comments
 (0)