P50 Paired-click

Design:

  • trial: two auditory clicks, 500 ms SOA

  • several seconds between trials, random ITI

[1]:
import os
import sys
from pathlib import Path
import re
import numpy as np
import pandas as pd
import mkpy
import spudtr
from spudtr import epf

from matplotlib import pyplot as plt
from mkpy import mkh5

# path wrangling for nbsphinx
if "MDE_HOME" in os.environ.keys():
    MDE_HOME = Path(os.environ["MDE_HOME"])
else:
    from conf import MDE_HOME
DOCS_DATA = MDE_HOME / "docs/_data"

print(os.environ["CONDA_DEFAULT_ENV"])
print(sys.version)
for pkg in [np, pd, mkpy, spudtr]:
    print(pkg.__name__, pkg.__version__, pkg.__file__)
mkconda_dev_py39_053022
3.9.13 | packaged by conda-forge | (main, May 27 2022, 16:56:21)
[GCC 10.3.0]
numpy 1.21.6 /home/turbach/miniconda39/envs/mkconda_dev_py39_053022/lib/python3.9/site-packages/numpy/__init__.py
pandas 1.1.5 /home/turbach/miniconda39/envs/mkconda_dev_py39_053022/lib/python3.9/site-packages/pandas/__init__.py
mkpy 0.2.7 /mnt/cube/home/turbach/TPU_Projects/mkpy/mkpy/__init__.py
spudtr 0.1.0 /home/turbach/miniconda39/envs/mkconda_dev_py39_053022/lib/python3.9/site-packages/spudtr/__init__.py
[2]:
# set recorded ERPSS data filenames
crw = MDE_HOME / "mkdig/sub000p5.crw"    # EEG recording
log = MDE_HOME / "mkdig/sub000p5.x.log"  # events
yhdr = MDE_HOME / "mkpy/sub000p5.yhdr"   # extra header info

# set calibration data filenames
cals_crw = MDE_HOME / "mkdig/sub000c5.crw"
cals_log = MDE_HOME / "mkdig/sub000c5.x.log"
cals_yhdr = MDE_HOME / "mkpy/sub000c5.yhdr"

# HDF5 file with EEG recording, events, and header
p50_h5_f = DOCS_DATA / "sub000p50.h5"

mkh5 EEG data, event code log, header information

[3]:
p50_h5 = mkh5.mkh5(p50_h5_f)  # make ready

p50_h5.reset_all()  # wipe previous
p50_h5.create_mkdata("sub000", crw, log, yhdr) # convert the data to HDF5

# add calibration data to this subject
p50_h5.append_mkdata("sub000", cals_crw, cals_log, cals_yhdr)

# set calibration parameters and calibrate
pts, pulse, lo, hi, ccode = 5, 10, -40, 40, 0
p50_h5.calibrate_mkdata(
    "sub000",  # data group to calibrate with these cal pulses
    n_points=pts,  # pts to average
    cal_size=pulse,  # uV
    lo_cursor=lo,  # lo_cursor ms
    hi_cursor=hi,  # hi_cursor ms
    cal_ccode=ccode,  # condition code
)
/mnt/cube/home/turbach/TPU_Projects/mkpy/mkpy/mkh5.py:3666: UserWarning: negative event code(s) found for cal condition code 0 -16384
  warnings.warn(msg)
Found cals in /sub000/dblock_2
Calibrating block /sub000/dblock_0 of 3: (219904,)
Calibrating block /sub000/dblock_1 of 3: (225280,)
Calibrating block /sub000/dblock_2 of 3: (56064,)

codemap

For this simple design the codemap a few lines of hand-typed YAML.

The tag column labels are for illustration, bin is used because it can be, not because it must.

The calibration events are included for comparison

[4]:
%%bash
head -14 $MDE_HOME/mkpy/p50_codemap.ytbl
# mkpy YAML codemap
#
# columns and rows must align, regexp is a mandatory column, all else is whatever

---
name: p5

columns:
    [ regexp,  ccode,  instrument,   bin,      click,    type          ]
rows:
  - ['(#\d+)',     0,         cal,     0,        cal,    cal           ]
  - ['(#1)',       1,         eeg,     1,    click_1,    conditioning  ]
  - ['(#2)',       1,         eeg,     2,    click_2,    test          ]

1. get_event_table(codemap)

Scan the event codes for pattern matches and collect the HDF5 index, matching code, and tags

Event table modifications are not needed for this simple design.

[5]:
p50_event_table = p50_h5.get_event_table(MDE_HOME / "mkpy/p50_codemap.ytbl")
searching codes in: sub000/dblock_0
searching codes in: sub000/dblock_1
searching codes in: sub000/dblock_2
/mnt/cube/home/turbach/TPU_Projects/mkpy/mkpy/mkh5.py:1059: UserWarning:
As of mkpy 0.2.0 to match events with a codemap regexp pattern, the
ccode column in p50_codemap.ytbl must also match the log_ccode
in the datablock. If this behavior is not desired, delete or rename
the ccode column in the codemap.
  warnings.warn(msg)

inspect the event table

Check that the codemap scan returned all and only the correct events and tags.

[6]:
print("event table shape:", p50_event_table.shape)
print("event table columns:", p50_event_table.columns.to_list())

# select some columns to show
example_columns = [
    "dblock_path", "dblock_ticks", "log_evcodes", "log_ccodes", "log_flags",
    "regexp", "match_code", "instrument", "bin", "click", "type"
]

# first few stimulus events
display(p50_event_table[example_columns].head())

# last few calibration pulse events
display(p50_event_table[example_columns].tail())

# tally and check event counts
pd.crosstab(
    index=[
        p50_event_table.ccode,
        p50_event_table.instrument,
        p50_event_table.bin,
        p50_event_table.click,
    ],
    columns=[
        p50_event_table.log_flags,
    ],
    margins=True
)
event table shape: (448, 29)
event table columns: ['data_group', 'dblock_path', 'dblock_tick_idx', 'dblock_ticks', 'crw_ticks', 'raw_evcodes', 'log_evcodes', 'log_ccodes', 'log_flags', 'epoch_match_tick_delta', 'epoch_ticks', 'dblock_srate', 'match_group', 'idx', 'dlim', 'anchor_str', 'match_str', 'anchor_code', 'match_code', 'anchor_tick', 'match_tick', 'anchor_tick_delta', 'is_anchor', 'regexp', 'ccode', 'instrument', 'bin', 'click', 'type']
dblock_path dblock_ticks log_evcodes log_ccodes log_flags regexp match_code instrument bin click type
120 sub000/dblock_0 606 1 1 0 (#1) 1 eeg 1 click_1 conditioning
121 sub000/dblock_0 3792 1 1 0 (#1) 1 eeg 1 click_1 conditioning
122 sub000/dblock_0 7714 1 1 0 (#1) 1 eeg 1 click_1 conditioning
123 sub000/dblock_0 10771 1 1 0 (#1) 1 eeg 1 click_1 conditioning
124 sub000/dblock_0 13980 1 1 0 (#1) 1 eeg 1 click_1 conditioning
dblock_path dblock_ticks log_evcodes log_ccodes log_flags regexp match_code instrument bin click type
683 sub000/dblock_2 54179 4 0 0 (#\d+) 4 cal 0 cal cal
684 sub000/dblock_2 54437 2 0 0 (#\d+) 2 cal 0 cal cal
685 sub000/dblock_2 54695 3 0 0 (#\d+) 3 cal 0 cal cal
686 sub000/dblock_2 54954 4 0 0 (#\d+) 4 cal 0 cal cal
687 sub000/dblock_2 55212 2 0 0 (#\d+) 2 cal 0 cal cal
[6]:
log_flags 0 32 48 64 All
ccode instrument bin click
0 cal 0 cal 207 0 0 1 208
1 eeg 1 click_1 117 3 0 0 120
2 click_2 114 5 1 0 120
All 438 8 1 1 448

2. set_epochs(name, pre, post)

Stash the the event table events and tags with a name and epoch interval boundaries.

[7]:
p50_h5.set_epochs("ms1500", p50_event_table, -750, 750)
Sanitizing event table data types for mkh5 epochs table ...
/mnt/cube/home/turbach/TPU_Projects/mkpy/mkpy/mkh5.py:1515: UserWarning: data error: pre-stimulus interval is out of bounds left ... skipping epoch (240, b'sub000', b'sub000/dblock_2', 0, 360, 360, 1, 1, 0, 64, -375, 750, 500., 1, 0, 1, b'1', b'1', 1, 1, 360, 360, 0, True, b'(#\\d+)', 0, b'cal', 0, b'cal', b'cal', 0, 0, 0, 360, -375, 750)
  warnings.warn(

3. export_epochs(name, file_format=…)

Export the time-stamped, tagged, fixed-length segments of EEG data for analysis as HDF5, pandas HDF5 or feather

[8]:
# this exports the epochs DATA ... indexed, timstamped, EEG, events, and tags.
p50_epochs_f = DOCS_DATA / "sub000p50.ms1500.epochs.feather"
p50_h5.export_epochs("ms1500", p50_epochs_f, file_format="feather")

analyze epochs

[9]:
p50_epochs = pd.read_feather(p50_epochs_f)

# sanitize epoch_id for pandas index
p50_epochs['epoch_id'] = p50_epochs['epoch_id'].astype('int')

# look up pattern-matched events with timestamp 0
p50_events = p50_epochs.query("match_time==0")
p50_counts = pd.crosstab(
    [p50_events.instrument, p50_events.ccode, p50_events.bin, p50_events.click, p50_events.type],
    [p50_events.log_flags],
    margins=True
)

# lookup scalp potential channels ... EEG, EOG, A2
p50_chans = [col for col in p50_epochs.columns if re.match(r"^[MLRAHlr]\w{1,3}$", col)]
p50_chandesc = ""
for i, c in enumerate(p50_chans):
    sep = " " if i % 8 < 7 else "\n"
    p50_chandesc += f"{i:2d}:{c:4s}{sep}"

print(f"channels: {len(p50_chans)}\nchdesc:{p50_chandesc}")
display(p50_counts)
channels: 32
chdesc: 0:lle   1:lhz   2:MiPf  3:LLPf  4:RLPf  5:LMPf  6:RMPf  7:LDFr
 8:RDFr  9:LLFr 10:RLFr 11:LMFr 12:RMFr 13:LMCe 14:RMCe 15:MiCe
16:MiPa 17:LDCe 18:RDCe 19:LDPa 20:RDPa 21:LMOc 22:RMOc 23:LLTe
24:RLTe 25:LLOc 26:RLOc 27:MiOc 28:A2   29:HEOG 30:rle  31:rhz

log_flags 0 32 48 All
instrument ccode bin click type
cal 0 0 cal cal 207 0 0 207
eeg 1 1 click_1 conditioning 117 3 0 120
2 click_2 test 114 5 1 120
All 438 8 1 447
[10]:
# for illustration ... cal pulses and first click
midline = ["MiPf", "MiCe", "MiPa", "MiOc"]
bins = p50_epochs.bin.unique()

midline_epochs = p50_epochs[
    ["epoch_id", "click", "match_time"] + midline
].query("click in ['cal', 'click_1']")

# center each channel
midline_epochs = epf.center_eeg(
    midline_epochs,
    midline,
    -100, 0,
    epoch_id="epoch_id",
    time="match_time"
)
[11]:
# look up single trial click_2 event timestamp relative to click_1 onset (match_time)
click2_onsets = p50_epochs.query(
    "ccode > 0 and match_code == 1 and log_evcodes == 2"
).match_time

click2_onset = int(np.round(click2_onsets.mean(), 0))

display(click2_onsets.value_counts())
display(click2_onsets.describe())
display(click2_onset)

502    101
506      7
504      7
500      5
Name: match_time, dtype: int64
count    120.000000
mean     502.266667
std        1.128197
min      500.000000
25%      502.000000
50%      502.000000
75%      502.000000
max      506.000000
Name: match_time, dtype: float64
502

time-domain average paired-click ERPs

The 10 microvolt calibration square is for illustration.

[12]:
midline_erps = midline_epochs.groupby(["click", "match_time"]).mean().reset_index()
midline_erps
[12]:
click match_time epoch_id MiPf MiCe MiPa MiOc
0 cal -750 344.0 0.031777 0.030263 -0.008570 -0.021974
1 cal -748 344.0 0.026993 0.027894 -0.053359 -0.093315
2 cal -746 344.0 0.034188 0.004735 -0.031715 -0.074623
3 cal -744 344.0 0.036570 0.013279 -0.016684 -0.045383
4 cal -742 344.0 0.027017 0.016949 0.012113 -0.041882
... ... ... ... ... ... ... ...
1495 click_1 740 89.5 -3.296837 -4.691502 -1.160323 0.269139
1496 click_1 742 89.5 -3.073172 -4.794128 -1.390727 0.152291
1497 click_1 744 89.5 -2.999067 -5.094552 -1.737775 -0.031446
1498 click_1 746 89.5 -3.031741 -5.235142 -1.946156 -0.063351
1499 click_1 748 89.5 -2.809215 -5.238110 -1.969507 -0.031548

1500 rows × 7 columns

[13]:
f, axs = plt.subplots(4, 1, figsize=(8,12), sharey=True)
colors = {"cal": "lightgray", "click_1": "red", "click_2": "green"}
for tag, erp in midline_erps.groupby('click'):
    for axi, chan in enumerate(midline):

        # mark onsets
        axs[axi].axvline(0, color='gray')
        axs[axi].axvline(click2_onset, color='gray')

        axs[axi].plot(
            erp["match_time"],
            erp[chan],
            color=colors[tag],
            label=tag,
        )

        # channel
        axs[axi].set_title(chan, loc='left')


axs[0].legend()
axs[0].set_title("Paired-click P50 all trials")
f.tight_layout()
_images/p50_21_0.png