Skip to content

Commit

Permalink
ENH: Allow triggering after onset (#419)
Browse files Browse the repository at this point in the history
* ENH: Allow triggering after onset

* FIX: Naming

* FIX: Get info

* FIX: Try

* FIX: Bad Pyglet version
  • Loading branch information
larsoner authored Apr 27, 2021
1 parent d971681 commit 2aa219a
Show file tree
Hide file tree
Showing 9 changed files with 67 additions and 33 deletions.
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jobs:
- run: pip install --quiet --upgrade --user pip
- run: pip install --quiet --upgrade --user numpy scipy matplotlib sphinx pillow pandas h5py mne pyglet psutil sphinx_bootstrap_theme sphinx_fontawesome numpydoc https://api.github.com/repos/sphinx-gallery/sphinx-gallery/zipball/master
- run: python -c "import mne; mne.sys_info()"
- run: python -c "import pyglet; print(pyglet.version)"
- run: python setup.py develop --user
- run: cd doc && make html

Expand Down
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ before_install:
pip install pyglet;
fi;
fi;
- python -c "import pyglet; print(pyglet.version)"
# Import matplotlib ahead of time so it doesn't affect test timings
# (e.g., building fc-cache)
- python -c "import matplotlib.pyplot as plt"
Expand Down
3 changes: 2 additions & 1 deletion appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ platform:
install:
- "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
- "python --version"
- "pip install -q numpy scipy matplotlib coverage setuptools h5py pandas pytest pytest-cov pytest-timeout pytest-xdist codecov pyglet mne tdtpy joblib numpydoc pillow"
- "pip install -q numpy scipy matplotlib coverage setuptools h5py pandas pytest pytest-cov pytest-timeout pytest-xdist codecov \"pyglet!=1.5.16\" mne tdtpy joblib numpydoc pillow"
- "python -c \"import mne; mne.sys_info()\""
- "python -c \"import pyglet; print(pyglet.version)\""
# Get a virtual sound card / VBAudioVACWDM device
- "git clone --depth 1 git://github.com/LABSN/sound-ci-helpers.git"
- "powershell sound-ci-helpers/windows/setup_sound.ps1"
Expand Down
3 changes: 2 additions & 1 deletion azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ jobs:
addToPath: true
- powershell: |
pip install --upgrade numpy scipy vtk
pip install -q numpy scipy matplotlib coverage setuptools h5py pandas pytest pytest-cov pytest-timeout pytest-xdist codecov pyglet pyglet-ffmpeg mne tdtpy joblib numpydoc pillow
pip install -q numpy scipy matplotlib coverage setuptools h5py pandas pytest pytest-cov pytest-timeout pytest-xdist codecov "pyglet!=1.5.16" pyglet-ffmpeg mne tdtpy joblib numpydoc pillow
python -c "import mne; mne.sys_info()"
python -c "import matplotlib.pyplot as plt"
python -c "import pyglet; print(pyglet.version)"
displayName: 'Install pip dependencies'
- powershell: |
git clone --depth 1 git://github.com/LABSN/sound-ci-helpers.git
Expand Down
45 changes: 26 additions & 19 deletions expyfun/_experiment_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ class ExperimentController(object):
If ``None``, the type will be read from the system configuration file.
If a string, must be 'dummy', 'parallel', 'sound_card', or 'tdt'.
By default the mode is 'dummy', since setting up the parallel port
can be a pain. Can also be a dict with entries 'type' ('parallel'),
and 'address' (None).
can be a pain. Can also be a dict with entries 'TYPE' ('parallel'),
and 'TRIGGER_ADDRESS' (None).
session : str | None
If ``None``, a GUI will be used to acquire this information.
check_rms : str | None
Expand Down Expand Up @@ -420,15 +420,22 @@ def __init__(self, exp_name, audio_controller=None, response_device=None,
if trigger_controller is None:
trigger_controller = get_config('TRIGGER_CONTROLLER', 'dummy')
if isinstance(trigger_controller, string_types):
trigger_controller = dict(type=trigger_controller)
logger.info('Expyfun: Initializing {} triggering mode'
''.format(trigger_controller['type']))
if trigger_controller['type'] == 'tdt':
trigger_controller = dict(TYPE=trigger_controller)
assert isinstance(trigger_controller, dict)
trigger_controller = trigger_controller.copy()
known_keys = ('TYPE',)
if set(trigger_controller) != set(known_keys):
raise ValueError(
'Unknown keys for trigger_controller, must be '
f'{known_keys}, got {set(trigger_controller)}')
logger.info(f'Expyfun: Initializing {trigger_controller["TYPE"]} '
'triggering mode')
if trigger_controller['TYPE'] == 'tdt':
if not isinstance(self._ac, TDTController):
raise ValueError('trigger_controller can only be "tdt" if '
'tdt is used for audio')
self._tc = self._ac
elif trigger_controller['type'] == 'sound_card':
elif trigger_controller['TYPE'] == 'sound_card':
if not isinstance(self._ac, SoundCardController):
raise ValueError('trigger_controller can only be '
'"sound_card" if the sound card is '
Expand All @@ -438,23 +445,21 @@ def __init__(self, exp_name, audio_controller=None, response_device=None,
'when SOUND_CARD_TRIGGER_CHANNELS is '
'zero')
self._tc = self._ac
elif trigger_controller['type'] in ['parallel', 'dummy']:
if 'address' not in trigger_controller:
addr = get_config('TRIGGER_ADDRESS')
trigger_controller['address'] = addr
elif trigger_controller['TYPE'] in ['parallel', 'dummy']:
addr = trigger_controller.get(
'TRIGGER_ADDRESS', get_config('TRIGGER_ADDRESS', None))
self._tc = ParallelTrigger(
trigger_controller['type'],
trigger_controller.get('address'),
trigger_controller['TYPE'], addr,
trigger_duration, ec=self)
self._extra_cleanup_fun.insert(0, self._tc.close)
# The TDT always stamps "1" on stimulus onset. Here we need
# to manually mimic that behavior.
self._ofp_critical_funs.insert(
0, lambda: self._stamp_ttl_triggers([1], False))
0, lambda: self._stamp_ttl_triggers([1], False, False))
else:
raise ValueError('trigger_controller type must be '
'"parallel", "dummy", "sound_card", or "tdt",'
'got {0}'.format(trigger_controller['type']))
'got {0}'.format(trigger_controller['TYPE']))
self._id_call_dict['ttl_id'] = self._stamp_binary_id

# other basic components
Expand Down Expand Up @@ -2072,7 +2077,7 @@ def _stamp_binary_id(self, id_, wait_for_last=True):
if not np.all(np.in1d(id_, [0, 1])):
raise ValueError('All values of id must be 0 or 1')
id_ = (id_.astype(int) + 1) << 2 # 0, 1 -> 4, 8
self._stamp_ttl_triggers(id_, wait_for_last)
self._stamp_ttl_triggers(id_, wait_for_last, True)

def stamp_triggers(self, ids, check='binary', wait_for_last=True):
"""Stamp binary values
Expand All @@ -2087,6 +2092,7 @@ def stamp_triggers(self, ids, check='binary', wait_for_last=True):
1 and 15.
wait_for_last : bool
If True, wait for last trigger to be stamped before returning.
If False, don't wait at all (if possible).
Notes
-----
Expand All @@ -2107,11 +2113,12 @@ def stamp_triggers(self, ids, check='binary', wait_for_last=True):
if not all(id_ in _vals for id_ in ids):
raise ValueError('with check="binary", ids must all be '
'1, 2, 4, or 8: {0}'.format(ids))
self._stamp_ttl_triggers(ids, wait_for_last)
self._stamp_ttl_triggers(ids, wait_for_last, False)

def _stamp_ttl_triggers(self, ids, wait_for_last):
def _stamp_ttl_triggers(self, ids, wait_for_last, is_trial_id):
logger.exp('Stamping TTL triggers: %s', ids)
self._tc.stamp_triggers(ids, wait_for_last=wait_for_last)
self._tc.stamp_triggers(
ids, wait_for_last=wait_for_last, is_trial_id=is_trial_id)
self.flush()

def flush(self):
Expand Down
28 changes: 20 additions & 8 deletions expyfun/_sound_controllers/_sound_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
'SOUND_CARD_NAME', 'SOUND_CARD_FS', 'SOUND_CARD_FIXED_DELAY',
'SOUND_CARD_TRIGGER_CHANNELS', 'SOUND_CARD_API_OPTIONS',
'SOUND_CARD_TRIGGER_SCALE', 'SOUND_CARD_TRIGGER_INSERTION',
'SOUND_CARD_TRIGGER_ID_AFTER_ONSET',
)


Expand Down Expand Up @@ -76,6 +77,8 @@ class SoundCardController(object):
by 8). The default value (``1. / (2 ** 32 - 1)``) is meant to
be appropriate for bit-perfect mapping to the 24 bit output of
a SPDIF channel.
- 'SOUND_CARD_TRIGGER_ID_AFTER_ONSET': bool
If True, TTL IDs will be stored and stamped after the 1 trigger.
Note that the defaults are superseded on individual machines by
the configuration file.
Expand All @@ -89,13 +92,17 @@ def __init__(self, params, stim_fs, n_channels=2, trigger_duration=0.01,
SOUND_CARD_TRIGGER_CHANNELS=0,
SOUND_CARD_TRIGGER_SCALE=1. / float(2 ** 31 - 1),
SOUND_CARD_TRIGGER_INSERTION='prepend',
SOUND_CARD_TRIGGER_ID_AFTER_ONSET=False,
) # any omitted become None
params = _check_params(params, _SOUND_CARD_KEYS, defaults, 'params')

self.backend, self.backend_name = _import_backend(
params['SOUND_CARD_BACKEND'])
self._n_channels_stim = int(params['SOUND_CARD_TRIGGER_CHANNELS'])
trig_scale = float(params['SOUND_CARD_TRIGGER_SCALE'])
self._id_after_onset = (
str(params['SOUND_CARD_TRIGGER_ID_AFTER_ONSET']).lower() == 'true')
self._extra_onset_triggers = list()
assert self._n_channels_stim >= 0
self._n_channels = int(operator.index(n_channels))
del n_channels
Expand Down Expand Up @@ -198,7 +205,7 @@ def load_buffer(self, samples):
self.audio.delete()
self.audio = None
if self._n_channels_stim > 0:
stim = self._make_digital_trigger([1])
stim = self._make_digital_trigger([1] + self._extra_onset_triggers)
extra = len(samples) - len(stim)
if extra > 0: # stim shorter than samples (typical)
stim = np.pad(stim, ((0, extra), (0, 0)), 'constant')
Expand Down Expand Up @@ -250,7 +257,8 @@ def _make_digital_trigger(self, trigs, delay=None):
offset += n_each
return stim

def stamp_triggers(self, triggers, delay=None, wait_for_last=True):
def stamp_triggers(self, triggers, delay=None, wait_for_last=True,
is_trial_id=False):
"""Stamp a list of triggers with a given inter-trigger delay.
Parameters
Expand All @@ -263,7 +271,13 @@ def stamp_triggers(self, triggers, delay=None, wait_for_last=True):
If None, will use twice the trigger duration (50% duty cycle).
wait_for_last : bool
If True, wait for last trigger to be stamped before returning.
is_trial_id : bool
If True and SOUND_CARD_TRIGGER_ID_AFTER_ONSET, the triggers will
be stashed and appended to the 1 trigger for the sound onset.
"""
if is_trial_id and self._id_after_onset:
self._extra_onset_triggers = list(triggers)
return
if delay is None:
delay = 2 * self._trigger_duration
stim = self._make_digital_trigger(triggers, delay)
Expand All @@ -274,12 +288,10 @@ def stamp_triggers(self, triggers, delay=None, wait_for_last=True):
t_each = self._trigger_duration + delay
duration = len(triggers) * t_each
extra_delay = 0.1
if not wait_for_last:
delta = (delay - self._trigger_duration)
duration -= delta
extra_delay += delta
self.ec.wait_secs(duration)
# Impose an extra delay on the "stop" action
if wait_for_last:
self.ec.wait_secs(duration)
else:
extra_delay += duration
stim.stop(wait=False, extra_delay=extra_delay)

def play(self):
Expand Down
5 changes: 4 additions & 1 deletion expyfun/_tdt_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,8 @@ def _set_delay(self, delay, delay_trig):
logger.info('Expyfun: Setting TDT trigger delay to %s' % delay_trig)

# ############################### TRIGGER METHODS #############################
def stamp_triggers(self, triggers, delay=None, wait_for_last=True):
def stamp_triggers(self, triggers, delay=None, wait_for_last=True,
is_trial_id=False):
"""Stamp a list of triggers with a given inter-trigger delay.
Parameters
Expand All @@ -301,6 +302,8 @@ def stamp_triggers(self, triggers, delay=None, wait_for_last=True):
If None, will use twice the trigger duration (50% duty cycle).
wait_for_last : bool
If True, wait for last trigger to be stamped before returning.
is_trial_id : bool
No effect for this controller.
"""
if delay is None:
delay = 0.02 # we have a fixed trig duration of 0.01
Expand Down
5 changes: 4 additions & 1 deletion expyfun/_trigger_controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ def _stamp_trigger(self, trig):
self.ec.wait_secs(self.trigger_duration)
self._set_data(0)

def stamp_triggers(self, triggers, delay=None, wait_for_last=True):
def stamp_triggers(self, triggers, delay=None, wait_for_last=True,
is_trial_id=False):
"""Stamp a list of triggers with a given inter-trigger delay.
Parameters
Expand All @@ -119,6 +120,8 @@ def stamp_triggers(self, triggers, delay=None, wait_for_last=True):
If None, will use twice the trigger duration (50% duty cycle).
wait_for_last : bool
If True, wait for last trigger to be stamped before returning.
is_trial_id : bool
No effect for this trigger controller.
"""
if delay is None:
delay = 2 * self.trigger_duration
Expand Down
9 changes: 7 additions & 2 deletions expyfun/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import os.path as op
import inspect
import sys
import time
import tempfile
import traceback
import ssl
Expand Down Expand Up @@ -588,6 +589,7 @@ def get_config_path():
'SOUND_CARD_TRIGGER_CHANNELS',
'SOUND_CARD_TRIGGER_INSERTION',
'SOUND_CARD_TRIGGER_SCALE',
'SOUND_CARD_TRIGGER_ID_AFTER_ONSET',
'TDT_CIRCUIT_PATH',
'TDT_DELAY',
'TDT_INTERFACE',
Expand Down Expand Up @@ -765,10 +767,13 @@ def _wait_secs(secs, ec=None):
while (clock() - t0) < secs:
ec._dispatch_events()
ec.check_force_quit()
time.sleep(0.0001)
else:
wins = _get_display().get_windows()
for win in wins:
win.dispatch_events()
while (clock() - t0) < secs:
for win in wins:
win.dispatch_events()
time.sleep(0.0001)


def running_rms(signal, win_length):
Expand Down

0 comments on commit 2aa219a

Please sign in to comment.