From 94981d1560a800ca27e95a85396577db04e8b701 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Nov 2025 12:33:30 +0000 Subject: [PATCH 1/4] Add comprehensive documentation to Pattern Reversal VEP experiment Added detailed docstrings and comments to the VisualPatternReversalVEP class: - Module-level documentation explaining the P100 component and clinical applications - Class-level documentation with usage examples - Comprehensive docstrings for all methods including __init__, load_stimulus, present_stimulus, etc. - Inline comments explaining VR vs monitor modes, checkerboard parameters, and timing - Documentation of standard clinical parameters (1 degree check size, 2 Hz reversal rate) - Updated __init__.py to properly export the VisualPatternReversalVEP class The documentation follows Python docstring conventions and matches the style used in other eeg-notebooks experiments. --- eegnb/experiments/visual_vep/__init__.py | 10 + .../visual_vep/pattern_reversal_vep.py | 519 ++++++++++++++++++ 2 files changed, 529 insertions(+) create mode 100644 eegnb/experiments/visual_vep/pattern_reversal_vep.py diff --git a/eegnb/experiments/visual_vep/__init__.py b/eegnb/experiments/visual_vep/__init__.py index e69de29bb..a6101e9a7 100644 --- a/eegnb/experiments/visual_vep/__init__.py +++ b/eegnb/experiments/visual_vep/__init__.py @@ -0,0 +1,10 @@ +"""Visual Evoked Potential (VEP) experiments module. + +This module contains experiments for measuring visual evoked potentials, +including pattern reversal VEP for assessing the P100 component. +""" + +from .vep import VisualVEP +from .pattern_reversal_vep import VisualPatternReversalVEP + +__all__ = ['VisualVEP', 'VisualPatternReversalVEP'] diff --git a/eegnb/experiments/visual_vep/pattern_reversal_vep.py b/eegnb/experiments/visual_vep/pattern_reversal_vep.py new file mode 100644 index 000000000..f0a181dbe --- /dev/null +++ b/eegnb/experiments/visual_vep/pattern_reversal_vep.py @@ -0,0 +1,519 @@ +"""eeg-notebooks/eegnb/experiments/visual_vep/pattern_reversal_vep.py + +Pattern Reversal Visual Evoked Potential (PR-VEP) Experiment +============================================================= + +This module implements a Pattern Reversal Visual Evoked Potential (PR-VEP) experiment, +which is used to measure the P100 component of the visual evoked potential. + +The P100 is a positive deflection in the EEG signal that occurs approximately 100ms after +a visual stimulus. It is most prominent in the occipital region and is elicited by presenting +a checkerboard pattern that reverses (swaps black and white squares) at regular intervals. + +This implementation supports both: +- Standard monitor-based presentation with monocular viewing (one eye at a time) +- VR-based presentation with stereoscopic viewing (Meta Quest 2) + +The experiment runs in blocks, alternating between left and right eye stimulation to allow +for monocular visual evoked potential measurements. + +Clinical and Research Applications: +- Assessment of visual pathway function +- Detection of optic nerve lesions +- Multiple sclerosis diagnosis +- Visual system maturation studies + +Standard Parameters: +- Check size: 1 degree (0.5 cycles per degree) +- Reversal rate: 2 Hz (pattern reverses twice per second) +- Recording duration: Typically 50 seconds per block +- Electrodes: Focus on occipital channels (O1, Oz, O2, PO3, PO4) +""" + +from time import time +import numpy as np + +from psychopy import visual +from typing import Optional, Dict, Any +from eegnb.devices.eeg import EEG +from eegnb.experiments.BlockExperiment import BlockExperiment +from stimupy.stimuli.checkerboards import contrast_contrast + +# Pixels per degree for Oculus/Meta Quest 2 with PC link +QUEST_PPD = 20 + + +class VisualPatternReversalVEP(BlockExperiment): + """Pattern Reversal Visual Evoked Potential (PR-VEP) experiment class. + + This experiment presents a checkerboard pattern that reverses contrast at a fixed rate + to elicit the P100 visual evoked potential component. The experiment alternates between + left and right eye stimulation across blocks to allow for monocular VEP measurements. + + The checkerboard consists of black and white squares that reverse at 2 Hz (twice per second), + with the participant fixating on a central red dot throughout the presentation. + + Attributes: + display_refresh_rate (int): Expected display refresh rate in Hz (must be divisible by reversal rate) + black_background (visual.Rect): Black rectangle covering the non-stimulated eye or periphery + grey_background (visual.Rect): Grey background for VR to maintain luminance + parameter (np.ndarray): Array indicating which eye to stimulate for each trial (0=left, 1=right) + + Example: + >>> from eegnb.devices.eeg import EEG + >>> eeg = EEG(device='muse') + >>> experiment = VisualPatternReversalVEP(display_refresh_rate=60, eeg=eeg) + >>> experiment.run() + """ + + def __init__(self, display_refresh_rate: int, eeg: Optional[EEG] = None, save_fn=None, + block_duration_seconds=50, block_trial_size: int=100, n_blocks: int=4, + use_vr=False, use_fullscr=True): + """Initialize the Pattern Reversal VEP experiment. + + Args: + display_refresh_rate (int): Expected refresh rate of the display in Hz. + Must be divisible by the stimulus reversal rate (2 Hz). Common values: 60, 120. + eeg (Optional[EEG]): EEG device object for recording. If None, runs without EEG recording. + save_fn (Optional[str]): Filename for saving recorded data. If None, generates default filename. + block_duration_seconds (int): Duration of each block in seconds. Default: 50. + block_trial_size (int): Number of trials (pattern reversals) per block. Default: 100. + n_blocks (int): Total number of blocks to run. Default: 4 (alternates between eyes). + use_vr (bool): If True, uses VR headset (Meta Quest 2) for stereoscopic presentation. + If False, uses standard monitor with monocular viewing. Default: False. + use_fullscr (bool): If True, runs in fullscreen mode. Default: True. + + Note: + - The stimulus reverses at 2 Hz (SOA = 0.5 seconds) + - Blocks alternate between left eye (even blocks) and right eye (odd blocks) + - For monitor-based presentation, participants manually close one eye + - For VR presentation, the non-stimulated eye sees a black screen + """ + self.display_refresh_rate = display_refresh_rate + + # Timing parameters for pattern reversal VEP + soa = 0.5 # Stimulus Onset Asynchrony: 0.5s = 2 Hz reversal rate (standard for P100) + iti = 0 # No inter-trial interval (continuous presentation) + jitter = 0 # No jitter (precise timing critical for VEP) + + # Initialize parent BlockExperiment class with stereoscopic support + super().__init__("Visual Pattern Reversal VEP", block_duration_seconds, eeg, save_fn, + block_trial_size, n_blocks, iti, soa, jitter, use_vr, use_fullscr, + stereoscopic=True) + + # Instruction text shown at the start of the experiment + self.instruction_text = f"""Welcome to the Visual Pattern Reversal VEP experiment! + + This experiment will run for {n_blocks} blocks of {block_duration_seconds} seconds each. + + Press spacebar or controller to continue. + """ + + # Setting up the trial and parameter list + # 0 = left eye, 1 = right eye + left_eye = 0 + right_eye = 1 + + # Alternate between left and right eye blocks + # Even blocks (0, 2, 4...) = left eye + # Odd blocks (1, 3, 5...) = right eye + block_eyes = [] + for block_num in range(n_blocks): + eye = left_eye if block_num % 2 == 0 else right_eye + block_eyes.extend([eye] * block_trial_size) + self.parameter = np.array(block_eyes) + + @staticmethod + def create_monitor_checkerboard(intensity_checks): + """Create a checkerboard stimulus for standard monitor presentation. + + Uses standard clinical parameters for pattern reversal VEP: + - Check size: 1 degree of visual angle (0.5 cycles per degree) + - Field size: 16 x 16 degrees + - Viewing distance and pixels per degree calibrated for typical monitor setup + + Args: + intensity_checks (tuple): Tuple of two values specifying the intensity pattern + for the checkerboard squares. Typically (1, -1) or (-1, 1) for full contrast + black and white reversal. + + Returns: + dict: Dictionary containing the checkerboard image and metadata from stimupy. + The 'img' key contains the numpy array of the checkerboard pattern. + + Note: + The standard 1-degree check size is optimal for eliciting the P100 response. + Smaller or larger check sizes may produce different VEP components. + """ + # Standard parameters for monitor-based pattern reversal VEP + # Using standard 1 degree check size at 72 pixels per degree + return contrast_contrast( + visual_size=(16, 16), # Visual field size in degrees + ppd=72, # Pixels per degree (assumes ~60cm viewing distance on typical monitor) + frequency=(0.5, 0.5), # Spatial frequency: 0.5 cpd = 1 degree check size (standard) + intensity_checks=intensity_checks, # Contrast pattern for checkerboard squares + target_shape=(0, 0), # No target (full field checkerboard) + alpha=0, # No transparency + tau=0 # No temporal modulation + ) + + @staticmethod + def create_vr_checkerboard(intensity_checks): + """Create a checkerboard stimulus optimized for VR presentation (Meta Quest 2). + + Parameters are adjusted for the Meta Quest 2's display characteristics: + - Resolution: ~20 pixels per degree + - Field of view: ~90 degrees + - Check size: 1 degree (standard for VEP) + + Args: + intensity_checks (tuple): Tuple of two values specifying the intensity pattern + for the checkerboard squares. Typically (1, -1) or (-1, 1) for full contrast + black and white reversal. + + Returns: + dict: Dictionary containing the checkerboard image and metadata from stimupy. + The 'img' key contains the numpy array of the checkerboard pattern. + + Note: + VR presentation offers advantages including: + - Better control of monocular presentation (no need to manually close one eye) + - Larger field of view + - More immersive experience reducing distractions + """ + # Optimized parameters for Oculus/Meta Quest 2 with PC link + # Quest 2 has approximately 20 pixels per degree and a ~90° FOV + # Using standard 1 degree check size (0.5 cpd) + return contrast_contrast( + visual_size=(20, 20), # Size in degrees - covers central field while fitting in FOV + ppd=QUEST_PPD, # Pixels per degree for Quest 2 + frequency=(0.5, 0.5), # Spatial frequency: 0.5 cpd = 1 degree check size (standard) + intensity_checks=intensity_checks, # Contrast pattern for checkerboard squares + target_shape=(0, 0), # No target (full field checkerboard) + alpha=0, # No transparency + tau=0 # No temporal modulation + ) + + def load_stimulus(self) -> Dict[str, Any]: + """Load and prepare all visual stimuli for the experiment. + + This method creates the checkerboard patterns, fixation points, and instruction text + needed for the experiment. For VR mode, it creates separate stimuli for left and right + eyes to enable stereoscopic presentation. For monitor mode, it creates a single set + of stimuli. + + The checkerboard reversals are pre-computed in two phases (black/white and white/black) + to ensure precise timing during presentation. + + Returns: + Dict[str, Any]: Dictionary containing all stimulus objects organized by eye/mode: + + For VR mode: + { + 'left': { + 'checkerboards': [phase1_stim, phase2_stim], + 'fixation': fixation_stim, + 'vr_block_instructions': instruction_text_stim + }, + 'right': { + 'checkerboards': [phase1_stim, phase2_stim], + 'fixation': fixation_stim, + 'vr_block_instructions': instruction_text_stim + } + } + + For monitor mode: + { + 'monoscopic': { + 'checkerboards': [phase1_stim, phase2_stim], + 'fixation': fixation_stim, + 'vr_block_instructions': instruction_text_stim + } + } + + Raises: + AssertionError: If the display refresh rate doesn't match expected rate or + if refresh rate is not divisible by stimulus rate (SOA). + + Note: + This method is called once at the start of the experiment. The loaded stimuli + are then reused across all blocks for efficiency. + """ + # Frame rate verification in Hz + # GetActualFrameRate() crashes in psychxr due to 'EndFrame called before BeginFrame' + actual_frame_rate = np.round(self.window.displayRefreshRate if self.use_vr else self.window.getActualFrameRate()) + + # Ensure the expected frame rate matches and is divisible by the stimulus rate (soa) + assert actual_frame_rate % self.soa == 0, \ + f"Expected frame rate divisible by stimulus rate: {self.soa}, but got {actual_frame_rate} Hz" + assert self.display_refresh_rate == actual_frame_rate, \ + f"Expected frame rate {self.display_refresh_rate} Hz, but got {actual_frame_rate} Hz" + + # Select checkerboard creation function based on presentation mode + if self.use_vr: + # Create the VR checkerboard + create_checkerboard = self.create_vr_checkerboard + # The window is large over the eye, checkerboard should only cover the central vision + size = self.window.size / 1.5 + else: + # Create the Monitor checkerboard + create_checkerboard = self.create_monitor_checkerboard + # Use square aspect ratio based on window height + size = (self.window_size[1], self.window_size[1]) + + # The surrounding / periphery needs to be dark when not using VR. + # Also used for covering eye which is not being stimulated. + self.black_background = visual.Rect(self.window, + width=self.window.size[0], + height=self.window.size[1], + fillColor='black') + + # A grey background behind the checkerboard must be used in VR to maintain luminance. + # This prevents adaptation effects and maintains consistent visual conditions. + self.grey_background = visual.Rect(self.window, + width=self.window.size[0], + height=self.window.size[1], + fillColor=[-0.22, -0.22, -0.22]) + + # Create checkerboard stimuli + def create_checkerboard_stim(intensity_checks, pos): + """Helper function to create a checkerboard stimulus at a specific position. + + Args: + intensity_checks (tuple): Intensity pattern for the checkerboard + pos (tuple): Position (x, y) in pixels for the stimulus center + + Returns: + visual.ImageStim: PsychoPy image stimulus object containing the checkerboard + """ + return visual.ImageStim(self.window, + image=create_checkerboard(intensity_checks)['img'], + units='pix', size=size, color='white', pos=pos) + + # Create fixation stimuli + def create_fixation_stim(pos): + """Helper function to create a red fixation dot at a specific position. + + The fixation point is critical for maintaining stable gaze during the experiment, + which ensures consistent P100 responses. + + Args: + pos (tuple): Position (x, y) for the fixation point + + Returns: + visual.GratingStim: PsychoPy grating stimulus configured as a fixation dot + """ + fixation = visual.GratingStim( + win=self.window, + pos=pos, + sf=400 if self.use_vr else 0.2, # High spatial frequency for VR + color=[1, 0, 0] # Red color + ) + fixation.size = 0.02 if self.use_vr else 0.4 + return fixation + + # Create VR block instruction stimuli + def create_vr_block_instruction(pos): + """Helper function to create instruction text for VR blocks. + + Args: + pos (tuple): Position (x, y) for the instruction text + + Returns: + visual.TextStim: PsychoPy text stimulus with block instructions + """ + return visual.TextStim( + win=self.window, + text="Focus on the red dot, and try not to blink whilst the squares are flashing, " + "press the spacebar or pull the controller trigger when ready to commence.", + color=[-1, -1, -1], # Black text + pos=pos, + height=0.1 + ) + + # Create and position stimulus + def create_eye_stimuli(eye_x_pos, pix_x_pos): + """Helper function to create a complete set of stimuli for one eye. + + Args: + eye_x_pos (float): X position in normalized coordinates (-1 to 1) + pix_x_pos (float): X position in pixels + + Returns: + dict: Dictionary containing checkerboards, fixation, and instructions for one eye + """ + return { + 'checkerboards': [ + create_checkerboard_stim((1, -1), pos=(pix_x_pos, 0)), # Phase 1: white-black + create_checkerboard_stim((-1, 1), pos=(pix_x_pos, 0)) # Phase 2: black-white + ], + 'fixation': create_fixation_stim([eye_x_pos, 0]), + 'vr_block_instructions': create_vr_block_instruction((eye_x_pos, 0)) + } + + # Structure all stimuli in organized dictionary + if self.use_vr: + # Calculate pixel positions for stereoscopic presentation + # Each eye sees stimuli positioned appropriately for stereoscopic viewing + window_width = self.window.size[0] + left_pix_x_pos = self.left_eye_x_pos * (window_width / 2) + right_pix_x_pos = self.right_eye_x_pos * (window_width / 2) + + return { + 'left': create_eye_stimuli(self.left_eye_x_pos, left_pix_x_pos), + 'right': create_eye_stimuli(self.right_eye_x_pos, right_pix_x_pos) + } + else: + # Monoscopic presentation - centered stimuli + return { + 'monoscopic': create_eye_stimuli(0, 0) + } + + def _present_vr_block_instructions(self, open_eye, closed_eye): + """Present block instructions for VR mode with one eye active and one blocked. + + This internal helper method sets up the VR buffers so that one eye sees the + instructions and fixation point, while the other eye sees a black screen. + + Args: + open_eye (str): Eye to show instructions to ('left' or 'right') + closed_eye (str): Eye to show black screen to ('left' or 'right') + + Note: + This method only draws to the buffers; it does not flip the window. + The flip must be called by the parent method. + """ + # Set buffer to the eye that should see the instructions + self.window.setBuffer(open_eye) + self.stim[open_eye]['vr_block_instructions'].draw() + self.stim[open_eye]['fixation'].draw() + + # Set buffer to the eye that should be blocked + self.window.setBuffer(closed_eye) + self.black_background.draw() + + def present_block_instructions(self, current_block: int) -> None: + """Present instructions at the beginning of each block. + + Instructions inform the participant which eye will be stimulated during the + upcoming block. For VR mode, the instructions appear in the active eye while + the other eye sees black. For monitor mode, text instructions ask the participant + to close the appropriate eye. + + Args: + current_block (int): The current block number (0-indexed). + Even blocks (0, 2, 4...) stimulate the left eye. + Odd blocks (1, 3, 5...) stimulate the right eye. + + Note: + This method is called repeatedly in a loop until the participant presses + spacebar or the controller trigger to begin the block. The actual flip() + is handled by the parent class. + """ + if self.use_vr: + # VR mode: Show instructions to one eye, black to the other + if current_block % 2 == 0: + # Even block: left eye open + self._present_vr_block_instructions(open_eye="left", closed_eye="right") + else: + # Odd block: right eye open + self._present_vr_block_instructions(open_eye="right", closed_eye="left") + else: + # Monitor mode: Display text instructions for manual eye closure + if current_block % 2 == 0: + instruction_text = ( + "Close your right eye, then focus on the red dot with your left eye. " + "Press spacebar or controller when ready." + ) + else: + instruction_text = ( + "Close your left eye, then focus on the red dot with your right eye. " + "Press spacebar or controller when ready." + ) + # Draw instruction text and fixation point + text = visual.TextStim(win=self.window, text=instruction_text, color=[-1, -1, -1]) + text.draw() + self.stim['monoscopic']['fixation'].draw() + + # Note: window.flip() is called by the parent class after this method returns + + def present_stimulus(self, idx: int): + """Present a single pattern reversal stimulus. + + This method is called for each trial in the experiment. It determines which eye + should be stimulated based on the current block, displays the appropriate phase + of the checkerboard (alternating black/white pattern), and sends a marker to the + EEG recording system. + + The checkerboard alternates between two phases (black/white and white/black) with + each call, creating the pattern reversal effect at 2 Hz (when called with SOA=0.5s). + + Args: + idx (int): Trial index within the current block (0 to block_trial_size-1). + Used to determine which checkerboard phase to display (even/odd alternation). + + Note: + The method pushes EEG markers to enable time-locking analysis: + - Marker 1: Left eye stimulation + - Marker 2: Right eye stimulation + + These markers are used during analysis to epoch the EEG data and compute + averaged VEP waveforms. + """ + # Get the label of the trial (which eye is being stimulated) + trial_idx = self.current_block_index * self.block_trial_size + idx + label = self.parameter[trial_idx] # 0 = left eye, 1 = right eye + + # Determine which eye should see the stimulus and which should be blocked + open_eye = 'left' if label == 0 else 'right' + closed_eye = 'left' if label == 1 else 'right' + + # Draw checkerboard and fixation + if self.use_vr: + # VR mode: Draw to the active eye buffer + self.window.setBuffer(open_eye) + self.grey_background.draw() + display = self.stim['left' if label == 0 else 'right'] + else: + # Monitor mode: Draw to single buffer + self.black_background.draw() + display = self.stim['monoscopic'] + + # Alternate between two checkerboard phases to create reversal effect + # idx % 2 gives 0 or 1, selecting between the two pre-created checkerboard phases + checkerboard_frame = idx % 2 + display['checkerboards'][checkerboard_frame].draw() + display['fixation'].draw() + + if self.use_vr: + # VR mode: Draw black screen to the blocked eye + self.window.setBuffer(closed_eye) + self.black_background.draw() + + # Flip window to display the stimulus + self.window.flip() + + # Push EEG marker with timestamp + # These markers are critical for time-locked averaging during analysis + marker = self.markernames[label] # Get marker name for this eye (1 or 2) + self.eeg.push_sample(marker=marker, timestamp=time()) + + def present_iti(self): + """Present the inter-trial interval (ITI) screen. + + For this experiment, the ITI is set to 0 (continuous presentation), but this + method is still implemented to maintain compatibility with the base class. + It displays a black screen in VR mode or flips the window in monitor mode. + + Note: + In pattern reversal VEP, continuous presentation is standard to maintain + a steady state of visual stimulation and maximize the number of epochs + for averaging. + """ + if self.use_vr: + # VR mode: Draw black to both eye buffers + for eye in ['left', 'right']: + self.window.setBuffer(eye) + self.black_background.draw() + # Flip window to display the ITI screen (black in both modes) + self.window.flip() From a250f31df0c3cd74001c349998e75a3671de7940 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Nov 2025 12:50:56 +0000 Subject: [PATCH 2/4] Add VEP experiment documentation to Sphinx docs - Created comprehensive VEP documentation page (doc/experiments/vvep.rst) - Added VEP experiment to doc index toctree - Documented Pattern Reversal VEP for P100 measurement - Included clinical applications, standard parameters, and experiment design - Documentation builds successfully and renders correctly in HTML - Follows existing documentation style and structure The documentation includes: - Overview of Visual Evoked Potentials - P100 component characteristics and significance - Clinical applications (MS diagnosis, optic neuritis, etc.) - ISCEV standard parameters (1 degree checks, 2 Hz reversal) - Support for both monitor and VR (Meta Quest 2) modes - Monocular recording with alternating eye blocks --- doc/experiments/vvep.rst | 85 ++++++++++++++++++++++++++++++++++++++++ doc/index.rst | 1 + 2 files changed, 86 insertions(+) create mode 100644 doc/experiments/vvep.rst diff --git a/doc/experiments/vvep.rst b/doc/experiments/vvep.rst new file mode 100644 index 000000000..400c7fcb9 --- /dev/null +++ b/doc/experiments/vvep.rst @@ -0,0 +1,85 @@ +******************************** +_ +********************************* + +Visual Evoked Potential (VEP) +========================================================= + +Visual Evoked Potentials (VEPs) are electrical signals generated by the visual cortex in response to visual stimulation. They are widely used in clinical and research settings to assess the functional integrity of the visual pathways from the retina to the occipital cortex. + +Pattern Reversal VEP +--------------------- + +The Pattern Reversal Visual Evoked Potential (PR-VEP) is the most commonly used VEP paradigm in clinical practice. It involves presenting a checkerboard pattern that alternates (reverses) between black and white squares at a regular rate, typically 2 Hz (twice per second). This elicits a characteristic positive potential called the **P100**, which occurs approximately 100ms after each pattern reversal. + +The P100 Component +~~~~~~~~~~~~~~~~~~ + +The P100 is a large positive deflection in the EEG signal that is most prominent at occipital electrode sites (O1, Oz, O2) and can also be detected at parietal-occipital sites (PO3, POz, PO4). Key characteristics include: + +- **Latency**: ~100ms after stimulus onset (typically 95-110ms in healthy adults) +- **Amplitude**: Variable, but typically 5-20 μV +- **Polarity**: Positive at occipital sites +- **Retinotopic**: Larger amplitude in the hemisphere contralateral to the stimulated eye + +Clinical Applications +~~~~~~~~~~~~~~~~~~~~~ + +Pattern Reversal VEP is used to: + +- **Assess visual pathway function**: Detect abnormalities in the optic nerve, chiasm, or visual cortex +- **Diagnose optic neuritis**: Common in multiple sclerosis, shows delayed P100 latency +- **Evaluate amblyopia**: Assess visual development in children +- **Monitor visual recovery**: Track improvements after treatment +- **Detect subclinical lesions**: Identify demyelinating diseases before symptoms appear + +Standard Parameters +~~~~~~~~~~~~~~~~~~~ + +The implementation in eeg-notebooks follows international clinical standards (ISCEV): + +- **Check size**: 1 degree of visual angle (0.5 cycles per degree) +- **Reversal rate**: 2 Hz (pattern reverses twice per second) +- **Field size**: 16-20 degrees of visual angle +- **Contrast**: High contrast black and white (≥80%) +- **Luminance**: Equal for black and white (mean luminance ~50 cd/m²) +- **Fixation**: Central red dot for stable gaze +- **Recording**: Monocular (one eye at a time) with blocks alternating between eyes + +Experiment Design +~~~~~~~~~~~~~~~~~ + +This implementation supports both standard monitor-based and VR-based (Meta Quest 2) presentation: + +**Monitor Mode**: +- Participants manually close one eye +- Standard viewing distance and check size +- Text instructions guide eye closure + +**VR Mode**: +- Stereoscopic presentation with precise monocular control +- Non-stimulated eye sees black screen +- Optimized parameters for Quest 2 display characteristics +- Immersive experience reduces distractions + +The experiment runs in blocks, alternating between left and right eye stimulation to enable monocular VEP measurements and assess each visual pathway independently. + +**Visual VEP Experiment Examples:** + +When example notebooks and scripts are available, they will appear here. + +.. Note: Once example files are added to examples/visual_vep/, uncomment the line below +.. .. include:: ../auto_examples/visual_vep/index.rst + +API Reference +~~~~~~~~~~~~~ + +.. automodule:: eegnb.experiments.visual_vep + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: eegnb.experiments.visual_vep.VisualPatternReversalVEP + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/index.rst b/doc/index.rst index 278093db3..ffa41e26f 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -19,6 +19,7 @@ experiments/vn170 experiments/vp300 experiments/vssvep + experiments/vvep experiments/cueing experiments/gonogo experiments/all_examples From 99fc525069fc6922dcb6108952733abbdbe91877 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Nov 2025 02:15:17 +0000 Subject: [PATCH 3/4] Replace 'eeg-notebooks' references with 'EEG-ExPy' - Updated module docstring path reference in pattern_reversal_vep.py - Updated project name reference in VEP documentation (vvep.rst) - Ensures consistent branding throughout the documentation --- doc/experiments/vvep.rst | 2 +- eegnb/experiments/visual_vep/pattern_reversal_vep.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/experiments/vvep.rst b/doc/experiments/vvep.rst index 400c7fcb9..bc471f5a6 100644 --- a/doc/experiments/vvep.rst +++ b/doc/experiments/vvep.rst @@ -36,7 +36,7 @@ Pattern Reversal VEP is used to: Standard Parameters ~~~~~~~~~~~~~~~~~~~ -The implementation in eeg-notebooks follows international clinical standards (ISCEV): +The implementation in EEG-ExPy follows international clinical standards (ISCEV): - **Check size**: 1 degree of visual angle (0.5 cycles per degree) - **Reversal rate**: 2 Hz (pattern reverses twice per second) diff --git a/eegnb/experiments/visual_vep/pattern_reversal_vep.py b/eegnb/experiments/visual_vep/pattern_reversal_vep.py index f0a181dbe..8a0533bf6 100644 --- a/eegnb/experiments/visual_vep/pattern_reversal_vep.py +++ b/eegnb/experiments/visual_vep/pattern_reversal_vep.py @@ -1,4 +1,4 @@ -"""eeg-notebooks/eegnb/experiments/visual_vep/pattern_reversal_vep.py +"""EEG-ExPy/eegnb/experiments/visual_vep/pattern_reversal_vep.py Pattern Reversal Visual Evoked Potential (PR-VEP) Experiment ============================================================= From b4c06d4fa71aa3b4a1fc39e3ee04fcb3f7a29a57 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Nov 2025 03:00:08 +0000 Subject: [PATCH 4/4] Update 'Quest 2' references to 'Meta Quest' - Updated all references from 'Quest 2' to 'Meta Quest' for consistency - Updated in both code documentation and RST files - Changes include: - Module docstring - Function parameters and docstrings - Inline comments - Documentation RST file This provides more generic VR headset naming that applies to the entire Meta Quest line rather than a specific model. --- doc/experiments/vvep.rst | 4 ++-- .../visual_vep/pattern_reversal_vep.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/doc/experiments/vvep.rst b/doc/experiments/vvep.rst index bc471f5a6..22448ebc1 100644 --- a/doc/experiments/vvep.rst +++ b/doc/experiments/vvep.rst @@ -49,7 +49,7 @@ The implementation in EEG-ExPy follows international clinical standards (ISCEV): Experiment Design ~~~~~~~~~~~~~~~~~ -This implementation supports both standard monitor-based and VR-based (Meta Quest 2) presentation: +This implementation supports both standard monitor-based and VR-based (Meta Quest) presentation: **Monitor Mode**: - Participants manually close one eye @@ -59,7 +59,7 @@ This implementation supports both standard monitor-based and VR-based (Meta Ques **VR Mode**: - Stereoscopic presentation with precise monocular control - Non-stimulated eye sees black screen -- Optimized parameters for Quest 2 display characteristics +- Optimized parameters for Meta Quest display characteristics - Immersive experience reduces distractions The experiment runs in blocks, alternating between left and right eye stimulation to enable monocular VEP measurements and assess each visual pathway independently. diff --git a/eegnb/experiments/visual_vep/pattern_reversal_vep.py b/eegnb/experiments/visual_vep/pattern_reversal_vep.py index 8a0533bf6..89e003983 100644 --- a/eegnb/experiments/visual_vep/pattern_reversal_vep.py +++ b/eegnb/experiments/visual_vep/pattern_reversal_vep.py @@ -12,7 +12,7 @@ This implementation supports both: - Standard monitor-based presentation with monocular viewing (one eye at a time) -- VR-based presentation with stereoscopic viewing (Meta Quest 2) +- VR-based presentation with stereoscopic viewing (Meta Quest) The experiment runs in blocks, alternating between left and right eye stimulation to allow for monocular visual evoked potential measurements. @@ -39,7 +39,7 @@ from eegnb.experiments.BlockExperiment import BlockExperiment from stimupy.stimuli.checkerboards import contrast_contrast -# Pixels per degree for Oculus/Meta Quest 2 with PC link +# Pixels per degree for Oculus/Meta Quest with PC link QUEST_PPD = 20 @@ -79,7 +79,7 @@ def __init__(self, display_refresh_rate: int, eeg: Optional[EEG] = None, save_fn block_duration_seconds (int): Duration of each block in seconds. Default: 50. block_trial_size (int): Number of trials (pattern reversals) per block. Default: 100. n_blocks (int): Total number of blocks to run. Default: 4 (alternates between eyes). - use_vr (bool): If True, uses VR headset (Meta Quest 2) for stereoscopic presentation. + use_vr (bool): If True, uses VR headset (Meta Quest) for stereoscopic presentation. If False, uses standard monitor with monocular viewing. Default: False. use_fullscr (bool): If True, runs in fullscreen mode. Default: True. @@ -159,9 +159,9 @@ def create_monitor_checkerboard(intensity_checks): @staticmethod def create_vr_checkerboard(intensity_checks): - """Create a checkerboard stimulus optimized for VR presentation (Meta Quest 2). + """Create a checkerboard stimulus optimized for VR presentation (Meta Quest). - Parameters are adjusted for the Meta Quest 2's display characteristics: + Parameters are adjusted for the Meta Quest's display characteristics: - Resolution: ~20 pixels per degree - Field of view: ~90 degrees - Check size: 1 degree (standard for VEP) @@ -181,12 +181,12 @@ def create_vr_checkerboard(intensity_checks): - Larger field of view - More immersive experience reducing distractions """ - # Optimized parameters for Oculus/Meta Quest 2 with PC link - # Quest 2 has approximately 20 pixels per degree and a ~90° FOV + # Optimized parameters for Oculus/Meta Quest with PC link + # Meta Quest has approximately 20 pixels per degree and a ~90° FOV # Using standard 1 degree check size (0.5 cpd) return contrast_contrast( visual_size=(20, 20), # Size in degrees - covers central field while fitting in FOV - ppd=QUEST_PPD, # Pixels per degree for Quest 2 + ppd=QUEST_PPD, # Pixels per degree for Meta Quest frequency=(0.5, 0.5), # Spatial frequency: 0.5 cpd = 1 degree check size (standard) intensity_checks=intensity_checks, # Contrast pattern for checkerboard squares target_shape=(0, 0), # No target (full field checkerboard)