1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package org.chromium.latency.walt; 18 19 import android.content.Context; 20 import android.content.SharedPreferences; 21 22 import androidx.preference.PreferenceManager; 23 import androidx.annotation.StringRes; 24 25 import com.github.mikephil.charting.data.Entry; 26 27 import java.util.ArrayList; 28 import java.util.Collections; 29 import java.util.List; 30 31 /** 32 * Kitchen sink for small utility functions 33 */ 34 public class Utils { median(ArrayList<Double> arrList)35 public static double median(ArrayList<Double> arrList) { 36 ArrayList<Double> lst = new ArrayList<>(arrList); 37 Collections.sort(lst); 38 int len = lst.size(); 39 if (len == 0) { 40 return Double.NaN; 41 } 42 43 if (len % 2 == 1) { 44 return lst.get(len / 2); 45 } else { 46 return 0.5 * (lst.get(len / 2) + lst.get(len / 2 - 1)); 47 } 48 } 49 mean(double[] x)50 public static double mean(double[] x) { 51 double s = 0; 52 for (double v: x) s += v; 53 return s / x.length; 54 } 55 56 /** 57 * Linear interpolation styled after numpy.interp() 58 * returns values at points x interpolated using xp, yp data points 59 * Both x and xp must be monotonically increasing. 60 */ interp(double[] x, double[] xp, double[] yp)61 public static double[] interp(double[] x, double[] xp, double[] yp) { 62 // assuming that x and xp are already sorted. 63 // go over x and xp as if we are merging them 64 double[] y = new double[x.length]; 65 int i = 0; 66 int ip = 0; 67 68 // skip x points that are outside the data 69 while (i < x.length && x[i] < xp[0]) i++; 70 71 while (ip < xp.length && i < x.length) { 72 // skip until we see an xp >= current x 73 while (ip < xp.length && xp[ip] < x[i]) ip++; 74 if (ip >= xp.length) break; 75 if (xp[ip] == x[i]) { 76 y[i] = yp[ip]; 77 } else { 78 double dy = yp[ip] - yp[ip-1]; 79 double dx = xp[ip] - xp[ip-1]; 80 y[i] = yp[ip-1] + dy/dx * (x[i] - xp[ip-1]); 81 } 82 i++; 83 } 84 return y; 85 } 86 stdev(double[] a)87 public static double stdev(double[] a) { 88 double m = mean(a); 89 double sumsq = 0; 90 for (double v : a) sumsq += (v-m)*(v-m); 91 return Math.sqrt(sumsq / a.length); 92 } 93 94 /** 95 * Similar to numpy.extract() 96 * returns a shorter array with values taken from x at indices where indicator == value 97 */ extract(int[] indicator, int value, double[] arr)98 public static double[] extract(int[] indicator, int value, double[] arr) { 99 if (arr.length != indicator.length) { 100 throw new IllegalArgumentException("Length of arr and indicator must be the same."); 101 } 102 int newLen = 0; 103 for (int v: indicator) if (v == value) newLen++; 104 double[] newx = new double[newLen]; 105 106 int j = 0; 107 for (int i=0; i<arr.length; i++) { 108 if (indicator[i] == value) { 109 newx[j] = arr[i]; 110 j++; 111 } 112 } 113 return newx; 114 } 115 array2string(double[] a, String format)116 public static String array2string(double[] a, String format) { 117 StringBuilder sb = new StringBuilder(); 118 sb.append("array(["); 119 for (double x: a) { 120 sb.append(String.format(format, x)); 121 sb.append(", "); 122 } 123 sb.append("])"); 124 return sb.toString(); 125 } 126 argmax(double[] a)127 public static int argmax(double[] a) { 128 int imax = 0; 129 for (int i=1; i<a.length; i++) if (a[i] > a[imax]) imax = i; 130 return imax; 131 } 132 argmin(double[] a)133 public static int argmin(double[] a) { 134 int imin = 0; 135 for (int i=1; i<a.length; i++) if (a[i] < a[imin]) imin = i; 136 return imin; 137 } 138 getShiftError(double[] laserT, double[] touchT, double[] touchY, double shift)139 private static double getShiftError(double[] laserT, double[] touchT, double[] touchY, double shift) { 140 double[] T = new double[laserT.length]; 141 for (int j=0; j<T.length; j++) { 142 T[j] = laserT[j] + shift; 143 } 144 double [] laserY = Utils.interp(T, touchT, touchY); 145 // TODO: Think about throwing away a percentile of most distanced points for noise reduction 146 return Utils.stdev(laserY); 147 } 148 149 /** 150 * Simplified Java re-implementation or py/qslog/minimization.py. 151 * This is very specific to the drag latency algorithm. 152 * 153 * tl;dr: Shift laser events by some time delta and see how well they fit on a horizontal line. 154 * Delta that results in the best looking straight line is the latency. 155 */ findBestShift(double[] laserT, double[] touchT, double[] touchY)156 public static double findBestShift(double[] laserT, double[] touchT, double[] touchY) { 157 int steps = 1500; 158 double[] shiftSteps = new double[]{0.1, 0.01}; // milliseconds 159 double[] stddevs = new double[steps]; 160 double bestShift = shiftSteps[0]*steps/2; 161 for (final double shiftStep : shiftSteps) { 162 for (int i = 0; i < steps; i++) { 163 stddevs[i] = getShiftError(laserT, touchT, touchY, bestShift + shiftStep * i - shiftStep * steps / 2); 164 } 165 bestShift = argmin(stddevs) * shiftStep + bestShift - shiftStep * steps / 2; 166 } 167 return bestShift; 168 } 169 char2byte(char c)170 static byte[] char2byte(char c) { 171 return new byte[]{(byte) c}; 172 } 173 getIntPreference(Context context, @StringRes int keyId, int defaultValue)174 static int getIntPreference(Context context, @StringRes int keyId, int defaultValue) { 175 SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); 176 return preferences.getInt(context.getString(keyId), defaultValue); 177 } 178 getBooleanPreference(Context context, @StringRes int keyId, boolean defaultValue)179 static boolean getBooleanPreference(Context context, @StringRes int keyId, boolean defaultValue) { 180 SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); 181 return preferences.getBoolean(context.getString(keyId), defaultValue); 182 } 183 getStringPreference(Context context, @StringRes int keyId, String defaultValue)184 static String getStringPreference(Context context, @StringRes int keyId, String defaultValue) { 185 SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); 186 return preferences.getString(context.getString(keyId), defaultValue); 187 } 188 min(List<Entry> entries)189 static float min(List<Entry> entries) { 190 float min = Float.MAX_VALUE; 191 for (Entry e : entries) { 192 min = Math.min(min, e.getY()); 193 } 194 return min; 195 } 196 max(List<Entry> entries)197 static float max(List<Entry> entries) { 198 float max = Float.MIN_VALUE; 199 for (Entry e : entries) { 200 max = Math.max(max, e.getY()); 201 } 202 return max; 203 } 204 mean(List<Entry> entries)205 static float mean(List<Entry> entries) { 206 float mean = 0; 207 for (Entry e : entries) { 208 mean += e.getY()/entries.size(); 209 } 210 return mean; 211 } 212 213 public enum ListenerState { 214 RUNNING, 215 STARTING, 216 STOPPED, 217 STOPPING 218 } 219 220 } 221