Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
ffc7094
add: Sequential Sampling Model, Evidence Accumulation Simulation
Zooaal Feb 13, 2025
0324d96
update: major fixes
Zooaal Feb 19, 2025
a0f9253
Update src/component.jl
Zooaal Feb 25, 2025
ff43378
Update src/component.jl
Zooaal Feb 25, 2025
115bd41
Apply suggestions from code review
Zooaal Feb 25, 2025
6d0e09e
Apply suggestions from code review
Zooaal Feb 25, 2025
cf1a757
fixture from code review
Zooaal Feb 25, 2025
7bdc931
Update DriftComponent to use max_length instead of an time vector
Zooaal Feb 26, 2025
844262e
changed two KellyModel Parameter names
Zooaal Feb 28, 2025
c524e9b
Update CITATION.cff
jschepers Feb 20, 2025
0da7b23
Change citation title to paper title
jschepers Feb 21, 2025
e3e8e54
added joss badge to readme
jschepers Mar 18, 2025
46d3e28
Edit citation section in README
jschepers Mar 18, 2025
ae90422
Add BibTeX entry to readme
jschepers Mar 18, 2025
af929ef
Format citation section in README
jschepers Mar 18, 2025
8de419c
Format citation section in README
jschepers Mar 18, 2025
3974473
Minor change
jschepers Mar 20, 2025
9a7b332
Fix and update CITATION.cff file
jschepers Mar 25, 2025
1c35c8d
Add JOSS paper as preferred citation
jschepers Mar 25, 2025
f020998
Apply basic suggestions from code review
Zooaal Apr 3, 2025
e68c5dc
Update from code review
Zooaal Apr 14, 2025
82834d2
Apply suggestions from code review
Zooaal Apr 20, 2025
9c0cdf7
add onset transform to Int and comment on Kelly Model String Fields
Zooaal Apr 23, 2025
b0500b0
v0.4.1 - compat DSP and SignalAnalysis
behinger May 13, 2025
2e23679
merge
behinger May 16, 2025
dae4cc9
Kelly Model is no longer exported, fix
behinger May 16, 2025
0e2657a
added SequentialSamplingModels to docs
behinger May 16, 2025
d48031c
Update CI.yml to run CI for PR on all branches not just main
jschepers Jun 10, 2025
3ccfa77
Update unfold family table & redirect doc links
xuyg16 Apr 26, 2025
192f7b3
Update CI.yml
behinger Jan 10, 2025
acec4ff
Update CI.yml
behinger Jan 10, 2025
07cc43a
merged tutorials, added Truncated, added urgency signal
behinger Jun 13, 2025
ba1ee0b
merge main
behinger Jun 13, 2025
04460ce
fix merge conflict remainder in tests
behinger Jun 13, 2025
d1ff5e8
added empty docstrings to abstracttypes
behinger Jun 13, 2025
bfa3e80
fix tests, added warning tau=0, export KellyModel,LBA,DDM
behinger Jun 14, 2025
4a042b8
oops forgot to save
behinger Jun 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/UnfoldSim.jl
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,5 @@ export AbstractHeadmodel, Hartmut, headmodel, leadfield, orientation, magnitude
export MultichannelComponent

# evidence accumulation
export DriftComponent, DriftOnset, SequenceOnset, KellyModel
export DriftComponent, DriftOnset, SequenceOnset
end
12 changes: 6 additions & 6 deletions src/component.jl
Original file line number Diff line number Diff line change
Expand Up @@ -518,14 +518,14 @@ end

"""
DriftComponent <: AbstractComponent
A component that adds an evidence accumulation signal, following a specified sequential sampling model.
A component that adds an evidence accumulation trace, following a specified sequential sampling model.

Works best with [`SequenceDesign`](@ref).

# Fields
- `max_length::Int`: maximum length of the simulated signal.
- `sfreq::Real`: sample frequency used to simulate the signal.
- `model_type`: Model struct which defines the model to use to generate the signal, e.g. `KellyModel`
- `max_length::Int`: maximum length of the simulated trace.
- `sfreq::Real`: sample frequency used to simulate the trace, used to evaluate the parameters in the model_type.
- `model_type`: Model struct which defines the model to use to generate the trace, e.g. `KellyModel`
- `model_parameters`: Dict. Containing the parameters for the sequential sampling simulation model specified in `model_type`.

# Examples
Expand Down Expand Up @@ -576,7 +576,7 @@ julia> simulate_component(StableRNG(1),c,design_seq)
```
"""
function UnfoldSim.simulate_component(rng, c::DriftComponent, design::AbstractDesign)
_, traces = trace_sequential_sampling_model(deepcopy(rng), c, design)
_, traces = simulate_drift_component(deepcopy(rng), c, design)
return traces
end
"""
Expand Down Expand Up @@ -611,7 +611,7 @@ julia> calculate_response_times_for_ssm(StableRNG(1),c,design_seq)
```
"""
function calculate_response_times_for_ssm(rng, component::DriftComponent, design::UnfoldSim.SubselectDesign)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
function calculate_response_times_for_ssm(rng, component::DriftComponent, design::UnfoldSim.SubselectDesign)
function calculate_response_times_for_ssm(
rng,
component::DriftComponent,
design::UnfoldSim.SubselectDesign,
)

rts, _ = trace_sequential_sampling_model(deepcopy(rng), component, design)
rts, _ = simulate_drift_component(deepcopy(rng), component, design)
return rts
end
Base.length(c::DriftComponent) = c.max_length
Expand Down
17 changes: 4 additions & 13 deletions src/onset.jl
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ sequence_onset = SequenceOnset(
struct SequenceOnset <: AbstractOnset
onset::Dict
end

SequenceOnset(args::Pair...) = SequenceOnset(Dict(args...))
"""
DriftOnset<: AbstractOnset
A type that defines the onsets for a [`DriftComponent`](@ref).
Expand All @@ -239,10 +239,7 @@ Works best with [`DriftComponent`](@ref).

# Examples
```julia-repl
sequence_onset = SequenceOnset(
Dict('S'=>UniformOnset(width=0,offset=85*fs/100),
'C'=>(DriftOnset(), UniformOnset(width=0, offset=150)),
'R'=>UniformOnset(width=0,offset=120*fs/100)))
drift_onset = DriftOnset()
```
"""
struct DriftOnset{T} <: AbstractOnset
Expand Down Expand Up @@ -299,14 +296,8 @@ Generates list of onsets for multiple [`DriftComponent`](@ref) in an [`SequenceD
# Returns
- `Vector{Float64}`: the generated onsets for the drift components in the SequenceDesign.
"""
function UnfoldSim.simulate_interonset_distances(rng, onset::Tuple{DriftOnset, UniformOnset}, design::AbstractDesign, components::AbstractComponent)
rts = calculate_response_times_for_ssm(deepcopy(rng), components, design)
jitter = Int.(
round.(rand(deepcopy(rng), onset[2].offset:(onset[2].offset+onset[2].width), size(design)))
)
rts = Int.(round.(rts))
rts = rts .+ jitter
return rts
function UnfoldSim.simulate_interonset_distances(rng, onset::Tuple, design::AbstractDesign, components::AbstractComponent)
return reduce(.+, simulate_interonset_distances.(deepcopy(rng), onset, Ref(design), Ref(components)))
end

"""
Expand Down
157 changes: 52 additions & 105 deletions src/sequentialSamplingModelSimulation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@ A advanced drift diffusion Model which can be used to simulate evidence accumula

All fields can be named. Is used with [`DriftComponent`](@ref).

# Fields T::Real
# Fields T::Union{Real,String}
- `drift_rate::T`: defines the amount of evidence accumulated per time step. (roughly the steepness of the trace)
- `event_onset::T`: constant event onset delay in seconds. (mimics sensory evidence)
- `sensor_encoding_delay::T`: uniform variability in the delay of the event onset in seconds, added ontop of `event_onset`. (mimics sensory encoding delay)
- `accumulative_level_noise::T`: \sigma of the normal-distributed noise added to the accumulation process.
- `sensor_encoding_delay::T`: constant event onset delay in seconds. (mimics sensory evidence)
- `sensor_encoding_delay_variability::T`: Normal variability in the delay of the event onset in seconds, added ontop of `event_onset`. (mimics sensory encoding delay)
- `event_onset_distribution::Normal`: Normal distribution for sensor encoding delay
- `accumulative_level_noise::T`: sigma of the normal-distributed noise added to the accumulation process.
- `boundary::T`: the threshold of evidence needed to make a decision.
- `motor_onset::T`: fixed delay between boundary reached and response time in seconds. (mimics motor time)
- `motor_delay::T`: variability in delay between boundary reached and response time in seconds. (mimics different reaction times of participants)
- `motor_delay::T`: fixed delay between boundary reached and response time in seconds. (mimics motor time)
- `motor_delay_variability::T`: variability in delay between boundary reached and response time in seconds. (mimics different reaction times of participants)
- `motor_onset_distribution::Normal`: Normal distribution for motor delay
- `post_accumulation_duration::T`: fixed time the accumulation process resumes after boundary reached in seconds. (mimics evidence overshoot)
- `post_accumulation_duration_variability::T`: variability in time the accumulation process resumes after boundary reached in seconds. (mimics diff of participants)
- `post_accumulation_duration_variability::T`: variability in time the accumulation process resumes after boundary reached in seconds.
- `post_accumulation_distribution::Normal`: Normal distribution for post accumulation duration
- `ramp_down_duration::T`: duration (in s) of the post accumulation ramp down process.

# Examples
Expand All @@ -25,35 +28,20 @@ KellyModel{Float64}(6.0, 0.2, 0.4, 0.5, 1.0, 0.1, 0.4, 0.1, 0.2, 0.1)

See also [`LinearModelComponent`](@ref), [`MultichannelComponent`](@ref).
"""
mutable struct KellyModel
drift_rate::Union{Real,String} # drift rate
event_onset::Union{Real,String} # onset(sensory evidence)
sensor_encoding_delay::Union{Real,String} # var(sensory encoding delay)
accumulative_level_noise::Union{Real,String} # accum level noise
boundary::Union{Real,String} # boundaryary height
motor_onset::Union{Real,String} # onset(motor)
motor_delay::Union{Real,String} # var(motor)
post_accumulation_duration::Union{Real,String} # mean(post decision)
post_accumulation_duration_variability::Union{Real,String} # var(post decision)
ramp_down_duration::Union{Real,String} # CPPrampdown duration

# Constructor with default values
function KellyModel(;
drift_rate = 6.0,
event_onset = 0.2,
sensor_encoding_delay = 0.1,
accumulative_level_noise = 0.5,
boundary = 1.0,
motor_onset = 0.4,
motor_delay = 0.1,
post_accumulation_duration = 0.1,
post_accumulation_duration_variability = 0.2,
ramp_down_duration = 0.1,
)
return new(drift_rate, event_onset, sensor_encoding_delay, accumulative_level_noise, boundary, motor_onset,
motor_delay, post_accumulation_duration, post_accumulation_duration_variability,
ramp_down_duration)
end
Base.@kwdef mutable struct KellyModel <: SequentialSamplingModels.SSM2D
drift_rate::Union{Real,String} = 6.0 # drift rate
sensor_encoding_delay::Union{Real,String} = 0.2 # onset (sensory evidence)
sensor_encoding_delay_variability::Union{Real,String} = 0.1 # sensory encoding delay
event_onset_distribution::Normal = Normal(sensor_encoding_delay, sensor_encoding_delay_variability) # Normal distribution for sensor encoding delay
accumulative_level_noise::Union{Real,String} = 0.5 # accumulation level noise
boundary::Union{Real,String} = 1.0 # boundary height
motor_delay::Union{Real,String} = 0.4 # motor onset
motor_delay_variability::Union{Real,String} = 0.1 # motor delay
motor_onset_distribution::Normal = Normal(motor_delay, motor_delay_variability) # Normal distribution for motor delay
post_accumulation_duration::Union{Real,String} = 0.1 # mean post-decision duration
post_accumulation_duration_variability::Union{Real,String} = 0.001 # variability post-decision
post_accumulation_distribution::Normal = Normal(post_accumulation_duration, post_accumulation_duration_variability) # Normal distribution for post accumulation duration
ramp_down_duration::Union{Real,String} = 0.1 # CPPrampdown duration
end

"""
Expand Down Expand Up @@ -107,15 +95,14 @@ julia> KellyModel_simulate_cpp(StableRNG(1), KellyModel(), 0:1/500:1.0, 1/500)
"""
function KellyModel_simulate_cpp(rng, model::KellyModel, time_vec, Δt)
evidence = zeros(length(time_vec));
evidence[time_vec .>= (model.event_onset+(rand(rng) -.5)*model.sensor_encoding_delay)] .= 1;
evidence[time_vec .>= rand(rng,model.event_onset_distribution)] .= 1;
startAccT = time_vec[findfirst(evidence .== 1)];

noise = vcat(zeros(
sum(time_vec .< startAccT)),
randn(rng,sum(time_vec .>= startAccT)) .* model.accumulative_level_noise .*sqrt(Δt));
ev=evidence;
ev[time_vec .< startAccT] .= 0; # set to zero all of the evidence before accT
cum_evidence = cumsum(ev .* model.drift_rate .* Δt .+ noise,dims=1); # This is the cumulative differential evidence, just as in a 1d DDM.

cum_evidence = cumsum(evidence .* model.drift_rate .* Δt .+ noise); # This is the cumulative differential evidence, just as in a 1d DDM.


# terminate the decision process on boundary crossing, record threshold-crossing samplepoint:
Expand All @@ -125,10 +112,10 @@ function KellyModel_simulate_cpp(rng, model::KellyModel, time_vec, Δt)
dti = length(time_vec) # Set to the last time step
end
# now record RT in sec after adding motor time, with variability
rt = time_vec[dti] + model.motor_onset + (rand(rng) - 0.5) * model.motor_delay
rt = time_vec[dti] + rand(rng,model.motor_onset_distribution)

# now make the CPP peak and go down linearly after a certain amount of post-dec accum time for this trial:
post_acc_duration = model.post_accumulation_duration .+ model.post_accumulation_duration_variability .* rand(rng);
post_acc_duration = rand(rng,model.post_accumulation_distribution)
# so post_acc_duration is the post accumulation duration time, where the accumulation spikes over the threshold

# acc_stop_index is the accumulation Stop index which is the index from the time Vector where the accumulation really stops
Expand All @@ -145,7 +132,7 @@ end


"""
trace_sequential_sampling_model(rng, component::DriftComponent, design::AbstractDesign)
simulate_drift_component(rng, component::DriftComponent, design::AbstractDesign)

Generate response times and evidence Vectors of an given [`AbstractDesign`](@ref) with a [`DriftComponent`](@ref) which contains the model used for the simulation.

Expand Down Expand Up @@ -173,7 +160,7 @@ Vector{Float64}, 501x6 Matrix{Float64}:
([96.65745162948949, 273.7368235451535, 271.86040880709123, 128.41057786118193, 342.35208862144276, 237.14773586760617], [0.0 0.0 … 0.0 0.0; 0.0 0.0 … 0.0 0.0; … ; 0.0 0.0 … 0.0 0.0; 0.0 0.0 … 0.0 0.0])
```
"""
function trace_sequential_sampling_model(rng, component::DriftComponent, design::AbstractDesign)
function simulate_drift_component(rng, component::DriftComponent, design::AbstractDesign)
events = generate_events(deepcopy(rng), design)
traces = Matrix{Float64}(undef, component.max_length, size(events, 1))
rts = Vector{Float64}(undef, size(events, 1))
Expand All @@ -189,70 +176,35 @@ function trace_sequential_sampling_model(rng, component::DriftComponent, design:
end

"""
SSM_Simulate(rng, model::KellyModel, sfreq, max_length)

Generate response time and evidence Vector of max_length by using the Kelly Model for the simulation.

# Arguments
- `rng::StableRNG`: Random seed to ensure the same traces are created for reconstruction.
- `model::KellyModel`: SequentialSamplingModel to simulate the evidence and response time.
- `sfreq::Real`: sample frequency used to simulate the signal.
- `max_length::Int`: maximum length of the simulated signal.

# Returns
- `Float64`: Simulated response time for the trial.
- `Vector{Float64}`: evidence values over time. The output dimension is `c.max_length`.

# Examples
```julia-repl
julia> model = KellyModel()

julia> SSM_Simulate(StableRNG(1), model, 500, 500)
Float64, Vector{Float64}:
(96.65745162948949, [0.0 0.0 … 0.0 0.0])
```
"""
function SSM_Simulate(rng, model::KellyModel, sfreq, max_length)
Δt = 1/sfreq
time_vec = 0:Δt:max_length*Δt
rt, evidence = KellyModel_simulate_cpp(rng, model, time_vec, Δt)
if length(evidence) < max_length
final_value = 0
append!(evidence, fill(final_value, max_length - length(evidence)))
end
evidence = evidence[1:max_length]
return rt, evidence
end

"""
SSM_Simulate(rng, model::LBA, sfreq, max_length)
SSM_Simulate(rng, model::SequentialSamplingModels.SSM2D, sfreq, max_length)

Generate response time and evidence Vector of c.max_length by using the LBA for the simulation.
Generate response time and evidence Vector of component.max_length by using a SequentialSamplingModels.SSM2D as DDM or LBA for the simulation.

# Arguments
- `rng::StableRNG`: Random seed to ensure the same traces are created for reconstruction.
- `model::LBA`: SequentialSamplingModel to simulate the evidence and response time.
- `model::SequentialSamplingModels.SSM2D`: SequentialSamplingModel to simulate the evidence and response time.
- `sfreq::Real`: sample frequency used to simulate the signal.
- `max_length::Int`: maximum length of the simulated signal.

# Returns
- `Float64`: Simulated response time for the trial.
- `Vector{Float64}`: evidence values over time. The output dimension is `c.max_length`.
- `Vector{Float64}`: evidence values over time. The output dimension is `component.max_length`.

# Examples
```julia-repl
julia> model = LBA()

julia> model = DDM()
julia> SSM_Simulate(StableRNG(1), model, 500, 500)
Float64, Vector{Float64}:
(96.65745162948949, [0.0 0.0 … 0.0 0.0])
```
"""
function SSM_Simulate(rng, model::LBA, sfreq, max_length)
function SSM_Simulate(rng, model::SequentialSamplingModels.SSM2D, sfreq, max_length)
Δt = 1 / sfreq
time_steps, evidence = SequentialSamplingModels.simulate(rng, model; Δt)
evidence = hcat(evidence...)
evidence = collect(vec(evidence))
if !(evidence isa Vector{Float64})
evidence = hcat(evidence...)
evidence = collect(vec(evidence))
end
# Store results for this trial
rt = (time_steps[end] + model.τ) / Δt
if length(evidence) < max_length
Expand All @@ -266,42 +218,37 @@ function SSM_Simulate(rng, model::LBA, sfreq, max_length)
end

"""
SSM_Simulate(rng, model::DDM, sfreq, max_length)
SSM_Simulate(rng, model::KellyModel, sfreq, max_length)

Generate response time and evidence Vector of c.max_length by using the DDM for the simulation.
Generate response time and evidence Vector of max_length by using the Kelly Model for the simulation.

# Arguments
- `rng::StableRNG`: Random seed to ensure the same traces are created for reconstruction.
- `model::DDM`: SequentialSamplingModel to simulate the evidence and response time.
- `sfreq::Real`: sample frequency used to simulate the signal.
- `max_length::Int`: maximum length of the simulated signal.
- `model::KellyModel`: SequentialSamplingModel to simulate the evidence and response time.
- `sfreq::Real`: sample frequency used to simulate the trace.
- `max_length::Int`: maximum length of the simulated trace.

# Returns
- `Float64`: Simulated response time for the trial.
- `Vector{Float64}`: evidence values over time. The output dimension is `c.max_length`.

# Examples
```julia-repl
julia> model = DDM()
julia> model = KellyModel()

julia> SSM_Simulate(StableRNG(1), model, 500, 500)
Float64, Vector{Float64}:
(96.65745162948949, [0.0 0.0 … 0.0 0.0])
```
"""
function SSM_Simulate(rng, model::DDM, sfreq, max_length)
Δt = 1 / sfreq
time_steps, evidence = SequentialSamplingModels.simulate(rng, model; Δt)
evidence = evidence .- model.α * model.z

# Store results for this trial
rt = (time_steps[end] + model.τ) / Δt
function SSM_Simulate(rng, model::KellyModel, sfreq, max_length)
Δt = 1/sfreq
time_vec = 0:Δt:max_length*Δt
rt, evidence = KellyModel_simulate_cpp(rng, model, time_vec, Δt)
if length(evidence) < max_length
final_value = 0
append!(evidence, fill(final_value, max_length - length(evidence)))
else
rt = max_length + (model.τ / Δt)
evidence = evidence[1:max_length]
end
evidence = evidence[1:max_length]
return rt, evidence
end
4 changes: 2 additions & 2 deletions test/sequentialSamplingModelSimulation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
@test result_trace == result_sim_trace
end

@testset "trace_sequential_sampling_model" begin
@testset "simulate_drift_component" begin
boundary = 1.0
model_parameter = Dict(:boundary => boundary);
c = UnfoldSim.DriftComponent(
Expand All @@ -47,7 +47,7 @@
)
design_seq = UnfoldSim.SequenceDesign(design_single, "SCR_")

result_rts, result_traces = UnfoldSim.trace_sequential_sampling_model(rng, c, design_seq)
result_rts, result_traces = UnfoldSim.simulate_drift_component(rng, c, design_seq)
@test size(result_rts) == (6,)
@test size(result_traces) == (501, 6)
@test any(result_traces .>= 1.0)
Expand Down
Loading