Esempio n. 1
0
    def test_timestamps_lin(self):
        np.random.seed(4132)
        n = 50
        drift = 17.14
        offset = 34.323
        tsa = np.cumsum(np.random.random(n) * 10)
        tsb = tsa * (1 + drift / 1e6) + offset

        # test linear drift
        _fcn, _drift = sync_timestamps(tsa, tsb)
        assert np.all(np.isclose(_fcn(tsa), tsb))
        assert np.isclose(drift, _drift)

        # test missing indices on a
        imiss = np.setxor1d(np.arange(n), [1, 2, 34, 35])
        _fcn, _drift, _ia, _ib = sync_timestamps(tsa[imiss], tsb, return_indices=True)
        assert np.all(np.isclose(_fcn(tsa[imiss[_ia]]), tsb[_ib]))

        # test missing indices on b
        _fcn, _drift, _ia, _ib = sync_timestamps(tsa, tsb[imiss], return_indices=True)
        assert np.all(np.isclose(_fcn(tsa[_ia]), tsb[imiss[_ib]]))

        # test missing indices on both
        imiss2 = np.setxor1d(np.arange(n), [14, 17])
        _fcn, _drift, _ia, _ib = sync_timestamps(tsa[imiss], tsb[imiss2], return_indices=True)
        assert np.all(np.isclose(_fcn(tsa[imiss[_ia]]), tsb[imiss2[_ib]]))
Esempio n. 2
0
def groom_pin_state(gpio,
                    audio,
                    ts,
                    tolerance=2.,
                    display=False,
                    take='first',
                    min_diff=0.):
    """
    Align the GPIO pin state to the FPGA audio TTLs.  Any audio TTLs not reflected in the pin
    state are removed from the dict and the times of the detected fronts are converted to FPGA time

    Note:
      - This function is ultra safe: we probably don't need assign all the ups and down fronts
      separately and could potentially even align the timestamps without removing the missed fronts
      - The input gpio and audio dicts may be modified by this function
      - For training sessions the frame rate is only 30Hz and the TTLs tend to be broken up by
      small gaps.  Setting the min_diff to 5ms helps the timestamp assignment accuracy.
    :param gpio: array of GPIO pin state values
    :param audio: dict of FPGA audio TTLs (see ibllib.io.extractors.ephys_fpga._get_sync_fronts)
    :param ts: camera frame times
    :param tolerance: two pulses need to be within this many seconds to be considered related
    :param take:  If 'first' the first value within tolerance is assigned; if 'nearest' the
    closest value is assigned
    :param display: If true, the resulting timestamps are plotted against the raw audio signal
    :param min_diff: Audio TTL fronts less than min_diff seconds apart will be removed
    :returns: dict of GPIO FPGA front indices, polarities and FPGA aligned times
    :returns: audio times and polarities sans the TTLs not detected in the frame data
    :returns: frame times in FPGA time
    """
    # Check that the dimensions match
    if np.any(gpio['indices'] >= ts.size):
        _logger.warning('GPIO events occurring beyond timestamps array length')
        keep = gpio['indices'] < ts.size
        gpio = {k: gpio[k][keep] for k, v in gpio.items()}
    assert audio['times'].size == audio[
        'polarities'].size, 'audio data dimension mismatch'
    # make sure that there are no 2 consecutive fall or consecutive rise events
    assert (np.all(np.abs(np.diff(audio['polarities'])) == 2)
            ), 'consecutive high/low audio events'
    # make sure first TTL is high
    assert audio['polarities'][0] == 1
    # make sure audio times in order
    assert np.all(np.diff(audio['times']) > 0)
    # make sure raw timestamps increase
    assert np.all(np.diff(ts) > 0), 'timestamps must strictly increase'
    # make sure there are state changes
    assert gpio['indices'].any(), 'no TTLs detected in GPIO'
    # # make sure first GPIO state is high
    assert gpio['polarities'][0] == 1
    """
    Some audio TTLs appear to be so short that they are not recorded by the camera.  These can
    be as short as a few microseconds.  Applying a cutoff based on framerate was unsuccessful.
    Assigning each audio TTL to each pin state change is not easy because some onsets occur very
    close together (sometimes < 70ms), on the order of the delay between TTL and frame time.
    Also, the two clocks have some degree of drift, so the delay between audio TTL and pin state
    change may be zero or even negative.

    Here we split the events into audio onsets (lo->hi) and audio offsets (hi->lo).  For each
    uptick in the GPIO pin state, we take the first audio onset time that was within 100ms of it.
    We ensure that each audio TTL is assigned only once, so a TTL that is closer to frame 3 than
    frame 1 may still be assigned to frame 1.
    """
    ifronts = gpio['indices']  # The pin state flips
    audio_times = audio['times']
    if ifronts.size != audio['times'].size:
        _logger.warning(
            'more audio TTLs than GPIO state changes, assigning timestamps')
        to_remove = np.zeros(ifronts.size,
                             dtype=bool)  # unassigned GPIO fronts to remove
        low2high = ifronts[gpio['polarities'] == 1]
        high2low = ifronts[gpio['polarities'] == -1]
        assert low2high.size >= high2low.size

        # Remove and/or fuse short TTLs
        if min_diff > 0:
            short, = np.where(np.diff(audio['times']) < min_diff)
            audio_times = np.delete(audio['times'], np.r_[short, short + 1])
            _logger.debug(f'Removed {short.size * 2} fronts TLLs less than '
                          f'{min_diff * 1e3:.0f}ms apart')

        # Onsets
        ups = ts[low2high] - ts[low2high][
            0]  # times relative to first GPIO high
        onsets = audio_times[::2] - audio_times[
            0]  # audio times relative to first onset
        # assign GPIO fronts to audio onset
        assigned = attribute_times(onsets, ups, tol=tolerance, take=take)
        unassigned = np.setdiff1d(np.arange(onsets.size),
                                  assigned[assigned > -1])
        if unassigned.size > 0:
            _logger.debug(
                f'{unassigned.size} audio TTL rises were not detected by the camera'
            )
        # Check that all pin state upticks could be attributed to an onset TTL
        missed = assigned == -1
        if np.any(missed):
            # if np.any(missed := assigned == -1):  # py3.8
            _logger.warning(f'{sum(missed)} pin state rises could '
                            f'not be attributed to an audio TTL')
            if display:
                ax = plt.subplot()
                vertical_lines(ups[assigned > -1],
                               linestyle='-',
                               color='g',
                               ax=ax,
                               label='assigned GPIO up state')
                vertical_lines(ups[missed],
                               linestyle='-',
                               color='r',
                               ax=ax,
                               label='unassigned GPIO up state')
                vertical_lines(onsets[unassigned],
                               linestyle=':',
                               color='k',
                               ax=ax,
                               alpha=0.3,
                               label='audio onset')
                vertical_lines(onsets[assigned],
                               linestyle=':',
                               color='b',
                               ax=ax,
                               label='assigned audio onset')
                plt.legend()
                plt.show()
            # Remove the missed fronts
            to_remove = np.in1d(gpio['indices'], low2high[missed])
            assigned = assigned[~missed]
        onsets_ = audio_times[::2][assigned]

        # Offsets
        downs = ts[high2low] - ts[high2low][0]
        offsets = audio_times[1::2] - audio_times[1]
        assigned = attribute_times(offsets, downs, tol=tolerance, take=take)
        unassigned = np.setdiff1d(np.arange(onsets.size),
                                  assigned[assigned > -1])
        if unassigned.size > 0:
            _logger.debug(
                f'{unassigned.size} audio TTL falls were not detected by the camera'
            )
        # Check that all pin state downticks could be attributed to an offset TTL
        missed = assigned == -1
        if np.any(missed):
            # if np.any(missed := assigned == -1):  # py3.8
            _logger.warning(f'{sum(missed)} pin state falls could '
                            f'not be attributed to an audio TTL')
            # Remove the missed fronts
            to_remove = np.logical_or(
                to_remove, np.in1d(gpio['indices'], high2low[missed]))
            assigned = assigned[~missed]
        offsets_ = audio_times[1::2][assigned]

        # Audio groomed
        if np.any(to_remove):
            # Check for any orphaned fronts (only one pin state edge was assigned)
            to_remove = np.pad(to_remove, (0, to_remove.size % 2),
                               'edge')  # Ensure even size
            # Perform xor to find GPIOs where only onset or offset is marked for removal
            orphaned = to_remove.reshape(-1, 2).sum(axis=1) == 1
            if orphaned.any():
                """If there are orphaned GPIO fronts (i.e. only one edge was assigned to an
                audio front), remove the orphaned front its assigned audio TTL. In other words
                if both edges cannot be assigned to an audio TTL, we ignore the TTL entirely.
                This is a sign that the assignment was bad and extraction may fail."""
                _logger.warning(
                    'Some onsets but not offsets (or vice versa) were not assigned; '
                    'this may be a sign of faulty wiring or clock drift')
                # Remove orphaned onsets and offsets
                orphaned_onsets, = np.where(~to_remove.reshape(-1, 2)[:, 0]
                                            & orphaned)
                orphaned_offsets, = np.where(~to_remove.reshape(-1, 2)[:, 1]
                                             & orphaned)
                onsets_ = np.delete(onsets_, orphaned_onsets)
                offsets_ = np.delete(offsets_, orphaned_offsets)
                to_remove.reshape(-1, 2)[orphaned] = True

            # Remove those unassigned GPIOs
            gpio = {k: v[~to_remove[:v.size]] for k, v in gpio.items()}
            ifronts = gpio['indices']

            # Assert that we've removed discrete TTLs
            # A failure means e.g. an up-going front of one TTL was missed
            # but not the down-going one.
            assert (np.all(np.abs(np.diff(gpio['polarities'])) == 2))
            assert gpio['polarities'][0] == 1

        audio_ = {
            'times': np.empty(ifronts.size),
            'polarities': gpio['polarities']
        }
        audio_['times'][::2] = onsets_
        audio_['times'][1::2] = offsets_
    else:
        audio_ = audio

    # Align the frame times to FPGA
    fcn_a2b, drift_ppm = dsp.sync_timestamps(ts[ifronts], audio_['times'])
    _logger.debug(f'frame audio alignment drift = {drift_ppm:.2f}ppm')
    # Add times to GPIO dict
    gpio['times'] = fcn_a2b(ts[ifronts])

    if display:
        # Plot all the onsets and offsets
        ax = plt.subplot()
        # All Audio TTLS
        squares(audio['times'],
                audio['polarities'],
                ax=ax,
                label='audio TTLs',
                linestyle=':',
                color='k',
                yrange=[0, 1],
                alpha=0.3)
        # GPIO
        x = np.insert(gpio['times'], 0, 0)
        y = np.arange(x.size) % 2
        squares(x, y, ax=ax, label='GPIO')
        y = within_ranges(np.arange(ts.size),
                          ifronts.reshape(-1, 2))  # 0 or 1 for each frame
        ax.plot(fcn_a2b(ts), y, 'kx', label='cam times')
        # Assigned audio
        squares(audio_['times'],
                audio_['polarities'],
                ax=ax,
                label='assigned audio TTL',
                linestyle=':',
                color='g',
                yrange=[0, 1])
        ax.legend()
        plt.xlabel('FPGA time (s)')
        ax.set_yticks([0, 1])
        ax.set_title('GPIO - audio TTL alignment')
        plt.show()

    return gpio, audio_, fcn_a2b(ts)