Using Steady-State Visual Evoked Potentials to Characterize Wide-Ranging Retinopathy Linked to <i>CRB1</i>: Implications for Clinical Trials.
The 10 matches
- [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] § 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] § 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] § 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] § 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] § Methods › ssVEP assessment: stimuli and paradigm ↔ src/optoflicker23/experiment.py, lines 47–141 · score 0.57 · refresh rate, gamma, PsychoPy, setup, gratings, position
- [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] § 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] § 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] § 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
- import tempfile
- from datetime import datetime, timedelta
- from copy import deepcopy
- import pyedflib
- from pyedflib.highlevel import write_edf
- from pyedflib import highlevel
- import os
- import mne
- import mne_bids
- import pandas as pd
- import numpy as np
- from typeguard import typechecked
- from joblib import Parallel, delayed
- from typing import Union, Dict
- from matplotlib import pyplot as plt
- from matplotlib.backends.backend_pdf import PdfPages
- from matplotlib.figure import Figure
- from yaml import safe_load
- from pathlib import Path
- import inspect
- @typechecked
- def ensure_dir(file_path: os.PathLike):
- directory = os.path.dirname(file_path)
- if not os.path.exists(directory):
- os.makedirs(directory)
- @typechecked
- def select_epochs(epochs: mne.BaseEpochs, events: pd.DataFrame, func: callable, **kwargs) -> mne.BaseEpochs:
- """
- Convenience function to select epochs based on events and a lambda function. The function should take a single argument,
- which is a pandas DataFrame containing the events. The function should return a boolean array
- of the same length as the events DataFrame. The boolean array indicates which events should be selected.
- Parameters
- ----------
- epochs : mne.Epochs
- Epochs.
- events : pd.DataFrame
- Events.
- func : callable
- Function to select epochs.
- **kwargs :
- Additional arguments passed to func.
- Returns
- -------
- mne.Epochs
- Selected epochs.
- """
- # check that events and epochs have the same length
- assert len(events) == len(
- epochs), "Events and epochs must have the same length."
- # select events
- select = func(events, **kwargs)
- # convet select to a list of conditions
- select = events["id"][select].unique().astype(str).tolist()
- # select epochs
- _epochs = epochs[select]
- return _epochs
- # define a decorator that vectorizes a function by calling the wrapped function for each element of the inputs lists
- # 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
- def vectorize(func: callable) -> callable:
- """
- Decorator that vectorizes a function by calling the wrapped function for each element of the inputs lists.
- 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.
- Parameters
- ----------
- func : callable
- Function to vectorize.
- Returns
- -------
- callable
- Vectorized function.
- """
- def wrapper(*args, **kwargs):
- # check for 'n_jobs' argument
- if "n_jobs" in kwargs:
- # if a single number is passed, just use that number of jobs
- if isinstance(kwargs["n_jobs"], int):
- n_jobs = kwargs["n_jobs"]
- del kwargs["n_jobs"]
- # if a tuple is passed, use the first element for the number of jobs here and pass the second element to the function
- elif isinstance(kwargs["n_jobs"], list):
- n_jobs = kwargs["n_jobs"][0]
- kwargs["n_jobs"] = kwargs["n_jobs"][1]
- else:
- n_jobs = 1
- # find all arguments that are lists and are longer than 1
- lists = [arg for arg in args if isinstance(arg, list) and len(arg) > 1]
- if len(lists) == 0:
- # unlists lists with a single element
- args = [arg[0] if (isinstance(arg, list) and len(
- arg) == 1) else arg for arg in args]
- # no lists found, call the function
- return func(*args, **kwargs)
- # check that all lists are of equal length
- assert all(len(l) == len(lists[0])
- for l in lists), "All lists must be of equal length"
- # create a list of arguments topass for very iteration of the function
- outer_args = []
- for i in range(len(lists[0])):
- inner_args = []
- outer_args.append(inner_args)
- for arg in args:
- if isinstance(arg, list):
- inner_args.append(arg[i])
- else:
- inner_args.append(arg)
- # iter over all elements of the lists. use parallelization if n_jobs is set to a number > 1
- if n_jobs > 1 or n_jobs < 0:
- print("Running {} iterations of {} in parallel using {} jobs.".format(
- len(outer_args), func.__name__, n_jobs))
- return Parallel(n_jobs=n_jobs)(delayed(func)(*inner_args, **kwargs) for inner_args in outer_args)
- else:
- return [func(*inner_args, **kwargs) for inner_args in outer_args]
- return wrapper
- @typechecked
- def session_dict_to_lists(d: dict[str, list[str]]) -> (list[str], list[str]):
- """
- 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.
- Parameters
- ----------
- d : dict[str, list[str]]
- Dictionary of subject/session combinations. The keys are the subjects, and the values are lists of sessions.
- Returns
- -------
- list[str]
- List of subjects (one element per subject/session combination).
- list[str]
- List of sessions (one element per subject/session combination).
- """
- sessions = []
- subjects = []
- for subject, session_list in d.items():
- for session in session_list:
- subjects.append(subject)
- sessions.append(session)
- return subjects, sessions
- @typechecked
- def get_events_as_data_frame(raw: mne.io.BaseRaw, events: np.ndarray) -> pd.DataFrame:
- """
- Get events as a pandas DataFrame.
- Parameters
- ----------
- raw : mne.Raw
- Raw data.
- events : np.ndarray
- Events.
- Returns
- -------
- pd.DataFrame
- Events as a pandas DataFrame.
- """
- # convert events to a data frame
- events_df = pd.DataFrame(
- events, columns=["onset_sample", "duration", "event_id"])
- # add the time of each event
- events_df["onset"] = raw.times[events_df["onset_sample"] - raw.first_samp]
- return events_df
- @typechecked
- class AnalysisLogger:
- def __init__(self, file: os.PathLike, verbose: bool = False):
- """
- Create a logger that logs messages to a file and optionally prints them to the console.
- Parameters
- ----------
- file : os.PathLike
- Path to the log file.
- verbose : bool, optional
- If True, print messages to the console. The default is False.
- Returns
- -------
- None.
- """
- self.file = file
- self.verbose = verbose
- def log(self, msg: str, verbose: bool = None, to_file=True) -> None:
- """
- Print a message if verbose is True.
- Parameters
- ----------
- msg : str
- Message to print.
- verbose : bool, optional
- If True, print the message. The default is self.verbose.
- to_file : bool, optional
- If True, write the message to the log file. The default is True.
- Returns
- -------
- None.
- """
- if verbose is None:
- verbose = self.verbose
- if verbose:
- print(msg)
- def log_file(self, msg: str) -> None:
- """
- Write a message to the log file.
- Parameters
- ----------
- msg : str
- Message to write.
- Returns
- -------
- None.
- """
- with open(self.file, "a") as f:
- f.write(msg + "\n")
- @typechecked
- def df_append(df: pd.DataFrame, d: dict) -> pd.DataFrame:
- """
- Append a dictionary to a DataFrame.
- Parameters
- ----------
- df : pd.DataFrame
- DataFrame.
- d : dict
- Dictionary.
- Returns
- -------
- pd.DataFrame
- DataFrame with the dictionary appended.
- """
- # append() is no longer supported in pandas
- # convert the dictionary to a DataFrame
- _d = pd.DataFrame.from_records(d, index=[0])
- # append the DataFrame to the DataFrame
- return pd.concat([df, _d], ignore_index=True)
- def find_matching_paths(*args, expected_num=-1, **kwargs) -> list[mne_bids.BIDSPath]:
- """
- Find all files matching the arguments.
- Parameters
- ----------
- *args :
- Arguments passed to mne_bids.find_matching_paths.
- expected_num : int, optional
- Expected number of files. If set to -1, no check is performed. The default is -1.
- **kwargs :
- Keyword arguments passed to mne_bids.find_matching_paths.
- Returns
- -------
- list[mne_bids.BIDSPath]
- List of matching files.
- """
- files = mne_bids.find_matching_paths(*args, **kwargs)
- # make sure that all files are unique
- ofiles = []
- for f in files:
- if f.fpath not in [f.fpath for f in ofiles]:
- ofiles.append(f)
- if expected_num > -1:
- assert len(
- ofiles) == expected_num, f"Found {len(ofiles)} files while expecting {expected_num}."
- return ofiles
- @typechecked
- def save_combined_pdf(fname: Union[str, bytes, os.PathLike], figs: list[Figure]) -> None:
- """
- Save a list of matplotlib figures as a single pdf file using PdfPages.
- Parameters
- ----------
- fname : Union[str, bytes, os.PathLike]
- Path to the output file.
- figs : list[Figure]
- List of figures.
- Returns
- -------
- None.
- """
- # make sure that the output directory exists
- ensure_dir(fname)
- with PdfPages(fname) as pdf:
- for fig in figs:
- pdf.savefig(fig)
- @typechecked
- def snr_spectrum(psd: np.ndarray, noise_n_neighbor_freqs: int, noise_skip_neighbor_freqs: int = 0) -> np.ndarray:
- """Compute SNR spectrum from PSD spectrum using convolution.
- Parameters
- ----------
- psd : ndarray, shape ([n_trials, n_channels,] n_frequency_bins)
- Data object containing PSD values. Works with arrays as produced by
- MNE's PSD functions or channel/trial subsets.
- noise_n_neighbor_freqs : int
- Number of neighboring frequencies used to compute noise level.
- increment by one to add one frequency bin ON BOTH SIDES
- noise_skip_neighbor_freqs : int
- set this >=1 if you want to exclude the immediately neighboring
- frequency bins in noise level calculation
- Returns
- -------
- snr : ndarray, shape ([n_trials, n_channels,] n_frequency_bins)
- Array containing SNR for all epochs, channels, frequency bins.
- NaN for frequencies on the edges, that do not have enough neighbors on
- one side to calculate SNR.
- """
- # Construct a kernel that calculates the mean of the neighboring
- # frequencies
- averaging_kernel = np.concatenate(
- (
- np.ones(noise_n_neighbor_freqs),
- np.zeros(2 * noise_skip_neighbor_freqs + 1),
- np.ones(noise_n_neighbor_freqs),
- )
- )
- averaging_kernel /= averaging_kernel.sum()
- # Calculate the mean of the neighboring frequencies by convolving with the
- # averaging kernel.
- mean_noise = np.apply_along_axis(
- lambda psd_: np.convolve(psd_, averaging_kernel, mode="valid"), axis=-1, arr=psd
- )
- # The mean is not defined on the edges so we will pad it with nas. The
- # padding needs to be done for the last dimension only so we set it to
- # (0, 0) for the other ones.
- edge_width = noise_n_neighbor_freqs + noise_skip_neighbor_freqs
- pad_width = [(0, 0)] * (mean_noise.ndim - 1) + [(edge_width, edge_width)]
- mean_noise = np.pad(mean_noise, pad_width=pad_width,
- constant_values=np.nan)
- return psd / mean_noise
- @typechecked
- def px_to_degrees_of_visual_angle(
- pix: Union[float, np.ndarray],
- screen_width: float,
- screen_distance: float,
- screen_resolution_width: float,
- ) -> Union[float, np.ndarray]:
- """
- Convert pixels to degrees of visual angle.
- Parameters
- ----------
- pix : Union[float, np.ndarray]
- Pixels.
- screen_width : float
- Screen width in m.
- screen_distance : float
- Screen distance in m.
- screen_resolution_width : float
- Screen resolution width in pixels.
- Returns
- -------
- Union[float, np.ndarray]
- Degrees of visual angle.
- """
- return np.degrees(np.arctan2(pix * screen_width, screen_distance * screen_resolution_width))
- @typechecked
- def load_config(max_iter: int = 3) -> Dict:
- """
- 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.
- Then, it will look for config.local.yaml in the directory where config.yaml was found.
- Finally, it look for a config file called FILENAME.yaml in the current directory, where FILENAME is the name of the calling file.
- All config files are loaded and merged into a single dictionary, with the order of precedence being config.yaml < config.local.yaml < FILENAME.yaml.
- Parameters
- ----------
- max_iter : int, optional
- Maximum number of directories to traverse upwards. The default is 3.
- Returns
- -------
- Dict
- Configuration dictionary.
- """
- config = {}
- # try to find config.yaml
- config_fname = Path("config.yaml").resolve()
- i = 0
- while not config_fname.exists() and i < max_iter:
- config_fname = config_fname.parent / config_fname.name
- i += 1
- if config_fname.exists():
- with open(config_fname, "r") as f:
- config.update(safe_load(f))
- # try to find config.local.yaml
- config_local_fname = config_fname.parent / "config.local.yaml"
- if config_local_fname.exists():
- with open(config_local_fname, "r") as f:
- config.update(safe_load(f))
- # try to find the calling file's filename
- try:
- filename = inspect.stack()[1].filename
- assert filename.endswith(".py"), "Calling file must be a python file."
- config_fname = Path(filename).resolve().with_suffix(".yaml")
- with open(config_fname, "r") as f:
- config.update(safe_load(f))
- assert isinstance(config, dict)
- except:
- pass
- return config
- @typechecked
- def find_all_subjects_and_sessions(dir: Union[str, bytes, os.PathLike]) -> dict[str, list[str]]:
- """
- Find all subjects and sessions in a directory, assuming that the directory structure follows this general pattern:
- dir
- ├── sub-01
- │ ├── ses-01
- │-- sub-02
- │ ├── ses-01
- │ ├── ses-02
- Parameters
- ----------
- dir : Union[str, bytes, os.PathLike]
- Path to the directory.
- Returns
- -------
- dict[str, list[str]]
- Dictionary of subject/session combinations. The keys are the subjects, and the values are lists of sessions.
- """
- subjects = {}
- # use pathlib to traverse the directory tree
- for path in Path(dir).iterdir():
- if path.is_dir():
- # if we find a directory, check if it starts with "sub-"
- if path.name.startswith("sub-"):
- subject = path.name[4:]
- sessions = []
- # find all sessions in the directory
- for session_path in path.iterdir():
- if session_path.is_dir():
- if session_path.name.startswith("ses-"):
- sessions.append(session_path.name[4:])
- if len(sessions) > 0:
- subjects[subject] = sessions
- return subjects
- @typechecked
- def get_snr_spectrum(psd: np.ndarray, noise_n_neighbor_freqs: int, noise_skip_neighbor_freqs: int = 0) -> np.ndarray:
- """Compute SNR spectrum from PSD spectrum using convolution.
- Parameters
- ----------
- psd : ndarray, shape ([n_trials, n_channels,] n_frequency_bins)
- Data object containing PSD values. Works with arrays as produced by
- MNE's PSD functions or channel/trial subsets.
- noise_n_neighbor_freqs : int
- Number of neighboring frequencies used to compute noise level.
- increment by one to add one frequency bin ON BOTH SIDES
- noise_skip_neighbor_freqs : int
- set this >=1 if you want to exclude the immediately neighboring
- frequency bins in noise level calculation
- Returns
- -------
- snr : ndarray, shape ([n_trials, n_channels,] n_frequency_bins)
- Array containing SNR for all epochs, channels, frequency bins.
- NaN for frequencies on the edges, that do not have enough neighbors on
- one side to calculate SNR.
- """
- # Construct a kernel that calculates the mean of the neighboring
- # frequencies
- averaging_kernel = np.concatenate((
- np.ones(noise_n_neighbor_freqs),
- np.zeros(2 * noise_skip_neighbor_freqs + 1),
- np.ones(noise_n_neighbor_freqs)))
- averaging_kernel /= averaging_kernel.sum()
- # Calculate the mean of the neighboring frequencies by convolving with the
- # averaging kernel.
- mean_noise = np.apply_along_axis(
- lambda psd_: np.convolve(psd_, averaging_kernel, mode='valid'),
- axis=-1, arr=psd
- )
- # The mean is not defined on the edges so we will pad it with nas. The
- # padding needs to be done for the last dimension only so we set it to
- # (0, 0) for the other ones.
- edge_width = noise_n_neighbor_freqs + noise_skip_neighbor_freqs
- pad_width = [(0, 0)] * (mean_noise.ndim - 1) + [(edge_width, edge_width)]
- mean_noise = np.pad(
- mean_noise, pad_width=pad_width, constant_values=np.nan
- )
- return psd / mean_noise
- # def crop_edf_tmp(edf_file, *, new_start=None, new_stop=None, verbose=True):
- # """Crop an EDF file to desired start/stop times and save it to a temporary file.
- # Parameters
- # ----------
- # edf_file : str
- # The path to the EDF file.
- # new_start : datetime.datetime
- # The new start timestamp. Can be None to keep the original start time.
- # new_stop : datetime.datetime
- # The new stop timestamp. Can be None to keep the original stop time.
- # verbose : bool
- # If True (default), print some details about the original and cropped file.
- # Returns
- # -------
- # str
- # Path to the temporary file.
- # """
- # edf_file_p = Path(edf_file)
- # # create a temporary file
- # tmpfile = os.path.join(tmpdir, edf_file_p.name)
- # # crop the file
- # crop_edf(edf_file, new_file=tmpfile, new_start=new_start, new_stop=new_stop, verbose=verbose)
- # # return the temporary file
- # return tmpfile
- def crop_edf(edf_file, *, new_file=None, new_start=None, new_stop=None, write_full_datarecord=True,
- verbose=True):
- """Crop an EDF file to desired start/stop times.
- Parameters
- ----------
- edf_file : str
- The path to the EDF file.
- new_file : str | None
- The path to the new cropped file. If None (default), the input
- filename appended with '_cropped' is used.
- new_start : flaot | None
- The new start time in seconds. Can be None to keep the original start
- new_stop : float | None
- The new stop time in seconds. Can be None to keep the original stop
- write_full_datarecord : bool
- If True (default), write the full datarecord to the new file. If False,
- will pad the datarecord with zeros to the next full datarecord.
- 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)
- Also, this will raise an error if the newly calculated end sample is after the end of the file.
- verbose : bool
- If True (default), print some details about the original and cropped file.
- """
- # Print some info
- print(f"Cropping {edf_file} to {new_start} - {new_stop}.")
- # Open the original EDF file
- edf = pyedflib.EdfReader(edf_file)
- signals_headers = edf.getSignalHeaders()
- header = edf.getHeader()
- # Define new start time
- if new_start is None:
- start_diff_seconds = 0
- else:
- start_diff_seconds = new_start
- # Define new stop time
- if new_stop is None:
- stop_diff_from_start = edf.getFileDuration()
- else:
- stop_diff_from_start = new_stop
- # if write_full_datarecord is True, we adjust the stop time to the next full datarecord
- if write_full_datarecord:
- # edf files are divided into datarecords and one can only write full datarecords
- # edf.datarecord_duration
- stop_diff_from_start = stop_diff_from_start - \
- ((stop_diff_from_start - start_diff_seconds) %
- edf.datarecord_duration) + edf.datarecord_duration
- print(
- f"Extending stop time to {stop_diff_from_start} to write full datarecord. New duration is {stop_diff_from_start - start_diff_seconds}.")
- # Crop each signal
- signals = []
- digital_min = []
- digital_max = []
- for i in range(len(edf.getSignalHeaders())):
- # Question: should we use `_get_sample_frequency` instead?
- sf = edf.getSampleFrequency(i)
- start_idx = int(start_diff_seconds * sf)
- stop_idx = int(stop_diff_from_start * sf)
- # We use digital=True in reading and writing to avoid precision loss
- signals.append(edf.readSignal(i, start=start_idx,
- n=stop_idx - start_idx, digital=True))
- # get digital min/max
- digital_min.append(signals_headers[i]['digital_min'])
- digital_max.append(signals_headers[i]['digital_max'])
- edf.close()
- # Update header startdate and save file
- header["startdate"] = edf.getStartdatetime(
- ) + timedelta(seconds=start_diff_seconds)
- if new_file is None:
- file, ext = os.path.splitext(edf_file)
- new_file = file + '_cropped' + ext
- write_edf(new_file, signals, signals_headers, header, digital=True)
- # Safety check: are we able to load the new EDF file?
- # Get new EDF start, stop and duration
- edf = pyedflib.EdfReader(new_file)
- start = edf.getStartdatetime()
- stop = start + timedelta(seconds=edf.getFileDuration())
- duration = stop - start
- edf.close()
- def find_bad_epochs(epochs: mne.Epochs, reject: dict[str, float], reject_by_annotation=False, verbose: bool = True) -> np.ndarray:
- """
- Find bad epochs based on peak-to-peak amplitude rejection thresholds. Return the drop log.
- Parameters
- ----------
- epochs : mne.Epochs
- Epochs.
- reject : dict[str, float]
- Rejection thresholds.
- verbose : bool, optional
- If True, print some details. The default is True.
- Returns
- -------
- tuple of tuples
- The drop log.
- """
- # copy epochs
- _epochs = deepcopy(epochs)
- # reject epochs
- _epochs.drop_bad(reject=reject, verbose=verbose,
- reject_by_annotation=reject_by_annotation)
- # get bad epochs indices
- return _epochs.drop_log
utils.py at commit 0ad609e, no license · at the source
Overview
- Institute of Ophthalmology, University College London, London EC1V 9EL, UK
- Experimental Psychology, Division of Psychology and Language Sciences, University College London, London WC1H 0AP, UK
- Moorfields Eye Hospital NHS Foundation Trust, London EC1V 2PD, UK
- Great Ormond Street Hospital for Children NHS Foundation Trust, London WC1N 3JH, UK
- Department of Psychology, University of Amsterdam, 1018 WT Amsterdam, Netherlands
- The Francis Crick Institute, London NW1 1AT, UK
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-bas
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
0ad609e477d51491b49bf5e2a9895b0ec6182613, 16 March 2026Availability: 1 check, the latest on 30 September 2026: the link answers
- 30 September 2026: the link answers
38 files
- 01_eeg_preprocessing/
pipelines/ , Python, 87 linesconvert_CRB1s.py - 01_eeg_preprocessing/
pipelines/ , Python, 86 linespipeline1.py - 01_eeg_preprocessing/
setup.sh , Shell, 7 lines - 01_eeg_preprocessing/
src/ , Python, 7 linessubject_level_eeg_analys is/ __init__.py - 01_eeg_preprocessing/
src/ , Python, 271 linessubject_level_eeg_analys is/ annotation_utils.py - 01_eeg_preprocessing/
src/ , JavaScript, 1 linesubject_level_eeg_analys is/ assets/ reports.js - 01_eeg_preprocessing/
src/ , Python, 193 linessubject_level_eeg_analys is/ compute_epochs.py - 01_eeg_preprocessing/
src/ , Python, 308 linessubject_level_eeg_analys is/ compute_tfr.py - 01_eeg_preprocessing/
src/ , Python, 220 linessubject_level_eeg_analys is/ convert_to_bids.py - 01_eeg_preprocessing/
src/ , Python, 125 linessubject_level_eeg_analys is/ filter_subepochs.py - 01_eeg_preprocessing/
src/ , Python, 54 linessubject_level_eeg_analys is/ fix_raw_data.py - 01_eeg_preprocessing/
src/ , Python, 83 linessubject_level_eeg_analys is/ make_report.py - 01_eeg_preprocessing/
src/ , Python, 470 linessubject_level_eeg_analys is/ plots.py - 01_eeg_preprocessing/
src/ , Python, 195 linessubject_level_eeg_analys is/ preprocess.py - 01_eeg_preprocessing/
src/ , Python, 428 linessubject_level_eeg_analys is/ reliability_analysis.py - 01_eeg_preprocessing/
src/ , Python, 297 linessubject_level_eeg_analys is/ reports.py - 01_eeg_preprocessing/
src/ , Python, 131 linessubject_level_eeg_analys is/ subepoch.py - 01_eeg_preprocessing/
src/ , Python, 728 lines, 2 matchessubject_level_eeg_analys is/ utils.py - 02_data_analysis/
src/ , Jupyter, 220 lines01_preprocessing/ CRB1_preprocessing.ipynb - 02_data_analysis/
src/ , R, 504 lines, 2 matches02_analysis/ 01_Demographics.Rmd - 02_data_analysis/
src/ , R, 373 lines02_analysis/ 02_SubgroupComparison.Rm d - 02_data_analysis/
src/ , R, 590 lines, 1 match02_analysis/ 03_fitCSF.Rmd - 02_data_analysis/
src/ , R, 235 lines, 1 match02_analysis/ 04_Correlation.Rmd - 02_data_analysis/
src/ , R, 149 lines02_analysis/ 05_Reliability.Rmd - 02_data_analysis/
src/ , R, 698 lines, 1 match02_analysis/ 06_Supplementary_PhenoCo mp_CRDexcl.Rmd - 02_data_analysis/
src/ , Jupyter, 1,227 lines03_plotting/ CRB1_plotting.ipynb - 02_data_analysis/
src/ , Jupyter, 659 lines, 1 match04_reliability/ CRB1_ssVEP_retest_analys is.ipynb - 02_data_analysis/
src/ , Jupyter, 524 lines05_supplementary/ 01_Replication_CRB1_plot ting_CRD_excluded.ipynb - 02_data_analysis/
src/ , Jupyter, 479 lines05_supplementary/ 02_Replication_ssVEP_ret est_analysis_CRD_exclude d.ipynb - 02_data_analysis/
src/ , R, 136 linesutils/ R/ curve_fitting.R - 02_data_analysis/
src/ , Python, 61 linesutils/ python/ analysis.py - 02_data_analysis/
src/ , Python, 250 linesutils/ python/ calculate_ssVEP_VA.py - 02_data_analysis/
src/ , Python, 408 linesutils/ python/ df_prep.py - 02_data_analysis/
src/ , Python, 13 linesutils/ python/ load_config.py - 02_data_analysis/
src/ , Jupyter, 65 linesutils/ python/ plot_epoch_example.ipynb - 02_data_analysis/
src/ , Python, 218 linesutils/ python/ plotting.py - 02_data_analysis/
src/ , Python, 18 linesutils/ python/ reliability_analysis.py - README.md, Text, 158 lines
ChildVisionLab/EEG_Stimulation_CRB1
1ef24e7daaae83e8e743a689fa9a35fa124b735b, 6 March 2026Availability: 1 check, the latest on 30 September 2026: the link answers
- 30 September 2026: the link answers
8 files
- run.py, Python, 100 lines
- src/
optoflicker23/ , Python, 1 line__init__.py - src/
optoflicker23/ , Python, 730 lines, 2 matchesexperiment.py - src/
optoflicker23/ , Python, 156 linesgamma_corrected.py - src/
optoflicker23/ , Python, 296 lineshelpers.py - src/
optoflicker23/ , Python, 728 linestest_Luminance.py - src/
optoflicker23/ , Python, 103 linestriggers.py - LICENSE, License, 21 lines
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
- zenodo:18956093, at Zenodo; found in “Data Availability”
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://
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 &
BibTeX
@article{staubli2026usin
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 \&
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/
url = {https://
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 &
T2 - Computational and structural biotechnology journal
J2 - Comput Struct Biotechnol J
PY - 2026
DA - 2026/
VL - 35
IS - 1
SP - 0042
SN - 2001-0370
PB - AAAS Science Partner Journal Program
DO - 10.34133/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.34133/
"type": "article-journal",
"title": "Using Steady-State Visual Evoked Potentials to Characterize Wide-Ranging Retinopathy Linked to &
"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":
"volume": "35",
"issue": "1",
"page": "0042",
"DOI": "10.34133/
"PMID": "42078489",
"PMCID": "PMC13132498",
"ISSN": "2001-0370",
"publisher": "AAAS Science Partner Journal Program",
"URL": "https://
"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 dataIn 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 dataIn 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 consciousnessIn 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 communicationsIn 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 navigationJournal: n/aIn 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 communicationsIn 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. ClinicalIn 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.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 2 repositories of the authors' code, each at its verified commit and with its license, 44 scripts, and 10 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:6190d55a0cca4d64…
Add the badge to its README
The badge links the code to this page. Copy one of these into the README of the paper's code: only you decide where it goes, and nothing is changed for you.
Markdown
[, paste the snippet at the top, then “Commit changes…” and, to review it first, “Create a new branch and start a pull request”. You open the pull request; OSCR asks for no permission.
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.
