ISSN (Online): 2348-991X | ISSN (Print): 2454-9576
server-injected
Research Article
Open Access

A Shape-sorting Tool to Maximise the Accuracy of Electrophysiological Estimation of Visual Acuity

,
DOI: 10.18535/ijmsci/v13i.06.01· Pages: 7888-7911· Vol. 13, No. 06, (2026)· Published: June 30, 2026
PDFAuto
Views: 7 PDF downloads: 10

Abstract

Morphology of the Visual Evoked Potential (VEP) varies with age, pathology and the physical properties of the stimulus. For example, hydrocephalus can displace and compress the tissue of the visual brain, causing delays and distortion of measurements made from the standard Oz site. Steady-state stimuli enable quantitative analysis of VEPs by Fourier Transform and bespoke statistics, however, their rapidity increases the likelihood of standing waves and delayed far-field responses at the measurement electrodes, mimicking a contemporaneous response. This communication describes the development of new software that considers the morphology of an automatically detected VEP, compares it to a set of templates, then proposes its most likely origin. This tool could be used to reject non-spatial and other artefactual responses in exported data files, or added to existing real-time VEP VA tests enabling the stimulus presentation software to rapidly converge on the individuals threshold.

Keywords

Cross-correlation Normalisation Objective VEP Visual Acuity.

Introduction

The morphology of the normal VEP varies with stimulus pattern, [1] luminance, [2] field size, [3] onset vs reversal presentation, [4] contrast, [5] and colour. [6; 7] Maturity of the visual system influences VEP amplitude, which peaks during adolescence, [8; 9] though there is a lack of literature covering the teenage years in detail. Reductions in response amplitude are caused by fatigue, [10] uncorrected, or inadequately corrected refractive errors, [11] and voluntary movements when stimulation frequency falls between seven and 12 Hz. [12; 13] Prolonged latency has been observed in hypertension.[14]. Yet, there is a paucity of reports describing how such factors influence morphology.

In health, occipital ssVEPs are a combination of the V1/V2 response to visual stimulation (the near field) [15], responses from distant sources generated by an earlier visual stimulus (the far field) [16] and EEG standing waves [17] when the stimulation rate matches the brain’s resonant frequency. [18] This resonance happens between eight and 12 Hz for visual stimuli, [19] is dependent on head size, [20] and is most prominent for full-field luminance flicker stimuli.[idem]

Travelling waves propagate from the occiput to the pre-frontal cortex, [21] and while they form the primary response at Oz, they may also contribute, to the signal at the reference electrode Fz, modulating the signal subject to individual anatomy, and recording duration.

Numerous clinical textbooks have been written about the effect of specific conditions on electrophysiological recordings, however, we have an incomplete understanding of many complex conditions- leading to the design of this project. For example, Cortical Visual Impairment (CVI) can cause hydrocephalus that displaces and compresses the tissue of the visual brain and causes a range of functional deficits.[22]. While imaging can reveal the extent of the anatomical damage, functional deficits require electrophysiological investigation with techniques that maintain attention.[23; 24] Prior imaging will guide re-positioning of VEP electrodes, and facilitate more accurate measurements. [25] Considered inversely, the vector linking a VEP from the standard Oz-Fz montage and an adjusted position provides an index of how specific anatomy changes the VEP. Creating a bank of such data could be used to retrospectively correct standard VEPs once neuro-anatomy is revealed by imaging.

While continuous, simultaneous EEG-MRI measurements are possible in specialist university labs, [26] the aim of this study is to develop a tool that can be used by healthcare professionals in a range of settings. The objectives are: 1) to create a database of VEP templates given what is already known about stimulus and clinical conditions; 2) to create an interface allowing users to add waveforms and detailed labels to this database; 3) to create software to allow threshold VEPs to be compared to the waveforms in the database.

Methods

Normal and Clinical VEPs were digitised and stored in Microsoft Excel Spreadsheets. Software was then developed in Python to read in the files in .csv format, normalise for amplitude then latency, and store three respective templates. Next, code was written to compare the morphology of a new, test waveform to a set of normal templates and calculate the index of cross-correlation for each comparison. (Equation 1). Dynamic Type Warping (DTW) was employed to represent these differences graphically. Algorithms were then developed to calculate the difference between concurrent standard VEP recording and VEPs recorded from a site shifted anatomically given prior imaging. This applied to recordings that are normalised for amplitude only. The residual waveform is stored alongside metrics describing the displacement of the visual cortex (vector of translation) and the degree of hydrocephalus (qDESH and Intracranial pressure (ICP)) if known. A user-friendly interface was added to facilitate use by a range of professional groups.

Equation 1: Cxy[t] = ∑ x[t]y[t]

Results

The code is included as Appendices 1-3 and was effective at reading in test waveforms, comparing them, and illustrating their simularity in numerical and graphical form. This provides the basis for a retrospective clinical analysis and the potential for real-time analysis if incorporated into existing software, e.g. the Step VEP.

Discussion

Where both Clinical VA and VEP VA estimations are available in the same patient, the relationship and agreement between the two measures has been described extensively [27][30] to support its diagnostic use when other tests are not possible, or unavailable. A reduction in Step VEP stimulus reversal rate has previously been proposed (in this journal) to optimise this relationship. [31] Here we have described analyses to allow further improvements in accuracy by rejection of responses that are unlikely to be spatial in origin, and by identifying and correcting for anatomical irregularity. The latter should increase the sensitivity of VEP VA assessment.

The effects of excessive noise cancellation and distortion secondary to traveling waves has been considered, but implementation of code to address this was beyond the scope of this study. This could lead to our code rejecting genuine waveforms that have been modulated (a redution of specificity). Evidence of an interaction between the VEP and the ongoing alpha-rhythm has been proven by other authors [32] and scaling multifocal VEP responses given underlying EEG activity reduced inter-subject variability. [33] Similar local scaling could be also be attempted in real-time VEP VA assessements, and may further increase sensitivity and specificity in response detection, enabling the software to display the most appropriate range of stimuli for the individual.

Acknowledgement

Thank you to Helen Sadler for recruiting her son, Alex, to expedite the technical aspects of this work. Also thanks to my parents for their continuing support. Finally, I would like to express my gratitude to local friends Geoff Boyle, and Keith and Karen Barlow, for providing coffee, curry, wine, and excellent company.

References

  1. Yadav NK, Ludlam DP, Ciuffreda KJ. Effect of different stimulus configurations on the visual evoked potential (VEP). Doc Ophthalmol. 2012;124(3):177-196. Google Scholar ↗
  2. Fimreite V, Ciuffreda KJ, Yadav NK. Effect of luminance on the visually-evoked potential in visually-normal individuals and in mTBI/concussion. Brain Inj. 2015;29(10):1199-1210. Google Scholar ↗
  3. American Clinical Neurophysiology Society. Guideline 9B: Guidelines on visual evoked potentials. J Clin Neurophysiol. 2006;23(2):138-156. doi: DOI ↗ Google Scholar ↗
  4. Šuštar Habjan M, Bach M, van Genderen MM, et al. ISCEV standard for clinical visual evoked potentials (2025 update). Doc Ophthalmol. 2025;151(2):97-112. Google Scholar ↗
  5. Souza GS, Gomes BD, Saito CA, da Silva Filho M, Silveira LC. Spatial luminance contrast sensitivity measured with transient VEP: comparison with psychophysics and evidence of multiple mechanisms. Invest Ophthalmol Vis Sci. 2007;48(7):3396-3404. 6. Chu L., Fernandez-Vargas J., Kita K., Yu W., Chen W., Hosoda K., Menegatti E., Shimizu M., Wang H. Influence of Stimulus Color on Steady State Visual Evoked Potentials. In: Advances in Intelligent Systems and Computing. ? EDs Google Scholar ↗
  6. Kaneko S, Kuriki I, Andersen SK, Peterzell DH. Individual variability in steady-state VEP responses for hues sweeping around cardinal color axes: Clues to cortical color coding?. J Vis. 2025;25(12):2. Google Scholar ↗
  7. Mackay A.M, Bradnam, M.S, Hamilton, R. Rapid detection of threshold VEPs, Clin. Neurophysiol. 2003; 114(6): 1009-1020. Google Scholar ↗
  8. Mahajan Y, McArthur G. Maturation of visual evoked potentials across adolescence. Brain Dev. 2012;34(8):655-666. Google Scholar ↗
  9. Balakrishnan A, Natarajan N. A comparative study on visual evoked potential in normotensive and hypertensive individuals. Natl J Physiol Pharm Pharmacol. 2018; 8(10): 1437-1440. Google Scholar ↗
  10. Cao T, Wan F, Wong CM, da Cruz JN, Hu Y. Objective evaluation of fatigue by EEG spectralanalysis in steady-state visual evoked potential-based brain-computer interfaces. Biomed Eng Online. 2014;13(1):28. 11. Kothari, R, Bokariya, P, Singh, S, Narang, P Singh, R. Refractive errors and their effects on visual evoked potentials. Journal of Clinical Ophthalmology and Research 2(1):3-6. Google Scholar ↗
  11. Lin Y-P, Wang Y, Wei C-S, and Jung T-P (2014) Assessing the quality of steady-state visual-evoked potentials for moving humans using a mobile electroencephalogram headset. Front. Hum.Neurosci. 8:182. Google Scholar ↗
  12. Rahman M, Karwowski W, Fafrowicz M and Hancock PA (2019) Neuroergonomics Applications of Electroencephalography in Physical Activities: A Systematic Review. Front. Hum. Neurosci.13:182. Google Scholar ↗
  13. Balakrishnan A, Natarajan N. A comparative study on visual evoked potential in normotensive and hypertensive individuals. Natl J Physiol Pharm Pharmacol 2018;8(10):1437-1440. Google Scholar ↗
  14. Zeki, S. et al. (1991). A direct demonstration of functional specialization in human visual cortex. J. Neurosci. 11:641–649. Google Scholar ↗
  15. Kimura, J. Electrodiagnosis in diseases of nerve and muscle. Principles and Practice. Fourth Edition. Oxford University Press. 2013. ISBN 978–0–19–973868–7. Google Scholar ↗
  16. Nunez, PL. Srinivasan, R. A theoretical basis for standing and traveling brain waves measured with human EEG with implications for an integrated consciousness. Clin. Neurophysiol. 2006; 117(11):2424-2435. Google Scholar ↗
  17. Cabral, J., Castaldo, F., Vohryzek, J. et al. Metastable oscillatory modes emerge from synchronization in the brain spacetime connectome. Commun Phys 5, 184 (2022). Google Scholar ↗
  18. Burkitt GR, Silberstein RB, Cadusch PJ, Wood AW. Steady-state visual evoked potentials and travelling waves. Clin Neurophysiol. 2000;111(2):246-258. Google Scholar ↗
  19. Nunez PL, Reid L, Bickford RG. The relationship of head size to alpha frequency with implications to a brain wave model. Electroencephalogr Clin Neurophysiol. 1978;44(3):344-352. Google Scholar ↗
  20. Thorpe SG, Nunez PL, Srinivasan R. Identification of wave-like spatial structure in the SSVEP: comparison of simultaneous EEG and MEG. Stat Med. 2007;26(21):3911-3926. Google Scholar ↗
  21. Bennett RG, Tibaudo ME, Mazel EC, Y N. Implications of cerebral/cortical visual impairment on life and learning: insights and strategies from lived experiences. Front Hum Neurosci. 2025;18:1496153. Google Scholar ↗
  22. Mackay, AM. Bradnam, MS. Hamilton, R. Elliot, AT. Dutton, GN. Real-Time Rapid AcuityAssessment Using VEPs: Development and Validation of the Step VEP Technique. Invest. Ophthalmol. Vis. Sci. 2008;49(1):438-441. Google Scholar ↗
  23. Tyler CW, Apkarian P, Levi DM, Nakayama K. Rapid assessment of visual function: an electronic sweep technique for the pattern visual evoked potential. Invest Ophthalmol Vis Sci. 1979;18(7):703-713. Google Scholar ↗
  24. Whittingstall K, Wilson D, Schmidt M, Stroink G. Correspondence of visual evoked potentials with FMRI signals in human visual cortex. Brain Topogr. 2008;21(2):86-92. Google Scholar ↗
  25. Bullock M, Jackson GD and Abbott DF. Artifact Reduction in Simultaneous EEG-fMRI: A Systematic Review of Methods and Contemporary Usage. Front. Neurol. 2021; 12:622719 Google Scholar ↗
  26. Mackay AM. Step VEP visual acuity in a pediatric neuro-ophthalmological cohort. Int J Clin Exp Ophthalmol. 2022; 6: 026-030. Google Scholar ↗
  27. Mackay AM. VEP visual acuity in children with cortical visual impairment. Int J Clin Exp Ophthalmol. 2022; 6: 031-034. Google Scholar ↗
  28. Bach M, Maurer JP, Wolf ME. Visual evoked potential-based acuity assessment in normal vision, artificially degraded vision, and in patients. Br J Ophthalmol. 2008;92(3):396-403. Google Scholar ↗
  29. Hamilton R, Bach M, Heinrich SP, et al. VEP estimation of visual acuity: a systematic review. Doc Ophthalmol. 2021;142(1):25-74. Google Scholar ↗
  30. Mackay AM. Adjustments to Stimulation Frequency and Duration of STEP VEPs in Paediatric CVI. International Journal of Medical Science and Clinical Invention. 2023; 10 (7):6801-6803. Google Scholar ↗
  31. Risner ML, Aura CJ, Black JE, Gawne TJ. The Visual Evoked Potential is independent of surface alpha rhythm phase. Neuroimage. 2009;45(2):463-469. Google Scholar ↗
  32. Klistorner AI, Graham SL. Electroencephalogram-based scaling of multifocal visual evoked potentials: effect on intersubject amplitude variability. Invest Ophthalmol Vis Sci. 2001;42(9):2145-2152. Google Scholar ↗

Appendix 1

VEP Analysis - Main Application

A comprehensive tool for VEP waveform generation, analysis, and comparison

================

FILE STRUCTURE

================

vep-analysis/

├── vep_main.py              # This file - main application

├── vep_generator.py         # VEP generator with clinical norms

├── vep_comparison.py        # Comparison functionality

├── data/                    # For storing CSV files and results

│   ├── csv_files/

│   ├── results/

│   └── templates/

└── generate_test_csv.py     # CSV generator

=============

MENU TREE:

=============

Main Menu:

├── 1. Generate VEP Waveforms

│   ├── 1. Clean VEP (ideal reference)

│   ├── 2. VEP with physiological variability  

│   ├── 3. Averaged VEP (100 trials)

│   └── 4. Custom VEP parameters

│       ├── 1. Modify P100 parameters

│       └── 2. Modify all component parameters

│   

│   Sub-menu for morphology:

│   ├── 1. Ideal (symmetric Gaussian)

│   └── 2. Asymmetric Gaussian (realistic rise/fall)

├── 2. Export Template VEP Data

├── 3. Load and Examine CSV File

│   ├── [List of available CSV files]

│   ├── Browse for file...

│   └── Load physiological measurement system file (optimized)

├── 4. Compare CSV to Synthetic VEP

|   └── Shape-based Comparison (correlation analysis)

|       ├── Normalization: max_abs, p100_peak, rms, max_pos

|       └── Reference: clean, variable, averaged

├── 5. View Analysis History

├── 6. Settings and Configuration

│   ├── 1. Sampling Rate

│   ├── 2. Duration

│   ├── 3. Data Directory

│   ├── 4. Default Normalization

│   └── 5. Default Reference

├── 7. Troubleshoot CSV Format

├── 8. VEP Generator Demonstration

├── 9. Help and Documentation

└── 0. Exit

"""

import os

import sys

import glob

from pathlib import Path

import warnings

warnings.filterwarnings('ignore')

try:

from vep_generator import VEPGenerator

from vep_comparison import VEPComparator

except ImportError as e:

print(f"Error importing modules: {e}")

print("Make sure vep_generator.py and vep_comparison.py are in the same directory")

sys.exit(1)

def get_script_directory():

    """Get the directory where this script is located."""

    return os.path.dirname(os.path.abspath(__file__))

class VEPAnalysisApp:

    """

    Main application class for VEP analysis.

    Provides a menu-driven interface for various VEP analysis functions.

    """

    def __init__(self):

        """Initialize the application with default settings."""

        self.generator = None

        self.comparator = None

        self.current_csv_file = None

        self.results_history = []

        # Get script directory for proper path handling

        self.script_dir = get_script_directory()

        # Default settings with paths relative to script location

        self.settings = {

            'sampling_rate': 1000,

            'duration': 0.5,

            'data_dir': os.path.join(self.script_dir, 'data'),

            'results_dir': os.path.join(self.script_dir, 'data', 'results'),

            'default_norm_method': 'max_abs',

            'default_reference_type': 'clean'

        }

        # Create directories if they don't exist

        self._setup_directories()

        # Initialize generator and comparator

        self._initialize_tools()

    def _setup_directories(self):

        """Create necessary directories for data and results."""

        dirs_to_create = [

            self.settings['data_dir'],

            self.settings['results_dir'],

            os.path.join(self.settings['data_dir'], 'csv_files'),

            os.path.join(self.settings['data_dir'], 'templates')

        ]

        for directory in dirs_to_create:

            Path(directory).mkdir(parents=True, exist_ok=True)

        print(f"Data directories verified in: {self.settings['data_dir']}")

    def _initialize_tools(self):

        """Initialize VEP generator and comparator with current settings."""

        self.generator = VEPGenerator(

            sampling_rate=self.settings['sampling_rate'],

            duration=self.settings['duration']

        )

        self.comparator = VEPComparator(self.generator)

    def display_main_menu(self):

        """Display the main application menu."""

        print("\n" + "="*60)

        print("           VEP ANALYSIS")

        print("="*60)

        print("1. Generate VEP Waveforms")

        print("2. Export Template VEP Data")

        print("3. Load and Examine CSV File")

        print("4. Compare CSV to Synthetic VEP")

        print("5. View Analysis History")

        print("6. Settings and Configuration")

        print("7. Troubleshoot CSV Format")

        print("8. VEP Generator Demonstration")

        print("9. Help and Documentation")

        print("0. Exit")

        print("="*60)

        if self.current_csv_file:

            print(f"Current CSV: {os.path.basename(self.current_csv_file)}")

        # Show relative path for cleaner display

        rel_data_dir = os.path.relpath(self.settings['data_dir'], os.getcwd())

        print(f"Data Directory: {rel_data_dir}")

    def run(self):

        """Main application loop."""

        print("Welcome to the VEP Analysis!")

        print(f"Working directory: {self.script_dir}")

        while True:

            try:

                self.display_main_menu()

                choice = input("\nSelect option (0-9): ").strip()

                if choice == '0':

                    print("Thank you for using VEP Analysis!")

                    break

                elif choice == '1':

                    self.generate_vep_waveforms()

                elif choice == '2':

                    self.export_template_data()

                elif choice == '3':

                    self.load_csv_file()

                elif choice == '4':

                    self.compare_csv_to_synthetic()

                elif choice == '5':

                    self.view_history()

                elif choice == '6':

                    self.settings_menu()

                elif choice == '7':

                    self.troubleshoot_csv()

                elif choice == '8':

                    self.vep_demonstration()

                elif choice == '9':

                    self.show_help()

                else:

                    print("Invalid choice. Please try again.")

                input("\nPress Enter to continue...")

            except KeyboardInterrupt:

                print("\n\nExiting...")

                break

            except Exception as e:

                print(f"An error occurred: {e}")

                input("Press Enter to continue...")

    def generate_vep_waveforms(self):

        """Generate and display VEP waveforms with morphology options."""

        print("\n" + "="*50)

        print("GENERATE VEP WAVEFORMS")

        print("="*50)

        print("VEP Types:")

        print("1. Clean VEP (ideal reference)")

        print("2. VEP with physiological variability")

        print("3. Averaged VEP (100 trials)")

        print("4. Custom VEP parameters")

        vep_choice = input("\nSelect VEP type (1-4): ").strip()

        if vep_choice not in ['1', '2', '3', '4']:

            print("Invalid choice.")

            return

        # For options 1-3, ask for morphology type

        if vep_choice in ['1', '2', '3']:

            print("\nMorphology Options:")

            print("1. Ideal (symmetric Gaussian)")

            print("2. Asymmetric Gaussian (realistic rise/fall)")

            morph_choice = input("Select morphology (1-2): ").strip()

            morphology_map = {

                '1': 'gaussian',

                '2': 'asymmetric_gaussian'

            }

            morphology = morphology_map.get(morph_choice, 'asymmetric_gaussian')

            print(f"\nGenerating {['Clean', 'Variable', 'Averaged'][int(vep_choice)-1]} VEP with {morphology} morphology...")

        try:

            if vep_choice == '1':

                # Clean VEP

                vep = self.generator.generate_baseline_vep(add_variability=False, morphology=morphology)

                title = f'Clean VEP ({morphology.replace("_", " ").title()} Morphology)'

                self.generator.plot_vep(vep, title=title)

            elif vep_choice == '2':

                # VEP with variability

                vep = self.generator.generate_baseline_vep(add_variability=True, morphology=morphology)

                title = f'VEP with Variability ({morphology.replace("_", " ").title()} Morphology)'

                self.generator.plot_vep(vep, title=title)

            elif vep_choice == '3':

                # Averaged VEP

                n_trials = int(input("Enter number of trials (default 100): ") or "100")

                trials, averaged = self.generator.generate_trial_batch(

                    n_trials=n_trials, 

                    add_noise=False,

                    morphology=morphology

                )

                title = f'Averaged VEP - {n_trials} trials ({morphology.replace("_", " ").title()})'

                # Show comparison plot

                self.generator.plot_trials_comparison(trials, averaged, morphology)

            elif vep_choice == '4':

                # Custom parameters

                self.custom_vep_parameters()

        except ValueError as e:

            print(f"Invalid input: {e}")

        except Exception as e:

            print(f"Error generating VEP: {e}")

    def custom_vep_parameters(self):

        """Generate VEP with custom clinical parameters."""

        print("\n" + "="*40)

        print("CUSTOM VEP PARAMETERS")

        print("="*40)

        print("Choose parameter modification:")

        print("1. Modify P100 parameters")

        print("2. Modify all component parameters")

        choice = input("Select option (1-2): ").strip()

        try:

            if choice == '1':

                self._modify_p100_parameters()

            elif choice == '2':

                self._modify_all_parameters()

            else:

                print("Invalid choice.")

        except Exception as e:

            print(f"Error in custom parameters: {e}")

    def _modify_p100_parameters(self):

        """Modify P100 component parameters."""

        print("\nModify P100 Parameters (press Enter to keep current):")

        current_p100 = self.generator.clinical_norms['P100']

        # P100 latency

        lat_input = input(f"P100 latency mean (ms) [current: {current_p100['latency_mean']}]: ")

        if lat_input:

            self.generator.clinical_norms['P100']['latency_mean'] = float(lat_input)

        # P100 amplitude

        amp_input = input(f"P100 amplitude mean (μV) [current: {current_p100['amplitude_mean']}]: ")

        if amp_input:

            self.generator.clinical_norms['P100']['amplitude_mean'] = float(amp_input)

        # Variability

        sd_input = input(f"P100 latency SD (ms) [current: {current_p100['latency_sd']}]: ")

        if sd_input:

            self.generator.clinical_norms['P100']['latency_sd'] = float(sd_input)

        # Select morphology

        print("\nMorphology:")

        print("1. Gaussian  2. Asymmetric")

        morph_choice = input("Select morphology (1-2): ").strip()

        morphologies = {'1': 'gaussian', '2': 'asymmetric_gaussian'}

        morphology = morphologies.get(morph_choice, 'asymmetric_gaussian')

        # Generate and plot

        vep = self.generator.generate_baseline_vep(add_variability=True, morphology=morphology)

        title = f'Custom P100 VEP - Lat:{self.generator.clinical_norms["P100"]["latency_mean"]}ms, Amp:{self.generator.clinical_norms["P100"]["amplitude_mean"]}μV'

        self.generator.plot_vep(vep, title=title)

    def _modify_all_parameters(self):

        """Modify all component parameters."""

        print("\nModify All Component Parameters:")

        print("(press Enter to keep current values)")

        for comp_name in ['N75', 'P100', 'N135']:

            print(f"\n{comp_name} Component:")

            current = self.generator.clinical_norms[comp_name]

            # Latency

            lat_input = input(f"  Latency mean (ms) [current: {current['latency_mean']}]: ")

            if lat_input:

                self.generator.clinical_norms[comp_name]['latency_mean'] = float(lat_input)

            # Amplitude

            amp_input = input(f"  Amplitude mean (μV) [current: {current['amplitude_mean']}]: ")

            if amp_input:

                self.generator.clinical_norms[comp_name]['amplitude_mean'] = float(amp_input)

        # Select morphology

        print("\nMorphology:")

        print("1. Gaussian  2. Asymmetric")

        morph_choice = input("Select morphology (1-2): ").strip()

        morphologies = {'1': 'gaussian', '2': 'asymmetric_gaussian'}

        morphology = morphologies.get(morph_choice, 'asymmetric_gaussian')

        # Generate and plot

        vep = self.generator.generate_baseline_vep(add_variability=True, morphology=morphology)

        self.generator.plot_vep(vep, title=f'Custom All Components VEP ({morphology.replace("_", " ").title()})')

    def export_template_data(self):

        """Export template VEP data for reference."""

        print("\n" + "="*50)

        print("EXPORT TEMPLATE VEP DATA")

        print("="*50)

        output_dir = os.path.join(self.settings['data_dir'], 'templates')

        print("Generating template VEP data...")

        try:

            # Generate different types of VEP data with different morphologies

            templates = {}

            for morphology in ['gaussian', 'asymmetric_gaussian']:

                morph_name = morphology.replace('_', '-')

                templates[f'clean-vep-{morph_name}.csv'] = self.generator.generate_baseline_vep(

                    add_variability=False, morphology=morphology)

                templates[f'variable-vep-{morph_name}.csv'] = self.generator.generate_baseline_vep(

                    add_variability=True, morphology=morphology)

                # Generate averaged VEP

                _, avg_vep = self.generator.generate_trial_batch(

                    n_trials=100, add_noise=False, morphology=morphology)

                templates[f'averaged-vep-{morph_name}.csv'] = avg_vep

            # Export to CSV files

            import pandas as pd

            time_ms = self.generator.times * 1000

            for filename, vep_data in templates.items():

                filepath = os.path.join(output_dir, filename)

                df = pd.DataFrame({

                    'Time_ms': time_ms,

                    'Amplitude_uV': vep_data

                })

                df.to_csv(filepath, index=False)

                print(f"Exported: {filename}")

            # Show relative path for cleaner display

            rel_output_dir = os.path.relpath(output_dir, os.getcwd())

            print(f"\nTemplate files saved to: {rel_output_dir}")

            print(f"Generated {len(templates)} template files with 3 morphology types")

        except Exception as e:

            print(f"Error exporting templates: {e}")

    def load_csv_file(self):

        """Load and examine a CSV file."""

        print("\n" + "="*50)

        print("LOAD AND EXAMINE CSV FILE")

        print("="*50)

        # Show available CSV files

        csv_pattern = os.path.join(self.settings['data_dir'], "**/*.csv")

        csv_files = glob.glob(csv_pattern, recursive=True)

        if csv_files:

            print("Available CSV files:")

            for i, file in enumerate(csv_files, 1):

                rel_path = os.path.relpath(file, self.settings['data_dir'])

                print(f"{i:2d}. {rel_path}")

            print(f"{len(csv_files)+1:2d}. Browse for file...")

            print(f"{len(csv_files)+2:2d}. Load physiological measurement system file (optimized)")

        else:

            print("No CSV files found in data directory.")

            print("1. Browse for file...")

            print("2. Load physiological measurement system file (optimized)")

            print("\nTip: Run option 2 (Export Template Data) to create test files!")

        try:

            max_choice = len(csv_files) + 2 if csv_files else 2

            choice = input(f"\nSelect file (1-{max_choice}): ").strip()

            if csv_files and choice.isdigit() and 1 <= int(choice) <= len(csv_files):

                filepath = csv_files[int(choice)-1]

                # Use standard loading

                real_data = self.comparator.load_csv_data(filepath)

            elif choice == str(len(csv_files)+1 if csv_files else 1):

                filepath = input("Enter full path to CSV file: ").strip()

                if not os.path.exists(filepath):

                    print(f"File not found: {filepath}")

                    return

                real_data = self.comparator.load_csv_data(filepath)

            elif choice == str(len(csv_files)+2 if csv_files else 2):

                # Optimized loading for physiological measurement systems

                filepath = input("Enter path to physiological measurement CSV: ").strip()

                if not os.path.exists(filepath):

                    print(f"File not found: {filepath}")

                    return

                print("Loading with physiological measurement system optimizations...")

                real_data = self.load_physiological_csv(filepath)

            else:

                print("Invalid choice.")

                return

            self.current_csv_file = filepath

            print(f"\nFile loaded successfully!")

            print(f"Duration: {real_data['duration']:.3f}s")

            print(f"Samples: {real_data['n_samples']}")

            print(f"Sampling Rate: {real_data['sampling_rate']:.1f}Hz")

            # Plot the loaded data

            import matplotlib.pyplot as plt

            time_ms = real_data['time'] * 1000

            plt.figure(figsize=(12, 6))

            plt.plot(time_ms, real_data['amplitude'], 'b-', linewidth=2)

            plt.xlabel('Time (ms)')

            plt.ylabel('Amplitude (μV)')

            plt.title(f'Loaded VEP Data: {os.path.basename(filepath)}')

            plt.grid(True, alpha=0.3)

            plt.show()

        except Exception as e:

            print(f"Error loading CSV file: {e}")

    def load_physiological_csv(self, filepath):

        """

        Optimized loading for physiological measurement system CSV files.

        Handles formats like: "1ms; 0.00001V"

        """

        print("Detecting physiological measurement format...")

        # First, examine the file structure

        with open(filepath, 'r') as f:

            first_lines = [f.readline().strip() for _ in range(5)]

        print("First few lines:")

        for i, line in enumerate(first_lines, 1):

            print(f"  {i}: {line}")

        # Detect separator and format

        separators = [';', ',', '\t']

        separator = ';'  # Default for physiological systems

        for sep in separators:

            if sep in first_lines[0]:

                separator = sep

                break

        print(f"Using separator: '{separator}'")

        # Check if first line looks like header or data

        has_header = not any(char.isdigit() for char in first_lines[0].split(separator)[0])

        print(f"Has header: {has_header}")

        try:

            # Load with detected parameters

            real_data = self.comparator.load_csv_data(

                filepath=filepath,

                time_col=0,           # First column is time

                data_col=1,           # Second column is voltage

                time_unit='ms',       # Assume milliseconds

                has_header=has_header

            )

            print("Successfully loaded physiological measurement data")

            return real_data

        except Exception as e:

            print(f"Error with optimized loading: {e}")

            # Fallback to standard loading

            print("Trying standard loading...")

            return self.comparator.load_csv_data(filepath)

    def compare_csv_to_synthetic(self):

        """Compare loaded CSV file - choose comparison type."""

        if not self.current_csv_file:

            print("No CSV file loaded. Please load a file first (option 3).")

            return

        print("\n" + "="*50)

        print("VEP ANALYSIS OPTIONS")

        print("="*50)

        print(f"Current file: {os.path.basename(self.current_csv_file)}")

        # Only shape-based comparison is implemented in this version

        self.shape_based_comparison()

    def shape_based_comparison(self):

        """Original shape-based comparison method."""

        print("\n" + "="*50)

        print("SHAPE-BASED COMPARISON")

        print("="*50)

        # Comparison options

        print("\nNormalization methods:")

        print("1. Maximum absolute value")

        print("2. P100 peak (90-120ms region)")

        print("3. RMS value")

        print("4. Maximum positive value")

        norm_methods = {

            '1': 'max_abs',

            '2': 'p100_peak', 

            '3': 'rms',

            '4': 'max_pos'

        }

        norm_choice = input("Select normalization method (1-4): ").strip()

        norm_method = norm_methods.get(norm_choice, 'max_abs')

        print("\nReference waveform types:")

        print("1. Clean (no variability)")

        print("2. With physiological variability")

        print("3. Averaged (100 trials)")

        ref_types = {

            '1': 'clean',

            '2': 'variable',

            '3': 'averaged'

        }

        ref_choice = input("Select reference type (1-3): ").strip()

        ref_type = ref_types.get(ref_choice, 'clean')

        try:

            # Load the current CSV data

            real_data = self.comparator.load_csv_data(self.current_csv_file)

            # Perform comparison

            print(f"\nPerforming shape-based comparison...")

            print(f"Normalization: {norm_method}")

            print(f"Reference: {ref_type}")

            comparison = self.comparator.compare_with_reference(

                real_data,

                norm_method=norm_method,

                reference_type=ref_type,

                plot=True

            )

            # Store result in history

            result_summary = {

                'file': self.current_csv_file,

                'analysis_type': 'shape_based',

                'norm_method': norm_method,

                'reference_type': ref_type,

                'quality': comparison['quality_assessment']['overall'],

                'shape_score': comparison['quality_assessment']['shape_score'],

                'pearson_r': comparison['metrics']['pearson_r'],

                'rmse': comparison['metrics']['rmse']

            }

            self.results_history.append(result_summary)

            # Display summary

            print(f"\n" + "="*40)

            print("SHAPE-BASED COMPARISON SUMMARY")

            print("="*40)

            print(f"Quality: {comparison['quality_assessment']['overall']}")

            print(f"Shape Score: {comparison['quality_assessment']['shape_score']:.3f}")

            print(f"Pearson r: {comparison['metrics']['pearson_r']:.3f}")

            print(f"RMSE: {comparison['metrics']['rmse']:.3f}")

            print(f"Cosine Similarity: {comparison['metrics']['cosine_similarity']:.3f}")

            # Save results option

            save_choice = input("\nSave detailed results to file? (y/n): ").strip().lower()

            if save_choice == 'y':

                self.save_comparison_results(comparison)

        except Exception as e:

            print(f"Error performing shape-based comparison: {e}")

    def view_history(self):

        """Updated view history to handle both analysis types."""

        print("\n" + "="*50)

        print("ANALYSIS HISTORY")

        print("="*50)

        if not self.results_history:

            print("No analysis history available.")

            return

        # Group by analysis type for better organization

        shape_based = [r for r in self.results_history if r.get('analysis_type') == 'shape_based']

        if shape_based:

            print("\nSHAPE-BASED COMPARISONS:")

            print(f"{'#':<3} {'File':<25} {'Quality':<10} {'Score':<6} {'r':<6} {'RMSE':<6}")

            print("-" * 60)

            for i, result in enumerate(shape_based, 1):

                filename = os.path.basename(result['file'])[:24]

                print(f"{i:<3} {filename:<25} {result['quality']:<10} "

                    f"{result['shape_score']:<6.3f} {result['pearson_r']:<6.3f} "

                    f"{result['rmse']:<6.3f}")

    def settings_menu(self):

        """Display and modify application settings."""

        print("\n" + "="*50)

        print("SETTINGS AND CONFIGURATION")

        print("="*50)

        while True:

            print(f"\nCurrent Settings:")

            print(f"1. Sampling Rate: {self.settings['sampling_rate']} Hz")

            print(f"2. Duration: {self.settings['duration']} s")

            # Show relative path for cleaner display

            rel_data_dir = os.path.relpath(self.settings['data_dir'], os.getcwd())

            print(f"3. Data Directory: {rel_data_dir}")

            print(f"4. Default Normalization: {self.settings['default_norm_method']}")

            print(f"5. Default Reference: {self.settings['default_reference_type']}")

            print(f"0. Back to main menu")

            choice = input("\nSelect setting to change (0-5): ").strip()

            if choice == '0':

                break

            elif choice == '1':

                try:

                    new_rate = int(input(f"Enter sampling rate (current: {self.settings['sampling_rate']}): "))

                    self.settings['sampling_rate'] = new_rate

                    self._initialize_tools()  # Reinitialize with new settings

                    print("Sampling rate updated.")

                except ValueError:

                    print("Invalid input.")

            elif choice == '2':

                try:

                    new_duration = float(input(f"Enter duration in seconds (current: {self.settings['duration']}): "))

                    self.settings['duration'] = new_duration

                    self._initialize_tools()  # Reinitialize with new settings

                    print("Duration updated.")

                except ValueError:

                    print("Invalid input.")

            elif choice == '3':

                print("Data directory is fixed relative to script location.")

                print(f"Current absolute path: {self.settings['data_dir']}")

            elif choice == '4':

                print("1=max_abs, 2=p100_peak, 3=rms, 4=max_pos")

                norm_choice = input("Select default normalization: ")

                norm_map = {'1': 'max_abs', '2': 'p100_peak', '3': 'rms', '4': 'max_pos'}

                if norm_choice in norm_map:

                    self.settings['default_norm_method'] = norm_map[norm_choice]

                    print("Default normalization updated.")

            elif choice == '5':

                print("1=clean, 2=variable, 3=averaged")

                ref_choice = input("Select default reference: ")

                ref_map = {'1': 'clean', '2': 'variable', '3': 'averaged'}

                if ref_choice in ref_map:

                    self.settings['default_reference_type'] = ref_map[ref_choice]

                    print("Default reference updated.")

    def troubleshoot_csv(self):

        """Help troubleshoot CSV format issues."""

        print("\n" + "="*50)

        print("CSV FORMAT TROUBLESHOOTING")

        print("="*50)

        filepath = input("Enter path to problematic CSV file: ").strip()

        if not os.path.exists(filepath):

            print(f"File not found: {filepath}")

            return

        try:

            # Examine file structure

            print(f"\nExamining: {filepath}")

            print("-" * 40)

            # Read first few lines

            with open(filepath, 'r') as f:

                lines = f.readlines()[:10]

            print("First 10 lines:")

            for i, line in enumerate(lines, 1):

                print(f"{i:2d}: {line.strip()}")

            # Analyze separators

            print(f"\nSeparator Analysis:")

            first_line = lines[0].strip()

            separators = [',', ';', '\t', '|']

            for sep in separators:

                count = first_line.count(sep)

                if count > 0:

                    parts = first_line.split(sep)

                    print(f"  '{sep}': {count} occurrences -> {len(parts)} parts")

                    if len(parts) == 2:

                        print(f"       Good for 2-column data")

                        print(f"       Part 1: '{parts[0]}'")

                        print(f"       Part 2: '{parts[1]}'")

            # Try pandas analysis with different methods

            import pandas as pd

            print(f"\nPandas Reading Tests:")

            methods = [

                ("Default", {}),

                ("Semicolon + Python engine", {"sep": ";", "engine": "python"}),

                ("Semicolon + Default engine", {"sep": ";"}),

                ("No header + Semicolon", {"sep": ";", "header": None}),

            ]

            for method_name, kwargs in methods:

                try:

                    df = pd.read_csv(filepath, **kwargs)

                    print(f"  {method_name}: Success - Shape {df.shape}")

                    if df.shape[1] >= 2:

                        print(f"    Columns: {list(df.columns)}")

                        print(f"    Sample data: {df.iloc[0].tolist()}")

                    else:

                        print(f"    Warning: Only {df.shape[1]} column(s)")

                except Exception as e:

                    print(f"  {method_name}: Failed - {str(e)[:50]}...")

            # Manual parsing test for semicolon data

            if ';' in first_line:

                print(f"\nManual Semicolon Parsing Test:")

                try:

                    parsed_data = []

                    for i, line in enumerate(lines[:5]):

                        line = line.strip()

                        if ';' in line:

                            parts = line.split(';')

                            if len(parts) >= 2:

                                parsed_data.append([parts[0], parts[1]])

                                print(f"  Row {i+1}: '{parts[0]}' | '{parts[1]}'")

                    if parsed_data:

                        print(f"  Manual parsing successful for {len(parsed_data)} rows")

                    else:

                        print(f"  No valid rows found")

                except Exception as e:

                    print(f"  Manual parsing failed: {e}")

            # Unit detection

            print(f"\nUnit Detection:")

            time_units = ['ms', 's', 'samples']

            voltage_units = ['V', 'mV', 'uV', 'μV']

            text_content = ''.join(lines[:5])

            detected_time_units = [unit for unit in time_units if unit in text_content]

            detected_voltage_units = [unit for unit in voltage_units if unit in text_content]

            print(f"  Time units detected: {detected_time_units}")

            print(f"  Voltage units detected: {detected_voltage_units}")

            # Provide specific recommendations

            print(f"\n" + "="*40)

            print("RECOMMENDATIONS:")

            print("="*40)

            if ';' in first_line:

                print("SEMICOLON FORMAT DETECTED:")

                print("  Your file uses semicolons (common in physiological systems)")

                print("  Solutions:")

                print("  1. Try the 'Load physiological measurement system file' option")

                print("  2. Use manual parsing in the comparison tool")

            if any(unit in text_content for unit in time_units + voltage_units):

                print("UNITS IN DATA DETECTED:")

                print("  Your data contains units (ms, V, etc.) embedded in values")

                print("  The system should automatically remove these")

            print("\nIf issues persist:")

            print("  • Try creating new CSV with Export Template Data (option 2)")

            print("  • Use Load physiological measurement system file (option 3.2)")

            print("  • Check that your CSV has exactly 2 columns")

        except Exception as e:

            print(f"Error examining file: {e}")

            print("\nBasic checks:")

            print("• File exists and is readable")

            print("• Contains numeric data")

            print("• Has consistent separator throughout")

            print("• No extra quotes or special characters")

    def vep_demonstration(self):

        """Run the VEP generator demonstration."""

        print("\n" + "="*50)

        print("VEP GENERATOR DEMONSTRATION")

        print("="*50)

        print("This will run the full VEP generator demonstration...")

        proceed = input("Continue? (y/n): ").strip().lower()

        if proceed == 'y':

            try:

                # Import and run demonstration from original file

                from vep_generator import demonstrate_vep_generator

                demonstrate_vep_generator()

            except Exception as e:

                print(f"Error running demonstration: {e}")

    def show_help(self):

        """Display comprehensive help and documentation."""

        help_text = """

        VEP ANALYSIS - HELP & DOCUMENTATION

        OVERVIEW:

        This application provides comprehensive tools for Visual Evoked Potential (VEP) 

        analysis, including synthetic waveform generation, data loading, and clinical 

        assessment capabilities.

        MENU OPTIONS EXPLAINED:

        ═══════════════════════════════════════════════════════════════════════════════

        1. GENERATE VEP WAVEFORMS

        ═══════════════════════════════════════════════════════════════════════════════

        Creates synthetic VEP waveforms for testing and reference purposes.

        Sub-options:

        - Clean VEP: Perfect theoretical waveform with no noise or variability

        - Variable VEP: Realistic waveform with physiological variability added  

        - Averaged VEP: Simulates multiple trials averaged together (reduces noise)

        - Custom VEP: Modify clinical parameters (latencies, amplitudes) manually

        Morphology Types:

        - Gaussian: Symmetric mathematical model (ideal but unrealistic)

        - Asymmetric Gaussian: More realistic with different rise/fall times

        Use When: Testing analysis methods, creating reference data, education

        ═══════════════════════════════════════════════════════════════════════════════

        2. EXPORT TEMPLATE VEP DATA

        ═══════════════════════════════════════════════════════════════════════════════

        Generates and saves standard VEP waveforms as .csv files for testing.

        What It Does:

        - Creates clean, variable, and averaged VEPs for each morphology type

        - Saves as properly formatted CSV files in data/templates/ directory  

        - Generates 9 template files total (3 types × 3 morphologies)

        Use When: Need test data for analysis, validating CSV loading, demonstration

        ═══════════════════════════════════════════════════════════════════════════════

        3. LOAD AND EXAMINE CSV FILE

        ═══════════════════════════════════════════════════════════════════════════════

        Loads VEP data from CSV files and displays basic information.

        Features:

        - Lists all available CSV files in data directory

        - Manual file browsing option

        - Specialized loading for physiological measurement systems

        - Automatic format detection and unit conversion

        - Visual plot of loaded waveform

        - Handles various separators (comma, semicolon, tab)

        Supported Formats:

        - Standard: "Time_ms, Amplitude_uV"

        - Physiological: "1ms; 0.00001V" 

        - Time units: ms, s, sample indices (auto-detected)

        - Voltage units: V, mV, μV (auto-converted to μV)

        Use When: Loading real VEP recordings, examining data quality, preparation for analysis

        ═══════════════════════════════════════════════════════════════════════════════

        4. COMPARE CSV TO SYNTHETIC VEP

        ═══════════════════════════════════════════════════════════════════════════════

        Main analysis engine - compares loaded VEP data using two methods.

        Analysis Options:

        A) SHAPE-BASED COMPARISON (Research/Development):

        • Correlation analysis against synthetic references

        • Multiple normalization methods (max_abs, p100_peak, rms, max_pos)

        • Shape similarity scoring (Pearson r, cosine similarity, RMSE)

        • Good for waveform validation and research applications

        B) CLINICAL ASSESSMENT (Medical/Diagnostic):

        • Extracts N75, P100, N135 component parameters

        • Compares against clinical normative ranges

        • Calculates Z-scores and percentiles

        • Provides clinical interpretation (normal/borderline/abnormal)

        • Generates diagnostic recommendations

        • Matches hospital/clinical workflow

        C) BOTH ANALYSES:

        • Runs complete evaluation with both methods

        • Comprehensive report for research and clinical use

        Use When: Analyzing patient VEP recordings, research studies, clinical diagnosis

        ═══════════════════════════════════════════════════════════════════════════════

        5. VIEW ANALYSIS HISTORY

        ═══════════════════════════════════════════════════════════════════════════════

        Displays summary of all previous analyses performed in current session.

        What It Shows:

        - Shape-based results: quality scores, correlation values, RMSE

        - Clinical results: severity assessments, P100 parameters, Z-scores

        - File names and analysis parameters used

        - Organized by analysis type for easy review

        Use When: Reviewing previous results, comparing multiple files, session summary

        ═══════════════════════════════════════════════════════════════════════════════

        6. SETTINGS AND CONFIGURATION

        ═══════════════════════════════════════════════════════════════════════════════

        Modify application parameters and default behaviors.

        Configurable Settings:

        - Sampling Rate: Hz for generated waveforms (default: 1000 Hz)

        - Duration: Length of generated waveforms (default: 0.5 s)

        - Data Directory: Location for CSV files (fixed relative to script)

        - Default Normalization: Method for shape-based comparisons

        - Default Reference: Type of synthetic reference waveform

        Use When: Adapting to different recording systems, changing analysis defaults

        ═══════════════════════════════════════════════════════════════════════════════

        7. TROUBLESHOOT CSV FORMAT

        ═══════════════════════════════════════════════════════════════════════════════

        Diagnostic tool for problematic CSV files.

        What It Does:

        - Examines file structure and content

        - Tests different separator detection methods

        - Identifies encoding and formatting issues

        - Shows parsing attempts with multiple methods

        - Provides specific recommendations for fixes

        Common Issues It Helps With:

        - Wrong separator (semicolon vs comma)

        - Units embedded in data ("1ms", "0.5V")  

        - Header detection problems

        - Encoding issues

        - Multiple columns or unexpected format

        Use When: CSV files won't load properly, format errors, data import problems

        ═══════════════════════════════════════════════════════════════════════════════

        8. VEP GENERATOR DEMONSTRATION

        ═══════════════════════════════════════════════════════════════════════════════

        Runs comprehensive demonstration of VEP generation capabilities.

        What It Shows:

        - All morphology types with comparisons

        - Effect of variability and noise

        - Individual trials vs averaged responses

        - Clinical parameter variations

        - Full plotting and visualization suite

        Use When: Learning about VEPs, understanding morphology differences, education

        ═══════════════════════════════════════════════════════════════════════════════

        9. HELP AND DOCUMENTATION

        ═══════════════════════════════════════════════════════════════════════════════

        This help system - comprehensive documentation and usage guide.

        ═══════════════════════════════════════════════════════════════════════════════

        0. EXIT

        ═══════════════════════════════════════════════════════════════════════════════

        Safely exits the application.

        ═══════════════════════════════════════════════════════════════════════════════

        TYPICAL WORKFLOWS:

        FOR LEARNING/EDUCATION:

        1 → 8 → 2 → 3 → 4 (Generate → Demo → Export → Load → Analyze)

        FOR CLINICAL USE:  

        3 → 4 (Clinical Assessment) → 5 (Load patient data → Diagnose → Review)

        FOR RESEARCH/DEVELOPMENT:

        1 → 2 → 3 → 4 (Shape-based) → 6 (Generate → Export → Load → Analyze → Configure)

        FOR TROUBLESHOOTING:

        7 → 3 → 4 (Fix format → Load → Analyze)

        ═══════════════════════════════════════════════════════════════════════════════

        FILE LOCATIONS:

        - Input CSV files: vep-analysis/data/csv_files/

        - Template files: vep-analysis/data/templates/  

        - Analysis results: vep-analysis/data/results/

        - All paths relative to script location

        TECHNICAL REQUIREMENTS:

        - Python 3.6+ with matplotlib, pandas, numpy, scipy

        - CSV files: 2 columns (time, amplitude)

        - Recommended sampling rate: 1000+ Hz

        - Typical VEP duration: 200-500 ms

        For technical details and source code documentation, see individual module files:

        - vep_generator.py - Waveform generation

        - vep_comparison.py - Shape-based analysis  

        - vep_clinical_assessor.py - Clinical assessment

        ═══════════════════════════════════════════════════════════════════════════════

        """

        print(help_text)

    def save_comparison_results(self, comparison):

        """Save detailed comparison results to file."""

        try:

            import json

            import datetime

            timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")

            filename = f"vep_comparison_{timestamp}.json"

            filepath = os.path.join(self.settings['results_dir'], filename)

            # Prepare data for JSON serialization

            result_data = {

                'timestamp': timestamp,

                'file_analyzed': comparison['real_data']['filepath'],

                'settings': {

                    'normalization_method': comparison['normalized']['method'],

                    'reference_type': comparison['reference']['type']

                },

                'metrics': comparison['metrics'],

                'quality_assessment': comparison['quality_assessment']

            }

            with open(filepath, 'w') as f:

                json.dump(result_data, f, indent=2)

            # Show relative path for cleaner display

            rel_filepath = os.path.relpath(filepath, os.getcwd())

            print(f"Results saved to: {rel_filepath}")

        except Exception as e:

            print(f"Error saving results: {e}")

# Main execution

def main():

    """Main entry point for the VEP Analysis."""

    try:

        app = VEPAnalysisApp()

        app.run()

    except Exception as e:

        print(f"Fatal error: {e}")

        input("Press Enter to exit...")

if __name__ == "__main__":

    main()

Appendix 2

Simple VEP Test CSV Generator

Creates clean, ideal VEP waveforms for testing using Gaussian components

Note: Component amplitudes are adjusted to compensate for Gaussian overlap.

When multiple Gaussians sum together, their tails interact, reducing peak values.

The adjusted amplitudes ensure the actual peaks match clinical expectations:

- P100 peak: ~10 μV

- N75 trough: ~-4 μV

- N135 trough: ~-8 μV

"""

import numpy as np

import os

from pathlib import Path

def create_data_folders():

"""Create the necessary data folder structure."""

script_dir = os.path.dirname(os.path.abspath(__file__))

folders = [

os.path.join(script_dir, 'data'),

os.path.join(script_dir, 'data', 'csv_files'),

os.path.join(script_dir, 'data', 'results')

]

for folder in folders:

Path(folder).mkdir(parents=True, exist_ok=True)

print(f"✓ Data folders created/verified in: {os.path.join(script_dir, 'data')}")

return os.path.join(script_dir, 'data', 'csv_files')

def generate_gaussian_component(time_data, latency, amplitude, width):

"""

Generate a single Gaussian VEP component.

Parameters:

-----------

time_data : array

Time points in seconds

latency : float

Component latency in seconds

amplitude : float

Peak amplitude in microvolts

width : float

Standard deviation in seconds

Returns:

--------

component : array

Gaussian component waveform

"""

return amplitude * np.exp(-0.5 * ((time_data - latency) / width) ** 2)

def generate_ideal_vep(time_data):

"""

Generate a clean, ideal VEP waveform using Gaussian components.

Parameters:

-----------

time_data : array

Time points in seconds

Returns:

--------

vep : array

Clean VEP waveform in microvolts

"""

vep = np.zeros(len(time_data))

# N75 component - negative deflection at 75ms

n75 = generate_gaussian_component(

time_data,

latency=0.075, # 75ms

amplitude=-5.0, # -4 μV

width=0.015 # 15ms width

)

vep += n75

# P100 component - main positive peak at 100ms

p100 = generate_gaussian_component(

time_data,

latency=0.100, # 100ms

amplitude=14.5, # 10 μV (most prominent)

width=0.020 # 20ms width

)

vep += p100

# N135 component - negative deflection at 135ms

n135 = generate_gaussian_component(

time_data,

latency=0.135, # 135ms

amplitude=-11.0, # -8 μV

width=0.025 # 25ms width

)

vep += n135

return vep

def generate_delayed_p100_vep(time_data):

"""

Generate VEP with delayed P100 component (abnormal).

Parameters:

-----------

time_data : array

Time points in seconds

Returns:

--------

vep : array

VEP with delayed P100 in microvolts

"""

vep = np.zeros(len(time_data))

# N75 - normal timing

n75 = generate_gaussian_component(

time_data,

latency=0.075,

amplitude=-4.0,

width=0.015

)

vep += n75

# P100 - DELAYED to 120ms (abnormal)

p100_delayed = generate_gaussian_component(

time_data,

latency=0.120, # 120ms (delayed by 20ms)

amplitude=10.0,

width=0.020

)

vep += p100_delayed

# N135 - shifted later due to P100 delay

n135_shifted = generate_gaussian_component(

time_data,

latency=0.155, # 155ms (also shifted)

amplitude=-8.0,

width=0.025

)

vep += n135_shifted

return vep

def generate_absent_p100_vep(time_data):

"""

Generate VEP with absent P100 component (severe abnormality).

Parameters:

-----------

time_data : array

Time points in seconds

Returns:

--------

vep : array

VEP without P100 in microvolts

"""

vep = np.zeros(len(time_data))

# N75 - present but reduced

n75 = generate_gaussian_component(

time_data,

latency=0.075,

amplitude=-3.0, # Reduced amplitude

width=0.020 # Slightly wider

)

vep += n75

# P100 - ABSENT (no component added)

# N135 - present

n135 = generate_gaussian_component(

time_data,

latency=0.135,

amplitude=-7.0, # Slightly reduced

width=0.030 # Wider

)

vep += n135

return vep

def generate_reduced_amplitude_vep(time_data):

"""

Generate VEP with reduced overall amplitude.

Parameters:

-----------

time_data : array

Time points in seconds

Returns:

--------

vep : array

Low amplitude VEP in microvolts

"""

# Generate normal VEP and reduce amplitude

vep = generate_ideal_vep(time_data)

return vep * 0.5 # 50% reduced amplitude

def generate_noisy_vep(time_data, noise_level=2.0):

"""

Generate ideal VEP with added noise for testing filtering.

Parameters:

-----------

time_data : array

Time points in seconds

noise_level : float

Standard deviation of noise in microvolts

Returns:

--------

vep : array

VEP with noise in microvolts

"""

# Start with ideal VEP

vep = generate_ideal_vep(time_data)

# Add Gaussian noise

noise = np.random.normal(0, noise_level, len(time_data))

return vep + noise

def save_csv(filename, time_ms, amplitude_uv):

"""

Save VEP data to CSV file with headers.

Parameters:

-----------

filename : str

Output filename

time_ms : array

Time points in milliseconds

amplitude_uv : array

Amplitude in microvolts

"""

with open(filename, 'w') as f:

# Write header

f.write("Time_ms,Amplitude_uV\n")

# Write data

for t, a in zip(time_ms, amplitude_uv):

f.write(f"{t:.3f},{a:.6f}\n")

print(f" ✓ Saved: {os.path.basename(filename)}")

def create_test_files(output_dir):

"""

Create a set of test VEP CSV files.

Parameters:

-----------

output_dir : str

Directory to save files

"""

# Setup time data

duration = 0.5 # 500ms

sampling_rate = 1000 # 1000 Hz

time_data = np.arange(0, duration, 1/sampling_rate)

time_ms = time_data * 1000

files_created = []

print("\nGenerating test VEP files:")

print("-" * 40)

# 1. Ideal VEP

print("1. Ideal VEP (clean reference)")

vep = generate_ideal_vep(time_data)

filename = os.path.join(output_dir, "ideal_vep.csv")

save_csv(filename, time_ms, vep)

files_created.append(filename)

# 2. Delayed P100

print("2. Delayed P100 (abnormal)")

vep = generate_delayed_p100_vep(time_data)

filename = os.path.join(output_dir, "delayed_p100_vep.csv")

save_csv(filename, time_ms, vep)

files_created.append(filename)

# 3. Absent P100

print("3. Absent P100 (severe abnormality)")

vep = generate_absent_p100_vep(time_data)

filename = os.path.join(output_dir, "absent_p100_vep.csv")

save_csv(filename, time_ms, vep)

files_created.append(filename)

# 4. Reduced amplitude

print("4. Reduced amplitude VEP")

vep = generate_reduced_amplitude_vep(time_data)

filename = os.path.join(output_dir, "reduced_amplitude_vep.csv")

save_csv(filename, time_ms, vep)

files_created.append(filename)

# 5. VEP with noise

print("5. VEP with noise (SNR testing)")

vep = generate_noisy_vep(time_data, noise_level=3.0)

filename = os.path.join(output_dir, "noisy_vep.csv")

save_csv(filename, time_ms, vep)

files_created.append(filename)

return files_created

def create_single_file(output_dir, file_type):

"""

Create a single test file of specified type.

Parameters:

-----------

output_dir : str

Directory to save file

file_type : str

Type of file to create

"""

# Setup time data

duration = 0.5

sampling_rate = 1000

time_data = np.arange(0, duration, 1/sampling_rate)

time_ms = time_data * 1000

if file_type == "ideal":

vep = generate_ideal_vep(time_data)

filename = os.path.join(output_dir, "ideal_vep.csv")

elif file_type == "delayed":

vep = generate_delayed_p100_vep(time_data)

filename = os.path.join(output_dir, "delayed_p100_vep.csv")

elif file_type == "absent":

vep = generate_absent_p100_vep(time_data)

filename = os.path.join(output_dir, "absent_p100_vep.csv")

elif file_type == "reduced":

vep = generate_reduced_amplitude_vep(time_data)

filename = os.path.join(output_dir, "reduced_amplitude_vep.csv")

elif file_type == "noisy":

vep = generate_noisy_vep(time_data, noise_level=3.0)

filename = os.path.join(output_dir, "noisy_vep.csv")

else:

print("Unknown file type")

return None

save_csv(filename, time_ms, vep)

return filename

def main():

"""Main menu for VEP test file generation."""

print("=" * 50)

print("Simple VEP Test CSV Generator")

print("=" * 50)

print("Creates clean test VEP waveforms using Gaussian components")

# Create output directory

output_dir = create_data_folders()

while True:

print("\n" + "=" * 50)

print("Menu Options:")

print("-" * 50)

print("1. Generate all test files")

print("2. Generate ideal VEP only")

print("3. Generate delayed P100 VEP")

print("4. Generate absent P100 VEP")

print("5. Generate reduced amplitude VEP")

print("6. Generate noisy VEP")

print("0. Exit")

print("=" * 50)

choice = input("\nSelect option (0-6): ").strip()

if choice == "0":

print("\nExiting...")

break

elif choice == "1":

files = create_test_files(output_dir)

print(f"\n✓ Created {len(files)} test files")

elif choice == "2":

filename = create_single_file(output_dir, "ideal")

print(f"\n✓ Created ideal VEP file")

elif choice == "3":

filename = create_single_file(output_dir, "delayed")

print(f"\n✓ Created delayed P100 VEP file")

elif choice == "4":

filename = create_single_file(output_dir, "absent")

print(f"\n✓ Created absent P100 VEP file")

elif choice == "5":

filename = create_single_file(output_dir, "reduced")

print(f"\n✓ Created reduced amplitude VEP file")

elif choice == "6":

filename = create_single_file(output_dir, "noisy")

print(f"\n✓ Created noisy VEP file")

else:

print("Invalid choice. Please select 0-6.")

input("\nPress Enter to continue...")

if __name__ == "__main__":

main()

Appendix 3

Basic Visual Evoked Potential (VEP) Waveform Generator

Using MNE-Python for neurophysiological signal synthesis

This implementation creates synthetic VEP waveforms based on clinical standards

with proper N75, P100, and N135 components.

"""

import numpy as np

import matplotlib.pyplot as plt

import mne #https://mne.tools/stable/index.html

from scipy import signal, stats

import warnings

warnings.filterwarnings('ignore')

class VEPGenerator:

"""

Generates synthetic Visual Evoked Potential waveforms

based on clinical specifications and ISCEV standards.

"""

def __init__(self, sampling_rate=1000, duration=0.5):

"""

Initialize VEP generator with basic parameters.

Parameters:

-----------

sampling_rate : int

Sampling frequency in Hz (recommended: 500-1000 Hz)

duration : float

Total duration of VEP epoch in seconds

"""

self.sampling_rate = sampling_rate

self.duration = duration

self.times = np.arange(0, duration, 1/sampling_rate)

# Standard VEP component parameters (in seconds and microvolts)

# Based on research findings

'''

Stimulus → [0ms]

Retinal response → [~20-30ms] (not seen in VEP)

Optic nerve conduction → [~40-60ms]

N75: Initial cortical arrival → [75ms] ← "Signal received"

P100: Main cortical processing → [100ms] ← "Pattern detected!"

N135: Higher-level processing → [135ms] ← "Analyzing meaning"

'''

self.components = {

'N75': {

'latency': 0.075, # 75 ms

'amplitude': -4.0, # -4 μV

'width': 0.015, # 15 ms width

'latency_range': (0.067, 0.110),

'amplitude_range': (-6, -2)

},

'P100': {

'latency': 0.100, # 100 ms

'amplitude': 10.0, # 10 μV (most prominent)

'width': 0.020, # 20 ms width

'latency_range': (0.090, 0.120),

'amplitude_range': (5, 20)

},

'N135': {

'latency': 0.135, # 135 ms

'amplitude': -8.0, # -8 μV

'width': 0.025, # 25 ms width

'latency_range': (0.121, 0.246),

'amplitude_range': (-10, -6)

}

}

self.clinical_norms = {

'N75': {

'latency_mean': 75, # ms

'latency_sd': 3.5, # ms

'latency_range': (65, 80), # Clinical limits

'amplitude_mean': -4.0, # μV

'amplitude_sd': 1.0, # μV

'amplitude_range': (-6, -2),

'rise_time': 12, # ms

'fall_time': 18, # ms

},

'P100': {

'latency_mean': 100, # ms - most important clinically

'latency_sd': 5.0, # ms

'latency_range': (95, 115), # >115ms is abnormal

'amplitude_mean': 10.0, # μV

'amplitude_sd': 3.0, # μV

'amplitude_range': (5, 20),

'rise_time': 15,

'fall_time': 25,

},

'N135': {

'latency_mean': 135,

'latency_sd': 6.0,

'latency_range': (120, 145),

'amplitude_mean': -7.0,

'amplitude_sd': 2.0,

'amplitude_range': (-12, -4),

'rise_time': 20,

'fall_time': 30,

}

}

def sample_clinical_parameters(self, component_name, add_variability=True):

"""

Sample parameters from clinical distributions.

Parameters:

-----------

component_name : str

Name of VEP component ('N75', 'P100', 'N135')

add_variability : bool

Whether to add statistical variability

Returns:

--------

params : dict

Sampled parameters for the component

"""

norms = self.clinical_norms[component_name]

if add_variability:

# Sample from normal distributions

latency_ms = np.random.normal(norms['latency_mean'], norms['latency_sd'])

amplitude = np.random.normal(norms['amplitude_mean'], norms['amplitude_sd'])

# Add variability to rise/fall times (±20%)

rise_time_ms = norms['rise_time'] * np.random.uniform(0.8, 1.2)

fall_time_ms = norms['fall_time'] * np.random.uniform(0.8, 1.2)

# Clamp to clinical ranges

latency_ms = np.clip(latency_ms, norms['latency_range'][0], norms['latency_range'][1])

amplitude = np.clip(amplitude, norms['amplitude_range'][0], norms['amplitude_range'][1])

else:

# Use mean values for clean/reference waveform

latency_ms = norms['latency_mean']

amplitude = norms['amplitude_mean']

rise_time_ms = norms['rise_time']

fall_time_ms = norms['fall_time']

return {

'latency': latency_ms / 1000, # Convert to seconds

'amplitude': amplitude,

'rise_time': rise_time_ms / 1000, # Convert to seconds

'fall_time': fall_time_ms / 1000 # Convert to seconds

}

def generate_component(self, component_name, add_variability=True,

morphology='asymmetric_gaussian'):

"""

Generate a single VEP component using clinical parameters.

Parameters:

-----------

component_name : str

Name of component ('N75', 'P100', 'N135')

add_variability : bool

Add physiological variability

morphology : str

Component morphology model ('gaussian', 'asymmetric_gaussian')

Returns:

--------

component : ndarray

Generated component waveform

"""

# Sample clinical parameters

params = self.sample_clinical_parameters(component_name, add_variability)

if morphology == 'asymmetric_gaussian':

component = self._generate_asymmetric_gaussian_component(params)

else: # Default Gaussian

width = (params['rise_time'] + params['fall_time']) / 4 # Approximate width

component = params['amplitude'] * np.exp(-0.5 * ((self.times - params['latency']) / width) ** 2)

return component

def _generate_asymmetric_gaussian_component(self, params):

"""

Generate asymmetric Gaussian component with realistic rise/fall times.

"""

latency = params['latency']

amplitude = params['amplitude']

rise_time = params['rise_time']

fall_time = params['fall_time']

component = np.zeros(len(self.times))

for i, t in enumerate(self.times):

if t <= latency:

# Rising phase - steeper

if t >= latency - 3*rise_time: # Start 3 rise-times before peak

sigma = rise_time / 2.5 # Steeper rise

component[i] = amplitude * np.exp(-0.5 * ((t - latency) / sigma) ** 2)

else:

# Falling phase - more gradual

sigma = fall_time / 2.0 # More gradual fall

component[i] = amplitude * np.exp(-0.5 * ((t - latency) / sigma) ** 2)

return component

def generate_baseline_vep(self, add_variability=False, morphology='asymmetric_gaussian'):

"""

Generate a complete VEP waveform using clinical norms.

Parameters:

-----------

add_variability : bool

Add realistic inter-trial variability

morphology : str

Component morphology model

Returns:

--------

vep : ndarray

Complete VEP waveform in microvolts

"""

vep = np.zeros(len(self.times))

# Generate each component using clinical norms

for comp_name in ['N75', 'P100', 'N135']:

component = self.generate_component(comp_name, add_variability, morphology)

vep += component

# Optional: Add component interaction effects

if add_variability:

vep = self._apply_component_interactions(vep, comp_name, component)

return vep

def _apply_component_interactions(self, vep, comp_name, component):

"""

Apply realistic interactions between VEP components.

"""

# P100 amplitude affects N135 amplitude (refractory effects)

if comp_name == 'P100':

p100_amplitude = np.max(component)

# Store for N135 adjustment

self._last_p100_amplitude = p100_amplitude

elif comp_name == 'N135' and hasattr(self, '_last_p100_amplitude'):

# Larger P100 -> slightly larger N135 (within limits)

if self._last_p100_amplitude > 12: # Above average P100

enhancement_factor = 1.1

component_idx = np.argmin(component) # Find N135 peak (most negative)

vep[component_idx] *= enhancement_factor

return vep

def generate_ideal_vep(self, morphology='asymmetric_gaussian'):

"""

Generate the 'ideal' reference VEP using mean clinical values.

"""

return self.generate_baseline_vep(add_variability=False, morphology=morphology)

def add_eeg_background(self, vep, snr_db=10):

"""

Add realistic EEG background activity to VEP.

Parameters:

-----------

vep : ndarray

Clean VEP waveform

snr_db : float

Signal-to-noise ratio in decibels

Returns:

--------

vep_with_noise : ndarray

VEP with added EEG background

"""

# Generate 1/f pink noise for EEG background

freqs = np.fft.fftfreq(len(self.times), 1/self.sampling_rate)

pink_spectrum = np.where(freqs > 0, 1/np.sqrt(freqs), 0)

# Generate random phase

random_phase = np.random.uniform(0, 2*np.pi, len(freqs))

pink_fft = pink_spectrum * np.exp(1j * random_phase)

# Convert to time domain

pink_noise = np.real(np.fft.ifft(pink_fft))

# Scale noise based on desired SNR

signal_power = np.mean(vep ** 2)

noise_power = np.mean(pink_noise ** 2)

snr_linear = 10 ** (snr_db / 10)

noise_scale = np.sqrt(signal_power / (snr_linear * noise_power))

scaled_noise = pink_noise * noise_scale

# Add alpha rhythm (8-12 Hz) common in visual areas

alpha_freq = np.random.uniform(8, 12)

alpha_amplitude = np.random.uniform(2, 5) # 2-5 μV

alpha = alpha_amplitude * np.sin(2 * np.pi * alpha_freq * self.times)

return vep + scaled_noise + alpha

def generate_trial_batch(self, n_trials=100, add_noise=False, morphology='asymmetric_gaussian'):

"""

Generate multiple VEP trials for averaging simulation.

Parameters:

-----------

n_trials : int

Number of trials to generate

add_noise : bool

Add EEG background noise

Returns:

--------

trials : ndarray

Array of VEP trials (n_trials x n_samples)

averaged : ndarray

Averaged VEP across all trials

"""

trials = np.zeros((n_trials, len(self.times)))

for i in range(n_trials):

# Generate VEP with variability

vep = self.generate_baseline_vep(add_variability=True, morphology=morphology)

if add_noise:

# Add background with varying SNR

snr = np.random.uniform(5, 15) # Variable SNR

vep = self.add_eeg_background(vep, snr_db=snr)

trials[i, :] = vep

# Calculate averaged VEP (standard clinical practice)

averaged = np.mean(trials, axis=0)

return trials, averaged

def apply_clinical_filters(self, signal_data):

"""

Apply standard clinical filtering to VEP signal.

Parameters:

-----------

signal_data : ndarray

Raw VEP signal

Returns:

--------

filtered : ndarray

Filtered VEP signal

"""

# Design filters based on ISCEV standards

# High-pass: 1 Hz

# Low-pass: 100 Hz

nyquist = self.sampling_rate / 2

# Butterworth filters (standard in clinical practice)

b_high, a_high = signal.butter(2, 1/nyquist, btype='high')

b_low, a_low = signal.butter(4, 100/nyquist, btype='low')

# Apply filters

filtered = signal.filtfilt(b_high, a_high, signal_data)

filtered = signal.filtfilt(b_low, a_low, filtered)

return filtered

def detect_components(self, vep_data):

"""

Detect actual component peaks/troughs in VEP waveform.

Returns:

--------

components : dict

Detected latencies and amplitudes for each component

"""

time_ms = self.times * 1000

components = {}

# N75: Find minimum in 60-85ms window

n75_window = (time_ms >= 60) & (time_ms <= 85)

if np.any(n75_window):

n75_idx = np.where(n75_window)[0][np.argmin(vep_data[n75_window])]

components['N75'] = {

'latency': time_ms[n75_idx],

'amplitude': vep_data[n75_idx],

'index': n75_idx

}

# P100: Find maximum in 85-120ms window

p100_window = (time_ms >= 85) & (time_ms <= 120)

if np.any(p100_window):

p100_idx = np.where(p100_window)[0][np.argmax(vep_data[p100_window])]

components['P100'] = {

'latency': time_ms[p100_idx],

'amplitude': vep_data[p100_idx],

'index': p100_idx

}

# N135: Find minimum in 120-150ms window

n135_window = (time_ms >= 120) & (time_ms <= 150)

if np.any(n135_window):

n135_idx = np.where(n135_window)[0][np.argmin(vep_data[n135_window])]

components['N135'] = {

'latency': time_ms[n135_idx],

'amplitude': vep_data[n135_idx],

'index': n135_idx

}

return components

def plot_vep(self, vep_data, title='VEP Waveform', show_clinical_ranges=True,

mark_actual_components=True):

"""

Create clinical-style VEP plot with clinical range highlighting.

Parameters:

-----------

vep_data : ndarray

VEP signal to plot

title : str

Plot title

show_clinical_ranges : bool

Whether to show clinical normal/abnormal ranges

mark_actual_components : bool

True - detect and mark actual peaks/troughs

False - mark at expected times (75, 100, 135ms)

"""

fig, ax = plt.subplots(1, 1, figsize=(12, 6))

# Time domain plot

time_ms = self.times * 1000 # Convert to milliseconds

# Add clinical range shading BEFORE plotting the waveform

if show_clinical_ranges:

# P100 normal range (95-115ms) - most clinically important

ax.axvspan(95, 115, alpha=0.2, color='green',

label='P100 Normal Range (95-115ms)')

# N75 normal range (65-80ms)

ax.axvspan(65, 80, alpha=0.15, color='blue',

label='N75 Normal Range (65-80ms)')

# N135 normal range (120-145ms)

ax.axvspan(120, 145, alpha=0.15, color='orange',

label='N135 Normal Range (120-145ms)')

# Plot the waveform on top

ax.plot(time_ms, vep_data, 'b-', linewidth=2, zorder=5)

ax.axhline(y=0, color='k', linestyle='-', alpha=0.3)

# Component markers

if mark_actual_components:

# Detect and mark actual components

detected = self.detect_components(vep_data)

if 'N75' in detected:

ax.axvline(x=detected['N75']['latency'], color='darkblue',

linestyle='--', alpha=0.7, linewidth=1.5, label='N75')

ax.text(detected['N75']['latency']-5, ax.get_ylim()[1]*0.9,

f"N75\n{detected['N75']['latency']:.1f}ms",

ha='center', fontsize=8, color='darkblue')

if 'P100' in detected:

ax.axvline(x=detected['P100']['latency'], color='darkgreen',

linestyle='--', alpha=0.7, linewidth=2, label='P100')

ax.text(detected['P100']['latency']+5, ax.get_ylim()[1]*0.9,

f"P100\n{detected['P100']['latency']:.1f}ms",

ha='center', fontsize=8, color='darkgreen')

if 'N135' in detected:

ax.axvline(x=detected['N135']['latency'], color='darkorange',

linestyle='--', alpha=0.7, linewidth=1.5, label='N135')

ax.text(detected['N135']['latency']-5, ax.get_ylim()[1]*0.9,

f"N135\n{detected['N135']['latency']:.1f}ms",

ha='center', fontsize=8, color='darkorange')

else:

# Mark at expected times

ax.axvline(x=75, color='darkblue', linestyle='--', alpha=0.5, label='N75')

ax.text(80, ax.get_ylim()[1]*0.95, 'N75',

ha='center', fontsize=8, color='darkblue')

ax.axvline(x=100, color='darkgreen', linestyle='--', alpha=0.7, label='P100')

ax.text(105, ax.get_ylim()[1]*0.95, 'P100',

ha='center', fontsize=8, color='darkgreen')

ax.axvline(x=135, color='darkorange', linestyle='--', alpha=0.5, label='N135')

ax.text(140, ax.get_ylim()[1]*0.95, 'N135',

ha='center', fontsize=8, color='darkorange')

ax.set_xlabel('Time (ms)')

ax.set_ylabel('Amplitude (μV)')

ax.set_title(title)

ax.grid(True, alpha=0.3)

ax.legend(loc='upper right', fontsize=9)

ax.set_xlim([0, 250])

plt.tight_layout()

plt.show()

def plot_trials_comparison(self, trials, averaged, morphology, show_clinical_ranges=True):

"""Plot individual trials with averaged response and clinical ranges."""

fig, axes = plt.subplots(1, 1, figsize=(12, 8))

time_ms = self.times * 1000

# Add clinical range shading first

if show_clinical_ranges:

# P100 normal range

axes.axvspan(95, 115, alpha=0.2, color='green',

label='P100 Normal Range (95-115ms)', zorder=1)

# N75 normal range

axes.axvspan(65, 80, alpha=0.1, color='blue',

label='N75 Normal Range (65-80ms)', zorder=1)

# N135 normal range

axes.axvspan(120, 145, alpha=0.1, color='orange',

label='N135 Normal Range (120-145ms)', zorder=1)

# Plot x number of trials

x = 25

for i in range(x):

axes.plot(time_ms, trials[i, :], alpha=0.3, color='gray',

linewidth=0.8, zorder=2)

# Plot averaged response on top

axes.plot(time_ms, averaged, 'b-', linewidth=3,

label=f'Averaged (n={len(trials)})', zorder=3)

# Add component markers

axes.axvline(x=75, color='darkblue', linestyle=':', alpha=0.8, label='N75')

axes.axvline(x=100, color='darkgreen', linestyle=':', alpha=0.8,

linewidth=2, label='P100')

axes.axvline(x=135, color='darkorange', linestyle=':', alpha=0.8, label='N135')

axes.set_title(f'Individual Trials ({x} shown) with Clinical Ranges - {morphology.replace("_", "

").title()}')

axes.set_xlabel('Time (ms)')

axes.set_ylabel('Amplitude (μV)')

axes.legend(loc='upper right', fontsize=9)

axes.grid(True, alpha=0.5)

axes.set_xlim([0, 250])

plt.tight_layout()

plt.show()

# Example usage and demonstration

def demonstrate_vep_generator():

"""

Demonstrate the VEP generator with various examples.

"""

print("=" * 60)

print("VEP Waveform Generator Demonstration")

print("=" * 60)

# Initialize generator

generator = VEPGenerator(sampling_rate=1000, duration=0.5)

time_ms = generator.times * 1000 # Time in milliseconds

# 1. Generate single clean VEP

print("\n1. Generating single clean VEP...")

clean_vep = generator.generate_baseline_vep(add_variability=False,

morphology='asymmetric_gaussian')

# clean_vep = generator.generate_reference_vep()

generator.plot_vep(clean_vep, title='Clean VEP Waveform (No Variability) - Asymmetric Gaussian')

# 2. Generate VEP with EEG background

print("\n2. Adding EEG background noise...")

noisy_vep = generator.add_eeg_background(clean_vep, snr_db=10)

generator.plot_vep(noisy_vep, title='VEP with EEG Background (SNR=10dB)')

# 3. Apply clinical filtering

print("\n3. Applying clinical filters...")

filtered_vep = generator.apply_clinical_filters(noisy_vep)

# Plot filtered vs unfiltered

fig, ax = plt.subplots(figsize=(12, 6))

ax.plot(time_ms, noisy_vep, 'b-', alpha=0.5, label='Unfiltered')

ax.plot(time_ms, filtered_vep, 'r-', linewidth=2, label='Filtered (1-100 Hz)')

ax.set_xlabel('Time (ms)')

ax.set_ylabel('Amplitude (μV)')

ax.set_title('Effect of Clinical Filtering')

ax.legend()

ax.grid(True, alpha=0.3)

plt.show()

# 4. Generate and average multiple trials

print("\n4. Generating 100 trials and averaging...")

trials, averaged_vep = generator.generate_trial_batch(n_trials=100, add_noise=True)

generator.plot_trials_comparison(trials, averaged_vep, morphology='asymmetric_gaussian')

return generator

# Run demonstration if script is executed directly

if __name__ == "__main__":

generator, raw = demonstrate_vep_generator()

print("\n✓ VEP Generator demonstration complete!")

print("Generator object and MNE Raw object are available for further use.")

Author details
A.M. Mackay
Flex Clin Sci Ltd, 45B Bewsey Street, Warrington, Cheshire, UK, WA2 7JQ
✉ Corresponding Author
👤 View Profile →🔗 Is this you? Claim this publication
A. Sadler
Flex Clin Sci Ltd, 45B Bewsey Street, Warrington, Cheshire, UK, WA2 7JQ
👤 View Profile →🔗 Is this you? Claim this publication