OSCR

Cortical Contributions to Attentional Orienting and Response Cancellation in Action Stopping.

Code ↔ Paper

The paper beside its authors' code: matches between them have not been computed for this paper yet.

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Jupyter notebook · 577 lines · 21 KB · no license

  1. # %%
  2. ############################
  3. ## import modules
  4. ############################
  5. import numpy as np, os, glob, pandas as pd, warnings
  6. import scipy as sp, seaborn as sns, matplotlib.pyplot as plt
  7. import statsmodels.api as sm, statsmodels.formula.api as smf
  8. from scipy.stats import iqr
  9. from sklearn import preprocessing
  10. # %%
  11. # set directories
  12. workdir = 'F:/projects/okapi/fnirs/fnirs data/homer3/2024-averaging/'
  13. savedir = 'C:/Users/sarah/OneDrive - University of Tasmania/projects/okapi/data/'
  14. ## define some variables for the response window
  15. f=7.8125 # sampling frequency
  16. ETL=9.5 # trial length
  17. time = np.arange(0, ETL + 1/f, 1/f)
  18. # this for some reason is 1 longer than matlab so let's remove the last number
  19. # (first 75 values line up)
  20. new_time = time[:-1]
  21. # find segments of the column that's within the time window
  22. time_window = (new_time >= 2) & (new_time <= 7)
  23. print(time_window)
  24. print(time_window.astype(int))
  25. # so this is the time window in which we will look for peaks
  26. # %%
  27. ##
  28. ## create foldies
  29. ##
  30. # Want some nice subfolders to save this data into to keep it organised
  31. # If these folders don't exist, this module creates them
  32. # NB the next module assumes they exist with this structure - if you don't have your data organised this way, the saving at the end
  33. # will not work and that module will need to be modified (they're in subfolders because I was playing around with different ways
  34. # to do outlier removal). I do think this is a nice clean folder structure and lets you keep the raw + normalised data without risking
  35. # confusion.
  36. for subject in os.listdir(savedir):
  37. if 'sub-0' in subject:
  38. iqr_folder = 'iqr3'
  39. behav = 'behav'
  40. emg = 'emg'
  41. path = os.path.join(savedir, subject, iqr_folder)
  42. path1 = os.path.join(savedir, subject, iqr_folder, behav)
  43. path2 = os.path.join(savedir, subject, iqr_folder, emg)
  44. if not os.path.exists(path):
  45. os.makedirs(path)
  46. os.makedirs(path1)
  47. os.makedirs(path2)
  48. # %% [markdown]
  49. # ## Behavioural data crunching
  50. # %%
  51. # for each participant and region, we need to go through and the files that are relevant
  52. # then need to find the IQR and exclude, and probably let's save all the images
  53. # this is the BEHAVIOURAL DATA, ie this does not access the dataframes that have
  54. # information about partial burst splits
  55. # go through each participant and exclude the IQR per region
  56. regions = ['LIFG', 'RIFG', 'preSMA']
  57. for subject in os.listdir(workdir):
  58. if 'sub-0' in subject: # if it's an actual subject
  59. print('Processing', subject, '...')
  60. for region in regions:
  61. # load the files
  62. alltrials = pd.read_csv(workdir + subject + '/derivatives/homer/' + region + '_ALL.csv')
  63. alltrials = alltrials.dropna(axis=1) # get rid of nans
  64. fs = pd.read_csv(workdir + subject + '/derivatives/homer/' + region + '_FS.csv')
  65. ss = pd.read_csv(workdir + subject + '/derivatives/homer/' + region + '_SS.csv')
  66. ig = pd.read_csv(workdir + subject + '/derivatives/homer/' + region + '_I.csv')
  67. go = pd.read_csv(workdir + subject + '/derivatives/homer/' + region + '_GO.csv')
  68. stops = pd.concat([ss, fs], axis=1)
  69. stops = stops.dropna(axis=1)
  70. ig = ig.dropna(axis=1)
  71. go = go.dropna(axis=1)
  72. dfs = [stops, go, ig]
  73. df_names = ['stops', 'go', 'ig']
  74. # dictionary to store filtered dfs
  75. filtered_dfs = {}
  76. for i, df in enumerate(dfs):
  77. # Find the IQR - find the highest absolute value in each column and IQR from there
  78. highest_abs = [df[column].abs().max() for column in df.columns]
  79. iqr_value = iqr(highest_abs)
  80. # Filter columns
  81. filtered_columns = [column for column in df.columns if not (df[column].abs() > 3 * iqr_value).any()]
  82. filtered_dfs[df_names[i]] = df[filtered_columns]
  83. # print(f"{df_names[i].capitalize()}:")
  84. # print(f"Original number of columns: {len(df.columns)}")
  85. # print(f"Filtered number of columns: {len(filtered_dfs[df_names[i]].columns)}\n")
  86. stops_filtered = filtered_dfs['stops']
  87. go_filtered = filtered_dfs['go']
  88. ig_filtered = filtered_dfs['ig']
  89. # separate stops out again
  90. fs_columns = [col for col in stops_filtered.columns if "FS" in col]
  91. fs_filtered = stops[fs_columns]
  92. ss_columns = [col for col in stops_filtered.columns if "SS" in col]
  93. ss_filtered = stops[ss_columns]
  94. ## NORMALISE
  95. # Now each trial is going to get normalised by that person's average SD
  96. # First finds the SD of each trial, then finds the average SD, then divides all values by that avg SD
  97. individual_sds = alltrials.apply(np.std)
  98. avg_sd = np.mean(individual_sds)
  99. alltrials_normalised = alltrials.div(avg_sd)
  100. ## GET TRIALS
  101. # now we've normalised all the data, we want to retain only the columns that weren't outliers
  102. # Really tediously the headings aren't named consistently so let's rename them.
  103. fs_filtered.columns = fs_filtered.columns.str.replace('FS', 'ALL')
  104. ss_filtered.columns = ss_filtered.columns.str.replace('SS', 'ALL')
  105. ig_filtered.columns = ig_filtered.columns.str.replace('_I', '_ALL')
  106. go_filtered.columns = go_filtered.columns.str.replace('GO', 'ALL')
  107. #extract which headings are which trial type
  108. fs_headings = fs_filtered.columns
  109. ss_headings = ss_filtered.columns
  110. ig_headings = ig_filtered.columns
  111. go_headings = ig_filtered.columns
  112. # now grab the trials we want from the big normalised dataframe
  113. fs_norm = alltrials_normalised[[col for col in fs_headings if col in alltrials_normalised.columns]]
  114. ss_norm = alltrials_normalised[[col for col in ss_headings if col in alltrials_normalised.columns]]
  115. ig_norm = alltrials_normalised[[col for col in ig_headings if col in alltrials_normalised.columns]]
  116. go_norm = alltrials_normalised[[col for col in go_headings if col in alltrials_normalised.columns]]
  117. fs_norm.to_csv(savedir + subject + '/iqr3/behav/' + region + '_FS_norm.csv', index=False)
  118. ss_norm.to_csv(savedir + subject + '/iqr3/behav/' + region + '_SS_norm.csv', index=False)
  119. ig_norm.to_csv(savedir + subject + '/iqr3/behav/' + region + '_IG_norm.csv', index=False)
  120. go_norm.to_csv(savedir + subject + '/iqr3/behav/' + region + '_GO_norm.csv', index=False)
  121. print('finito')
  122. # %%
  123. ## EXTRACT PEAK VALUES >>>
  124. # for each trial, we want to get the value of the local maximum
  125. ## now let's find all the peaks and save them to a df
  126. warnings.filterwarnings("ignore")
  127. behavpeaks = pd.DataFrame(columns=['subject', 'region', 'peak_value'])
  128. timeseries = pd.DataFrame()
  129. for subject in os.listdir(savedir):
  130. number = 1
  131. if 'sub-0' in subject:
  132. print('Getting peaks for ', subject)
  133. # look through the subject's folder and find any CSV files
  134. csvs = glob.glob(savedir + subject +'/iqr3/behav/*norm.csv')
  135. for csv in csvs:
  136. # find the filename
  137. filename = os.path.basename(csv)
  138. filename = os.path.splitext(filename)[0]
  139. # load the file
  140. df = pd.read_csv(str(csv))
  141. # now look through the file
  142. for column in df.columns:
  143. if df[column].isnull().all(): # check it's not a null
  144. continue
  145. elif (df[column] == 0).all():
  146. continue
  147. # if the column has stuff in it, find the peak :)
  148. else:
  149. # find relevant segment in the time window
  150. relevant_segment = df.loc[time_window, column].values
  151. # find the peak of this
  152. peak_location = sp.signal.find_peaks(relevant_segment)
  153. # need to add an if statement checking that PL[0] is not empty
  154. # then find the value of the peak
  155. if not peak_location[0].any():
  156. peak_value = np.nan
  157. else:
  158. # this can either grab the largest of multiple points, or it can grab the first peak
  159. # I've got it getting the first peak but you might want to change it
  160. # just comment out the right bit
  161. first_peak = peak_location[0][0]
  162. peak_value = relevant_segment[first_peak]
  163. # highest_peak_index = np.argmax(relevant_segment[peak_location[0]])
  164. # highest_peak = peak_location[0][highest_peak_index]
  165. # peak_value = relevant_segment[highest_peak]
  166. # and grab that column for graphing later by appending time series
  167. timeseries[f'{subject}_{filename}_{number}'] = df[column]
  168. number = number + 1
  169. # save peak_value and add this new row to allpeaks
  170. new_row = pd.DataFrame({
  171. 'subject': [subject],
  172. 'region': [filename],
  173. 'peak_value': [peak_value]})
  174. behavpeaks = pd.concat([behavpeaks, new_row], ignore_index=True)
  175. #behavpeaks.to_csv(savedir + 'fnirs_behav.csv', index=False)
  176. # let's make the file called behavpeaks a bit prettier so we can analyse it
  177. def findTrialType (s):
  178. if 'SS' in s:
  179. return 'Successful stop'
  180. elif 'FS' in s:
  181. return 'Failed stop'
  182. elif 'IG' in s:
  183. return 'Ignore'
  184. elif 'GO' in s:
  185. return 'Go'
  186. else:
  187. return 'error'
  188. behavpeaks['trial'] = behavpeaks['region'].apply(findTrialType)
  189. behavpeaks['roi'] = behavpeaks['region'].apply(lambda x: x.split('_')[0]) # get the name before the first _
  190. behavpeaks.to_csv(savedir + 'fnirs_behav_norm.csv', index=False)
  191. print('done')
  192. # %% [markdown]
  193. # # graph data
  194. # %%
  195. fs = pd.read_csv(savedir + 'sub-009/behav/LIFG_FS_raw.csv')
  196. ss = pd.read_csv(savedir + 'sub-009/behav/LIFG_SS_raw.csv')
  197. ig = pd.read_csv(savedir + 'sub-009/behav/LIFG_IG_raw.csv')
  198. go = pd.read_csv(savedir + 'sub-009/behav/LIFG_GO_raw.csv')
  199. data_list = [(fs, 'Failed stops'),
  200. (ss, 'Successful stops'),
  201. (ig, 'Ignore trials'),
  202. (go, 'Go')]
  203. # Loop through each dataset and create a new figure for each plot
  204. for data, label in data_list:
  205. plt.figure()
  206. #plt.plot(data, linewidth=0.7)
  207. data.iloc[:, :8].plot()
  208. plt.title(label) # Optionally, add a title to each figure
  209. plt.show()
  210. # %%
  211. fs = pd.read_csv(savedir + 'sub-022/LIFG_FS_norm.csv')
  212. ss = pd.read_csv(savedir + 'sub-022/LIFG_SS_norm.csv')
  213. ig = pd.read_csv(savedir + 'sub-022/LIFG_IG_norm.csv')
  214. go = pd.read_csv(savedir + 'sub-022/LIFG_GO_norm.csv')
  215. data_list = [(fs, 'Failed stops'),
  216. (ss, 'Successful stops'),
  217. (ig, 'Ignore trials'),
  218. (go, 'Go')]
  219. # Loop through each dataset and create a new figure for each plot
  220. for data, label in data_list:
  221. plt.figure()
  222. plt.plot(data, linewidth=0.7)
  223. plt.title(label) # Optionally, add a title to each figure
  224. plt.show()
  225. # %% [markdown]
  226. # # EMG contrast
  227. # Now do the same thing I just did with the behavioural contrast, but using the EMG
  228. #
  229. # So in this instance we are just going to have successful stop and ignore trials and split them by partial burst presence
  230. # %%
  231. # I don't want this to rely on the behavioural portion of this script - want to be able to do this individually
  232. # therefore have some repetition here but we'll live with this
  233. # note to self probably an idea to try and use process at some point, this is a bit slow
  234. regions = ['LIFG', 'RIFG', 'preSMA']
  235. for subject in os.listdir(workdir):
  236. if 'sub-0' in subject: # if it's an actual subject
  237. print('Processing', subject, '...')
  238. for region in regions:
  239. # trial types:
  240. # s_pr > stop with partial response
  241. # s_nr > stop no partial response
  242. # i_pr > ignore with partial response
  243. # i_nr > ignore no partial response
  244. # load the files
  245. alltrials = pd.read_csv(workdir + subject + '/derivatives/homer/' + region + '_ALL.csv')
  246. alltrials = alltrials.dropna(axis=1) # get rid of nans
  247. s_pr = pd.read_csv(workdir + subject + '/derivatives/homer/' + region + '_SS_PB.csv')
  248. s_nr = pd.read_csv(workdir + subject + '/derivatives/homer/' + region + '_SS_NB.csv')
  249. i_pr = pd.read_csv(workdir + subject + '/derivatives/homer/' + region + '_I_PB.csv')
  250. i_nr = pd.read_csv(workdir + subject + '/derivatives/homer/' + region + '_I_NB.csv')
  251. # put stops and ignores together
  252. stops = pd.concat([s_pr, s_nr], axis=1)
  253. ignores = pd.concat([i_pr, i_nr], axis=1)
  254. stops = stops.dropna(axis=1)
  255. ignores = ignores.dropna(axis=1)
  256. dfs = [stops, ignores]
  257. df_names = ['stops', 'ignores']
  258. filtered_dfs = {}
  259. for i, df in enumerate(dfs):
  260. highest_abs = [df[column].abs().max() for column in df.columns]
  261. iqr_value = iqr(highest_abs)
  262. filtered_columns = [column for column in df.columns if not (df[column].abs() > 3 * iqr_value).any()]
  263. filtered_dfs[df_names[i]] = df[filtered_columns]
  264. stops_filtered = filtered_dfs['stops']
  265. ig_filtered = filtered_dfs['ignores']
  266. s_pr_columns = [col for col in stops_filtered.columns if "_PB" in col]
  267. s_pr_filtered = stops[s_pr_columns]
  268. s_nr_columns = [col for col in stops_filtered.columns if "_NB" in col]
  269. s_nr_filtered = stops[s_nr_columns]
  270. i_pr_columns = [col for col in ig_filtered.columns if "_PB" in col]
  271. i_pr_filtered = ignores[i_pr_columns]
  272. i_nr_columns = [col for col in ig_filtered.columns if "_NB" in col]
  273. i_nr_filtered = ignores[i_nr_columns]
  274. ## NORMALISE
  275. # First finds the SD of each trial (for that ROI)
  276. # then calculates the average SD, then divides all values by that avg SD
  277. individual_sds = alltrials.apply(np.std)
  278. avg_sd = np.mean(individual_sds)
  279. alltrials_normalised = alltrials.div(avg_sd)
  280. ## GET TRIALS
  281. # Rename the columns first
  282. s_pr_filtered.columns = s_pr_filtered.columns.str.replace('SS_PB', 'ALL')
  283. s_nr_filtered.columns = s_nr_filtered.columns.str.replace('SS_NB', 'ALL')
  284. i_pr_filtered.columns = i_pr_filtered.columns.str.replace('I_PB', 'ALL')
  285. i_nr_filtered.columns = i_nr_filtered.columns.str.replace('I_NB', 'ALL')
  286. #extract which headings are which trial type
  287. s_pr_heads = s_pr_filtered.columns
  288. s_nr_heads = s_nr_filtered.columns
  289. i_pr_heads = i_pr_filtered.columns
  290. i_nr_heads = i_nr_filtered.columns
  291. # now grab the trials we want from the now-normalised data
  292. s_pr_norm = alltrials_normalised[[col for col in s_pr_heads if col in alltrials_normalised.columns]]
  293. s_nr_norm = alltrials_normalised[[col for col in s_nr_heads if col in alltrials_normalised.columns]]
  294. i_pr_norm = alltrials_normalised[[col for col in i_pr_heads if col in alltrials_normalised.columns]]
  295. i_nr_norm = alltrials_normalised[[col for col in i_nr_heads if col in alltrials_normalised.columns]]
  296. # now save the normed data
  297. s_pr_norm.to_csv(savedir + subject + '/iqr3/emg/' + region + '_S_PR_norm.csv', index=False)
  298. s_nr_norm.to_csv(savedir + subject + '/iqr3/emg/' + region + '_S_NR_norm.csv', index=False)
  299. i_pr_norm.to_csv(savedir + subject + '/iqr3/emg/' + region + '_I_PR_norm.csv', index=False)
  300. i_nr_norm.to_csv(savedir + subject + '/iqr3/emg/' + region + '_I_NR_norm.csv', index=False)
  301. print('done!')
  302. # %%
  303. ## EXTRACT PEAK VALUES >>>
  304. # for each trial, we want to get the value of the local maximum
  305. ## now let's find all the peaks and save them to a df
  306. warnings.filterwarnings("ignore")
  307. emgpeaks = pd.DataFrame(columns=['subject', 'region', 'peak_value'])
  308. timeseries = pd.DataFrame()
  309. for subject in os.listdir(savedir):
  310. number = 1
  311. if 'sub-0' in subject:
  312. print('Getting peaks for ', subject)
  313. # look through the subject's folder and find any CSV files
  314. csvs = glob.glob(savedir + subject +'/iqr3/emg/*norm.csv')
  315. for csv in csvs:
  316. # find the filename
  317. filename = os.path.basename(csv)
  318. filename = os.path.splitext(filename)[0]
  319. # load the file
  320. df = pd.read_csv(str(csv))
  321. # now look through the file
  322. for column in df.columns:
  323. if df[column].isnull().all(): # check it's not a null
  324. continue
  325. elif (df[column] == 0).all():
  326. continue
  327. # if the column has stuff in it, find the peak :)
  328. else:
  329. # find relevant segment in the time window
  330. relevant_segment = df.loc[time_window, column].values
  331. # find the peak of this
  332. peak_location = sp.signal.find_peaks(relevant_segment)
  333. # need to add an if statement checking that PL[0] is not empty
  334. # then find the value of the peak
  335. if not peak_location[0].any():
  336. peak_value = np.nan
  337. else:
  338. # highest_peak_index = np.argmax(relevant_segment[peak_location[0]])
  339. # highest_peak = peak_location[0][highest_peak_index]
  340. # peak_value = relevant_segment[highest_peak]
  341. first_peak = peak_location[0][0]
  342. peak_value = relevant_segment[first_peak]
  343. # and grab that column for graphing later by appending time series
  344. timeseries[f'{subject}_{filename}_{number}'] = df[column]
  345. number = number + 1
  346. # save peak_value and add this new row to allpeaks
  347. new_row = pd.DataFrame({
  348. 'subject': [subject],
  349. 'region': [filename],
  350. 'peak_value': [peak_value]})
  351. emgpeaks = pd.concat([emgpeaks, new_row], ignore_index=True)
  352. # emgpeaks.to_csv(savedir + 'fnirs_emg_firstpeak.csv', index=False)
  353. # print('saved - now beautifying')
  354. # let's make the file called emgpeaks a bit prettier so we can analyse it
  355. def findTrialType (s):
  356. if '_S_' in s:
  357. return 'Stop'
  358. elif '_I_' in s:
  359. return 'Ignore'
  360. else:
  361. return 'error'
  362. def findPartialResponse (s):
  363. if '_PR_' in s:
  364. return 'Present'
  365. elif '_NR_' in s:
  366. return 'Absent'
  367. else:
  368. return 'error'
  369. emgpeaks['trial'] = emgpeaks['region'].apply(findTrialType)
  370. emgpeaks['partial_response'] = emgpeaks['region'].apply(findPartialResponse)
  371. emgpeaks['roi'] = emgpeaks['region'].apply(lambda x: x.split('_')[0]) # get the name before the first _
  372. emgpeaks.to_csv(savedir + 'fnirs_emg_norm.csv', index=False)
  373. print('done')
  374. # %% [markdown]
  375. # # graph data
  376. # %%
  377. # let's see if we have data
  378. s_pr = pd.read_csv(savedir + 'sub-002/emg/LIFG_S_PR_norm.csv')
  379. s_nr = pd.read_csv(savedir + 'sub-002/emg/LIFG_S_NR_norm.csv')
  380. i_pr = pd.read_csv(savedir + 'sub-002/emg/LIFG_I_PR_norm.csv')
  381. i_nr = pd.read_csv(savedir + 'sub-002/emg/LIFG_I_NR_norm.csv')
  382. data_list = [(s_pr, 'Stop partial resp'),
  383. (s_nr, 'Stop no resp'),
  384. (i_pr, 'Ignore partial resp'),
  385. (i_nr, 'Ignore no resp')]
  386. # Loop through each dataset and create a new figure for each plot
  387. for data, label in data_list:
  388. plt.figure()
  389. data.iloc[:, :8].plot()
  390. plt.title(label) # Optionally, add a title to each figure
  391. plt.show()
  392. # %%
  393. emgpeaks.groupby(['roi', 'trial'])['peak_value'].median()
  394. #emgpeaks
  395. # %%
  396. # let's do some visualising
  397. sns.displot(behavpeaks, x='peak_value', hue='subject', kind='kde', col='trial')
  398. #plt.show()
  399. # %%
  400. # presma
  401. sns.set_theme('paper')
  402. sns.pointplot(data=emgpeaks[emgpeaks['roi'] == 'preSMA'],
  403. x='trial', y='peak_value', errorbar='se')
  404. # %%
  405. sns.pointplot(data=behavpeaks[behavpeaks['roi'] == 'preSMA'],
  406. x='trial', y='peak_value', errorbar='se')
  407. # %%
  408. sns.pointplot(data=allpeaks[allpeaks['roi'] == 'LIFG'],
  409. x='trial', y='peak_value', errorbar='se')
  410. # %%
  411. # let's look at everyone's raw data
  412. for subject in os.listdir(savedir):
  413. number = 1
  414. if 'sub-0' in subject:
  415. print('Getting peaks for ', subject)
  416. # look through the subject's folder and find any CSV files
  417. csvs = glob.glob(savedir + subject +'/*_norm.csv')
  418. for csv in csvs:
  419. print('hello')
  420. # %% [markdown]
  421. # # checking stuff

fnirs peaks and figures.ipynb, no license · at the source

Overview

  1. School of Psychological Sciences, College of Health and Medicine, University of Tasmania, Hobart, Australia
  2. Department of Psychology, University of Amsterdam, Amsterdam, the Netherlands
  3. Wicking Dementia and Research Education Centre, University of Tasmania, Hobart, Australia
  4. Full Brain Picture Analytics, Leiden, the Netherlands
Institutions: University of Tasmania (Australia)
Journal: Psychophysiology, volume 63, issue 9, article e70397
Dates: received 28 July 2025; accepted 27 August 2026; published online 7 September 2026; in print September 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1111/psyp.70397 · PMID 42704230 · PMCID PMC13548964 · OpenAlex W4404170055
Open access: hybrid, a free copy (OpenAlex)
Status: code verified
Categories: other (modality), fNIRS (modality), human (organism), cognitive (subfield)
Methods: Spectral & time-frequency, Statistics, Evoked potentials, Connectivity, fMRI & imaging, Physiology & signal measures
MeSH: Attention*, Inhibition, Psychological*, Motor Cortex*, Orientation*, Prefrontal Cortex*, Psychomotor Performance*, Adult, Electromyography, Female, Humans, Male, Spectroscopy, Near-Infrared, Young Adult (* major topic)
Topic: Neural and Behavioral Psychology Studies (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Funding: Australian Research Council Discovery (DP200101696)
Citations: not cited yet (Europe PMC); 98 references in the paper

Abstract

Action cancellation involves the termination of planned or ongoing movement, likely initiated in the cortex by the pre‐supplementary motor area (preSMA) and inferior frontal gyrus (IFG). It is frequently examined using the stop‐signal task; electromyography (EMG) studies have shown action cancellation (in stop trials) can involve the partial activation of the responding muscles, which does not result in an overt behavioral response. The neural correlates of these partial responses are not well understood. Here, we combined functional near‐infrared spectroscopy (fNIRS) and EMG to examine the neural correlates of terminated actions (partial responses) in a response‐ and stimulus‐selective stop signal task, controlling for attentional confounds by comparing neural activity in stop and ignore trials. fNIRS analyses revealed increased preSMA activity in successful stop compared to ignore trials, but no such differences in the IFG, consistent with postulations that the preSMA is involved uniquely in action cancellation, while the IFG responds generically to unexpected stimuli. Critically, preSMA activity was greater in trials without partial muscle activation, potentially due to proactive inhibitory processes pre‐emptively suppressing motor output in these trials. These findings advance understanding of some of the neurophysiological dynamics involved in action cancellation and highlight the utility of combining fNIRS with EMG in this domain.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repository

Its files are read in the Code ↔ Paper reader above.

OSF ztb5m

License: none: the authors keep all their rights
State: the link answers, verified on 26 September 2026
Evidence: files inventoried
Languages: Jupyter (1)
Size: 10 files, 1 script
Software Heritage: not checked
Found in: the text, “fNIRS Data”
Holds: 1 notebook
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: Matplotlib (1 file), NumPy (1 file), pandas (1 file), scikit-learn (1 file), SciPy (1 file), seaborn (1 file), statsmodels (1 file)
Availability: 1 check, the latest on 26 September 2026: the link answers (HTTP 200)
  • 26 September 2026: the link answers (HTTP 200)
1 file
At the source: osf.io/ztb5m/

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 1 script, each with its path and the digest of its content;
  • no match between paragraphs and code yet;
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

No dataset and no data link were found in the paper.

Data Availability Statement

The data that support the findings of this study are available on request from the corresponding author. The data are not publicly available due to privacy or ethical restrictions.

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 3, 28 September 2026

  • Publisher: n/a → Wiley

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 6 authors, 13 MeSH terms, 1 funder, 93 references.

Cite

This paper

Kemp, S. A., Salomoni, S., Bazin, P., Pash, L., St George, R. J., & Hinder, M. R. (2026). Cortical Contributions to Attentional Orienting and Response Cancellation in Action Stopping. Psychophysiology, 63(9), e70397. https://doi.org/10.1111/psyp.70397

BibTeX

@article{kemp2026cortical,
author = {Kemp, Sarah A and Salomoni, Sauro and Bazin, Pierre‐Louis and Pash, Luke and St George, Rebecca J and Hinder, Mark R},
title = {{Cortical Contributions to Attentional Orienting and Response Cancellation in Action Stopping}},
journal = {Psychophysiology},
year = {2026},
month = sep,
volume = {63},
number = {9},
pages = {e70397},
publisher = {Wiley},
issn = {0048-5772},
doi = {10.1111/psyp.70397},
url = {https://doi.org/10.1111/psyp.70397},
pmid = {42704230},
pmcid = {PMC13548964}
}

RIS

TY - JOUR
AU - Kemp, Sarah A
AU - Salomoni, Sauro
AU - Bazin, Pierre‐Louis
AU - Pash, Luke
AU - St George, Rebecca J
AU - Hinder, Mark R
TI - Cortical Contributions to Attentional Orienting and Response Cancellation in Action Stopping
T2 - Psychophysiology
J2 - Psychophysiology
PY - 2026
DA - 2026/09/01
VL - 63
IS - 9
SP - e70397
SN - 0048-5772
PB - Wiley
DO - 10.1111/psyp.70397
UR - https://doi.org/10.1111/psyp.70397
LA - en
ER -

CSL-JSON

{
"id": "10.1111/psyp.70397",
"type": "article-journal",
"title": "Cortical Contributions to Attentional Orienting and Response Cancellation in Action Stopping",
"container-title": "Psychophysiology",
"author": [
{
"family": "Kemp",
"given": "Sarah A"
},
{
"family": "Salomoni",
"given": "Sauro"
},
{
"family": "Bazin",
"given": "Pierre‐Louis"
},
{
"family": "Pash",
"given": "Luke"
},
{
"family": "St George",
"given": "Rebecca J"
},
{
"family": "Hinder",
"given": "Mark R"
}
],
"container-title-short": "Psychophysiology",
"volume": "63",
"issue": "9",
"page": "e70397",
"DOI": "10.1111/psyp.70397",
"PMID": "42704230",
"PMCID": "PMC13548964",
"ISSN": "0048-5772",
"publisher": "Wiley",
"URL": "https://doi.org/10.1111/psyp.70397",
"language": "en",
"issued": {
"date-parts": [
[
2026,
9,
1
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1371/journal.pbio.3003635 [code]
A cortico-subthalamic circuit rapidly engages and releases inhibition of specific movements depending on the environmental context.
Journal: PLoS biology
In common: 14 references
[2] doi:10.1016/j.isci.2026.116034
Distinct temporal patterns of beta bursts differentiate successful and failed movement cancellation.
Journal: iScience
In common: cognitive, 10 references
[3] doi:10.1038/s41467-026-71404-0 [code]
Nonergodicity and Simpson's paradox in neurocognitive dynamics of cognitive control.
Journal: Nature communications
In common: statsmodels, seaborn, scikit-learn, 4 other tools, cognitive, 5 references
[4] doi:10.1162/imag.a.1208 [code]
Brain network analysis in Alzheimer's disease and mild cognitive impairment using high-density diffuse optical tomography.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: statsmodels, seaborn, scikit-learn, 4 other tools, fNIRS, 3 references
[5] doi:10.1117/1.nph.13.2.025001 [code]
Surface-based image reconstruction optimization for high-density functional near-infrared spectroscopy.
Journal: Neurophotonics
In common: statsmodels, seaborn, scikit-learn, 4 other tools, fNIRS, 2 references
[6] doi:10.1162/imag.a.1279 [code]
Multimodal laminar characterization of visual areas along the cortical hierarchy.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: statsmodels, pandas, SciPy, 2 other tools, author Pierre‐Louis Bazin
[7] doi:10.1371/journal.pcbi.1012966 [code]
How the dynamic interplay of cortico-basal ganglia-thalamic pathways shapes the time course of deliberation and commitment.
Journal: PLoS computational biology
In common: seaborn, pandas, SciPy, 2 other tools, cognitive, 3 references
[8] doi:10.1002/ana.78203 [code]
AI-Driven Mapping of Seizure Spread Patterns.
Journal: Annals of neurology
In common: statsmodels, seaborn, scikit-learn, 4 other tools, 2 references
[9] doi:10.1002/advs.202523009 [code]
Personalized Network-Guided Neuromodulation Enhances Human Working Memory.
Journal: Advanced science (Weinheim, Baden-Wurttemberg, Germany)
In common: statsmodels, seaborn, scikit-learn, 4 other tools, other, cognitive, 1 reference
[10] doi:10.1371/journal.pone.0358044 [code]
An exploratory fNIRS study on directional asymmetry in segmental discrimination: Lateral-rhotic (/la-ra/) and labial-dorsal (/ba-ga/) contrasts tested in 5- and 9-month-old Japanese infants.
Journal: PloS one
In common: seaborn, pandas, SciPy, 2 other tools, fNIRS, 2 references

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.