aboutsummaryrefslogtreecommitdiff
path: root/pywalt/pywalt/minimization.py
blob: dc5c0be8f86f2ec879c7254e19b8074bee39e580 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
#!/usr/bin/env python
#
# Copyright 2016 The Chromium OS Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

"""
Module for computing drag latency given logs of touchpad positions and
QuickStep laser crossing timestamps
"""

import numpy
import evparser

debug_mode = False


def load_laser_data(fname_laser):
    laser_data = numpy.loadtxt(fname_laser)
    t = laser_data[:, 0]
    transition = laser_data[:, 1].astype(int)
    if transition[0] != 0:
        print('WARNING: First laser transition should be from light to dark')
    return t, transition


def calc_ssr(x, y):
    """Return sum of squared residuals (SSR) of a linear least square fit"""
    p = numpy.polyfit(x, y, 1, full=True)
    r = p[1][0]
    return r


def minimize_lsq(tx, x, ty, y, tl, min_shift, max_shift, step):
    """Find best time shift so that the shifted laser crossing events fit nicely
    on a straight line. Upper and lower side are treated separately.

    """

    # generate an array of all shifts to try
    shifts = numpy.arange(min_shift, max_shift, step)

    # side = [0, 1, 1, 0, 0, 1, 1 ...
    # this is an indicator of which side of the beam the crossing belongs to
    side = ((numpy.arange(len(tl)) + 1) / 2) % 2

    residuals0 = []
    residuals1 = []
    for shift in shifts:
        # Find the locations of the finger at the shifted laser timestamps
        yl = numpy.interp(tl + shift, ty, y)
        xl = numpy.interp(tl + shift, tx, x)
        # Fit a line to each side separately and save the SSR for this fit
        residuals0.append(calc_ssr(xl[side == 0], yl[side == 0]))
        residuals1.append(calc_ssr(xl[side == 1], yl[side == 1]))

    # Find the shift with lower SSR for each side
    best_shift0 = shifts[numpy.argmin(residuals0)]
    best_shift1 = shifts[numpy.argmin(residuals1)]

    # Use average of the two sides
    best_shift = (best_shift0 + best_shift1) / 2
    return best_shift


def minimize(fname_evtest, fname_laser):

    # Load all the data
    tl, transition = load_laser_data(fname_laser)
    (tx, x, ty, y) = evparser.load_xy(fname_evtest)

    # Shift time so that first time point is 0
    t0 = min(tx[0], ty[0])
    tx = tx - t0
    ty = ty - t0
    tl = tl - t0

    # Sanity checks
    if numpy.std(x)*2 < numpy.std(y):
        print('WARNING: Not enough motion in X axis')

    # Search for minimum with coarse step of 1 ms in range of 0 to 200 ms
    coarse_step = 1e-3  # Seconds
    best_shift_coarse = minimize_lsq(tx, x, ty, y, tl, 0, 0.2, coarse_step)
    # Run another search with 0.02 ms step within +-3 ms of the previous result
    lmts = numpy.array([-1, 1]) * 3 * coarse_step + best_shift_coarse
    fine_step = 2e-5  # seconds
    best_shift_fine = minimize_lsq(tx, x, ty, y, tl, lmts[0], lmts[1], fine_step)

    print("Drag latency (min method) = %.2f ms" % (best_shift_fine*1000))
    if debug_mode:
        debug_plot(tx, x, ty, y, tl, best_shift_fine)

    return best_shift_fine


def debug_plot(tx, x, ty, y, tl, shift):
    """Plot the XY data with time-shifted laser events

    Note: this is a utility function used for offline debugging. It needs
    matplotlib which is not installed on CrOS images.

    """
    import matplotlib.pyplot as plt
    xx = numpy.interp(ty, tx, x)
    plt.plot(xx, y, '.b')

    yl = numpy.interp(tl + shift, ty, y)
    xl = numpy.interp(tl + shift, tx, x)
    sides = (((numpy.arange(len(tl)) + 1) / 2) % 2)
    colors = ['g', 'm']
    x_linear = numpy.array([min(x), max(x)])
    for side in [0, 1]:
        xls = xl[sides == side]
        yls = yl[sides == side]
        plt.plot(xls, yls, 'o' + colors[side])
        a, c = numpy.polyfit(xls, yls, 1)
        plt.plot(x_linear, a * x_linear + c, colors[side])
    plt.xlabel('X')
    plt.ylabel('Y')
    plt.title('Laser events shifted %.2f ms' % (shift*1000))
    plt.show()

# Debug & test
if __name__ == '__main__':

    fname = '/tmp/WALT_2016_06_22__1739_21_'
    fname_evtest = fname + 'evtest.log'
    fname_laser = fname + 'laser.log'

    minimize(fname_evtest, fname_laser)