OSCR

Using Steady-State Visual Evoked Potentials to Characterize Wide-Ranging Retinopathy Linked to <i>CRB1</i>: Implications for Clinical Trials.

Code ↔ Paper

10 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 10 matches
  1. [1] § Methods › Data analysis › Analysis of ssVEP data › Validity and reliability ↔ 02_data_analysis/src/04_reliability/CRB1_ssVEP_retest_analysis.ipynb, lines 1–54 · score 0.78 · intraclass correlation, original segment, epoched EEG, reliability assessment, coefficients, replacement
  2. [2] § Methods › Data analysis › Preprocessing ↔ 01_eeg_preprocessing/src/subject_level_eeg_analysis/utils.py, lines 339–387 · score 0.77 · neighboring frequency bins, power spectral density, immediate neighbors, PSD spectrum, noise, epochs
  3. [3] § Methods › Data analysis › Preprocessing ↔ 01_eeg_preprocessing/src/subject_level_eeg_analysis/utils.py, lines 339–387 · score 0.76 · neighboring frequency bins, power spectral density, immediate neighbors, noise, epochs, spectrum
  4. [4] § Methods › Data analysis › Analysis of ssVEP data › Comparisons of the ssVEP curve shape ↔ 02_data_analysis/src/02_analysis/03_fitCSF.Rmd, lines 95–210 · score 0.64 · peak gain, contrast sensitivity, maximal, likelihood, fitting, Gmax
  5. [5] § Methods › Participants ↔ 02_data_analysis/src/02_analysis/01_Demographics.Rmd, lines 54–86 · score 0.61 · disease duration, sighted control, gender, Demographic, females, SD
  6. [6] § Methods › ssVEP assessment: stimuli and paradigm ↔ src/optoflicker23/experiment.py, lines 47–141 · score 0.57 · refresh rate, gamma, PsychoPy, setup, gratings, position
  7. [7] § Methods › Data analysis › Analysis of ssVEP data › Validity and reliability ↔ 02_data_analysis/src/02_analysis/04_Correlation.Rmd, lines 94–141 · score 0.57 · linear mixed model, Categorical acuity, ssVEP, correlation, AUC, eyes
  8. [8] § Methods › Data analysis › Analysis of ssVEP data › Validity and reliability ↔ 02_data_analysis/src/02_analysis/06_Supplementary_PhenoComp_CRDexcl.Rmd, lines 659–697 · score 0.57 · linear mixed model, Categorical acuity, ssVEP, AUC, correlation, eyes
  9. [9] § Methods › Data analysis › Analysis of ssVEP data › Comparisons of overall ssVEP response ↔ 02_data_analysis/src/02_analysis/01_Demographics.Rmd, lines 394–484 · score 0.56 · Monte Carlo, CRB1 subgroups, permutation, eyes
  10. [10] § Methods › ssVEP assessment: stimuli and paradigm ↔ src/optoflicker23/experiment.py, lines 298–383 · score 0.51 · Visual stimuli, coin, jittered, sound, cycle, duration

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

Python · 728 lines · 23 KB · no license · 2 matches

  1. import tempfile
  2. from datetime import datetime, timedelta
  3. from copy import deepcopy
  4. import pyedflib
  5. from pyedflib.highlevel import write_edf
  6. from pyedflib import highlevel
  7. import os
  8. import mne
  9. import mne_bids
  10. import pandas as pd
  11. import numpy as np
  12. from typeguard import typechecked
  13. from joblib import Parallel, delayed
  14. from typing import Union, Dict
  15. from matplotlib import pyplot as plt
  16. from matplotlib.backends.backend_pdf import PdfPages
  17. from matplotlib.figure import Figure
  18. from yaml import safe_load
  19. from pathlib import Path
  20. import inspect
  21. @typechecked
  22. def ensure_dir(file_path: os.PathLike):
  23. directory = os.path.dirname(file_path)
  24. if not os.path.exists(directory):
  25. os.makedirs(directory)
  26. @typechecked
  27. def select_epochs(epochs: mne.BaseEpochs, events: pd.DataFrame, func: callable, **kwargs) -> mne.BaseEpochs:
  28. """
  29. Convenience function to select epochs based on events and a lambda function. The function should take a single argument,
  30. which is a pandas DataFrame containing the events. The function should return a boolean array
  31. of the same length as the events DataFrame. The boolean array indicates which events should be selected.
  32. Parameters
  33. ----------
  34. epochs : mne.Epochs
  35. Epochs.
  36. events : pd.DataFrame
  37. Events.
  38. func : callable
  39. Function to select epochs.
  40. **kwargs :
  41. Additional arguments passed to func.
  42. Returns
  43. -------
  44. mne.Epochs
  45. Selected epochs.
  46. """
  47. # check that events and epochs have the same length
  48. assert len(events) == len(
  49. epochs), "Events and epochs must have the same length."
  50. # select events
  51. select = func(events, **kwargs)
  52. # convet select to a list of conditions
  53. select = events["id"][select].unique().astype(str).tolist()
  54. # select epochs
  55. _epochs = epochs[select]
  56. return _epochs
  57. # define a decorator that vectorizes a function by calling the wrapped function for each element of the inputs lists
  58. # this is applied to all arguments that are lists and are longer than 1. errors are raised if the lists are not of equal length
  59. def vectorize(func: callable) -> callable:
  60. """
  61. Decorator that vectorizes a function by calling the wrapped function for each element of the inputs lists.
  62. This is applied to all arguments that are lists and are longer than 1. Errors are raised if the lists are not of equal length.
  63. Parameters
  64. ----------
  65. func : callable
  66. Function to vectorize.
  67. Returns
  68. -------
  69. callable
  70. Vectorized function.
  71. """
  72. def wrapper(*args, **kwargs):
  73. # check for 'n_jobs' argument
  74. if "n_jobs" in kwargs:
  75. # if a single number is passed, just use that number of jobs
  76. if isinstance(kwargs["n_jobs"], int):
  77. n_jobs = kwargs["n_jobs"]
  78. del kwargs["n_jobs"]
  79. # if a tuple is passed, use the first element for the number of jobs here and pass the second element to the function
  80. elif isinstance(kwargs["n_jobs"], list):
  81. n_jobs = kwargs["n_jobs"][0]
  82. kwargs["n_jobs"] = kwargs["n_jobs"][1]
  83. else:
  84. n_jobs = 1
  85. # find all arguments that are lists and are longer than 1
  86. lists = [arg for arg in args if isinstance(arg, list) and len(arg) > 1]
  87. if len(lists) == 0:
  88. # unlists lists with a single element
  89. args = [arg[0] if (isinstance(arg, list) and len(
  90. arg) == 1) else arg for arg in args]
  91. # no lists found, call the function
  92. return func(*args, **kwargs)
  93. # check that all lists are of equal length
  94. assert all(len(l) == len(lists[0])
  95. for l in lists), "All lists must be of equal length"
  96. # create a list of arguments topass for very iteration of the function
  97. outer_args = []
  98. for i in range(len(lists[0])):
  99. inner_args = []
  100. outer_args.append(inner_args)
  101. for arg in args:
  102. if isinstance(arg, list):
  103. inner_args.append(arg[i])
  104. else:
  105. inner_args.append(arg)
  106. # iter over all elements of the lists. use parallelization if n_jobs is set to a number > 1
  107. if n_jobs > 1 or n_jobs < 0:
  108. print("Running {} iterations of {} in parallel using {} jobs.".format(
  109. len(outer_args), func.__name__, n_jobs))
  110. return Parallel(n_jobs=n_jobs)(delayed(func)(*inner_args, **kwargs) for inner_args in outer_args)
  111. else:
  112. return [func(*inner_args, **kwargs) for inner_args in outer_args]
  113. return wrapper
  114. @typechecked
  115. def session_dict_to_lists(d: dict[str, list[str]]) -> (list[str], list[str]):
  116. """
  117. Convert a dictionary to a list of tuples. The dictionary keys are the first element of the tuples, and the dictionary values are the second element of the tuples.
  118. Parameters
  119. ----------
  120. d : dict[str, list[str]]
  121. Dictionary of subject/session combinations. The keys are the subjects, and the values are lists of sessions.
  122. Returns
  123. -------
  124. list[str]
  125. List of subjects (one element per subject/session combination).
  126. list[str]
  127. List of sessions (one element per subject/session combination).
  128. """
  129. sessions = []
  130. subjects = []
  131. for subject, session_list in d.items():
  132. for session in session_list:
  133. subjects.append(subject)
  134. sessions.append(session)
  135. return subjects, sessions
  136. @typechecked
  137. def get_events_as_data_frame(raw: mne.io.BaseRaw, events: np.ndarray) -> pd.DataFrame:
  138. """
  139. Get events as a pandas DataFrame.
  140. Parameters
  141. ----------
  142. raw : mne.Raw
  143. Raw data.
  144. events : np.ndarray
  145. Events.
  146. Returns
  147. -------
  148. pd.DataFrame
  149. Events as a pandas DataFrame.
  150. """
  151. # convert events to a data frame
  152. events_df = pd.DataFrame(
  153. events, columns=["onset_sample", "duration", "event_id"])
  154. # add the time of each event
  155. events_df["onset"] = raw.times[events_df["onset_sample"] - raw.first_samp]
  156. return events_df
  157. @typechecked
  158. class AnalysisLogger:
  159. def __init__(self, file: os.PathLike, verbose: bool = False):
  160. """
  161. Create a logger that logs messages to a file and optionally prints them to the console.
  162. Parameters
  163. ----------
  164. file : os.PathLike
  165. Path to the log file.
  166. verbose : bool, optional
  167. If True, print messages to the console. The default is False.
  168. Returns
  169. -------
  170. None.
  171. """
  172. self.file = file
  173. self.verbose = verbose
  174. def log(self, msg: str, verbose: bool = None, to_file=True) -> None:
  175. """
  176. Print a message if verbose is True.
  177. Parameters
  178. ----------
  179. msg : str
  180. Message to print.
  181. verbose : bool, optional
  182. If True, print the message. The default is self.verbose.
  183. to_file : bool, optional
  184. If True, write the message to the log file. The default is True.
  185. Returns
  186. -------
  187. None.
  188. """
  189. if verbose is None:
  190. verbose = self.verbose
  191. if verbose:
  192. print(msg)
  193. def log_file(self, msg: str) -> None:
  194. """
  195. Write a message to the log file.
  196. Parameters
  197. ----------
  198. msg : str
  199. Message to write.
  200. Returns
  201. -------
  202. None.
  203. """
  204. with open(self.file, "a") as f:
  205. f.write(msg + "\n")
  206. @typechecked
  207. def df_append(df: pd.DataFrame, d: dict) -> pd.DataFrame:
  208. """
  209. Append a dictionary to a DataFrame.
  210. Parameters
  211. ----------
  212. df : pd.DataFrame
  213. DataFrame.
  214. d : dict
  215. Dictionary.
  216. Returns
  217. -------
  218. pd.DataFrame
  219. DataFrame with the dictionary appended.
  220. """
  221. # append() is no longer supported in pandas
  222. # convert the dictionary to a DataFrame
  223. _d = pd.DataFrame.from_records(d, index=[0])
  224. # append the DataFrame to the DataFrame
  225. return pd.concat([df, _d], ignore_index=True)
  226. def find_matching_paths(*args, expected_num=-1, **kwargs) -> list[mne_bids.BIDSPath]:
  227. """
  228. Find all files matching the arguments.
  229. Parameters
  230. ----------
  231. *args :
  232. Arguments passed to mne_bids.find_matching_paths.
  233. expected_num : int, optional
  234. Expected number of files. If set to -1, no check is performed. The default is -1.
  235. **kwargs :
  236. Keyword arguments passed to mne_bids.find_matching_paths.
  237. Returns
  238. -------
  239. list[mne_bids.BIDSPath]
  240. List of matching files.
  241. """
  242. files = mne_bids.find_matching_paths(*args, **kwargs)
  243. # make sure that all files are unique
  244. ofiles = []
  245. for f in files:
  246. if f.fpath not in [f.fpath for f in ofiles]:
  247. ofiles.append(f)
  248. if expected_num > -1:
  249. assert len(
  250. ofiles) == expected_num, f"Found {len(ofiles)} files while expecting {expected_num}."
  251. return ofiles
  252. @typechecked
  253. def save_combined_pdf(fname: Union[str, bytes, os.PathLike], figs: list[Figure]) -> None:
  254. """
  255. Save a list of matplotlib figures as a single pdf file using PdfPages.
  256. Parameters
  257. ----------
  258. fname : Union[str, bytes, os.PathLike]
  259. Path to the output file.
  260. figs : list[Figure]
  261. List of figures.
  262. Returns
  263. -------
  264. None.
  265. """
  266. # make sure that the output directory exists
  267. ensure_dir(fname)
  268. with PdfPages(fname) as pdf:
  269. for fig in figs:
  270. pdf.savefig(fig)
  271. @typechecked
  272. def snr_spectrum(psd: np.ndarray, noise_n_neighbor_freqs: int, noise_skip_neighbor_freqs: int = 0) -> np.ndarray:
  273. """Compute SNR spectrum from PSD spectrum using convolution.
  274. Parameters
  275. ----------
  276. psd : ndarray, shape ([n_trials, n_channels,] n_frequency_bins)
  277. Data object containing PSD values. Works with arrays as produced by
  278. MNE's PSD functions or channel/trial subsets.
  279. noise_n_neighbor_freqs : int
  280. Number of neighboring frequencies used to compute noise level.
  281. increment by one to add one frequency bin ON BOTH SIDES
  282. noise_skip_neighbor_freqs : int
  283. set this >=1 if you want to exclude the immediately neighboring
  284. frequency bins in noise level calculation
  285. Returns
  286. -------
  287. snr : ndarray, shape ([n_trials, n_channels,] n_frequency_bins)
  288. Array containing SNR for all epochs, channels, frequency bins.
  289. NaN for frequencies on the edges, that do not have enough neighbors on
  290. one side to calculate SNR.
  291. """
  292. # Construct a kernel that calculates the mean of the neighboring
  293. # frequencies
  294. averaging_kernel = np.concatenate(
  295. (
  296. np.ones(noise_n_neighbor_freqs),
  297. np.zeros(2 * noise_skip_neighbor_freqs + 1),
  298. np.ones(noise_n_neighbor_freqs),
  299. )
  300. )
  301. averaging_kernel /= averaging_kernel.sum()
  302. # Calculate the mean of the neighboring frequencies by convolving with the
  303. # averaging kernel.
  304. mean_noise = np.apply_along_axis(
  305. lambda psd_: np.convolve(psd_, averaging_kernel, mode="valid"), axis=-1, arr=psd
  306. )
  307. # The mean is not defined on the edges so we will pad it with nas. The
  308. # padding needs to be done for the last dimension only so we set it to
  309. # (0, 0) for the other ones.
  310. edge_width = noise_n_neighbor_freqs + noise_skip_neighbor_freqs
  311. pad_width = [(0, 0)] * (mean_noise.ndim - 1) + [(edge_width, edge_width)]
  312. mean_noise = np.pad(mean_noise, pad_width=pad_width,
  313. constant_values=np.nan)
  314. return psd / mean_noise
  315. @typechecked
  316. def px_to_degrees_of_visual_angle(
  317. pix: Union[float, np.ndarray],
  318. screen_width: float,
  319. screen_distance: float,
  320. screen_resolution_width: float,
  321. ) -> Union[float, np.ndarray]:
  322. """
  323. Convert pixels to degrees of visual angle.
  324. Parameters
  325. ----------
  326. pix : Union[float, np.ndarray]
  327. Pixels.
  328. screen_width : float
  329. Screen width in m.
  330. screen_distance : float
  331. Screen distance in m.
  332. screen_resolution_width : float
  333. Screen resolution width in pixels.
  334. Returns
  335. -------
  336. Union[float, np.ndarray]
  337. Degrees of visual angle.
  338. """
  339. return np.degrees(np.arctan2(pix * screen_width, screen_distance * screen_resolution_width))
  340. @typechecked
  341. def load_config(max_iter: int = 3) -> Dict:
  342. """
  343. First, this function is looking for config.yaml in the current directory. If no config.yaml is found, it will traverse the directory tree upwards until it finds a config.yaml or reaches max_iter.
  344. Then, it will look for config.local.yaml in the directory where config.yaml was found.
  345. Finally, it look for a config file called FILENAME.yaml in the current directory, where FILENAME is the name of the calling file.
  346. All config files are loaded and merged into a single dictionary, with the order of precedence being config.yaml < config.local.yaml < FILENAME.yaml.
  347. Parameters
  348. ----------
  349. max_iter : int, optional
  350. Maximum number of directories to traverse upwards. The default is 3.
  351. Returns
  352. -------
  353. Dict
  354. Configuration dictionary.
  355. """
  356. config = {}
  357. # try to find config.yaml
  358. config_fname = Path("config.yaml").resolve()
  359. i = 0
  360. while not config_fname.exists() and i < max_iter:
  361. config_fname = config_fname.parent / config_fname.name
  362. i += 1
  363. if config_fname.exists():
  364. with open(config_fname, "r") as f:
  365. config.update(safe_load(f))
  366. # try to find config.local.yaml
  367. config_local_fname = config_fname.parent / "config.local.yaml"
  368. if config_local_fname.exists():
  369. with open(config_local_fname, "r") as f:
  370. config.update(safe_load(f))
  371. # try to find the calling file's filename
  372. try:
  373. filename = inspect.stack()[1].filename
  374. assert filename.endswith(".py"), "Calling file must be a python file."
  375. config_fname = Path(filename).resolve().with_suffix(".yaml")
  376. with open(config_fname, "r") as f:
  377. config.update(safe_load(f))
  378. assert isinstance(config, dict)
  379. except:
  380. pass
  381. return config
  382. @typechecked
  383. def find_all_subjects_and_sessions(dir: Union[str, bytes, os.PathLike]) -> dict[str, list[str]]:
  384. """
  385. Find all subjects and sessions in a directory, assuming that the directory structure follows this general pattern:
  386. dir
  387. ├── sub-01
  388. │ ├── ses-01
  389. │-- sub-02
  390. │ ├── ses-01
  391. │ ├── ses-02
  392. Parameters
  393. ----------
  394. dir : Union[str, bytes, os.PathLike]
  395. Path to the directory.
  396. Returns
  397. -------
  398. dict[str, list[str]]
  399. Dictionary of subject/session combinations. The keys are the subjects, and the values are lists of sessions.
  400. """
  401. subjects = {}
  402. # use pathlib to traverse the directory tree
  403. for path in Path(dir).iterdir():
  404. if path.is_dir():
  405. # if we find a directory, check if it starts with "sub-"
  406. if path.name.startswith("sub-"):
  407. subject = path.name[4:]
  408. sessions = []
  409. # find all sessions in the directory
  410. for session_path in path.iterdir():
  411. if session_path.is_dir():
  412. if session_path.name.startswith("ses-"):
  413. sessions.append(session_path.name[4:])
  414. if len(sessions) > 0:
  415. subjects[subject] = sessions
  416. return subjects
  417. @typechecked
  418. def get_snr_spectrum(psd: np.ndarray, noise_n_neighbor_freqs: int, noise_skip_neighbor_freqs: int = 0) -> np.ndarray:
  419. """Compute SNR spectrum from PSD spectrum using convolution.
  420. Parameters
  421. ----------
  422. psd : ndarray, shape ([n_trials, n_channels,] n_frequency_bins)
  423. Data object containing PSD values. Works with arrays as produced by
  424. MNE's PSD functions or channel/trial subsets.
  425. noise_n_neighbor_freqs : int
  426. Number of neighboring frequencies used to compute noise level.
  427. increment by one to add one frequency bin ON BOTH SIDES
  428. noise_skip_neighbor_freqs : int
  429. set this >=1 if you want to exclude the immediately neighboring
  430. frequency bins in noise level calculation
  431. Returns
  432. -------
  433. snr : ndarray, shape ([n_trials, n_channels,] n_frequency_bins)
  434. Array containing SNR for all epochs, channels, frequency bins.
  435. NaN for frequencies on the edges, that do not have enough neighbors on
  436. one side to calculate SNR.
  437. """
  438. # Construct a kernel that calculates the mean of the neighboring
  439. # frequencies
  440. averaging_kernel = np.concatenate((
  441. np.ones(noise_n_neighbor_freqs),
  442. np.zeros(2 * noise_skip_neighbor_freqs + 1),
  443. np.ones(noise_n_neighbor_freqs)))
  444. averaging_kernel /= averaging_kernel.sum()
  445. # Calculate the mean of the neighboring frequencies by convolving with the
  446. # averaging kernel.
  447. mean_noise = np.apply_along_axis(
  448. lambda psd_: np.convolve(psd_, averaging_kernel, mode='valid'),
  449. axis=-1, arr=psd
  450. )
  451. # The mean is not defined on the edges so we will pad it with nas. The
  452. # padding needs to be done for the last dimension only so we set it to
  453. # (0, 0) for the other ones.
  454. edge_width = noise_n_neighbor_freqs + noise_skip_neighbor_freqs
  455. pad_width = [(0, 0)] * (mean_noise.ndim - 1) + [(edge_width, edge_width)]
  456. mean_noise = np.pad(
  457. mean_noise, pad_width=pad_width, constant_values=np.nan
  458. )
  459. return psd / mean_noise
  460. # def crop_edf_tmp(edf_file, *, new_start=None, new_stop=None, verbose=True):
  461. # """Crop an EDF file to desired start/stop times and save it to a temporary file.
  462. # Parameters
  463. # ----------
  464. # edf_file : str
  465. # The path to the EDF file.
  466. # new_start : datetime.datetime
  467. # The new start timestamp. Can be None to keep the original start time.
  468. # new_stop : datetime.datetime
  469. # The new stop timestamp. Can be None to keep the original stop time.
  470. # verbose : bool
  471. # If True (default), print some details about the original and cropped file.
  472. # Returns
  473. # -------
  474. # str
  475. # Path to the temporary file.
  476. # """
  477. # edf_file_p = Path(edf_file)
  478. # # create a temporary file
  479. # tmpfile = os.path.join(tmpdir, edf_file_p.name)
  480. # # crop the file
  481. # crop_edf(edf_file, new_file=tmpfile, new_start=new_start, new_stop=new_stop, verbose=verbose)
  482. # # return the temporary file
  483. # return tmpfile
  484. def crop_edf(edf_file, *, new_file=None, new_start=None, new_stop=None, write_full_datarecord=True,
  485. verbose=True):
  486. """Crop an EDF file to desired start/stop times.
  487. Parameters
  488. ----------
  489. edf_file : str
  490. The path to the EDF file.
  491. new_file : str | None
  492. The path to the new cropped file. If None (default), the input
  493. filename appended with '_cropped' is used.
  494. new_start : flaot | None
  495. The new start time in seconds. Can be None to keep the original start
  496. new_stop : float | None
  497. The new stop time in seconds. Can be None to keep the original stop
  498. write_full_datarecord : bool
  499. If True (default), write the full datarecord to the new file. If False,
  500. will pad the datarecord with zeros to the next full datarecord.
  501. Note that if True, the duration of the new file will be slightly longer than the specified duration (up to the length of one datarecord)
  502. Also, this will raise an error if the newly calculated end sample is after the end of the file.
  503. verbose : bool
  504. If True (default), print some details about the original and cropped file.
  505. """
  506. # Print some info
  507. print(f"Cropping {edf_file} to {new_start} - {new_stop}.")
  508. # Open the original EDF file
  509. edf = pyedflib.EdfReader(edf_file)
  510. signals_headers = edf.getSignalHeaders()
  511. header = edf.getHeader()
  512. # Define new start time
  513. if new_start is None:
  514. start_diff_seconds = 0
  515. else:
  516. start_diff_seconds = new_start
  517. # Define new stop time
  518. if new_stop is None:
  519. stop_diff_from_start = edf.getFileDuration()
  520. else:
  521. stop_diff_from_start = new_stop
  522. # if write_full_datarecord is True, we adjust the stop time to the next full datarecord
  523. if write_full_datarecord:
  524. # edf files are divided into datarecords and one can only write full datarecords
  525. # edf.datarecord_duration
  526. stop_diff_from_start = stop_diff_from_start - \
  527. ((stop_diff_from_start - start_diff_seconds) %
  528. edf.datarecord_duration) + edf.datarecord_duration
  529. print(
  530. f"Extending stop time to {stop_diff_from_start} to write full datarecord. New duration is {stop_diff_from_start - start_diff_seconds}.")
  531. # Crop each signal
  532. signals = []
  533. digital_min = []
  534. digital_max = []
  535. for i in range(len(edf.getSignalHeaders())):
  536. # Question: should we use `_get_sample_frequency` instead?
  537. sf = edf.getSampleFrequency(i)
  538. start_idx = int(start_diff_seconds * sf)
  539. stop_idx = int(stop_diff_from_start * sf)
  540. # We use digital=True in reading and writing to avoid precision loss
  541. signals.append(edf.readSignal(i, start=start_idx,
  542. n=stop_idx - start_idx, digital=True))
  543. # get digital min/max
  544. digital_min.append(signals_headers[i]['digital_min'])
  545. digital_max.append(signals_headers[i]['digital_max'])
  546. edf.close()
  547. # Update header startdate and save file
  548. header["startdate"] = edf.getStartdatetime(
  549. ) + timedelta(seconds=start_diff_seconds)
  550. if new_file is None:
  551. file, ext = os.path.splitext(edf_file)
  552. new_file = file + '_cropped' + ext
  553. write_edf(new_file, signals, signals_headers, header, digital=True)
  554. # Safety check: are we able to load the new EDF file?
  555. # Get new EDF start, stop and duration
  556. edf = pyedflib.EdfReader(new_file)
  557. start = edf.getStartdatetime()
  558. stop = start + timedelta(seconds=edf.getFileDuration())
  559. duration = stop - start
  560. edf.close()
  561. def find_bad_epochs(epochs: mne.Epochs, reject: dict[str, float], reject_by_annotation=False, verbose: bool = True) -> np.ndarray:
  562. """
  563. Find bad epochs based on peak-to-peak amplitude rejection thresholds. Return the drop log.
  564. Parameters
  565. ----------
  566. epochs : mne.Epochs
  567. Epochs.
  568. reject : dict[str, float]
  569. Rejection thresholds.
  570. verbose : bool, optional
  571. If True, print some details. The default is True.
  572. Returns
  573. -------
  574. tuple of tuples
  575. The drop log.
  576. """
  577. # copy epochs
  578. _epochs = deepcopy(epochs)
  579. # reject epochs
  580. _epochs.drop_bad(reject=reject, verbose=verbose,
  581. reject_by_annotation=reject_by_annotation)
  582. # get bad epochs indices
  583. return _epochs.drop_log

utils.py at commit 0ad609e, no license · at the source

Overview

  1. Institute of Ophthalmology, University College London, London EC1V 9EL, UK
  2. Experimental Psychology, Division of Psychology and Language Sciences, University College London, London WC1H 0AP, UK
  3. Moorfields Eye Hospital NHS Foundation Trust, London EC1V 2PD, UK
  4. Great Ormond Street Hospital for Children NHS Foundation Trust, London WC1N 3JH, UK
  5. Department of Psychology, University of Amsterdam, 1018 WT Amsterdam, Netherlands
  6. The Francis Crick Institute, London NW1 1AT, UK
Journal: Computational and structural biotechnology journal, volume 35, issue 1, article 0042
Dates: received 27 December 2025; accepted 16 March 2026; published online 30 April 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.34133/csbj.0042 · PMID 42078489 · PMCID PMC13132498 · OpenAlex W7137910620
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: EEG (modality), human (organism), other condition (population), clinical / translational (subfield)
Methods: Spectral & time-frequency, Connectivity, Statistics, Preprocessing, Physiology & signal measures
Journal subjects: General
Topic: Drug-Induced Ocular Toxicity (Ophthalmology, Medicine), according to OpenAlex
Citations: cited by 1 paper (Europe PMC); 58 references in the paper

Abstract

Rapid advances in gene therapy are moving sight rescue treatments for inherited retinal diseases (IRDs) within reach, creating an urgent need to understand how these conditions affect neural signaling from the eye to the brain. However, capturing functional change across the diverse IRD sight-loss spectrum using a unified testing framework is challenging. Computational neuroimaging may help address this gap by exploiting known principles of visual-system tuning to derive more sensitive and computationally meaningful markers of visual function. This is particularly important in CRB1 retinopathy, an IRD with a strikingly wide phenotype range, where neural impacts across the full disease spectrum are not yet characterized. To investigate the functional impact of CRB1 retinopathy, we recorded steady-state visual evoked potentials (ssVEPs) in 72 eyes from 18 patients and 18 sighted controls using a patient-friendly, large-field protocol embedding phase-reversing sinusoidal gratings and full-screen flashes into age-appropriate videos. Fitting these data with neural tuning functions revealed significant ssVEP attenuation in patients, with the greatest reductions in those with generalized retinal degeneration, alongside shifts in spatial-frequency tuning toward lower frequencies. Our results show that with appropriate stimulus selection, ssVEPs offer a reliable response-free measure of visual function that correlates well with behavioral vision assessments and discriminates between CRB1 subgroups, with the optimally sensitive stimulus for detecting functional variation varying with the level of vision. Computational electroencephalogram-based neuroimaging thus provides quantitative insights across the wide spectrum of CRB1 pathology and represents a valuable complementary tool for evaluating disease progression and treatment outcomes in CRB1 retinopathy and related IRDs.

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

Repositories

Its files are read in the Code ↔ Paper reader above, with 10 matches between paragraphs and lines of code.

ChildVisionLab/crb1_ssvep_workflow

License: none: the authors keep all their rights
State: the link answers, verified on 30 September 2026
Evidence: files inventoried
Commit: 0ad609e477d51491b49bf5e2a9895b0ec6182613, 16 March 2026
Languages: Python (22), R (7), Jupyter (6), Shell (1), JavaScript (1)
Size: 50 files, 37 scripts
Software Heritage: not archived
Found in: “Data Availability”
Holds: README, environment (01_eeg_preprocessing/environment.yml, 01_eeg_preprocessing/pyproject.toml, 02_data_analysis/src/environment.yaml), 12 notebooks
Not found: license file, CITATION.cff, tests, continuous integration, documentation
Tools: pandas (24 files), NumPy (21 files), Matplotlib (16 files), MNE-Python (13 files), MNE-BIDS (12 files), seaborn (8 files), Plotly (7 files), SciPy (6 files), tidyverse (6 files), lme4 (5 files), lmerTest (5 files), psych (5 files), car (4 files), emmeans (4 files), scikit-learn (4 files), easystats (2 files), Pingouin (2 files), ggplot2 (1 file), Pillow (1 file), statsmodels (1 file), SymPy (1 file)
Availability: 1 check, the latest on 30 September 2026: the link answers
  • 30 September 2026: the link answers
38 files

ChildVisionLab/EEG_Stimulation_CRB1

License: MIT
State: the link answers, verified on 30 September 2026
Evidence: files inventoried
Commit: 1ef24e7daaae83e8e743a689fa9a35fa124b735b, 6 March 2026
Languages: Python (7)
Size: 10 files, 7 scripts
Software Heritage: not archived
Found in: “Data Availability”
Holds: license file, environment (pixi.toml)
Not found: README, CITATION.cff, tests, continuous integration, documentation
Tools: PsychoPy (5 files), NumPy (3 files), MNE-BIDS (1 file)
Availability: 1 check, the latest on 30 September 2026: the link answers
  • 30 September 2026: the link answers
8 files

The paper's code and data availability statement is in the Data section.

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:

  • 2 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 44 scripts, each with its path and the digest of its content;
  • 10 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • 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

Datasets cited

Data Availability

Due to the sensitive nature of the data, raw EEG data cannot be shared, but fully anonymized preprocessed data are available on Zenodo (https://doi.org/10.5281/zenodo.18956093). Code to reproduce the results reported in this paper are available at https://github.com/ChildVisionLab/crb1_ssvep_workflow, and code used for stimulus presentation are available at https://github.com/ChildVisionLab/EEG_Stimulation_CRB1.

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 1, 30 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 7 authors, 5 funders, 45 references.

Cite

This paper

Stäubli, K. E., Pabst, M., O. Maimon-Mor, R., Rodriguez-Martinez, A. C., Scholte, H. S., Moosajee, M., & Dekker, T. M. (2026). Using Steady-State Visual Evoked Potentials to Characterize Wide-Ranging Retinopathy Linked to &lt;i&gt;CRB1&lt;/i&gt;: Implications for Clinical Trials. Computational and structural biotechnology journal, 35(1), 0042. https://doi.org/10.34133/csbj.0042

BibTeX

@article{staubli2026using,
author = {Stäubli, Kim Eliane and Pabst, Marc and O. Maimon-Mor, Roni and Rodriguez-Martinez, Ana Catalina and Scholte, H. Steven and Moosajee, Mariya and Dekker, Tessa Marlijn},
title = {{Using Steady-State Visual Evoked Potentials to Characterize Wide-Ranging Retinopathy Linked to \&lt;i\&gt;CRB1\&lt;/i\&gt;: Implications for Clinical Trials}},
journal = {Computational and structural biotechnology journal},
year = {2026},
month = apr,
volume = {35},
number = {1},
pages = {0042},
publisher = {AAAS Science Partner Journal Program},
issn = {2001-0370},
doi = {10.34133/csbj.0042},
url = {https://doi.org/10.34133/csbj.0042},
pmid = {42078489},
pmcid = {PMC13132498}
}

RIS

TY - JOUR
AU - Stäubli, Kim Eliane
AU - Pabst, Marc
AU - O. Maimon-Mor, Roni
AU - Rodriguez-Martinez, Ana Catalina
AU - Scholte, H. Steven
AU - Moosajee, Mariya
AU - Dekker, Tessa Marlijn
TI - Using Steady-State Visual Evoked Potentials to Characterize Wide-Ranging Retinopathy Linked to &lt;i&gt;CRB1&lt;/i&gt;: Implications for Clinical Trials
T2 - Computational and structural biotechnology journal
J2 - Comput Struct Biotechnol J
PY - 2026
DA - 2026/04/30
VL - 35
IS - 1
SP - 0042
SN - 2001-0370
PB - AAAS Science Partner Journal Program
DO - 10.34133/csbj.0042
UR - https://doi.org/10.34133/csbj.0042
LA - en
ER -

CSL-JSON

{
"id": "10.34133/csbj.0042",
"type": "article-journal",
"title": "Using Steady-State Visual Evoked Potentials to Characterize Wide-Ranging Retinopathy Linked to &lt;i&gt;CRB1&lt;/i&gt;: Implications for Clinical Trials",
"container-title": "Computational and structural biotechnology journal",
"author": [
{
"family": "Stäubli",
"given": "Kim Eliane"
},
{
"family": "Pabst",
"given": "Marc"
},
{
"family": "O. Maimon-Mor",
"given": "Roni"
},
{
"family": "Rodriguez-Martinez",
"given": "Ana Catalina"
},
{
"family": "Scholte",
"given": "H. Steven"
},
{
"family": "Moosajee",
"given": "Mariya"
},
{
"family": "Dekker",
"given": "Tessa Marlijn"
}
],
"container-title-short": "Comput Struct Biotechnol J",
"volume": "35",
"issue": "1",
"page": "0042",
"DOI": "10.34133/csbj.0042",
"PMID": "42078489",
"PMCID": "PMC13132498",
"ISSN": "2001-0370",
"publisher": "AAAS Science Partner Journal Program",
"URL": "https://doi.org/10.34133/csbj.0042",
"language": "en",
"issued": {
"date-parts": [
[
2026,
4,
30
]
]
}
}

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.1038/s41597-026-07350-9 [code]
An open multi-center MEG-EEG dataset for studying conscious visual perception.
Journal: Scientific data
In common: MNE-BIDS, Pingouin, easystats, 13 other tools, EEG
[2] doi:10.1038/s41597-026-07377-y [code]
An open-access multi-site fMRI dataset for investigating conscious visual perception.
Journal: Scientific data
In common: MNE-BIDS, Pingouin, easystats, 13 other tools
[3] doi:10.1093/nc/niag029 [code]
A data-driven approach to identifying and evaluating connectivity-based neural correlates of conscious visual perception.
Journal: Neuroscience of consciousness
In common: MNE-BIDS, Pingouin, easystats, 13 other tools
[4] doi:10.1093/cercor/bhag113 [code]
Long-term reliability and stability of parameterized resting state EEG: evidence from a five-year follow-up.
Journal: Cerebral cortex (New York, N.Y. : 1991)
In common: MNE-BIDS, psych, easystats, 12 other tools, EEG
[5] doi:10.1093/braincomms/fcag176 [code]
Tau topography subtypes account for clinical heterogeneity and longitudinal trajectories in early-onset Alzheimer's disease.
Journal: Brain communications
In common: easystats, car, emmeans, 13 other tools, clinical / translational
[6] doi:10.1162/imag.a.1321 [code]
Phase similarity between similar objects indicates representational merging across retrieval training but not sleep.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: Pingouin, easystats, car, 12 other tools, EEG
[7] doi:10.1093/cercor/bhag077 [code]
The longitudinal development of intrinsic timescales in infancy and their relation to alpha brain rhythm.
Journal: Cerebral cortex (New York, N.Y. : 1991)
In common: Pingouin, psych, easystats, 12 other tools, EEG
[8] doi:10.1162/imag.a.105 [code]
Right posterior theta reflects human parahippocampal phase resetting by salient cues during goal-directed navigation
Journal: n/a
In common: Pingouin, psych, car, 11 other tools, EEG
[9] doi:10.1038/s41467-026-74824-0 [code]
Learning regularities in noise engages both neural predictive activity and representational changes.
Journal: Nature communications
In common: MNE-BIDS, Pingouin, easystats, 10 other tools
[10] doi:10.1016/j.nicl.2026.104012 [code]
Structural-functional multilayer brain network properties and outcome of combined repetitive transcranial magnetic stimulation and psychotherapy for obsessive-compulsive disorder.
Journal: NeuroImage. Clinical
In common: Pingouin, psych, car, 11 other tools, other condition

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.