diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml new file mode 100644 index 0000000..4d7eeeb --- /dev/null +++ b/.github/workflows/checks.yaml @@ -0,0 +1,37 @@ +name: Version Check + +on: + pull_request: + branches: + - main + +jobs: + checks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + cache: 'pip' + cache-dependency-path: 'pyproject.toml' + - name: Install dependencies + run: | + pip install --editable . + pip install packaging + - name: Fetch versions + run: | + git fetch origin --tags + previous_version=$(git describe --tags --match="[0-9]*" origin/main) + latest_version=$(pip show project_extraction | awk '/^Version: / {sub("^Version: ", ""); print}') + echo "Version tag on main: $previous_version" + echo "Version tag on this branch: $latest_version" + echo "PREVIOUS_VERSION=$previous_version" >> $GITHUB_ENV + echo "LATEST_VERSION=$latest_version" >> $GITHUB_ENV + - name: Assert version + run: | + python <= version.parse("$LATEST_VERSION"))) + EOP diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000..8c53804 --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,38 @@ +name: Version Tag + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + cache: 'pip' + cache-dependency-path: 'pyproject.toml' + - name: Install dependencies + run: | + pip install --editable . + - name: Fetch version + run: | + latest_version=$(pip show project_extraction | awk '/^Version: / {sub("^Version: ", ""); print}') + echo "LATEST_VERSION=$latest_version" >> $GITHUB_ENV + - name: Create tag + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git tag -a "$LATEST_VERSION" HEAD -m "Version v$LATEST_VERSION" + - name: Push changes + uses: ad-m/github-push-action@master + with: + force: true + tags: true diff --git a/iblrig_custom_tasks/nate_optoBiasedChoiceWorld/task.py b/iblrig_custom_tasks/nate_optoBiasedChoiceWorld/task.py index b7c39c5..a448a8e 100644 --- a/iblrig_custom_tasks/nate_optoBiasedChoiceWorld/task.py +++ b/iblrig_custom_tasks/nate_optoBiasedChoiceWorld/task.py @@ -62,6 +62,11 @@ def __init__( self.task_params['OPTO_STIM_STATES'] = opto_stim_states self.task_params['PROBABILITY_OPTO_STIM'] = probability_opto_stim + # loads in the settings in order to determine the main sync and thus the pipeline extractor tasks + is_main_sync = self.hardware_settings.get('MAIN_SYNC', False) + trials_task = 'OptoTrialsBpod' if is_main_sync else 'OptoTrialsNidq' + self.extractor_tasks = ['TrialRegisterRaw', trials_task, 'TrainingStatus'] + # generates the opto stimulation for each trial self.trials_table['opto_stimulation'] = np.random.choice( [0, 1], p=[1 - probability_opto_stim, probability_opto_stim], size=NTRIALS_INIT @@ -105,7 +110,7 @@ def extra_parser(): default=DEFAULTS['OPTO_STIM_STATES'], nargs='+', type=str, - help=f'list of the state machine states where opto stim should be delivered', + help='list of the state machine states where opto stim should be delivered', ) return parser diff --git a/iblrig_custom_tasks/nate_optoBiasedChoiceWorld/test_nate_optoBiasedChoiceWorld.py b/iblrig_custom_tasks/nate_optoBiasedChoiceWorld/test_nate_optoBiasedChoiceWorld.py index 0128d9d..1f1570e 100644 --- a/iblrig_custom_tasks/nate_optoBiasedChoiceWorld/test_nate_optoBiasedChoiceWorld.py +++ b/iblrig_custom_tasks/nate_optoBiasedChoiceWorld/test_nate_optoBiasedChoiceWorld.py @@ -7,3 +7,4 @@ assert task.task_params.get('PROBABILITY_OPTO_STIM', None) is not None +assert any(t.startswith('OptoTrials') for t in task.extractor_tasks or []) diff --git a/projects/extraction_tasks.py b/projects/extraction_tasks.py index 3aa3391..cb18605 100644 --- a/projects/extraction_tasks.py +++ b/projects/extraction_tasks.py @@ -6,3 +6,4 @@ NB: This may need changing in the future if one of these modules requires optional dependencies. """ from projects.samuel_cuedBiasedChoiceWorld import CuedBiasedTrials, CuedBiasedTrialsTimeline +from projects.nate_optoBiasedChoiceWorld import OptoTrialsNidq, OptoTrialsBpod diff --git a/projects/nate_optoBiasedChoiceWorld.py b/projects/nate_optoBiasedChoiceWorld.py new file mode 100644 index 0000000..0d97e15 --- /dev/null +++ b/projects/nate_optoBiasedChoiceWorld.py @@ -0,0 +1,64 @@ +"""Bpod extractor for nate_optoBiasedChoiceWorld task. + +This is the same as biasedChoiceWorld with the addition of one dataset, `laserStimulation.intervals`; The times the +laser was on. + +The pipeline task subclasses, OptoTrialsBpod and OptoTrialsNidq, aren't strictly necessary. They simply assert that the +laserStimulation datasets were indeed saved and registered by the Bpod extractor class. +""" +import numpy as np +import ibllib.io.raw_data_loaders as raw +from ibllib.io.extractors.base import BaseBpodTrialsExtractor, run_extractor_classes +from ibllib.io.extractors.bpod_trials import BiasedTrials +from ibllib.pipes.behavior_tasks import ChoiceWorldTrialsNidq, ChoiceWorldTrialsBpod + + +class OptoTrialsBpod(ChoiceWorldTrialsBpod): + """Extract bpod only trials and laser stimulation data.""" + + @property + def signature(self): + signature = super().signature + signature['output_files'].append(('*laserStimulation.intervals.npy', self.output_collection, True)) + return signature + + +class OptoTrialsNidq(ChoiceWorldTrialsNidq): + """Extract trials and laser stimulation data aligned to NI-DAQ clock.""" + + @property + def signature(self): + signature = super().signature + signature['output_files'].append(('*laserStimulation.intervals.npy', self.output_collection, True)) + return signature + + +class TrialsOpto(BaseBpodTrialsExtractor): + var_names = BiasedTrials.var_names + ('laser_intervals',) + save_names = BiasedTrials.save_names + ('_ibl_laserStimulation.intervals.npy',) + + def _extract(self, extractor_classes=None, **kwargs) -> dict: + settings = self.settings.copy() + if 'OPTO_STIM_STATES' in settings: + # It seems older versions did not distinguish start and stop states + settings['OPTO_TTL_STATES'] = settings['OPTO_STIM_STATES'][:1] + settings['OPTO_STOP_STATES'] = settings['OPTO_STIM_STATES'][1:] + assert {'OPTO_STOP_STATES', 'OPTO_TTL_STATES', 'PROBABILITY_OPTO_STIM'} <= set(settings) + # Get all detected TTLs. These are stored for QC purposes + self.frame2ttl, self.audio = raw.load_bpod_fronts(self.session_path, data=self.bpod_trials) + # Extract common biased choice world datasets + out, _ = run_extractor_classes( + [BiasedTrials], session_path=self.session_path, bpod_trials=self.bpod_trials, + settings=settings, save=False, task_collection=self.task_collection) + + # Extract laser dataset + laser_intervals = [] + for trial in filter(lambda t: t['opto_stimulation'], self.bpod_trials): + states = trial['behavior_data']['States timestamps'] + # Assumes one of these states per trial: takes the timestamp of the first matching state + start = next((v[0][0] for k, v in states.items() if k in settings['OPTO_TTL_STATES']), np.nan) + stop = next((v[0][0] for k, v in states.items() if k in settings['OPTO_STOP_STATES']), np.nan) + laser_intervals.append((start, stop)) + out['laser_intervals'] = np.array(laser_intervals, dtype=np.float64) + + return {k: out[k] for k in self.var_names} # Ensures all datasets present and ordered diff --git a/projects/task_extractor_map.json b/projects/task_extractor_map.json index 7559c28..0724258 100644 --- a/projects/task_extractor_map.json +++ b/projects/task_extractor_map.json @@ -1,5 +1,5 @@ { "samuel_cuedBiasedChoiceWorld": "BiasedTrials", - "nate_optoBiasedChoiceWorld": "EphysTrials", "_iblrig_tasks_neuromodulatorChoiceWorld": "projects.neuromodulators.TrialsTableNeuromodulator" + "nate_optoBiasedChoiceWorld": "projects.nate_optoBiasedChoiceWorld.TrialsOpto" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 3509381..7122515 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "project_extraction" -version = "0.2.0" +version = "0.2.1.post0" description = "Custom extractors for satellite tasks" dynamic = [ "readme" ] keywords = [ "IBL", "neuro-science" ]