Skip to content

Epoch Selection

EegFun.jl provides a system for selecting epochs from continuous data. This includes experimental paradigms that use simple single triggers or complex sequences of events with timing constraints.

This tutorial covers the various ways you can define epochs, from the simplest single-trigger approach to advanced pattern matching.

Quick Reference

FeatureField(s)Example
Single triggertrigger_sequences = [[1]]Epoch around trigger 1
Multiple triggers (OR)trigger_sequences = [[1], [2]]Either trigger 1 or 2
Trigger sequencetrigger_sequences = [[1, 10]]Trigger 1 followed by 10
Wildcardtrigger_sequences = [[1, :any, 3]]1, then anything, then 3
Rangetrigger_sequences = [[1:5, 10]]Any of 1–5, then 10
t=0 referencereference_index = 2Align to 2nd trigger in sequence
Timing constrainttiming_pairs, min_interval, max_intervalOnly if triggers 200–800ms apart
Position: after triggermask_before_trigger = 99Only after marker trigger 99
Position: before triggermask_after_trigger = 88Only before marker trigger 88
Hide stray triggersmask_triggers = [50]Ignore trigger 50 when matching sequences
Hide a practice blockmask_between_triggers = [(15, 16)]Ignore all triggers between 15 and 16

The EpochCondition Structure

At the heart of epoch selection is the EpochCondition structure. It allows you to define exactly what constitutes an epoch in your study.

julia
@kwdef struct EpochCondition
    name::String
    trigger_sequences::Vector{Vector{Union{Int,Symbol,UnitRange{Int}}}}
    reference_index::Int = 1
    timing_pairs::Union{Nothing,Vector{Tuple{Int,Int}}} = nothing
    min_interval::Union{Nothing,Float64} = nothing
    max_interval::Union{Nothing,Float64} = nothing
    mask_before_trigger::Union{Nothing,Int} = nothing   # only keep epochs AFTER this trigger
    mask_after_trigger::Union{Nothing,Int} = nothing    # only keep epochs BEFORE this trigger
    mask_triggers::Union{Nothing,Vector{Int}} = nothing # hide stray triggers before matching
    mask_between_triggers::Union{Nothing,Vector{Tuple{Int,Int}}} = nothing # hide whole blocks
end

Simple Epoching: Single Values

The most common case is extracting an epoch around a single trigger value.

julia
# Define a condition for trigger 1
condition = EegFun.EpochCondition(
    name = "Condition1", 
    trigger_sequences = [[1]]
)

# Extract epoch (-200ms to 1000ms around the trigger)
epochs = EegFun.extract_epochs(dat, condition, (-0.2, 1.0))

To match any of multiple trigger values for a single condition (OR logic), provide multiple sequences:

julia
# Match trigger 1 OR trigger 2
condition = EegFun.EpochCondition(
    name = "Condition1", 
    trigger_sequences = [[1], [2]] # matches either trigger 1 or trigger 2
)

# Extract epoch (-200ms to 1000ms around the trigger)
epochs = EegFun.extract_epochs(dat, condition, (-0.2, 1.0))

Multiple Conditions

Most experiments have more than one condition. Pass a vector of EpochCondition objects to extract_epochs to extract epochs for each condition separately:

julia
# Define two separate conditions
conditions = [
    EegFun.EpochCondition(name = "Condition1", trigger_sequences = [[1]]),
    EegFun.EpochCondition(name = "Condition2", trigger_sequences = [[2]]),
]

# Extract epochs for both conditions
epochs = EegFun.extract_epochs(dat, conditions, (-0.2, 1.0))

Each condition is matched independently, and the resulting EpochData retains the condition labels for later analysis (e.g., averaging, comparison).

Sequence Matching

Sometimes an "event" is actually a sequence of triggers. For example, a target stimulus (1) followed by a response (10).

julia
# Match the sequence [1, 2]
condition = EegFun.EpochCondition(
    name = "TargetResponse", 
    trigger_sequences = [[1, 2]] # a 1 followed by a 2 with t=0 being the 1
)

Wildcards and Ranges

You can use wildcards and ranges within your sequences:

  • :any: Matches any trigger value.

  • UnitRange (e.g., 1:10): Matches any value within the range.

julia
# Match trigger 1, then any trigger, then trigger 3
condition = EegFun.EpochCondition(
    name = "Wildcard", 
    trigger_sequences = [[1, :any, 3]]
)

# Match any trigger from 1 to 5, then trigger 10
condition = EegFun.EpochCondition(
    name = "Range", 
    trigger_sequences = [[1:5, 10]]
)

Onset (t=0) Reference

By default, the first trigger in a sequence is considered  . You can change this using reference_index.

julia
# Sequence: [Warning (1), Stimulus (2), Response (10)]
# We want t=0 to be the Stimulus (index 2)
condition = EegFun.EpochCondition(
    name = "StimulusOnset",
    trigger_sequences = [[1, 2, 10]], # 1 followed by 2 followed by 10 with t=0 being the 2
    reference_index = 2
)

reference_index refers to the position within the trigger sequence, not the trigger value itself. The sequence matcher internally tracks the actual sample position for each element in the matched sequence, so reference_index = 2 correctly resolves to the sample where trigger 2 occurred — even when zero-valued samples are skipped between triggers. :::

Timing Constraints

You can restrict matches to sequences where triggers occur within specific time intervals. This is useful for filtering out trials with late responses or accidental double-triggers.

julia
# Match [1, 10] only if they occur between 200ms and 800ms apart
condition = EegFun.EpochCondition(
    name = "ValidResponse",
    trigger_sequences = [[1, 10]],
    timing_pairs = [(1, 2)], # Calculate interval between 1st and 2nd trigger
    min_interval = 0.2,
    max_interval = 0.8
)

Position Constraints

You can also filter sequences based on whether they occur before or after certain "marker" triggers.

julia
# Only find sequences that occur AFTER trigger 99 (e.g., start of an experimental block)
condition = EegFun.EpochCondition(
    name = "Block2",
    trigger_sequences = [[1, 2]],
    mask_before_trigger = 99   # discard any sequence that comes before trigger 99
)

# Only find sequences that occur BEFORE trigger 88 (e.g., end of first half)
condition = EegFun.EpochCondition(
    name = "Phase1",
    trigger_sequences = [[1, 2]],
    mask_after_trigger = 88    # discard any sequence that comes after trigger 88
)

You cannot specify both `mask_before_trigger` and `mask_after_trigger` on the same condition — use separate conditions if needed.

Hiding Stray or Practice Triggers

Sometimes your trigger stream contains extra triggers that would break sequence matching — stray button presses, practice blocks, or inter-block markers. Two masking options cover these cases.

mask_triggers — hide individual trigger values

Adds a pre-processing step that temporarily removes specific trigger values before sequence matching runs. The surrounding sequence is unchanged; the masked trigger simply disappears.

julia
# The stream is: 101, 50, 111, 127
# We want [101, 111, 127] — but trigger 50 is a stray button press that gets in the way.
condition = EegFun.EpochCondition(
    name = "target_response",
    trigger_sequences = [[101, 111, 127]],
    reference_index = 1,
    mask_triggers = [50]   # hide trigger 50 before matching
)

mask_between_triggers — hide entire blocks

Marks all triggers that fall between a pair of boundary markers as invisible before matching. This is ideal for excluding complete practice blocks.

julia
# Practice block is bounded by triggers 15 (start) and 16 (end).
# Any sequence inside that range should be ignored.
condition = EegFun.EpochCondition(
    name = "experimental_trials",
    trigger_sequences = [[101, 111, 127]],
    reference_index = 1,
    mask_between_triggers = [(15, 16)]  # hide everything between 15 and 16
)

Both options can be combined with each other and with all other constraints.

External TOML Configuration

For complex studies, defining your epoch conditions in an external TOML file is often cleaner and is the recommended approach for the pipeline procedure.

The TOML Format

Create a file (e.g., epochs.toml):

toml
[epochs]

[[epochs.conditions]]
name = "condition_1"
trigger_sequences = [[1]]

[[epochs.conditions]]
name = "condition_2"
trigger_sequences = [[2]]

[[epochs.conditions]]
name = "condition_3" 
trigger_sequences = [[3]]

[[epochs.conditions]]
name = "condition_4"
trigger_sequences = [[4]]

Here is a more advanced example using sequence matching, timing constraints, and position constraints:

toml
[epochs]

# Stimulus followed by a response, time-locked to the stimulus
[[epochs.conditions]]
name = "stimulus_response1"
trigger_sequences = [[1, 10]]
reference_index = 1
timing_pairs = [[1, 2]]
min_interval = 0.1
max_interval = 0.8

[[epochs.conditions]]
name = "stimulus_response2"
trigger_sequences = [[2, 10]]
reference_index = 1
timing_pairs = [[1, 2]]
min_interval = 0.1
max_interval = 0.8

Loading and Using the TOML

julia
using TOML

# Load the configuration
config = TOML.parsefile("epochs.toml")

# Parse into EpochCondition objects
conditions = EegFun.condition_parse_epoch(config)

# Extract epochs
epochs = EegFun.extract_epochs(dat, conditions, (-0.2, 1.0))

Next Steps

  • Artifact handlingArtifact Handling for detecting and rejecting bad epochs after extraction

  • Batch pipelinesBatch Processing uses epoch condition TOML files as part of its automated workflow

  • Selection predicatesSelection Patterns for filtering channels, samples, and time intervals in downstream analysis