From 8405a1a92ec00c776814ad10b93ed3a53eef877b Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Fri, 23 Feb 2024 22:57:59 +0000 Subject: [PATCH 001/156] FormulaOnsets --- docs/literate/reference/onsettypes.jl | 22 +++++++ src/component.jl | 22 +++++-- src/onset.jl | 84 +++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 6 deletions(-) diff --git a/docs/literate/reference/onsettypes.jl b/docs/literate/reference/onsettypes.jl index 279a2226..e9967316 100644 --- a/docs/literate/reference/onsettypes.jl +++ b/docs/literate/reference/onsettypes.jl @@ -282,3 +282,25 @@ end # - if `offset` < `length(signal.basis)` -> there might be overlap, depending on the other parameters of the onset distribution # [^1]: Wikipedia contributors. (2023, December 5). Log-normal distribution. In Wikipedia, The Free Encyclopedia. Retrieved 12:27, December 7, 2023, from https://en.wikipedia.org/w/index.php?title=Log-normal_distribution&oldid=1188400077# + + + + +# ## Design-dependent `FormulaXOnset` + +# For additional control we provide `FormulaUniformOnset` and `FormulaLogNormalOnset` types, that allow to control all parameters by specifying formulas +o = UnfoldSim.FormulaUniformOnset( + width_formula = @formula(0 ~ 1 + cond), + width_β = [50, 20], +) +events = generate_events(design) +onsets = UnfoldSim.simulate_interonset_distances(MersenneTwister(42), o, design) + +f = Figure() +ax = f[1, 1] = Axis(f) +hist!(ax, onsets[events.cond.=="A"], bins = range(0, 100, step = 1), label = "cond: A") +hist!(ax, onsets[events.cond.=="B"], bins = range(0, 100, step = 1), label = "cond: B") +axislegend(ax) +f + +# Voila - the inter-onset intervals are `20` samples longer for condition `B`, exactly as specified.` \ No newline at end of file diff --git a/src/component.jl b/src/component.jl index a2d87fc1..ca31d99f 100644 --- a/src/component.jl +++ b/src/component.jl @@ -145,22 +145,32 @@ julia> simulate_component(StableRNG(1),c,design) """ function simulate_component(rng, c::LinearModelComponent, design::AbstractDesign) events = generate_events(design) + X = generate_designmatrix(c.formula, events, c.contrasts) + y = X * β + return y' .* c.basis +end + + +""" +Helper function to generate a designmatrix from formula, events and contrasts. +""" +function generate_designmatrix(formula, events, contrasts) # special case, intercept only # https://github.com/JuliaStats/StatsModels.jl/issues/269 - if c.formula.rhs == ConstantTerm(1) + if formula.rhs == ConstantTerm(1) X = ones(nrow(events), 1) else - if isempty(c.contrasts) - m = StatsModels.ModelFrame(c.formula, events) + if isempty(contrasts) + m = StatsModels.ModelFrame(formula, events) else - m = StatsModels.ModelFrame(c.formula, events; contrasts = c.contrasts) + m = StatsModels.ModelFrame(formula, events; contrasts = contrasts) end X = StatsModels.modelmatrix(m) end - y = X * c.β - return y' .* c.basis + return X end + """ simulate_component(rng, c::MixedModelComponent, design::AbstractDesign) Generates a MixedModel and simulates data according to c.β and c.σs. diff --git a/src/onset.jl b/src/onset.jl index 5e7d364a..22aa37b1 100644 --- a/src/onset.jl +++ b/src/onset.jl @@ -7,6 +7,8 @@ Provide a Uniform Distribution of the inter-event-distances. `width` is the width of the uniform distribution (=> the jitter). Since the lower bound is 0, `width` is also the upper bound. `offset` is the minimal distance. The maximal distance is `offset + width`. + +For a more advanced parameter specification, see `FormulaUniformOnset``, which allows to specify the onset-parameters depending on the `Design` employed via a linear regression model """ @with_kw struct UniformOnset <: AbstractOnset width = 50 # how many samples jitter? @@ -17,6 +19,8 @@ end Log-normal inter-event distances using the `Distributions.jl` truncated LogNormal distribution. Be careful with large `μ` and `σ` values, as they are on logscale. σ>8 can quickly give you out-of-memory sized signals! + +For a more advanced parameter specification, see `FormulaLogNormalOnset, which allows to specify the onset-parameters depending on the `Design` employed via linear regression model """ @with_kw struct LogNormalOnset <: AbstractOnset μ::Any # mean @@ -34,6 +38,8 @@ struct NoOnset <: AbstractOnset end """ simulate_interonset_distances(rng, onset::UniformOnset, design::AbstractDesign) simulate_interonset_distances(rng, onset::LogNormalOnset, design::AbstractDesign) + simulate_interonset_distances(rng, onset::FormulaUniformOnset, design::AbstractDesign) + simulate_interonset_distances(rng, onset::FormulaLogNormalOnset, design::AbstractDesign) Generate the inter-event-onset vector in samples (returns Int). """ @@ -69,3 +75,81 @@ function simulate_onsets(rng, onset::AbstractOnset, simulation::Simulation) return onsets_accum end + +""" + FormulaUniformOnset <: AbstractOnset +provide a Uniform Distribution of the inter-event-distances, but with regression formulas. +This is helpful if your overlap/event-distribution should be dependend on some condition, e.g. more overlap in cond='A' than cond='B'. + + - `width`: is the width of the uniform distribution (=> the jitter). Since the lower bound is 0, `width` is also the upper bound. + -`width_formula`: choose a formula depending on your `Design` + -`width_β`: Choose a vector of betas, number needs to fit the formula chosen + -`width_contrasts` (optional): Choose a contrasts-dictionary according to the StatsModels specifications + `offset` is the minimal distance. The maximal distance is `offset + width`. + -`offset_formula`: choose a formula depending on your `design` + -`offset_β`: Choose a vector of betas, number needs to fit the formula chosen + -`offset_contrasts` (optional): Choose a contrasts-dictionary according to the StatsModels specifications + +See `UniformOnset` for a simplified version without linear regression specifications +""" +@with_kw struct FormulaUniformOnset <: AbstractOnset + width_formula = @formula(0 ~ 1) + width_β::Vector = [50] + width_contrasts::Dict = Dict() + offset_formula = @formula(0 ~ 1) + offset_β::Vector = [0] + offset_contrasts::Dict = Dict() +end + + +function simulate_interonset_distances(rng, o::FormulaUniformOnset, design::AbstractDesign) + events = generate_events(design) + widths = + UnfoldSim.generate_designmatrix(o.width_formula, events, o.width_contrasts) * + o.width_β + offsets = + UnfoldSim.generate_designmatrix(o.offset_formula, events, o.offset_contrasts) * + o.offset_β + + return Int.( + round.(reduce(vcat, rand.(deepcopy(rng), range.(offsets, offsets .+ widths), 1))) + ) +end + + +@with_kw struct FormulaLogNormalOnset <: AbstractOnset + μ_formula = @formula(0 ~ 1) + μ_β::Vector = [0] + μ_contrasts::Dict = Dict() + σ_formula = @formula(0 ~ 1) + σ_β::Vector = [0] + σ_contrasts::Dict = Dict() + offset_formula = @formula(0 ~ 1) + offset_β::Vector = [0] + offset_contrasts::Dict = Dict() + truncate_upper = nothing # truncate at some sample? +end + +function simulate_interonset_distances( + rng, + o::FormulaLogNormalOnset, + design::AbstractDesign, +) + events = generate_events(design) + + + μs = UnfoldSim.generate_designmatrix(o.μ_formula, events, o.μ_contrasts) * o.μ_β + σs = UnfoldSim.generate_designmatrix(o.σ_formula, events, o.σ_contrasts) * o.σ_β + offsets = + UnfoldSim.generate_designmatrix(o.offset_formula, events, o.offset_contrasts) * + o.offset_β + + + funs = LogNormal.(μs, σs) + if !isnothing(o.truncate_upper) + fun = truncated.(fun; upper = o.truncate_upper) + end + return Int.(round.(offsets .+ rand.(deepcopy(rng), funs, 1))) +end + + From afc69d7437547943d9c44626d6e9844ef1327c62 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Sun, 25 Feb 2024 00:00:04 +0000 Subject: [PATCH 002/156] initial sequence tryout --- Project.toml | 1 + docs/literate/HowTo/sequence.jl | 52 +++++++++++++++++++++++++++ docs/make.jl | 1 + src/UnfoldSim.jl | 4 ++- src/component.jl | 63 ++++++++++++++++++++++++++++----- src/design.jl | 44 +++++++++++++++++++++++ src/onset.jl | 23 ++++++++++++ src/sequence.jl | 48 +++++++++++++++++++++++++ src/types.jl | 5 ++- 9 files changed, 230 insertions(+), 11 deletions(-) create mode 100644 docs/literate/HowTo/sequence.jl create mode 100644 src/sequence.jl diff --git a/Project.toml b/Project.toml index 8a8913b0..4f2b1613 100644 --- a/Project.toml +++ b/Project.toml @@ -5,6 +5,7 @@ version = "0.3.1" [deps] Artifacts = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" +Automa = "67c07d97-cdcb-5c2c-af73-a7f9c32a568b" DSP = "717857b8-e6f2-59f4-9121-6e50c889abd2" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" diff --git a/docs/literate/HowTo/sequence.jl b/docs/literate/HowTo/sequence.jl new file mode 100644 index 00000000..e66e5e95 --- /dev/null +++ b/docs/literate/HowTo/sequence.jl @@ -0,0 +1,52 @@ +using UnfoldSim +using CairoMakie +using StableRNGs + +# ## Stimulus - Response design + +# let's say we want to simulate a stimulus response, followed by a button press response. +# First we generate the minimal design of the experiment by specifying our conditins (a one-condition-two-levels design in our case) +design = SingleSubjectDesign(conditions = Dict(:condition => ["one", "two"])) +generate_events(design) +# next we use the `SequenceDesign` and nest our initial design in it. "SR_" is code for an "S" event and an "R" event - only single letter events are supported! The `_` is a signal for the Onset generator to generate a bigger pause - no overlap between adjacend `SR` pairs +design = SequenceDesign(design, "SRR_") +generate_events(design) +# The main thing that happened is that the design was repeated for every event (each 'letter') of the sequence, and an `eventtype` column was added. +# !!! hint +# more advaned sequences exist, like "SR{1,3}", or "A[BC]" etc. + +# Finally, let's repeat the design 2 times +design = RepeatDesign(design, 2) +generate_events(design) + +# This results in 20 trials that nicely follow our sequence + +# Next we have to specify for both events `S` and `R` what the responses should look like. + +p1 = LinearModelComponent(; + basis = p100(), + formula = @formula(0 ~ 1 + condition), + β = [1, 0.5], +); + +n1 = LinearModelComponent(; + basis = n170(), + formula = @formula(0 ~ 1 + condition), + β = [1, 0.5], +); +p3 = LinearModelComponent(; + basis = UnfoldSim.hanning(Int(0.5 * 100)), # sfreq = 100 for the other bases + formula = @formula(0 ~ 1 + condition), + β = [1, 2], +); + +components = Dict('S' => [p1, n1], 'R' => [p3]) + +data, evts = simulate( + StableRNG(1), + design, + components, + UniformOnset(offset = 10, width = 100), + NoNoise(), +) +lines(data) \ No newline at end of file diff --git a/docs/make.jl b/docs/make.jl index e97757f1..8cf95422 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -48,6 +48,7 @@ makedocs(; "Define a new duration & jitter component" => "./generated/HowTo/newComponent.md", "Generate multi channel data" => "./generated/HowTo/multichannel.md", "Use predefined design / onsets data" => "./generated/HowTo/predefinedData.md", + "Produce specific sequences of events" => "./generated/HowTo/sequence.md", ], "DocStrings" => "api.md", ], diff --git a/src/UnfoldSim.jl b/src/UnfoldSim.jl index cd1b0287..126264b8 100644 --- a/src/UnfoldSim.jl +++ b/src/UnfoldSim.jl @@ -14,6 +14,7 @@ using LinearAlgebra using ToeplitzMatrices # for AR Expo. Noise "Circulant" using StatsModels using HDF5, Artifacts, FileIO +using Automa # for sequence using LinearAlgebra # headmodel @@ -30,6 +31,7 @@ include("onset.jl") include("predefinedSimulations.jl") include("headmodel.jl") include("helper.jl") +include("sequence.jl") include("bases.jl") export size, length @@ -46,7 +48,7 @@ export Simulation export MixedModelComponent, LinearModelComponent # export designs -export MultiSubjectDesign, SingleSubjectDesign, RepeatDesign +export MultiSubjectDesign, SingleSubjectDesign, RepeatDesign, SequenceDesign # noise functions export PinkNoise, RedNoise, WhiteNoise, NoNoise, ExponentialNoise #,RealNoise (not implemented yet) diff --git a/src/component.jl b/src/component.jl index ca31d99f..310b4dd8 100644 --- a/src/component.jl +++ b/src/component.jl @@ -99,10 +99,16 @@ For a vector of `MultichannelComponent`s, return the first but asserts all are o """ function n_channels(c::Vector{<:AbstractComponent}) all_channels = n_channels.(c) - @assert length(unique(all_channels)) == 1 "Error - projections of different channels cannot be different from eachother" + @assert length(unique(all_channels)) == 1 "Error - projections of different components have to be of the same output (=> channel) dimension" return all_channels[1] end +function n_channels(components::Dict) + all_channels = [n_channels(c) for c in values(components)] + @assert length(unique(all_channels)) == 1 "Error - projections of different components have to be of the same output (=> channel) dimension" + return all_channels[1] + +end """ simulate_component(rng,c::MultichannelComponent,design::AbstractDesign) Return the projection of a component from source to "sensor" space. @@ -124,10 +130,13 @@ Base.length(c::AbstractComponent) = length(c.basis) """ maxlength(c::Vector{AbstractComponent}) = maximum(length.(c)) + maxlength(components::Dict) maximum of individual component lengths """ -maxlength(c::Vector{AbstractComponent}) = maximum(length.(c)) +maxlength(c::Vector{<:AbstractComponent}) = maximum(length.(c)) + +maxlength(components::Dict) = maximum([maximum(length.(c)) for c in values(components)]) """ simulate_component(rng, c::AbstractComponent, simulation::Simulation) By default call `simulate_component` with `(::Abstractcomponent,::AbstractDesign)` instead of the whole simulation. This allows users to provide a hook to do something completely different :) @@ -146,7 +155,7 @@ julia> simulate_component(StableRNG(1),c,design) function simulate_component(rng, c::LinearModelComponent, design::AbstractDesign) events = generate_events(design) X = generate_designmatrix(c.formula, events, c.contrasts) - y = X * β + y = X * c.β return y' .* c.basis end @@ -296,19 +305,55 @@ function simulate_responses( components::Vector{<:AbstractComponent}, simulation::Simulation, ) - if n_channels(components) > 1 - epoch_data = - zeros(n_channels(components), maxlength(components), length(simulation.design)) - else - epoch_data = zeros(maxlength(components), length(simulation.design)) - end + epoch_data = init_epoch_data(components, simulation.design) + simulate_responses!(rng, epoch_data, components, simulation) + return epoch_data +end +function simulate_responses!( + rng, + epoch_data::AbstractArray, + components::Vector, + simulation::Simulation, +) for c in components simulate_and_add!(epoch_data, c, simulation, rng) end return epoch_data end +function init_epoch_data(components, design) + if n_channels(components) > 1 + epoch_data = zeros(n_channels(components), maxlength(components), length(design)) + else + epoch_data = zeros(maxlength(components), length(design)) + end + return epoch_data +end +function simulate_responses(rng, components::Dict, s::Simulation) + epoch_data = init_epoch_data(components, s.design) + evts = generate_events(s.design) + multichannel = n_channels(components) > 1 + for key in keys(components) + if key == '_' + continue + end + s_key = Simulation( + s.design |> x -> SubselectDesign(x, key), + components[key], + s.onset, + s.noisetype, + ) + ix = evts.event .== key + if multichannel + simulate_responses!(rng, @view(epoch_data[:, :, ix]), components[key], s_key) + else + #@debug sum(ix), size(simulate_responses(rng, components[key], s_key)), key + simulate_responses!(rng, @view(epoch_data[:, ix]), components[key], s_key) + end + end + return epoch_data +end """ simulate_and_add!(epoch_data::AbstractMatrix, c, simulation, rng) diff --git a/src/design.jl b/src/design.jl index fffff39a..1ea0b177 100644 --- a/src/design.jl +++ b/src/design.jl @@ -147,6 +147,28 @@ design = RepeatDesign(designOnce,4); repeat::Int = 1 end + +@with_kw struct SequenceDesign{T} <: AbstractDesign + design::T + sequence::String = "" +end + +UnfoldSim.generate_events(design::SequenceDesign{MultiSubjectDesign}) = + error("not yet implemented") + +function UnfoldSim.generate_events(design::SequenceDesign) + df = generate_events(design.design) + nrows_df = size(df, 1) + currentsequence = sequencestring(design.sequence) + currentsequence = replace(currentsequence, "_" => "") + df = repeat(df, inner = length(currentsequence)) + df.event .= repeat(collect(currentsequence), nrows_df) + + return df + +end + + """ UnfoldSim.generate_events(design::RepeatDesign{T}) @@ -160,6 +182,28 @@ function UnfoldSim.generate_events(design::RepeatDesign) return df end + + +""" +Internal helper design to subset a sequence design in its individual components +""" +struct SubselectDesign{T} <: AbstractDesign + design::T + key::Char +end + +function generate_events(design::SubselectDesign) + return subset(generate_events(design.design), :event => x -> x .== design.key) +end + + Base.size(design::RepeatDesign{MultiSubjectDesign}) = size(design.design) .* (design.repeat, 1) Base.size(design::RepeatDesign{SingleSubjectDesign}) = size(design.design) .* design.repeat +Base.size(design::SequenceDesign) = + size(design.design) .* length(replace(design.sequence, "_" => "")) + +Base.size(design::RepeatDesign{SequenceDesign{SingleSubjectDesign}}) = + size(design.design) .* design.repeat + +Base.size(design::SubselectDesign) = size(generate_events(design), 1) \ No newline at end of file diff --git a/src/onset.jl b/src/onset.jl index 22aa37b1..4ef74702 100644 --- a/src/onset.jl +++ b/src/onset.jl @@ -59,6 +59,14 @@ function simulate_interonset_distances(rng, onset::LogNormalOnset, design::Abstr end +#function simulate_interonset_distances(rng, onset::AbstractOnset,design::) + + +contains_design(d::AbstractDesign, target::Type) = false +contains_design(d::Union{RepeatDesign,SequenceDesign,SubselectDesign}, target::Type) = + d.design isa target ? true : contains_design(d.design, target) + +sequencestring(d::RepeatDesign) = sequencestring(d.design) """ simulate_onsets(rng, onset::AbstractOnset, simulation::Simulation) @@ -70,6 +78,21 @@ function simulate_onsets(rng, onset::AbstractOnset, simulation::Simulation) # sample different onsets onsets = simulate_interonset_distances(rng, onset, simulation.design) + if contains_design(simulation.design, SequenceDesign) + currentsequence = sequencestring(simulation.design) + if !isnothing(findfirst("_", currentsequence)) + + @assert currentsequence[end] == '_' "the blank-indicator '_' has to be the last sequence element" + df = generate_events(simulation.design) + nrows_df = size(df, 1) + stepsize = length(currentsequence) - 1 + # add to every stepsize onset the maxlength of the response + #@debug onsets[stepsize:stepsize:end] + @debug stepsize + onsets[stepsize+1:stepsize:end] .= 2 .* maxlength(simulation.components) + #@debug onsets[stepsize:stepsize:end] + end + end # accumulate them onsets_accum = accumulate(+, onsets, dims = 1) diff --git a/src/sequence.jl b/src/sequence.jl new file mode 100644 index 00000000..bf56cd93 --- /dev/null +++ b/src/sequence.jl @@ -0,0 +1,48 @@ +function rand_re(machine::Automa.Machine) + out = IOBuffer() + node = machine.start + + while true + if node.state ∈ machine.final_states + (rand() ≤ 1 / (length(node.edges) + 1)) && break + end + + edge, node = rand(node.edges) + label = rand(collect(edge.labels)) + print(out, Char(label)) + end + + return String(take!(out)) +end + +sequencestring(d::SequenceDesign) = sequencestring(d.sequence) +function sequencestring(str::String) + #match curly brackets and replace them + @assert isnothing(findfirst("*", str)) && isnothing(findfirst("+", str)) "'infinite' sequences currently not supported" + crly = collect(eachmatch(r"(\{[\d],[\d]\})", str)) + for c in reverse(crly) + m = replace(c.match, "{" => "", "}" => "") + rep_minimum, rep_maximum = parse.(Int, split(m, ",")) + #@info str[c.offset-1] + if str[c.offset-1] == ']' + #@info "brackets found" + bracket_end_idx = c.offset - 1 + bracket_start_idx = findlast("[", str[1:bracket_end_idx])[1] + #@info bracket_end_idx,bracket_start_idx + repeat_string = "[" * str[bracket_start_idx+1:bracket_end_idx-1] * "]" + else + bracket_start_idx = c.offset - 1 + bracket_end_idx = c.offset - 1 + repeat_string = string(str[c.offset-1]) + end + + replacement_string = repeat(repeat_string, rand(rep_minimum:rep_maximum)) + #@info "rep" replacement_string + str = + str[1:bracket_start_idx-1] * + replacement_string * + str[bracket_end_idx+length(c.match)+1:end] + @info str + end + return rand_re(Automa.compile(RE(str))) +end \ No newline at end of file diff --git a/src/types.jl b/src/types.jl index fb29ef53..7154340c 100644 --- a/src/types.jl +++ b/src/types.jl @@ -13,7 +13,10 @@ abstract type AbstractHeadmodel end struct Simulation design::AbstractDesign - components::Vector{AbstractComponent} + components::Union{ + <:Dict{<:Char,<:Vector{<:AbstractComponent}}, + <:Vector{<:AbstractComponent}, + } onset::AbstractOnset noisetype::AbstractNoise end From 49db0ba0ebf53442c90aee76ff614b481c3b849a Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Sun, 25 Feb 2024 21:50:24 +0000 Subject: [PATCH 003/156] fix bug in predef_eeg --- src/predefinedSimulations.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/predefinedSimulations.jl b/src/predefinedSimulations.jl index 874bb870..5a1c2a65 100644 --- a/src/predefinedSimulations.jl +++ b/src/predefinedSimulations.jl @@ -79,7 +79,7 @@ function predef_eeg( kwargs..., ) - components = [] + components = AbstractComponent[] for c in comps append!(components, [T(c...)]) end From 41d73060c5cff85f588e85e5abfecc8592218fd6 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Mon, 26 Feb 2024 12:35:15 +0000 Subject: [PATCH 004/156] fix \beta missing --- src/component.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/component.jl b/src/component.jl index ca31d99f..fa853edb 100644 --- a/src/component.jl +++ b/src/component.jl @@ -146,7 +146,7 @@ julia> simulate_component(StableRNG(1),c,design) function simulate_component(rng, c::LinearModelComponent, design::AbstractDesign) events = generate_events(design) X = generate_designmatrix(c.formula, events, c.contrasts) - y = X * β + y = X * c.β return y' .* c.basis end From cd489c4fef1479b4c613ba426c5e5bb13058999e Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Wed, 28 Feb 2024 09:56:51 +0000 Subject: [PATCH 005/156] forgot the end --- src/onset.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/onset.jl b/src/onset.jl index f2e2469c..a30ce539 100644 --- a/src/onset.jl +++ b/src/onset.jl @@ -93,6 +93,7 @@ function simulate_onsets(rng, onset::AbstractOnset, simulation::Simulation) onsets[stepsize+1:stepsize:end] .= 2 .* maxlength(simulation.components) #@debug onsets[stepsize:stepsize:end] end + end if maximum(onsets) > 10000 @warn "Maximum of inter-event-distances was $(maximum(onsets)) - are you sure this is what you want?" From 6c35f3c903b9150df89547fad6410196f5706fc4 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Wed, 28 Feb 2024 13:41:49 +0000 Subject: [PATCH 006/156] half way through to success for sequence designs or something --- src/component.jl | 87 ++++++++++++++++++++++++++++++++++++++--------- src/design.jl | 15 ++++---- src/onset.jl | 4 +-- src/sequence.jl | 16 ++++----- src/simulation.jl | 27 +++++++++++++-- 5 files changed, 113 insertions(+), 36 deletions(-) diff --git a/src/component.jl b/src/component.jl index 75f1e54b..802a38aa 100644 --- a/src/component.jl +++ b/src/component.jl @@ -27,6 +27,7 @@ MixedModelComponent(; β::Vector σs::Dict # Dict(:subject=>[0.5,0.4]) or to specify correlationmatrix Dict(:subject=>[0.5,0.4,I(2,2)],...) contrasts::Dict = Dict() + offset::Int = 0 end """ @@ -55,9 +56,26 @@ LinearModelComponent(; formula::Any # e.g. 0~1+cond - left side must be "0" β::Vector contrasts::Dict = Dict() + offset::Int = 0 end +""" + offset(AbstractComponent) + +Should the `basis` be shifted? Returns c.offset for most components, if not implemented for a type, returns 0. Can be positive or negative, but has to be Integer +""" +offset(c::AbstractComponent)::Int = 0 +offset(c::LinearModelComponent)::Int = c.offset +offset(c::MixedModelComponent)::Int = c.offset + +maxoffset(c::Vector{<:AbstractComponent}) = maximum(offset.(c)) +maxoffset(d::Dict{<:Char,<:Vector{<:AbstractComponent}}) = maximum(maxoffset.(values(d))) +minoffset(c::Vector{<:AbstractComponent}) = minimum(offset.(c)) +minoffset(d::Dict{<:Char,<:Vector{<:AbstractComponent}}) = minimum(minoffset.(values(d))) + + + """ Wrapper for an `AbstractComponent` to project it to multiple target-channels via `projection`. optional adds `noise` to the source prior to projection. """ @@ -94,6 +112,7 @@ For `MultichannelComponent` return the length of the projection vector. """ n_channels(c::MultichannelComponent) = length(c.projection) + """ For a vector of `MultichannelComponent`s, return the first but asserts all are of equal length. """ @@ -145,7 +164,7 @@ simulate_component(rng, c::AbstractComponent, simulation::Simulation) = simulate_component(rng, c, simulation.design) """ - simulate_component(rng, c::AbstractComponent, simulation::Simulation) + simulate_component(rng, c::LinearModelComponent, design::AbstractDesign) Generate a linear model design matrix, weight it by c.β and multiply the result with the given basis vector. julia> c = UnfoldSim.LinearModelComponent([0,1,1,0],@formula(0~1+cond),[1,2],Dict()) @@ -192,13 +211,13 @@ Currently, it is not possible to use a different basis for fixed and random effe julia> design = MultiSubjectDesign(;n_subjects=2,n_items=50,items_between=(;:cond=>["A","B"])) julia> c = UnfoldSim.MixedModelComponent([0.,1,1,0],@formula(0~1+cond+(1|subject)),[1,2],Dict(:subject=>[2],),Dict()) -julia> simulate(StableRNG(1),c,design) +julia> simulate_component(StableRNG(1),c,design) """ function simulate_component( rng, c::MixedModelComponent, - design::AbstractDesign; + design::AbstractDesign, return_parameters = false, ) events = generate_events(design) @@ -335,34 +354,51 @@ function simulate_responses!( return epoch_data end function init_epoch_data(components, design) + max_offset = maxoffset(components) + min_offset = minoffset(components) + range_offset = (max_offset - min_offset) if n_channels(components) > 1 - epoch_data = zeros(n_channels(components), maxlength(components), length(design)) + epoch_data = zeros( + n_channels(components), + maxlength(components) + range_offset, + length(design), + ) else - epoch_data = zeros(maxlength(components), length(design)) + epoch_data = zeros(maxlength(components) + range_offset, length(design)) end return epoch_data end -function simulate_responses(rng, components::Dict, s::Simulation) - epoch_data = init_epoch_data(components, s.design) +function simulate_responses(rng, event_component_dict::Dict, s::Simulation) + epoch_data = init_epoch_data(event_component_dict, s.design) evts = generate_events(s.design) - multichannel = n_channels(components) > 1 - for key in keys(components) + multichannel = n_channels(event_component_dict) > 1 + for key in keys(event_component_dict) if key == '_' continue end s_key = Simulation( s.design |> x -> SubselectDesign(x, key), - components[key], + event_component_dict, s.onset, s.noisetype, ) ix = evts.event .== key if multichannel - simulate_responses!(rng, @view(epoch_data[:, :, ix]), components[key], s_key) + simulate_responses!( + rng, + @view(epoch_data[:, :, ix]), + event_component_dict[key], + s_key, + ) else - #@debug sum(ix), size(simulate_responses(rng, components[key], s_key)), key - simulate_responses!(rng, @view(epoch_data[:, ix]), components[key], s_key) + #@debug sum(ix), size(simulate_responses(rng, event_component_dict[key], s_key)), key + simulate_responses!( + rng, + @view(epoch_data[:, ix]), + event_component_dict[key], + s_key, + ) end end return epoch_data @@ -373,11 +409,28 @@ end simulate_and_add!(epoch_data::AbstractArray, c, simulation, rng) Helper function to call `simulate_component` and add it to a provided Array. """ -function simulate_and_add!(epoch_data::AbstractMatrix, c, simulation, rng) +function simulate_and_add!( + epoch_data::AbstractMatrix, + component::AbstractComponent, + simulation, + rng, +) @debug "matrix" - @views epoch_data[1:length(c), :] .+= simulate_component(rng, c, simulation) + + off = offset(component) - minoffset(simulation.components) + @debug off + @debug offset(component), minoffset(simulation.components) + @views epoch_data[1+off:length(component)+off, :] .+= + simulate_component(rng, component, simulation) end -function simulate_and_add!(epoch_data::AbstractArray, c, simulation, rng) +function simulate_and_add!( + epoch_data::AbstractArray, + component::AbstractComponent, + simulation, + rng, +) @debug "3D Array" - @views epoch_data[:, 1:length(c), :] .+= simulate_component(rng, c, simulation) + off = offset(component) - minoffset(simulation.components) + @views epoch_data[:, 1+off:length(component)+off, :] .+= + simulate_component(rng, component, simulation) end diff --git a/src/design.jl b/src/design.jl index 5e826895..f9d86932 100644 --- a/src/design.jl +++ b/src/design.jl @@ -156,15 +156,18 @@ end @with_kw struct SequenceDesign{T} <: AbstractDesign design::T sequence::String = "" + sequencelength::Int = 0 + rng = nothing end +SequenceDesign(design, sequence) = SequenceDesign(design = design, sequence = sequence) -UnfoldSim.generate_events(design::SequenceDesign{MultiSubjectDesign}) = - error("not yet implemented") +generate_events(design::SequenceDesign{MultiSubjectDesign}) = error("not yet implemented") -function UnfoldSim.generate_events(design::SequenceDesign) + +function generate_events(design::SequenceDesign) df = generate_events(design.design) nrows_df = size(df, 1) - currentsequence = sequencestring(design.sequence) + currentsequence = sequencestring(deepcopy(design.rng), design.sequence) currentsequence = replace(currentsequence, "_" => "") df = repeat(df, inner = length(currentsequence)) df.event .= repeat(collect(currentsequence), nrows_df) @@ -175,11 +178,11 @@ end """ - UnfoldSim.generate_events(design::RepeatDesign{T}) + generate_events(rng,design::RepeatDesign{T}) In a repeated design, iteratively calls the underlying {T} Design and concatenates. In case of MultiSubjectDesign, sorts by subject. """ -function UnfoldSim.generate_events(design::RepeatDesign) +function generate_events(design::RepeatDesign) df = map(x -> generate_events(design.design), 1:design.repeat) |> x -> vcat(x...) if isa(design.design, MultiSubjectDesign) sort!(df, [:subject]) diff --git a/src/onset.jl b/src/onset.jl index a30ce539..ea607506 100644 --- a/src/onset.jl +++ b/src/onset.jl @@ -66,7 +66,7 @@ contains_design(d::AbstractDesign, target::Type) = false contains_design(d::Union{RepeatDesign,SequenceDesign,SubselectDesign}, target::Type) = d.design isa target ? true : contains_design(d.design, target) -sequencestring(d::RepeatDesign) = sequencestring(d.design) +sequencestring(rng, d::RepeatDesign) = sequencestring(rng, d.design) """ simulate_onsets(rng, onset::AbstractOnset, simulation::Simulation) @@ -80,7 +80,7 @@ function simulate_onsets(rng, onset::AbstractOnset, simulation::Simulation) if contains_design(simulation.design, SequenceDesign) - currentsequence = sequencestring(simulation.design) + currentsequence = sequencestring(deepcopy(rng), simulation.design) if !isnothing(findfirst("_", currentsequence)) @assert currentsequence[end] == '_' "the blank-indicator '_' has to be the last sequence element" diff --git a/src/sequence.jl b/src/sequence.jl index bf56cd93..79ff3021 100644 --- a/src/sequence.jl +++ b/src/sequence.jl @@ -1,4 +1,4 @@ -function rand_re(machine::Automa.Machine) +function rand_re(rng::AbstractRNG, machine::Automa.Machine) out = IOBuffer() node = machine.start @@ -7,16 +7,16 @@ function rand_re(machine::Automa.Machine) (rand() ≤ 1 / (length(node.edges) + 1)) && break end - edge, node = rand(node.edges) - label = rand(collect(edge.labels)) + edge, node = rand(rng, node.edges) + label = rand(rng, collect(edge.labels)) print(out, Char(label)) end return String(take!(out)) end -sequencestring(d::SequenceDesign) = sequencestring(d.sequence) -function sequencestring(str::String) +sequencestring(rng, d::SequenceDesign) = sequencestring(rng, d.sequence) +function sequencestring(rng, str::String) #match curly brackets and replace them @assert isnothing(findfirst("*", str)) && isnothing(findfirst("+", str)) "'infinite' sequences currently not supported" crly = collect(eachmatch(r"(\{[\d],[\d]\})", str)) @@ -36,13 +36,13 @@ function sequencestring(str::String) repeat_string = string(str[c.offset-1]) end - replacement_string = repeat(repeat_string, rand(rep_minimum:rep_maximum)) + replacement_string = repeat(repeat_string, rand(rng, rep_minimum:rep_maximum)) #@info "rep" replacement_string str = str[1:bracket_start_idx-1] * replacement_string * str[bracket_end_idx+length(c.match)+1:end] - @info str + #@info str end - return rand_re(Automa.compile(RE(str))) + return rand_re(rng, Automa.compile(RE(str))) end \ No newline at end of file diff --git a/src/simulation.jl b/src/simulation.jl index 64652a49..7a292330 100755 --- a/src/simulation.jl +++ b/src/simulation.jl @@ -47,6 +47,21 @@ simulate( kwargs..., ) = simulate(rng, Simulation(design, signal, onset, noise); kwargs...) +function simulate( + rng::AbstractRNG, + design::SequenceDesign, + signal, + onset::AbstractOnset, + noise::AbstractNoise = NoNoise(); + kwargs..., +) + design = + isnothing(design.rng) ? + SequenceDesign(design.design, design.sequence, design.sequencelength, rng) : design + simulate(rng, Simulation(design, signal, onset, noise); kwargs...) +end + + function simulate(rng::AbstractRNG, simulation::Simulation; return_epoched::Bool = false) (; design, components, onset, noisetype) = simulation @@ -127,11 +142,13 @@ function create_continuous_signal(rng, responses, simulation) # combine responses with onsets max_length_component = maxlength(components) - max_length_continuoustime = Int(ceil(maximum(onsets))) .+ max_length_component + offset_range = maxoffset(simulation.components) - minoffset(simulation.components) + max_length_continuoustime = + Int(ceil(maximum(onsets))) .+ max_length_component .+ offset_range signal = zeros(n_chan, max_length_continuoustime, n_subjects) - + @debug size(signal), offset_range for e = 1:n_chan for s = 1:n_subjects for i = 1:n_trials @@ -141,7 +158,9 @@ function create_continuous_signal(rng, responses, simulation) responses, e, s, - one_onset:one_onset+max_length_component-1, + one_onset+minoffset(simulation.components):one_onset+max_length_component-1+maxoffset( + simulation.components, + ), (s - 1) * n_trials + i, ) end @@ -172,6 +191,8 @@ function add_responses!(signal, responses::Vector, e, s, tvec, erpvec) @views signal[e, tvec, s] .+= responses[:, erpvec] end function add_responses!(signal, responses::Matrix, e, s, tvec, erpvec)# + @debug size(signal), size(responses), e, s, size(tvec), size(erpvec) + @debug tvec, erpvec @views signal[e, tvec, s] .+= responses[:, erpvec] end function add_responses!(signal, responses::AbstractArray, e, s, tvec, erpvec) From 809dc2e9bf6c6abf1fd0305f2689d7cc55b3ceb2 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Wed, 28 Feb 2024 15:13:12 +0000 Subject: [PATCH 007/156] everythinig except sequencelength seems to be working now --- docs/literate/HowTo/sequence.jl | 27 +++++++++++++------ src/component.jl | 8 ++++-- src/design.jl | 46 +++++++++++++++++++++++++++------ src/onset.jl | 1 - src/sequence.jl | 9 ++++--- src/simulation.jl | 36 +++++++++++++++----------- 6 files changed, 90 insertions(+), 37 deletions(-) diff --git a/docs/literate/HowTo/sequence.jl b/docs/literate/HowTo/sequence.jl index e66e5e95..d9da5c4f 100644 --- a/docs/literate/HowTo/sequence.jl +++ b/docs/literate/HowTo/sequence.jl @@ -6,20 +6,20 @@ using StableRNGs # let's say we want to simulate a stimulus response, followed by a button press response. # First we generate the minimal design of the experiment by specifying our conditins (a one-condition-two-levels design in our case) -design = SingleSubjectDesign(conditions = Dict(:condition => ["one", "two"])) +design = SingleSubjectDesign(conditions = Dict(:condition => ["one", "two"]))#|>x->RepeatDesign(x,4) generate_events(design) # next we use the `SequenceDesign` and nest our initial design in it. "SR_" is code for an "S" event and an "R" event - only single letter events are supported! The `_` is a signal for the Onset generator to generate a bigger pause - no overlap between adjacend `SR` pairs -design = SequenceDesign(design, "SRR_") +design = SequenceDesign(design, "SR{1,2}_", 0, StableRNG(1)) generate_events(design) # The main thing that happened is that the design was repeated for every event (each 'letter') of the sequence, and an `eventtype` column was added. # !!! hint # more advaned sequences exist, like "SR{1,3}", or "A[BC]" etc. # Finally, let's repeat the design 2 times -design = RepeatDesign(design, 2) +design = RepeatDesign(design, 4) generate_events(design) -# This results in 20 trials that nicely follow our sequence +# This results in 12 trials that nicely follow our sequence # Next we have to specify for both events `S` and `R` what the responses should look like. @@ -37,16 +37,27 @@ n1 = LinearModelComponent(; p3 = LinearModelComponent(; basis = UnfoldSim.hanning(Int(0.5 * 100)), # sfreq = 100 for the other bases formula = @formula(0 ~ 1 + condition), - β = [1, 2], + β = [1, 0], ); -components = Dict('S' => [p1, n1], 'R' => [p3]) +resp = LinearModelComponent(; + basis = UnfoldSim.hanning(Int(0.5 * 100)), # sfreq = 100 for the other bases + formula = @formula(0 ~ 1 + condition), + β = [1, 2], + offset = -10, +); +components = Dict('S' => [p1, n1, p3], 'R' => [resp]) +#components = [p1, n1, resp] data, evts = simulate( StableRNG(1), design, components, - UniformOnset(offset = 10, width = 100), + UniformOnset(offset = 40, width = 10), NoNoise(), ) -lines(data) \ No newline at end of file + +lines(data) +vlines!(evts.latency, color = (:gray, 0.5)) +xlims!(0, 500) +current_figure() \ No newline at end of file diff --git a/src/component.jl b/src/component.jl index 802a38aa..d3a0c5e7 100644 --- a/src/component.jl +++ b/src/component.jl @@ -370,8 +370,12 @@ function init_epoch_data(components, design) end function simulate_responses(rng, event_component_dict::Dict, s::Simulation) + #@debug rng.state epoch_data = init_epoch_data(event_component_dict, s.design) + #@debug rng.state evts = generate_events(s.design) + #@debug rng.state + @debug size(epoch_data), size(evts) multichannel = n_channels(event_component_dict) > 1 for key in keys(event_component_dict) if key == '_' @@ -418,8 +422,8 @@ function simulate_and_add!( @debug "matrix" off = offset(component) - minoffset(simulation.components) - @debug off - @debug offset(component), minoffset(simulation.components) + + @views epoch_data[1+off:length(component)+off, :] .+= simulate_component(rng, component, simulation) end diff --git a/src/design.jl b/src/design.jl index f9d86932..5e315fe9 100644 --- a/src/design.jl +++ b/src/design.jl @@ -153,23 +153,45 @@ design = RepeatDesign(designOnce,4); end +function check_sequence(s::String) + blankfind = findall('_', s) + @assert length(blankfind) <= 1 && (length(blankfind) == 0 || length(s) == blankfind[1]) "the blank-indicator '_' has to be the last sequence element" + return s +end @with_kw struct SequenceDesign{T} <: AbstractDesign design::T sequence::String = "" sequencelength::Int = 0 rng = nothing + + SequenceDesign{T}(d, s, sl, r) where {T<:AbstractDesign} = + new(d, check_sequence(s), sl, r) end + SequenceDesign(design, sequence) = SequenceDesign(design = design, sequence = sequence) generate_events(design::SequenceDesign{MultiSubjectDesign}) = error("not yet implemented") -function generate_events(design::SequenceDesign) +generate_events(rng, design::AbstractDesign) = generate_events(design) +generate_events(design::SequenceDesign) = generate_events(deepcopy(design.rng), design) + +function generate_events(rng, design::SequenceDesign) df = generate_events(design.design) nrows_df = size(df, 1) - currentsequence = sequencestring(deepcopy(design.rng), design.sequence) + + rng = if isnothing(rng) + @warn "Could not (yet) find an rng for `SequenceDesign` - ignore this message if you called `generate_events` yourself, be worried if you called `simulate` and still see this message. Surpress this message by defining the `rng` when creating the `SequenceDesign`" + MersenneTwister(1) + else + rng + end + # @debug design.sequence + currentsequence = sequencestring(rng, design.sequence) + # @debug currentsequence currentsequence = replace(currentsequence, "_" => "") df = repeat(df, inner = length(currentsequence)) + df.event .= repeat(collect(currentsequence), nrows_df) return df @@ -177,13 +199,19 @@ function generate_events(design::SequenceDesign) end +get_rng(design::AbstractDesign) = nothing +get_rng(design::SequenceDesign) = design.rng + """ generate_events(rng,design::RepeatDesign{T}) In a repeated design, iteratively calls the underlying {T} Design and concatenates. In case of MultiSubjectDesign, sorts by subject. """ function generate_events(design::RepeatDesign) - df = map(x -> generate_events(design.design), 1:design.repeat) |> x -> vcat(x...) + design = deepcopy(design) + df = + map(x -> generate_events(get_rng(design.design), design.design), 1:design.repeat) |> + x -> vcat(x...) if isa(design.design, MultiSubjectDesign) sort!(df, [:subject]) end @@ -208,10 +236,12 @@ end Base.size(design::RepeatDesign{MultiSubjectDesign}) = size(design.design) .* (design.repeat, 1) Base.size(design::RepeatDesign{SingleSubjectDesign}) = size(design.design) .* design.repeat -Base.size(design::SequenceDesign) = - size(design.design) .* length(replace(design.sequence, "_" => "")) +#Base.size(design::SequenceDesign) = +#size(design.design) .* length(replace(design.sequence, "_" => "",r"\{.*\}"=>"")) -Base.size(design::RepeatDesign{SequenceDesign{SingleSubjectDesign}}) = - size(design.design) .* design.repeat +#Base.size(design::) = size(design.design) .* design.repeat -Base.size(design::SubselectDesign) = size(generate_events(design), 1) \ No newline at end of file +# No way to find out what size it is without actually generating first... +Base.size( + design::Union{<:SequenceDesign,<:SubselectDesign,<:RepeatDesign{<:SequenceDesign}}, +) = size(generate_events(design), 1) \ No newline at end of file diff --git a/src/onset.jl b/src/onset.jl index ea607506..fc921d75 100644 --- a/src/onset.jl +++ b/src/onset.jl @@ -66,7 +66,6 @@ contains_design(d::AbstractDesign, target::Type) = false contains_design(d::Union{RepeatDesign,SequenceDesign,SubselectDesign}, target::Type) = d.design isa target ? true : contains_design(d.design, target) -sequencestring(rng, d::RepeatDesign) = sequencestring(rng, d.design) """ simulate_onsets(rng, onset::AbstractOnset, simulation::Simulation) diff --git a/src/sequence.jl b/src/sequence.jl index 79ff3021..8322455f 100644 --- a/src/sequence.jl +++ b/src/sequence.jl @@ -16,10 +16,11 @@ function rand_re(rng::AbstractRNG, machine::Automa.Machine) end sequencestring(rng, d::SequenceDesign) = sequencestring(rng, d.sequence) + function sequencestring(rng, str::String) #match curly brackets and replace them @assert isnothing(findfirst("*", str)) && isnothing(findfirst("+", str)) "'infinite' sequences currently not supported" - crly = collect(eachmatch(r"(\{[\d],[\d]\})", str)) + crly = collect(eachmatch(r"(\{[\d]+,[\d]+\})", str)) for c in reverse(crly) m = replace(c.match, "{" => "", "}" => "") rep_minimum, rep_maximum = parse.(Int, split(m, ",")) @@ -42,7 +43,9 @@ function sequencestring(rng, str::String) str[1:bracket_start_idx-1] * replacement_string * str[bracket_end_idx+length(c.match)+1:end] - #@info str + # @debug str end return rand_re(rng, Automa.compile(RE(str))) -end \ No newline at end of file +end + +sequencestring(rng, d::RepeatDesign) = sequencestring(rng, d.design) diff --git a/src/simulation.jl b/src/simulation.jl index 7a292330..4392424c 100755 --- a/src/simulation.jl +++ b/src/simulation.jl @@ -29,6 +29,8 @@ Some remarks to how the noise is added: - If `return_epoched = true` and `onset =NoOnset()` the noise is added to the epoched data matrix - If `onset` is not `NoOnset`, a continuous signal is created and the noise is added to this i.e. this means that the noise won't be the same as in the `onset = NoOnset()` case even if `return_epoched = true`. - The case `return_epoched = false` and `onset = NoOnset()` is not possible and therefore covered by an assert statement + - `simulate(rng,design::SequenceDesign,...)` + If no `design.rng` was defined for `SequenceDesign`, we replace it with the `simulation`-function call `rng` object """ @@ -38,30 +40,34 @@ function simulate(args...; kwargs...) simulate(MersenneTwister(1), args...; kwargs...) end -simulate( - rng::AbstractRNG, - design::AbstractDesign, - signal, - onset::AbstractOnset, - noise::AbstractNoise = NoNoise(); - kwargs..., -) = simulate(rng, Simulation(design, signal, onset, noise); kwargs...) - function simulate( rng::AbstractRNG, - design::SequenceDesign, + design::AbstractDesign, signal, onset::AbstractOnset, noise::AbstractNoise = NoNoise(); kwargs..., ) - design = - isnothing(design.rng) ? - SequenceDesign(design.design, design.sequence, design.sequencelength, rng) : design + + if is_SequenceDesign(design) + design = sequencedesign_add_rng(rng, design) + end simulate(rng, Simulation(design, signal, onset, noise); kwargs...) end +sequencedesign_add_rng(rng, design::AbstractDesign) = design +sequencedesign_add_rng(rng, design::RepeatDesign) = + RepeatDesign(sequencedesign_add_rng(rng, design.design), design.repeat) +sequencedesign_add_rng(rng, design::SequenceDesign) = + isnothing(design.rng) ? + SequenceDesign(design.design, design.sequence, design.sequencelength, rng) : design + + +is_SequenceDesign(d::AbstractDesign) = false +is_SequenceDesign(d::RepeatDesign) = is_SequenceDesign(d.design) +is_SequenceDesign(d::SequenceDesign) = true + function simulate(rng::AbstractRNG, simulation::Simulation; return_epoched::Bool = false) (; design, components, onset, noisetype) = simulation @@ -191,8 +197,8 @@ function add_responses!(signal, responses::Vector, e, s, tvec, erpvec) @views signal[e, tvec, s] .+= responses[:, erpvec] end function add_responses!(signal, responses::Matrix, e, s, tvec, erpvec)# - @debug size(signal), size(responses), e, s, size(tvec), size(erpvec) - @debug tvec, erpvec + # @debug size(signal), size(responses), e, s, size(tvec), size(erpvec) + #@debug tvec, erpvec @views signal[e, tvec, s] .+= responses[:, erpvec] end function add_responses!(signal, responses::AbstractArray, e, s, tvec, erpvec) From 31c69e6611ec30eeec020fad724396d97890c1b0 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Wed, 28 Feb 2024 15:13:52 +0000 Subject: [PATCH 008/156] added string sequence tests --- test/runtests.jl | 1 + test/sequence.jl | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 test/sequence.jl diff --git a/test/runtests.jl b/test/runtests.jl index 723fc891..2cbc1de5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -8,4 +8,5 @@ include("setup.jl") include("onset.jl") include("simulation.jl") include("helper.jl") + include("sequence.jl") end diff --git a/test/sequence.jl b/test/sequence.jl new file mode 100644 index 00000000..e4a9ace0 --- /dev/null +++ b/test/sequence.jl @@ -0,0 +1,11 @@ +@testset "Check Sequences" begin + @test isa(UnfoldSim.check_sequence("bla_"), String) + @test isa(UnfoldSim.check_sequence("bla"), String) + @test_throws AssertionError UnfoldSim.check_sequence("b_la_") + @test_throws AssertionError UnfoldSim.check_sequence("b_la") + @test_throws AssertionError UnfoldSim.check_sequence("_bla") + +end +@test length(UnfoldSim.sequencestring(StableRNG(1), "A{10,10}")) == 10 +@test length(UnfoldSim.sequencestring(StableRNG(1), "A{10,10}B")) == 11 +@test length(UnfoldSim.sequencestring(StableRNG(1), "A{10,20}")) >= 10 \ No newline at end of file From 54e933490464f99a915f8ed9149026445d5c5fbb Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Fri, 1 Mar 2024 18:59:45 +0000 Subject: [PATCH 009/156] small doc update --- docs/literate/HowTo/sequence.jl | 9 +++++++-- src/design.jl | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/literate/HowTo/sequence.jl b/docs/literate/HowTo/sequence.jl index d9da5c4f..6a07d15f 100644 --- a/docs/literate/HowTo/sequence.jl +++ b/docs/literate/HowTo/sequence.jl @@ -1,3 +1,4 @@ +using Base: add_sum using UnfoldSim using CairoMakie using StableRNGs @@ -13,12 +14,16 @@ design = SequenceDesign(design, "SR{1,2}_", 0, StableRNG(1)) generate_events(design) # The main thing that happened is that the design was repeated for every event (each 'letter') of the sequence, and an `eventtype` column was added. # !!! hint -# more advaned sequences exist, like "SR{1,3}", or "A[BC]" etc. +# more advaned sequences are possible as well, like "SR{1,3}", or "A[BC]". Infinite sequences are not possible like "AB*" -# Finally, let's repeat the design 2 times +# Finally, let's repeat the design 2 times - because we can design = RepeatDesign(design, 4) generate_events(design) +#design = UnfoldSim.AddSaccadeAmplitudeDesign4(design,:rt,Normal(0,1),MersenneTwister(1)) +#generate_events(design) + + # This results in 12 trials that nicely follow our sequence # Next we have to specify for both events `S` and `R` what the responses should look like. diff --git a/src/design.jl b/src/design.jl index 5e315fe9..7779c423 100644 --- a/src/design.jl +++ b/src/design.jl @@ -199,6 +199,7 @@ function generate_events(rng, design::SequenceDesign) end + get_rng(design::AbstractDesign) = nothing get_rng(design::SequenceDesign) = design.rng From 7114cd65e5017db7c0bfe0ba55cbd96d3b54a71a Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Sat, 9 Mar 2024 14:05:06 +0000 Subject: [PATCH 010/156] added jitter to the '_' trial divisor --- src/onset.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onset.jl b/src/onset.jl index fc921d75..a6989c26 100644 --- a/src/onset.jl +++ b/src/onset.jl @@ -89,7 +89,7 @@ function simulate_onsets(rng, onset::AbstractOnset, simulation::Simulation) # add to every stepsize onset the maxlength of the response #@debug onsets[stepsize:stepsize:end] @debug stepsize - onsets[stepsize+1:stepsize:end] .= 2 .* maxlength(simulation.components) + onsets[stepsize+1:stepsize:end] .+= 2 .* maxlength(simulation.components) #@debug onsets[stepsize:stepsize:end] end end From b4a8ac8e8415aa433e34b44eb2c97dcffb2acada Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Sun, 10 Mar 2024 19:13:52 +0000 Subject: [PATCH 011/156] generalized LinearModelComponent to arbitrary functions instead of vectors --- docs/literate/HowTo/componentfunction.jl | 39 ++++++++++++++++ docs/literate/HowTo/newComponent.jl | 3 ++ docs/make.jl | 3 +- src/component.jl | 57 ++++++++++++++++++++++-- 4 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 docs/literate/HowTo/componentfunction.jl diff --git a/docs/literate/HowTo/componentfunction.jl b/docs/literate/HowTo/componentfunction.jl new file mode 100644 index 00000000..7d36c62f --- /dev/null +++ b/docs/literate/HowTo/componentfunction.jl @@ -0,0 +1,39 @@ +# # Component Functions +# HowTo put arbitrary functions into components + +using UnfoldSim +using Unfold +using Random +using DSP +using CairoMakie, UnfoldMakie + +sfreq = 100; + +# ## Design +# Let's generate a design with a categorical effect and a continuous duration effect +design = UnfoldSim.SingleSubjectDesign(; + conditions = Dict( + :category => ["dog", "cat"], + :duration => Int.(round.(20 .+ rand(100) .* sfreq)), + ), +); + + +# Instead of defining a boring vector basis function e.g. `[0,0,1,2,3,3,2,1,0,0,0]`, let's use function, generating random values for now. +# !!! important +# because any function depending on `design` can be used, two things have to be taken care of: +# +# 1. in case a random component exist, specify a `RNG`, the basis might be evaluated multiple times inside `simulate` +# 2. a `maxlength` has to be specified via a tuple `(function.maxlength)`` +mybasisfun = design -> hanning.(generate_events(design).duration) +signal = LinearModelComponent(; + basis = (mybasisfun, 100), + formula = @formula(0 ~ 1 + category), + β = [1, 0.5], +); + +erp = UnfoldSim.simulate_component(MersenneTwister(1), signal, design); + + +# Finally, let's plot it, sorted by duration +plot_erpimage(erp, sortvalues = generate_events(design).duration) diff --git a/docs/literate/HowTo/newComponent.jl b/docs/literate/HowTo/newComponent.jl index bb8bee72..80ad9a2f 100644 --- a/docs/literate/HowTo/newComponent.jl +++ b/docs/literate/HowTo/newComponent.jl @@ -1,6 +1,9 @@ # # New component: Duration + Shift # We want a new component that changes its duration and shift depending on a column in the event-design. This is somewhat already implemented in the HRF + Pupil bases +# !!! hint +# if you are just interested to use duration-dependency in your simulation, check out the component-function tutorial + using UnfoldSim using Unfold using Random diff --git a/docs/make.jl b/docs/make.jl index 35c5ff13..e539d980 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -45,8 +45,9 @@ makedocs(; ], "HowTo" => [ "Define a new, (imbalanced) design" => "./generated/HowTo/newDesign.md", + "Use a component-basis-function (duration)" => "./generated/HowTo/componentfunction.md", "Repeating a design" => "./generated/HowTo/repeatTrials.md", - "Define a new duration & jitter component" => "./generated/HowTo/newComponent.md", + "Define a new component" => "./generated/HowTo/newComponent.md", "Generate multi channel data" => "./generated/HowTo/multichannel.md", "Use predefined design / onsets data" => "./generated/HowTo/predefinedData.md", "Produce specific sequences of events" => "./generated/HowTo/sequence.md", diff --git a/src/component.jl b/src/component.jl index d3a0c5e7..c9ba43cc 100644 --- a/src/component.jl +++ b/src/component.jl @@ -52,11 +52,18 @@ LinearModelComponent(; ``` """ @with_kw struct LinearModelComponent <: AbstractComponent - basis::Any - formula::Any # e.g. 0~1+cond - left side must be "0" + basis::Union{Tuple{Function,Int},Array} + formula::FormulaTerm # e.g. 0~1+cond - left side must be "0" β::Vector contrasts::Dict = Dict() offset::Int = 0 + function LinearModelComponent(basis, formula, β, contrasts, offset) + @assert isa(basis, Tuple{Function,Int}) ".basis needs to be an `::Array` or a `Tuple(function::Function,maxlength::Int)`" + @assert basis[2] > 0 "`maxlength` needs to be longer than 0" + new(basis, formula, β, contrasts, offset) + end + LinearModelComponent(basis::Array, formula, β, contrasts, offset) = + new(basis, formula, β, contrasts, offset) end @@ -143,9 +150,51 @@ function simulate_component(rng, c::MultichannelComponent, design::AbstractDesig return reshape(y_proj, length(c.projection), size(y)...) end +""" + basis(c::AbstractComponent) + +returns the basis of the component (typically `c.basis`) +""" +basis(c::AbstractComponent) = c.basis + +""" + basis(c::AbstractComponent,design) +evaluates the basis, if basis is a vector, directly returns it. if basis is a tuple `(f::Function,maxlength::Int)`, evaluates the function with input `design`. Cuts the resulting vector or Matrix at `maxlength` +""" +basis(c::AbstractComponent, design) = basis(basis(c), design) + + +basis(b::AbstractVector, design) = b +function basis(basis::Tuple{Function,Int}, design) + f = basis[1] + maxlength = basis[2] + basis_out = f(design) + if isa(basis_out, AbstractVector{<:AbstractVector}) || isa(basis_out, AbstractMatrix) + @assert length(basis_out) == size(design)[1] "Component basis function needs to either return a Vector, a Matrix with dim(2) == size(design,1), or a Vector of Vectors with length(b) == size(design,1)" + end + limit_basis(basis_out, maxlength) +end + + +function limit_basis(b::AbstractVector{<:AbstractVector}, maxlength) + + # first cut off maxlength + b = limit_basis.(b, maxlength) + # now fill up with 0's + Δlengths = maxlength .- length.(b) + + b = pad_array.(b, Δlengths, 0) + basis_out = reduce(hcat, b) + + + return basis_out +end +limit_basis(b::AbstractVector{<:Number}, maxlength) = b[1:min(length(b), maxlength)] +limit_basis(b::AbstractMatrix, maxlength) = b[1:min(length(b), maxlength), :] + +Base.length(c::AbstractComponent) = isa(basis(c), Tuple) ? basis(c)[2] : length(basis(c)) -Base.length(c::AbstractComponent) = length(c.basis) """ maxlength(c::Vector{<:AbstractComponent}) = maximum(length.(c)) @@ -176,7 +225,7 @@ function simulate_component(rng, c::LinearModelComponent, design::AbstractDesign X = generate_designmatrix(c.formula, events, c.contrasts) y = X * c.β - return y' .* c.basis + return y' .* basis(c, design) end From 0c3a11dfdb9fb3b5d51999069e154ae9232b9d22 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Sun, 10 Mar 2024 19:14:17 +0000 Subject: [PATCH 012/156] bugfix with endless loop due to multiple dispatch --- src/simulation.jl | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/simulation.jl b/src/simulation.jl index 4392424c..4d8ea433 100755 --- a/src/simulation.jl +++ b/src/simulation.jl @@ -35,9 +35,15 @@ Some remarks to how the noise is added: """ -function simulate(args...; kwargs...) +function simulate( + design::AbstractDesign, + signal, + onset::AbstractOnset, + noise::AbstractNoise; + kwargs..., +) @warn "No random generator defined, used the default (`Random.MersenneTwister(1)`) with a fixed seed. This will always return the same results and the user is strongly encouraged to provide their own random generator!" - simulate(MersenneTwister(1), args...; kwargs...) + simulate(MersenneTwister(1), design, signal, onset, noise; kwargs...) end function simulate( From de8c2a8990bc2da394da4677839f69970d66889c Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Sun, 10 Mar 2024 19:30:36 +0000 Subject: [PATCH 013/156] function component for multi-subject --- src/component.jl | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/component.jl b/src/component.jl index c9ba43cc..9ecef258 100644 --- a/src/component.jl +++ b/src/component.jl @@ -170,7 +170,12 @@ function basis(basis::Tuple{Function,Int}, design) maxlength = basis[2] basis_out = f(design) if isa(basis_out, AbstractVector{<:AbstractVector}) || isa(basis_out, AbstractMatrix) - @assert length(basis_out) == size(design)[1] "Component basis function needs to either return a Vector, a Matrix with dim(2) == size(design,1), or a Vector of Vectors with length(b) == size(design,1)" + if isa(basis_out, AbstractMatrix) + l = size(basis_out, 2) + else + l = length(basis_out) # vector of vector case + end + @assert l == size(generate_events(design))[1] "Component basis function needs to either return a Vector of vectors or a Matrix with dim(2) == size(design,1) [l / $(size(design,1))], or a Vector of Vectors with length(b) == size(design,1) [$l / $(size(design,1))]. " end limit_basis(basis_out, maxlength) end @@ -266,7 +271,7 @@ julia> simulate_component(StableRNG(1),c,design) function simulate_component( rng, c::MixedModelComponent, - design::AbstractDesign, + design::AbstractDesign; return_parameters = false, ) events = generate_events(design) @@ -305,8 +310,15 @@ function simulate_component( rethrow(e) end + @debug size(basis(c, design)) # in case the parameters are of interest, we will return those, not them weighted by basis - epoch_data_component = kron(return_parameters ? [1.0] : c.basis, m.y') + b = return_parameters ? [1.0] : basis(c, design) + @debug :b, typeof(b), size(b), :m, size(m.y') + if isa(b, AbstractMatrix) + epoch_data_component = ((m.y' .* b)) + else + epoch_data_component = kron(b, m.y') + end return epoch_data_component #= else From 57a7f68eb10a992d8fc8fb5d21dc7c62a8db225e Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Mon, 11 Mar 2024 09:29:15 +0000 Subject: [PATCH 014/156] forgot to define offset in LinearModelFunction --- src/predefinedSimulations.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/predefinedSimulations.jl b/src/predefinedSimulations.jl index 5a1c2a65..85292ff9 100644 --- a/src/predefinedSimulations.jl +++ b/src/predefinedSimulations.jl @@ -46,9 +46,9 @@ function predef_eeg( # component / signal sfreq = 100, - p1 = (p100(; sfreq = sfreq), @formula(0 ~ 1), [5], Dict()), - n1 = (n170(; sfreq = sfreq), @formula(0 ~ 1 + condition), [5, -3], Dict()), - p3 = (p300(; sfreq = sfreq), @formula(0 ~ 1 + continuous), [5, 1], Dict()), + p1 = (p100(; sfreq = sfreq), @formula(0 ~ 1), [5], Dict(), 0), + n1 = (n170(; sfreq = sfreq), @formula(0 ~ 1 + condition), [5, -3], Dict(), 0), + p3 = (p300(; sfreq = sfreq), @formula(0 ~ 1 + continuous), [5, 1], Dict(), 0), kwargs..., ) From 85f037b3f04745a77ebcfceb826defc749d794f0 Mon Sep 17 00:00:00 2001 From: jschepers Date: Wed, 10 Jul 2024 16:15:44 +0000 Subject: [PATCH 015/156] Improve documentation especially quickstart, home and power analysis --- docs/literate/tutorials/poweranalysis.jl | 2 +- docs/literate/tutorials/quickstart.jl | 23 ++++++++++++++--------- docs/make.jl | 2 +- docs/src/index.md | 8 ++++---- src/noise.jl | 2 +- 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/docs/literate/tutorials/poweranalysis.jl b/docs/literate/tutorials/poweranalysis.jl index bbe7f1a5..5202ad66 100644 --- a/docs/literate/tutorials/poweranalysis.jl +++ b/docs/literate/tutorials/poweranalysis.jl @@ -5,7 +5,7 @@ using DataFrames using Random -# ## Simple Poweranalysis Script +# ## Simple Power analysis Script # For a power analysis, we will repeatedly simulate data, and check whether we can find a significant effect. # # We perform the power analysis on epoched data. diff --git a/docs/literate/tutorials/quickstart.jl b/docs/literate/tutorials/quickstart.jl index 86896185..5811931b 100644 --- a/docs/literate/tutorials/quickstart.jl +++ b/docs/literate/tutorials/quickstart.jl @@ -1,18 +1,23 @@ +# # Quickstart + using UnfoldSim -using Random -using CairoMakie +using Random # to get an RNG +using CairoMakie # for plotting +# To get started with data simulation, the user needs to provide four ingredients: an experimental design, an event basis function (component), an inter-onset distribution and a noise specification. # !!! tip # Use `subtypes(AbstractNoise)` (or `subtypes(AbstractComponent)` etc.) to find already implemented building blocks. -# ## "Experimental" Design +# ## Specify the simulation ingredients + +# ### Experimental Design # Define a 1 x 2 design with 20 trials. That is, one condition (`condaA`) with two levels. design = SingleSubjectDesign(; conditions = Dict(:condA => ["levelA", "levelB"])) |> x -> RepeatDesign(x, 10); -# #### Component / Signal +# ### Event basis function (Component) # Define a simple component and ground truth simulation formula. Akin to ERP components, we call one simulation signal a component. # # !!! note @@ -23,19 +28,19 @@ signal = LinearModelComponent(; β = [1, 0.5], ); -# #### Onsets and Noise -# We will start with a uniform (but overlapping, `offset` < `length(signal.basis)`) onset-distribution +# ### Onsets and Noise +# We will start with a uniform (but overlapping, `offset` < `length(signal.basis)`) inter-onset distribution. onset = UniformOnset(; width = 20, offset = 4); # And we will use some noise noise = PinkNoise(; noiselevel = 0.2); # ## Combine & Generate -# Finally, we will simulate some data +# Finally, we will combine all ingredients and simulate some data. data, events = simulate(MersenneTwister(1), design, signal, onset, noise); -# `Data` is a `n-sample` Vector (but could be a Matrix for e.g. `MultiSubjectDesign`). +# `data` is a `n-sample` Vector (but could be a Matrix for e.g. `MultiSubjectDesign` or epoched data). -# `Events` is a DataFrame that contains a column `latency` with the onsets of events. +# `events` is a DataFrame that contains a column `latency` with the onsets of events (in samples). # ## Plot them! lines(data; color = "black") diff --git a/docs/make.jl b/docs/make.jl index ffba630f..3403cbd7 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -34,7 +34,7 @@ makedocs(; "Tutorials" => [ "Quickstart" => "generated/tutorials/quickstart.md", "Simulate ERPs" => "generated/tutorials/simulateERP.md", - "Poweranalysis" => "generated/tutorials/poweranalysis.md", + "Power analysis" => "generated/tutorials/poweranalysis.md", "Multi-subject simulation" => "generated/tutorials/multisubject.md", ], "Reference" => [ diff --git a/docs/src/index.md b/docs/src/index.md index 415199d4..40ff2f14 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -2,14 +2,14 @@ CurrentModule = UnfoldSim ``` -# UnfoldSim +# UnfoldSim.jl -Documentation for [UnfoldSim](https://github.com/behinger/UnfoldSim.jl). +Documentation for [UnfoldSim.jl](https://github.com/unfoldtoolbox/UnfoldSim.jl). UnfoldSim.jl is a Julia package for simulating multivariate timeseries data with a special focus on EEG data. ## Start simulating timeseries -We offer some predefined signals, check them out! +We offer some predefined (EEG) signals, check them out! -For instance an P1/N170/P300 complex. +For instance a P1/N170/P300 complex (containing three typical ERP components). ```@example main using UnfoldSim using CairoMakie # plotting diff --git a/src/noise.jl b/src/noise.jl index 6b7b8592..28421ccd 100644 --- a/src/noise.jl +++ b/src/noise.jl @@ -43,7 +43,7 @@ end """ RealisticNoise <: AbstractNoise -Not implemented - planned to use Artefacts.jl to provide real EEG data to add. +Not implemented - planned to use Artifacts.jl to provide real EEG data to add. """ @with_kw struct RealisticNoise <: AbstractNoise noiselevel = 1 From 2c65a7fa69605d018bca7d02980616ba1f3a123f Mon Sep 17 00:00:00 2001 From: jschepers Date: Thu, 18 Jul 2024 06:09:35 +0000 Subject: [PATCH 016/156] adapted the order of reference overviews and adapted titles --- docs/literate/reference/basistypes.jl | 4 ++-- docs/literate/reference/noisetypes.jl | 5 +++-- docs/literate/reference/onsettypes.jl | 12 ++++++------ docs/make.jl | 8 ++++---- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/docs/literate/reference/basistypes.jl b/docs/literate/reference/basistypes.jl index c04d73cc..de4db4a6 100644 --- a/docs/literate/reference/basistypes.jl +++ b/docs/literate/reference/basistypes.jl @@ -3,14 +3,14 @@ using CairoMakie using DSP using StableRNGs -# ## Basistypes +# # Overview: Basis function (component) types # There are several basis types directly implemented. They can be easily used for the `components`. # # !!! note # You can use any arbitrary shape defined by yourself! We often make use of `hanning(50)` from the DSP.jl package. # ## EEG -# By default, the EEG bases assume a sampling rate of 100, which can easily be changed by e.g. p100(;sfreq=300) +# By default, the EEG bases assume a sampling rate of 100, which can easily be changed by e.g. p100(; sfreq=300) f = Figure() ax = f[1, 1] = Axis(f) for b in [p100, n170, p300, n400] diff --git a/docs/literate/reference/noisetypes.jl b/docs/literate/reference/noisetypes.jl index fe3efc4a..26af0e1e 100644 --- a/docs/literate/reference/noisetypes.jl +++ b/docs/literate/reference/noisetypes.jl @@ -1,10 +1,11 @@ +# # Overview: Noise types +# There are several noise types directly implemented. Here is a comparison: + using UnfoldSim using CairoMakie using DSP using StableRNGs import StatsBase.autocor -# ## What's the noise? -# There are several noise-types directly implemented. Here is a comparison: f = Figure() ax_sig = diff --git a/docs/literate/reference/onsettypes.jl b/docs/literate/reference/onsettypes.jl index 279a2226..0c0980b1 100644 --- a/docs/literate/reference/onsettypes.jl +++ b/docs/literate/reference/onsettypes.jl @@ -1,4 +1,4 @@ -# # Onset types +# # Overview: Onset types # The onset types determine the distances between event onsets in the continuous EEG signal. The distances are sampled from a certain probability distribution. # Currently, there are two types of onset distributions implemented: `UniformOnset` and `LogNormalOnset`. @@ -58,11 +58,11 @@ let # hide design, # hide ) # hide - hist!( - ax, - distances, - bins = range(0, 100, step = 1), - label = "($width, $offset)", + hist!( # hide + ax, # hide + distances, # hide + bins = range(0, 100, step = 1), # hide + label = "($width, $offset)", # hide ) # hide if label == "offset" && offset != 0 # hide diff --git a/docs/make.jl b/docs/make.jl index 3403cbd7..efe43840 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -38,10 +38,10 @@ makedocs(; "Multi-subject simulation" => "generated/tutorials/multisubject.md", ], "Reference" => [ - "Overview: Toolbox Functions" => "./generated/reference/overview.md", - "Overview: NoiseTypes" => "./generated/reference/noisetypes.md", - "Overview: OnsetTypes" => "./generated/reference/onsettypes.md", - "Overview: Components (EEG, fMRI, Pupil)" => "./generated/reference/basistypes.md", + "Overview of functionality" => "./generated/reference/overview.md", + "Overview: Basis function (component) types" => "./generated/reference/basistypes.md", + "Overview: Onset types" => "./generated/reference/onsettypes.md", + "Overview: Noise types" => "./generated/reference/noisetypes.md", ], "HowTo" => [ "Define a new, (imbalanced) design" => "./generated/HowTo/newDesign.md", From ee9e00ad8b4565df05fdb4c9b67e3f5a68e87e92 Mon Sep 17 00:00:00 2001 From: jschepers Date: Mon, 22 Jul 2024 13:13:16 +0000 Subject: [PATCH 017/156] Updated quickstart page --- docs/literate/tutorials/quickstart.jl | 31 ++++++++++++++++++++------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/docs/literate/tutorials/quickstart.jl b/docs/literate/tutorials/quickstart.jl index 5811931b..4d93bb68 100644 --- a/docs/literate/tutorials/quickstart.jl +++ b/docs/literate/tutorials/quickstart.jl @@ -1,9 +1,5 @@ # # Quickstart -using UnfoldSim -using Random # to get an RNG -using CairoMakie # for plotting - # To get started with data simulation, the user needs to provide four ingredients: an experimental design, an event basis function (component), an inter-onset distribution and a noise specification. # !!! tip @@ -11,10 +7,24 @@ using CairoMakie # for plotting # ## Specify the simulation ingredients +# ### Setup +# ```@raw html +#
+# Click to expand +# ``` +## Load required packages +using UnfoldSim +using Random # to get an RNG +using CairoMakie # for plotting + +# ```@raw html +#
+# ``` + # ### Experimental Design -# Define a 1 x 2 design with 20 trials. That is, one condition (`condaA`) with two levels. +# Define a 1 x 2 design with 20 trials. That is, one condition (`cond_A`) with two levels. design = - SingleSubjectDesign(; conditions = Dict(:condA => ["levelA", "levelB"])) |> + SingleSubjectDesign(; conditions = Dict(:cond_A => ["level_A", "level_B"])) |> x -> RepeatDesign(x, 10); # ### Event basis function (Component) @@ -24,7 +34,7 @@ design = # You could easily specify multiple components by providing a vector of components, which are automatically added at the same onsets. This procedure simplifies to generate some response that is independent of simulated condition, whereas other depends on it. signal = LinearModelComponent(; basis = [0, 0, 0, 0.5, 1, 1, 0.5, 0, 0], - formula = @formula(0 ~ 1 + condA), + formula = @formula(0 ~ 1 + cond_A), β = [1, 0.5], ); @@ -44,5 +54,10 @@ data, events = simulate(MersenneTwister(1), design, signal, onset, noise); # ## Plot them! lines(data; color = "black") -vlines!(events.latency; color = ["orange", "teal"][1 .+ (events.condA.=="levelB")]) +vlines!(events.latency; color = ["orange", "teal"][1 .+ (events.cond_A.=="level_B")]) + +current_axis().title = "Simulated data" +current_axis().xlabel = "Time [samples]" +current_axis().ylabel = "Amplitude [μV]" + current_figure() From ac0d3bd736e0393c0855e1855a992fe225e8f94d Mon Sep 17 00:00:00 2001 From: jschepers Date: Mon, 22 Jul 2024 13:13:43 +0000 Subject: [PATCH 018/156] minor changes --- README.md | 2 +- docs/literate/reference/onsettypes.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cdfb3b45..cac86db8 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ data, events = simulate( PinkNoise(), ); ``` -All components (design, components, onsets, noise) can be easily modified and you can simply plugin your own! +All simulation ingredients (design, components, onsets, noise) can be easily modified and you can simply plugin your own! ## Contributions Contributions of any kind are very welcome. Please have a look at [CONTRIBUTING.md](https://github.com/unfoldtoolbox/UnfoldSim.jl/blob/main/CONTRIBUTING.md) for guidance on contributing to UnfoldSim.jl. diff --git a/docs/literate/reference/onsettypes.jl b/docs/literate/reference/onsettypes.jl index 0c0980b1..bbadef15 100644 --- a/docs/literate/reference/onsettypes.jl +++ b/docs/literate/reference/onsettypes.jl @@ -7,7 +7,7 @@ #
# Click to expand # ``` - +## Load required packages using UnfoldSim using CairoMakie using Random From 373e535b3d09e93cd5d50b7867bfc5490897ef10 Mon Sep 17 00:00:00 2001 From: jschepers Date: Tue, 23 Jul 2024 16:18:16 +0000 Subject: [PATCH 019/156] fixed docstrings for predef_eeg and predef_2x2 --- src/predefinedSimulations.jl | 83 ++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 42 deletions(-) diff --git a/src/predefinedSimulations.jl b/src/predefinedSimulations.jl index 874bb870..8adf21f0 100644 --- a/src/predefinedSimulations.jl +++ b/src/predefinedSimulations.jl @@ -7,36 +7,36 @@ predef_eeg(; kwargs...) = predef_eeg(MersenneTwister(1); kwargs...) # without rn predef_eeg(nsubjects::Int; kwargs...) = predef_eeg(MersenneTwister(1), nsubjects; kwargs...) # without rng always call same one """ -predef_eeg(;kwargs...) -predef_eeg(rng;kwargs...) -predef_eeg(rng,n_subjects;kwargs...) + predef_eeg(; kwargs...) + predef_eeg(rng; kwargs...) + predef_eeg(rng, n_subjects; kwargs...) Generate a P1/N1/P3 complex. In case `n_subjects` is defined - `MixedModelComponents` are generated, else `LinearModelComponents`. -The most used `kwargs` is: `return_epoched=true` which returns already epoched data. If you want epoched data without overlap, specify `onset=NoOnset()` and `return_epoched=true` +The most used `kwargs` is: `return_epoched=true` which returns already epoched data. If you want epoched data without overlap, specify `onset = NoOnset()` and `return_epoched = true` +## Default parameters: -## Default params: +#### Design +- `n_repeats = 100`, +- `event_order_function = x -> shuffle(deepcopy(rng), x)`, # random trial order +- `conditions = Dict(...)`, -- n_repeats = 100 -- event_order_function = x->shuffle(deepcopy(rng),x # random trial order -- conditions = Dict(...), +#### Component / Signal +- `sfreq = 100`, +- `p1 = (p100(; sfreq = sfreq), @formula(0 ~ 1), [5], Dict())`, # P1 amp 5, no effects +- `n1 = (n170(; sfreq = sfreq), @formula(0 ~ 1 + condition), [5,-3], Dict())`, # N1 amp 5, dummy-coded condition effect (levels "car", "face") of -3 +- `p3 = (p300(; sfreq = sfreq), @formula(0 ~ 1 + continuous), [5,1], Dict())`, # P3 amp 5, continuous effect range [-5,5] with slope 1 -#### component / signal -- sfreq = 100, -- p1 = (p100(;sfreq=sfreq), @formula(0~1),[5],Dict()), # P1 amp 5, no effects -- n1 = (n170(;sfreq=sfreq), @formula(0~1+condition),[5,-3],Dict()), # N1 amp 5, dummycoded condition effect (levels "car", "face") of -3 -- p3 = (p300(;sfreq=sfreq), @formula(0~1+continuous),[5,1],Dict()), # P3 amp 5, continuous effect range [-5,5] with slope 1 +#### Onset +- `overlap = (0.5,0.2)`, # offset + width/length of Uniform noise. put offset to 1 for no overlap. put width to 0 for no jitter +- `onset = UniformOnset(; offset = sfreq * 0.5 * overlap[1], width = sfreq * 0.5 * overlap[2])`, -#### noise -- noiselevel = 0.2, -- noise = PinkNoise(;noiselevel=noiselevel), - -#### onset -- overlap = (0.5,0.2), # offset + width/length of Uniform noise. put offset to 1 for no overlap. put width to 0 for no jitter -- onset=UniformOnset(;offset=sfreq*0.5*overlap[1],width=sfreq*0.5*overlap[2]), +#### Noise +- `noiselevel = 0.2`, +- `noise = PinkNoise(; noiselevel = noiselevel)`, """ function predef_eeg( rng; @@ -139,34 +139,33 @@ function predef_eeg( end """ - predef_2x2(rng::AbstractRNG;kwargs...) + predef_2x2(rng::AbstractRNG; kwargs...) -The most used `kwargs` is: `return_epoched=true` which returns already epoched data. If you want epoched data without overlap, specify `onset=NoOnset()` and `return_epoched=true` +The most used `kwargs` is: `return_epoched = true` which returns already epoched data. If you want epoched data without overlap, specify `onset = NoOnset()` and `return_epoched = true` -#### design -- `n_items`=100, -- `n_subjects`=1, -- `conditions` = Dict(:A=>["a_small","a_big"],:B=>["b_tiny","b_large"]), -- `event_order_function` = x->shuffle(deepcopy(rng),x), +#### Design +- `n_items = 100`, +- `n_subjects = 1`, +- `conditions = Dict(:A => ["a_small","a_big"], :B => ["b_tiny","b_large"])`, +- `event_order_function = x -> shuffle(deepcopy(rng), x)`, -#### component / signal -- `signalsize` = 100, length of simulated hanning window -- `basis`` = hanning(signalsize), the actual "function", `signalsize` is only used here -- `β` = [1,-0.5,.5,+1], the parameters -- `σs` = Dict(:subject=>[1,0.5,0.5,0.5],:item=>[1]), - only in n_subjects>=2 case, specifies the random effects -- `contrasts` = Dict(:A=>EffectsCoding(),:B=>EffectsCoding()) - effect coding by default -- `formula` = n_subjects==1 ? @formula(0~1+A*B) : @formula(dv~1+A*B+(A*B|subject)+(1|item)), +#### Component / Signal +- `signalsize = 100`, # Length of simulated hanning window +- `basis = hanning(signalsize)`, # The actual "function", `signalsize` is only used here +- `β = [1, -0.5, .5, +1]`, # The parameters +- `σs = Dict(:subject => [1, 0.5, 0.5, 0.5],:item => [1])`, # Only in n_subjects >= 2 case, specifies the random effects +- `contrasts = Dict(:A => EffectsCoding(), :B => EffectsCoding())` # Effect coding by default +- `formula = n_subjects == 1 ? @formula(0 ~ 1 + A*B) : @formula(dv ~ 1 + A*B + (A*B|subject) + (1|item))`, -#### noise -- `noiselevel` = 0.2, -- `noise` = PinkNoise(;noiselevel=noiselevel), +#### Onset +- `overlap = (0.5,0.2)`, +- `onset = UniformOnset(; offset = signalsize * overlap[1], width = signalsize * overlap[2])`, # Put offset to 1 for no overlap. put width to 0 for no jitter -#### onset -- `overlap` = (0.5,0.2), -- `onset`=UniformOnset(;offset=signalsize*overlap[1],width=signalsize*overlap[2]), #put offset to 1 for no overlap. put width to 0 for no jitter +#### Noise +- `noiselevel = 0.2`, +- `noise = PinkNoise(; noiselevel = noiselevel)`, - -Careful if you modify n_items with n_subjects = 1, n_items has to be a multiple of 4 (or your equivalent conditions factorial, e.g. all combinations length) +Be careful if you modify n_items with n_subjects = 1, n_items has to be a multiple of 4 (or your equivalent conditions factorial, e.g. all combinations length) """ function predef_2x2( rng::AbstractRNG; From b073526492785ef1c0dda358a1e36b498b16a2ec Mon Sep 17 00:00:00 2001 From: jschepers Date: Tue, 23 Jul 2024 18:28:58 +0000 Subject: [PATCH 020/156] added draft of design types reference page --- docs/literate/HowTo/repeatTrials.jl | 2 +- docs/literate/reference/designtypes.jl | 100 +++++++++++++++++++++++++ docs/make.jl | 1 + 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 docs/literate/reference/designtypes.jl diff --git a/docs/literate/HowTo/repeatTrials.jl b/docs/literate/HowTo/repeatTrials.jl index 046bfb75..1add60e6 100644 --- a/docs/literate/HowTo/repeatTrials.jl +++ b/docs/literate/HowTo/repeatTrials.jl @@ -1,7 +1,7 @@ using UnfoldSim -# ## Repeating Design entries +# # [Repeating Design entries](@id howto_repeat_design) # Sometimes we want to repeat a design, that is, have multiple trials with identical values, but it is not always straight forward to implement. # For instance, there is no way to easily modify `MultiSubjectDesign` to have multiple identical subject/item combinations, # without doing awkward repetitions of condition-levels or something. diff --git a/docs/literate/reference/designtypes.jl b/docs/literate/reference/designtypes.jl new file mode 100644 index 00000000..d21693bf --- /dev/null +++ b/docs/literate/reference/designtypes.jl @@ -0,0 +1,100 @@ +# # Overview: Experimental design types + +# The experimental design specifies the experimental conditions and other variables that are supposed to have an influence on the simulated data. +# Currently, there are three types of designs implemented: `SingleSubjectDesign`, `MultiSubjectDesign` and `RepeatDesign`. + +# ## Setup +# ```@raw html +#
+# Click to expand +# ``` +## Load required packages +using UnfoldSim +using Random +# ```@raw html +#
+# ``` + +# ## Single-subject designs +# As the name suggests, the `SingleSubjectDesign` type can be used to specify the experimental design for a single subject. Using the `conditions` arguments, +# the user can specify all relevant conditions or predictors and their levels or value range. + +# The current implementation assumes a full factorial design (also called fully crossed design) +# in which each level of a factor occurs with each level of the other factors. Moreover, in the current implementation, there is exactly one instance of each of these factor combinations. + +# Example: +design_single = SingleSubjectDesign(; + conditions = Dict( + :stimulus_type => ["natural", "artificial"], + :contrast_level => range(0, 1, length = 3), + ), +); + +# In order to inspect the design, we can use the `generate_events` function to create an event table based on the design we specified. +generate_events(design_single) + +# To change the order of the trials e.g. to sort or shuffle them, one can use the `event_order_function` argument. +# Example: Randomize the order of trials +design_single_shuffled = SingleSubjectDesign(; + conditions = Dict( + :stimulus_type => ["natural", "artificial"], + :contrast_level => range(0, 1, length = 3), + ), + event_order_function = x -> shuffle(MersenneTwister(42), x), +); +# ```@raw html +#
+# Click to expand event table +# ``` +generate_events(design_single_shuffled) +# ```@raw html +#
+# ``` + +# ## Multi-subject designs +# The `MultiSubjectDesign` type can be used to simulate data for an experiment with multiple subjects. +# One needs to specify the number of subjects `n_subjects` and the number of items `n_items` i.e. stimuli. +# In addition, one needs to decide for every experimental factor whether it should be between- or within-subject (and item). + +# !!! note +# For factors that are not listed in `items_between` it is assumed that they vary within-item () + +design_multi = MultiSubjectDesign( + n_subjects = 6, + n_items = 4, + items_between = Dict(:colour => ["red", "blue"]), + subjects_between = Dict(:age_group => ["young", "old"]), + both_within = Dict(:luminance => range(0, 1, length = 3)), +); +# Caution, designs have to be balanced + +# ```@raw html +#
+# Click to expand event table +# ``` +generate_events(design_multi) +# ```@raw html +#
+# ``` + +# ## Repeat designs +# The `RepeatDesign` type is a functionality to encapsulate single- or multi-subject designs. It allows to repeat a generated event table multiple times. +# In other words, the `RepeatDesign` type allows to have multiple instances of the same item/subject/factor level combination. + +# Example: +# Assume, we have the following single-subject design from above: +# ```@raw html +#
+# Click to expand event table +# ``` +generate_events(design_single) +# ```@raw html +#
+# ``` + + +# But instead of having only one instance of the factor combinations e.g. `stimulus_type`: `natural` and `contrast_level`: `0`, we will repeat the design three times such that there are three occurrences of each combination. +design_repeated = RepeatDesign(design_single, 3); +generate_events(design_repeated) + +# [Here](@ref howto_repeat_design) one can find another example of how to repeat design entries for multi-subject designs. \ No newline at end of file diff --git a/docs/make.jl b/docs/make.jl index efe43840..72dc6219 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -39,6 +39,7 @@ makedocs(; ], "Reference" => [ "Overview of functionality" => "./generated/reference/overview.md", + "Overview: Experimental design types" => "./generated/reference/designtypes.md", "Overview: Basis function (component) types" => "./generated/reference/basistypes.md", "Overview: Onset types" => "./generated/reference/onsettypes.md", "Overview: Noise types" => "./generated/reference/noisetypes.md", From 89c09a1e71f3c9fc22e608a25933e52e3ad43b45 Mon Sep 17 00:00:00 2001 From: Benedikt Ehinger Date: Wed, 24 Jul 2024 13:05:46 +0200 Subject: [PATCH 021/156] Update quickstart.jl --- docs/literate/tutorials/quickstart.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/literate/tutorials/quickstart.jl b/docs/literate/tutorials/quickstart.jl index 4d93bb68..66d735a9 100644 --- a/docs/literate/tutorials/quickstart.jl +++ b/docs/literate/tutorials/quickstart.jl @@ -1,6 +1,10 @@ # # Quickstart -# To get started with data simulation, the user needs to provide four ingredients: an experimental design, an event basis function (component), an inter-onset distribution and a noise specification. +# To get started with data simulation, the user needs to provide four ingredients: +# 1) an experimental design, defining which conditions exist and how many events/"trials" +# 2) an event basis function, defining the simulated event-related response for every event (e.g. the ERP shape in EEG) +# 3) an inter-onset event distribution, defining the distances in time of the event sequence +# 4) a noise specification, defining, well, the noise :) # !!! tip # Use `subtypes(AbstractNoise)` (or `subtypes(AbstractComponent)` etc.) to find already implemented building blocks. From 1e2694f4b7b10bdf6b3700aadb322b9e471427c0 Mon Sep 17 00:00:00 2001 From: Judith Schepers Date: Wed, 24 Jul 2024 18:37:56 +0200 Subject: [PATCH 022/156] Add UnfoldSim logo to the documentation --- docs/assets/logo.svg | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 docs/assets/logo.svg diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg new file mode 100644 index 00000000..340ebbd2 --- /dev/null +++ b/docs/assets/logo.svg @@ -0,0 +1,28 @@ + + + + + + + From 9ab04aada02234dc611823279c0cea04dce33e22 Mon Sep 17 00:00:00 2001 From: jschepers Date: Wed, 24 Jul 2024 17:12:26 +0000 Subject: [PATCH 023/156] Finished experimental design reference page --- docs/literate/reference/designtypes.jl | 13 ++++++++++--- docs/{ => src}/assets/logo.svg | 0 2 files changed, 10 insertions(+), 3 deletions(-) rename docs/{ => src}/assets/logo.svg (100%) diff --git a/docs/literate/reference/designtypes.jl b/docs/literate/reference/designtypes.jl index d21693bf..3d4cb98c 100644 --- a/docs/literate/reference/designtypes.jl +++ b/docs/literate/reference/designtypes.jl @@ -52,12 +52,12 @@ generate_events(design_single_shuffled) # ``` # ## Multi-subject designs -# The `MultiSubjectDesign` type can be used to simulate data for an experiment with multiple subjects. +# The `MultiSubjectDesign` type can be used to simulate data for an experiment with multiple subjects. Internally, it uses the [MixedModelsSim.jl package](https://github.com/RePsychLing/MixedModelsSim.jl). # One needs to specify the number of subjects `n_subjects` and the number of items `n_items` i.e. stimuli. # In addition, one needs to decide for every experimental factor whether it should be between- or within-subject (and item). # !!! note -# For factors that are not listed in `items_between` it is assumed that they vary within-item () +# For factors that are not listed in `items_between` it is assumed that they vary within-item (accordingly for `subjects_between`). design_multi = MultiSubjectDesign( n_subjects = 6, @@ -66,7 +66,6 @@ design_multi = MultiSubjectDesign( subjects_between = Dict(:age_group => ["young", "old"]), both_within = Dict(:luminance => range(0, 1, length = 3)), ); -# Caution, designs have to be balanced # ```@raw html #
@@ -75,8 +74,15 @@ design_multi = MultiSubjectDesign( generate_events(design_multi) # ```@raw html #
+#
# ``` +# As with the `SingleSubjectDesign` one can use the `event_order_function` argument to determine the order of events/trials. + +# !!! important +# The number of subjects/items has to be a divisor of the number of factor level combinations, i.e. it is assumed that the design is balanced +# which means that there is an equal number of observations for all possible factor level combinations. + # ## Repeat designs # The `RepeatDesign` type is a functionality to encapsulate single- or multi-subject designs. It allows to repeat a generated event table multiple times. # In other words, the `RepeatDesign` type allows to have multiple instances of the same item/subject/factor level combination. @@ -90,6 +96,7 @@ generate_events(design_multi) generate_events(design_single) # ```@raw html #
+#
# ``` diff --git a/docs/assets/logo.svg b/docs/src/assets/logo.svg similarity index 100% rename from docs/assets/logo.svg rename to docs/src/assets/logo.svg From 5671e2e71d602f016a90d89d6650c6eca3a414b3 Mon Sep 17 00:00:00 2001 From: Judith Schepers Date: Thu, 25 Jul 2024 15:09:27 +0200 Subject: [PATCH 024/156] Replace logo file --- docs/src/assets/logo.svg | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/docs/src/assets/logo.svg b/docs/src/assets/logo.svg index 340ebbd2..676efd96 100644 --- a/docs/src/assets/logo.svg +++ b/docs/src/assets/logo.svg @@ -1,26 +1,27 @@ - - - >1pe z!mT2nXn60-LTv1Zt}1rsQ*lS-*PR#O+U}`uQGQ<3adGr2u4ZVo)pf*EH+g(96DkT4 z2Hy)9G^FMTGrpp-iGq``N+v2?vsAdn*Z4%C;9>Dp(D!ZhnOEzKhZOqS9ZT~41W|9f z)^+aIZe|UO3q(JVuc(}3N`*4R54EbN4*g~fH2`rYc1kNRzPJ5V+OXU3pSS|?#TLLeP`!eglJn`3M)8)$PZkBXzc)DWnUDr@F zhjcMRKzD~gk`2=cr&R0u0o!42ntz=;EGkXjUhX%c6S4}@XqndavhA6fU;32X9){n& z!lw>Xt%)LGa@<=uy&}K9pFi>D51Jg!DR7m7n{+?dx+r0^FUmCqwU~U@Y8xqzrYlP4d&`jZ*)TP0x=;RdHxIpRz0^| z(Th2iEdQ}gN6G%Sg`&ryY>%mL7rdzjM)iP1PVM4LX?_PcFqRFC3oQUcOa?<)X0n+j zO8I>2DR=<>v0`eP--`Z628z{AGwumK5j+ul^O(uKKQY9`42BJhN5}|zSEX|z1Rc@$ z_FEn3n{mpdz}V{Gv%bG~NHOr!4|4x8?u*2>Or6AiPf^9NB4XF^Jv6ij1r>#loc!74 zHn6EHU{kovHnXI*C~G<%vXK)x>Dvayr@8O}1Wg^27EC{f!pv6e$}tRLt5{Z^O~&@+ zHHO?6h(`D+1Kl|U;<$l}A{dO#!C~Z_qRc^6p(!hFodKs&nQYQ4Z3)k(FRC_dxNL4% zgr9qDg7eIBCgI?8Zmx@RrIM(71v{Je`SGSUEbr5e17@WAAwnj8N|=pz0adU;j;o-C z5aP8EtfixaDKXR;Q5c@eX&Wju9SCiP`r1ND>vC(vu?{Mcw;6rDi-Wb^rLL`%1{KniG24D!2obV zIB=;5sj-Y~O=4B>%nXL#h(j^a5~~8Q+g`g4-cwE`5R&(PX01Li+$_3Fz9YG@G4PfA zYEzIJshmZ|+C0{fAaH?G^`2k&-4TV;01+;eqBAPH9B?>=@SVWC-O$WYwkyI&qPQA@ zPlBpyGF8f2lT~DauO3^b*zP$r(LP`H7jlss2t1Dl%FxK5lHu_@&%>AYfWa~~i&ud2 z$nxu*95`FYZBbTweiwL7e`WMTLy_3uXmbxpY4ts?NQ)V)TY%4d2gL{H@3C$_RRx(9`XOENy7(1Ln7+o|foL<| zbyPYJeuNf7i48<^v$mDmPJnI_DJH-8|GbTMzAk#)aq` z0Qu)2<}kMYYQQ$aQRJ8~MKhWK)`c#G=T2+pC}eg^=bMCACXyPjURaMP)c3^fH}VC- zqS_nNb~E(B!21fO>Pj!I^MPNY`+h5pQMa)!8Vx=%7r{6JQSrvUeZZ2^a=n4JSSx4o zY)3))s_&fJj+rxq?#LE`Zbp={^pwtSmvdY&ZXVt+kBL4giQ@V{xukU2_UX*`$f?tM z2n8}4h4Xrml;9YypWIj=*_NYKd@7FRFb*_`6ZTxHs!($QfgI(wSYz9{Pos>x2zYb2 zsqNj&zf5KvX6^0&HyMSFUE-u-fghns>z62fR|gwz*3Odnsl* zsLVJ1X~{FX<0%CTor~Pt)vxW6AY)^y&11jB74}LES!J8Y#y#oICkO6dHIFH#Y#NG& z$okZ->`++k6qkrw$Y=Ru3P3ZnM{lMvh@7`SzcnP>ROsa6HMc3^t_(jrQ%=X!i^J`uRIu^=rgyY~XE`UJToDaBISnz*;3tcRrm+5A==w$hwAm zYzdj2-cr}(r2wazE?*b0S245&9M)vd+{1$!hCXtCY0;6`-IU;k-qF^*UJCDNPn>%q zYBRTPuo@2fT|@v^K^UTqW~#u4N@O#xhEu6^9X#`;V_wB{If*xPF+|yBi~!I<@2iim zRqok;0u4|%!!_pKYsg{ZaA6ejHm1q%~uZ&`tUOE)9#!r9@_yk(T~C ziTYBgV4YRuQBY0_VA2IWoq%U4JMUc^wy&-4u9cY@>t_n+Bwddd(-gvkQcO|K387JBoTv zN&bwn{q{p$+-a7VFD#MPX$bLTV`d~6t?w7=L~hr^!Qp!J5G&BN4a5eN4w-%(`+~lZ zsEPTQ#q+@D*sC1!XrBX9j_B?ZB`BGW--S7xuT0+|c0B+eYdV^DR}l}fVt>`&Z$NZ8 z6Orp)=VD{!gWSa5zKFUIhLB7{Lo@NJZFUMuLg#fwUZuuTb0rkRpP5cl`4sEHKZm^4 z_rpacesmk;$dYA>djBKTN(o44ppB}Vyn}6GLNdnrvgh~^U;Gf{!>6Bh-fV5hLPHM1UTb&qgx{r>F@9acvkT5KRhrW`4mi4DxISgSYZ;c-$`t^aJrM2QU66}PhD4*GW)v(riGl7Bf?W(xMT_h zMMLXbYk`)mHt43mx_|;KKzky3SO#qxtO-khi|mm>OQ*PNMe*0rHcv+w*Eb-E z+c45UQL#m=CUB4vC&M71M9j)xpay&?!XRUG`7}n zfdYxqBk01J_P31Lm6m;Wg{C|e(pMqf zk74xT$jX(3G?(!T%|+L*Rrv18rxKaD;6w>z94~%!=MRL*6LpD;8`YO@!EJi{kcQc6 zHx`yQf~cTm&kY|RKY@8^PgZI@5%5#9ggkV1#=VGR@(7JAK%hP$KPI=FVlX= zdFq4f!?1Sl<>!y%V?x|1BTYID93JFHi=tICaCN5T;8G+)2IUMTG_>L6)x0c}TN_{$ zvqW*Pq}?9FBOEi=AFR`Yatj;JlS+foeO}%P-X5l&8B4YplftqRNjeR!@3Lazm2cP& zgDWX_&*_ieB7Ay$0x`!Bs^>~o?99b18$*%q!A#ZS+t0`AbNsZ0@~_^X($mm5eg;Ax zhGNip`zsFI$)kg~>D!})`olgEVXyF+U4Zp7)9`m^#FH|O?cjH6K^oGsucto-2)0K? zL`1yy*N=63mjc(AvXJW$uVN4-B`xjuivg|nVCXB!J2bQaQy@b z`|&82o1I?rSXA!)s!{!T5j{L{^2~B3^c~3a=pIuJe~@w*!ackw6V?qQ3pvEW!CKH( zyLptvET z5hp=#=O(ITyv?3VC_G6aT_$VfHGdkEBGz^k~cmU_5DX;g>Fu@P-Sk#<; z62KZhJzfKSocC$jV@o z(*%t0`+r^Mw&Xx>cJ`@NJ6*3h>VIFic(?{NqPr-D7W|e8ebk2k90>=y=hrEp!Fl=r z&v_1nxX0NF{O?({r157;T!jvS@Z!Vc7_pRb79wWoig=StN>HV?hH=)YrAhPI@AdIW!o{~^qlA@_w=0n$Yph5sGQW=q-{1l zlr9uG^nD0&NMri3cOZ}J!TS_!4e6FxenYrR#zQK9A|wa`C6(*0YLv-`d#_PsM|CZP+=m0QQ&WHjK-Mo=baZW+O&q zj>Aro?v34EKDA>AebQbOSpwebONhr1Q#hR1y=&Lig-PCqV@$JlBUGK%+!v$9lOJQ- zXR)EIB8J# zqutHl+xk)(Po-u`!rahJQN;ASx;}&iBLu^<)7@4ylPk5)YQYo}jP2=0+l?*A!Vz-G z*4Pi>aye>x+xTz9R(O-FZn~Rq>};$}e%P1HAxFB0^9gl;O>hz~9jc-TH*w0UWAZ~M zOZYmo1sZi~*H|=2>&C9JZY5V2mNRT_=%w%~l2HgYOL9|5d}=jARka+0*j=68wV@_b z`s=(P_n$)5d2{=T?VW_Ui}JZ09qx3(bbgL$7>e<}=x_vn1Qzj9&o(Y>umH=MtiI_F*I}b}D4Gavq93IaSH3{t4uiUzNg#T@8$9uUF_?8@eq}OLZ>`jkQN_2$|)h+6vvfB@68KsN| zzbibUjNms!6?k%~m5rN?b_(y0=F=9V6m8kylPwVeCoVDH;ze zq6Ij@iR_K2z^M$qB}gyy;*C_cKb7DE<< z2xdYS>|dm*G;jHDu#WJ|H%7kwrmBn94~=K?t_Pb6KXE2;ZV2HQj>3y5=C>g^v4zPx zd$sTzGKQZI9-Se~n?zQ>OPPR7n4REQELagbH@N7pZR;#K_&R&hxzL@VvXv@7M|#7K zN_hk+liIpWWK$d+PwDTiPpC#%i|R z#>$V1sC>$^XAHxK`N14u(@^{{-t~KQ5pETJDtPuW_o+b14f>?05$%lj8sDXTg_V+8 zHjCYxAb`rYEZFzSGeq1z5lB3)ojrtCK?=gzlWzJMgtwx1irdRBD8hXdytKDfcj~Pk zdl)}JB07>;uHZ8g;E>fSrs@+&p`_R6ZfGVBdjccv@2vu~d|Lw^8^8(A_qid$&JDKtj$OfH`9Zkd2xX}BqsG6K z{s~Vf3@4Zc{p&vO+PfP~$oi?_6F1<%#X7@WTL~dYI`arNme?7>WIjp^a}jtpOcOqm z#Wjlxj8xin5JgT$nWoP=Ur|YYs=k_XS<=|+c4p72-*?%sdSdQ8z z4kJXD>}MUrn`&({YvY}H^yg+9Io%v~i3c}f{PO~?2ldx-q7_$YbQb)MCu@c|>~IH; zFjss08st(61f%ILH^S`yZ~-LzwbLC9T=_2Qqe0tdIFoO?oj$BnRJF8nlWaz z;OGk@a&Azkv={ZS9m9|1*0+*EMeqBN7;4zdoSq*05EXW8tvsb4%XuF|xLM)s1KZy< z?lm2Q(Eq^Ndyi({!Y;D9OhtE}?P@M{!c2jv_babyYYk%TXi`py%FrvPlCZ{F_L_xS;3&OhU-*{< zjLY?4P!?rl*{*Q~z5$LF9L zVHoXeEYIXipoDkA81d0S^^S#J^=D|KIITRHdNVs{gt?0jf$-$r{b@{^O$;zF& zBPC44xxr}FXgqOrkgq*`I+Ne$iV^Fe7#*7E*_Fc)L_ujb58StcynW_3fHA$uJddLaMXll^r?xViK?D-$CC zIRY6$d}L-;PG&aol2o4ouCs{OMo@zky6_>sowU}p-H~X^L`rA-b#qFT@{ndwyPbP0 z%Heuobv?_2!mZTlwQp+Jo{nw&CO=6l6{?P6mT(<-W6*1m5z&oiqX(qyv9@Km%@XfA zw&L^^TRQm5PqT@o`f&OStzhnct4Cogr5jz8)XP7Mj&5a@BDB9P$fe%IGhKGUo57rE zn)+;2&m&P!nRgmEEXAY4`wjeB6vIJT!1UuR!w9=`_qgTQy%@e;e$GF)RE3)-WDe~P zfV&LL-wk9K6-9MOt-d)4o9N+Hj4Ry4dkDH8InC01l+RZPZOK~llIQAz1>2TxO9aR* z98fPG_Sn!=g3Z`r^V>swuDsw+uBjvK`e#U4IjQ%H7jTrZdnP1$4NH9O zvniIrIRX5xPSX$;sTZTzbfXk10@IPoYKA%VDY>t+Zdhi&Ujq#AZ+%^21yvw@-mO#p zt6xEn@{!)DXDN?-xHZMRPGSeC`)g;x*m1gYIA*BIHgfyI*Y88MG_4!d9YLzp6VGi; zEvKz~lNJ@SGG)(Y7WCxXbp$zT9M@!Pv1v*aH#(3!6y(QD>H{t3P34eK-xNrA4? zP)K;m9uF)z-22U)LZS$`m@Uq2>J~hdgzn#=uP0fkXz?A}*hW5BaC=QZZBi0{g)=e} zD7CuadRE5|ik>Iv@VjYL{Nub7AC46#P~eHJdtT1OI|0yW z)^0^49E8lm^s;7!xaC%>y`|pF@&!xcI5<@6J>k*Jo|>?)M$4#>j}GF3t`$YI72?Qm z)>(+Z-m^A{lC-n)L2WH{KiW)bt=gxKqVAch-GD$Z*YNOitgO*4(mj;`VFX0~ z4}Rom3xu;`9Lyy18nz^t&N5!@lKm(w(yI&h&;ycIe$^q<*rCi3nSlM>#!xCgLrOly zYb(F$=*7%}L}5!={oUUIMN=pf7CmAlt)0sbzB8=byEeqD@V&fkvu~8^0&!>UZ4b6b z-pnTX)DDCZR9CNUdTk43{{u6Dz%rNC-_3no#a_L0!U|vs+we;c1jqGQreB-V_i3}m zR+pXOQVH0Pl030WEFDZ=ZU)8D4R48mP@A*Ytx?RYS8x~^5E<;zu|5$)G@7=|)F`8% znRimi@^7{?+~ZC3sco6&+4b5+zAAG|^-Xf17|ZFd&9cX#v66p>K9A2q()w~8GCmaT zg#d|j5EpelD4-Em$!d)tsjUkKK!>pf_cFp8!t_xkMQUPtHrt2I{)(%J)Pp@weJfR} z;`|uTkT@VwZdZ2u_9deiqoUGYk+6|WhkL!%L*x+6wpnso{;#>jg#@`q-X5z9J<73i zk7n+tW9k`F0nS&phvpP(TxUjyB_#Qx`P-nI}IXPpJ@2Zn6$Fkt$NA93&x_IIi@(> z^@%U3V)=Z%zd}#;;JrkdR(1`$oMDzv${Aaj#>jgG?gfhiJO)JjKR@sHEmNaKb?7V% z>Ca6*?C~<5oQFs6*H;8*@ZNL1y^9wOLSBX%A7PymX1~r`L^g|WdETo>SZuf{z-T9cBFgf z>#P%*WH_?I2YA?Ze??EovTtn}*4v829Em^1>|TLpqa}{ag!4-cDD>Kn{&D>$Anrt< zR&1GTu>EPU)ClQ4$5asU?PRp@H<3(33~i;I-$V{7{ZzfKu>6ii#%nQA;NtMp2Un8g z0m;nR)REi+Q{I~0W!tL}E+fgdfGzqZhpA7!KbhW43g>+5h$N|A`{S{iUfHa;O#JY1 z?>dS9#wO8G{8d{9Jd|?D!4uB8fyYJqk60zal+@HU>``|$_?pvUUO^8He>Zwi z17NPj$WQ&qKEc=-p<`jZ?so#oJUk$N>+Bmr7$UUKkDG87|lIe-n z>>hMC^JkKnX4laBILtY80qcijko8iAQ&Cp*&X#OzJBi)S*#4JE%Cih+&;6h_$`+Vj z{&D{=PkeWc2LHiM_Yq(^H#69BTh*7s8)T%!1Q~zqImC45uTqWhGvg^ob{OUv-=7gN zBnP$nEAr$IHukyHCZnfyFDcyx@mN^O-Kx1iS`lCqfI2wM$YmsQ;07(yr@vckI9gl{ z#+FuBDWbkFSPY%gT`8xx!OKXN90aA-Kkhs~A%x0%^3;=3hv=5i=Nz_2T6+d1sR;!n z_jeEi;37@);c@7Xg|+a|T^r*_y=ONWpI$GP40WHifOTsYVa~WNgippU`RtlyU&>GT zbr#tvBomGw5z}(PQQd6p^t^(rNiBXUtQ-X zhBp~5j(dkjvYT5rCuYRumFOf0X=v6vCxwbDQpos>^ssV`uGRDgD-v4RO6RW~3fV%= zQA6LO)15UkBt?|pFI|!zt@Erk=!hN%FpRcugTx)lvXuLaIFWCEHq^xTl|del(ciu`s<qwV~oU8>d|FaoKm^~OvJL*g_J ziyOj%SekyJj^MPcrCboD`ql2E!Po4Z3{=7yB3N&Xr$*b}NAFv89`yQ+#1#DMTSkYR zK{`@y;4uptkEiZ~kY~z5JzvkSUj-#sRoHq)-Y(=Eb~g*R&Bldx^)>JAhIz927h`;U zTuT2yGXqI4S!U5mtptT!56X$+BwL~pQnM@jxdL?&_n$QxVa>Ed&ks@mq8T}*z9Q2< zfP2#9AhEhIvgCj)h5l}G$7hQs3^v>tdI9h#z^kwddmdb9MYO2MXu;>U@!--tlTpLo zKHC;;@}Ow-W`2GeEuCjlh3qgi(%sv?c0N>9cYkgob3YqkWH@WPmiokhOxZo3NHR&L zo&LD}{Hool{Ea(vFALIG!JZk9%-)>aZ1>~cFj*@z9E~v|AE2c z=1-e2sXEs=k!-H66Ose+E%jm0dZ#^puZed-=f;HR*Ib4q5(?Y4X(NZ*0&Av15}yaK z^_|JVVLDDDrwh{&z#a(%*jy`e=vZoE6U+OR#)MSZu|z&ui0{5^d9h#h0N;U_m1Gh8 zIPZz~ZL%Qr>b{39$iFzc1zhzQlM=ZhD(Xyb29JGy_i9UQ_Nx=TiByjhB~fx{x~ zj#b0)iMWQ(d37P%A}RVVhQC#o&I0o#d>T_t`_oFn2w<%KBRxy}**y0q^{g%!RSEp_ zZLwz#emer&BPocn6q`-}lH4WSr0g#>eGCa~qxmy2^{AS#eVUMy9c1NGZLi_07&xr~ zDRX~1(%p(flSWi3bckdhq<{)bF@Cip5vi8LU8~FGbU<16M>)CX^?hZ?tbamhc;*%*0p4t{H?uE!o5njJ8Ln0Jz^7|9xZANEX$m zb5tNy(M%J<%Acd{r!xmCf{{M3zZ{Y7`uYs|Di*+J{m2+|!R_AFcbVK<>)jz+$Ce?L4~V@I0D4Zrl@!;ZsOrh$_7O*G5m9 zxKCA+lnAkQXV1U-Gr7!O#csuaO7issDM!^KD%8g?YGvHd4yxqXASjMkBRW9?Rq2&5 zb7NRkbZ(3wl}j?eA&7gArBLUxPC^h%8zwc_8(Nh1uz^%6>s2PocaO^z1KQ`oYZ-Pi ze*B5t`4pe&S9{2dhqh0oYUz;M`CA>S0F}LuiKLafiq}O}{qCZQ;Tc6!)^esYqoA4y z@$uTGPi6Bzr}{FLOKOp9Yaxh`Sck(8&~@N;Wn#nxuy-mD$Rz|GiW`VlR!32IdHAPX zYxX)l_XMsyVu`;5y(*Gbi+YNdD1JU#?EH@^4HwGY-n^3dwDbWRmD`yPi97eXQh-0D zPxF%dz^TF7LfDRTSLmR&ZLUU@#cVUG1%U?5V|-9Y6^V#s44exqsF&~j5AxJQs zqz5C_K1sG!9R%)2-nHc5(DQPq=1-j|Z9$WWn97jf9YX?ehV?pT19$le*gsG*n_R&E zp2km1sp8GleI~7BV|UM4S_K{{R7j)#xru76{v7}6{^}u_e>M6}H3!sphd{-nC>~tI znZl94Rh96b;5ZFPG@{}alEJ$bGIf7GlB5Z0isL1<1Xf-DclQ%X$p)-i{qZqhlyh5q z**1^Qo|FXrLpHJwWb3#3t zZ*;!4=RtHq9!!MO;Ft9|zT}7zUf{+%-0;dqaZgQwQ}>~?lCH!VilI$x}(8b3|+K<2UAlSe%-K@Hep`F_dX&nE zC`vMDHlW$>9`*z$wbX*%KaMOYS0}iN@kw3VlbZk4_;HqF@V;5INr|_G5ts`vW>I^Y z43j}Ng0;2xTwV{?Ey~|D8*s~zZ_pe)aXD7>B9{&-^F@840c|%8V>&`%Ra@!d(w1Wcs zZA&fd*>Ri6ku+pvZdw%L=0cpC6p=IA1EV_ar(~?<4#!Dn3fI>c?G01DCq?tC%u+AX~g9sbR)>hp)N~QC-esPg}k> zNBglA+53rXndF!lY|*Uq)kdUBs%NNC!G+Vt_DFbR16?`3!#U_mXnqcG$s(F0WAu*i z()Hom19(3IkJQ0lm|n%K2`SWc46*w!>nVP2xhr_#@{!F`E}y2nQ6hu=vx;NB0$9%TCXIFZPiHChF5mRsp zn(ZZ|bhoU`R4;qPNv~GfEi#urgF@(j74(mq99dUCoTmNYv`a&Ej$6I$;P$n-Y)v!g zcOaAs9^&VqrLNU93Ga_6*BM{BD_z>n^tuX)6E1B7x6Ls4%mlZsh3C`(}A{f?H-+#Kn(tem;1>n`J}cdEGyzT51t!1K2D-XfK0ngIn2f zcwH%i1sDOnQT*rvMQQ_64lRdri}qSijx@yQvIlU>n1NzT1|>Y*&neZHQ9rl81QDAy z%7eE6E;Lsve6~8N%L z$F#Q!Bw4Qe&lc<$_+weQhEQzvp_QAZGzDt1Z3JTUB(FeOIyFo+4~fp~osZ|-G1%Si zytj$*b;1z1L60G1WM!8#(-}C>jgN}i3gkWqvDWBaR3JZ)t9uOvE?I+J)h>((ExsB- z@(`CyJg9CpiZa)F1wxf7JWIJ{8ZP}O9E5y|Sd*#=tJj!_n-V5V%vKL^b=J3-uTl7v zZ#w6PKjl(IT6VokzAag^gPL-+A9GCdMW0o#)(=KA13{bb1i^Jqh@u(6vOgV8)T()< z$%XO_b>l4*Kkl!`?JTA{EL|VbeShPCJ7;h}(?`XH%v;S3T{+eSZ;oyIKbRa>^nRHj zF228zncdK!v@(s*$k1kpz!}+ZCn{}4^rOErZRV;JSLTkeire0O3@3ew9rsu;ln%Jx zKNT1JCe|<3S~fM8U%T$_HHm}Jo^Xq)&PXJ`{?Fc!?gPC^v8f=~Cf)emt{YkYL%mYSToPP4 z$54*X=oAlHR8k^KbV>O5jQMIc|Hch0n&#Chlo(nbY80gdoQK`jQoFhOqvUfQ7s0N| zPuK-BImWfQt2tRdf$CdLDF)TVH@qZFLWmxfgsl5GB@5I*S)s{V8z6TCnHD% zh?l{qRo25f>D*6)(kuqYmxa@u%)`FP|x-2@ZGTe`e#x z&Ry2EUD}4%iFcY?oleTLXjdo?h5DNgx_t{Of0>YvQn6~cA1_T0_>Yf)s*G;M$Ol*AKkACU=kj!W9XIHfp!aVnd zpVdT+^DSl3%})GCe8>9$w5vhk>^sjbo@Vz`NbWzXE-XxisIT07*ubJ%iA~5pxJLJIy{a33-n^A_^wowFnDc#zwMnQ22V1db8_JVm|JK3Yr$Rqq(PVP| zvntQd+z*mF_jDis`8{Qy*uUM;Ws8n&|d_3 zt#T&r_#v=37HjA*IWPSujWu*N*VV(7yW>1cE6QQtsddp|b*?0%=weZ!Y#jzlMIq_~ zvmAfys}!6Ht!gtPy+#*2d0xrL)ct#LBWRLi%_)MUnfT)?-Ve5rUb)?(i`R?U@|p4+ zTUZOJzLTLLoq24?px~)t-NlIdjtC0_M>j+pWIw@oJU*;;{&hI1xFg!p$`tx`qqijtH|N8I;N$sfD%xA46fom6S z)>d-=}J)L*^%u@>GOR%N4O00=pA+hePuimBS#S=65^{?T?Zg1a5#aU z7w`S;Cr7F_-e)&niH6fPIkXW=okC*^au){0mTw8O%GyayswAluP1$kTSHJW?bGZiY z4Eyba6)lbas)p4Pbm4UJ4eT3Fu+oB;Qv@>{C{vGs0chicp4It{On8J-<;xp}`t%*d z%lFSaTT-eYOpag=(uBi-3Yiw{Q1@9qbcu5$&9S@dPbB^aC6wzvH?FgEG@=Oc<55;+ zq!G3nE42%N4<3%ZIbPHya(ejsjpsJCL*?qa(%Z6{j(>M|WWgDavc7TsTsnGggMD^- z?^QTm)h%*S{?fM)0pj41Pg%rVf8WlTD9U!*KL%^TP8gh7JnC4hfv{5YpE{?d|yz48v>l5E0 z>4pnRey<}^P(Qx|{#lr+IQLxpfx$`#H$?oo^R2^}`$v#+8|ZBEE_o$hIN{y87_%`cs%aI&(#%Mnq=?EL`rv9? zEC$@dulE=#{v3smRh+oAN{88PAgFf2g&|j=l^d-y(EDERGo4Rt(FL{jzpEO}zYn4I zZ7#BbYzldF_Fu*=L^U?QOma8~s8oleUsguSG3`JlTKF@P^b3|DFVGG?gh}qd!stOq z)DNYE>0Ivs#-Uz(Q-CrIk*lWT)pZ5&p|8VK4{}BS-Y=<(LJbrMgg3$L^2OUO7EMTT zuSYFDr`U9&wEs&aqq=u3BY1}))S235jcnLG>Iej9&%9Pb!gO-~PY2wuv zryKJ6HzK!lk@(`+P!7``{`<1-byv3mdPsuRvx1bgpPvRJK|b|i$AZif^BzR{3`YsmaT-vg;aVT2E-_~`YlrL zX+Da1O}t8S>rsoXfna5Ncz0cM+r$7FXFarfuNGjm$*zZGO6n+ zV53ajZB|fyox%vb9n;$1t>RxzY-(n+BdlI{GZMiUr0 zQ>WJ19w=Su4(fLUL9HiFpVy+AwF}qv;@>&8d5#{!vUQq^N^W($N?Hd>KjCkbg)nPT zLGOD&sBy*K$uZxbx@}FOE|mFi9Rl^c>H%eP=5=CVkDE*eb z7wiTjl_puGB;mm#tah5x!4edPlLR>%K6X<~dpLbgQ?nu(tWI?mftvV*76t!h@KOgu z0PvStU#(OIS1{*r{WQ)ETu3eZDF#N;z088rMT z{|NtjY7OODJ2Qt*+0v#&8TUyjNUuPHw!4v32Tuq>Mv|~X>QvVZEsVVV!YFrGAKu}x zzWEk2`iWX%^xiuAhO;Eo%`I5xkZ+c@m{KHdkt*M4I9V}dm*cSoo#6EJ^+g;V{KcT) zM=!!m4YS4^6%MW9Bm%Z!%LV6uzYti!X=hu94lQs=eh{_FD}5DT;no@zbWTK18Q6_j zHn%bRT9v~+=A=K;bYIm8VtMKnm)_3D(J%O2{+gT$g+)ZXTMwilZ`wFMseqrSZMd?t z`)-Vd2oQFCs~OX1mSkL;XAuV4<;v7q^enu49 zXbLXRRlkz`E%sy|SvMB`-H3`eFtIUG6Lq&wNYnfkrn{#Udm6Tsn+?mbebUx+Uw5za z?O8rRlj*HB-k71RQq{)J1}v`XoS4c3W$5l-TYAwA8%>S!Xc1P-@7 z0*o!sG1OZXtDNK zzOL4MuB0Frs8T=<7=VdgAga@!M5#Au`shylvk^N>SiLDvby%Q;a#)^Vl_heHdlBmZ zdg1WZUHskZY2&};mFgx&6gYqLqw>VfQ9}EGwI`*OP}Va&ig*lOcRd#f@mU>;JCjP$fmk=iS~V`dZGx#FkONK!EOL{xdBhx^shj2$Gy+7R2YvLt1Wx;b`oyN6 zyG5a0JZO;*s3z=DfO|gyfCeCviJienH*GiH!cYM_0Tik=^fvsbWhRbbpL<=l3PywE zck12KQC1P(hCl0CZhEB_?R0&6bkW?I0wC1Yd&&X#PgH1qxx*a>vnfvh+rt~{+>jK- zDEZ!F@`C3hRHjg4W?f_ZKcM9Wtu(f4JLZlEi+$_s&Ea%Vje@6gf_cC;xmK0Oe@hie z(5{NGszKX;Y^xU6n%vx#PxBPo2xht^1eHt=VM_DxmAglqbv;#pVR?6QBqcrs{W84xP2-n10tlw=A!&<#Db z5B=y+9#IGB7fQHzCO_ADaATauqpoSl_w{I4A6vwjZR-ffAVti*9~t&I-%33$n>jmr z+~!<>D$UFpv|FdCQ5n~%TfWA%3m5}am~L67_5&+9{sX?98+2@7fAn49&X#uw(l1@# zZYD2zJFxB?NR+LaRq=N}oRGf#8GSyWteCcWx%D4m2U*UY%}J)q*n)ra?;8wG^vy@% zl{6lDgf#>dECw&+4TQF>=RW zZs>4V94pvyutFhZ1vfEQkH(or3K5g$0+9ad{+LO7$4}cg0T&hC=R1#zLDkdSc;sI; znLXU?Qa*PyZmJ~HpjM8*-RZFX3*+U>1ndVRm1}ahR+irAG7>;a{HsENl@It# zvRLdF7#;TcLkF2b&hTooD&CJyTaKXU3DwDMI9$iJ&dcrX9$#tN@bTcnf1^?QTA=Oa zSF4j*K}m+zimO{hc;Vx+KQo5_HV9EYq`HnlLAr#9=l{Ycig2P*p;_QkWB!p_%%G3p zJC@N!#+|KTi)8IBR<5@CIjvS7&i=OZ%c-odlN;dsQ9LZ0M8x;``!3C%Bj4-yKSoN_ z)Jk>&y~O~sFh^m_8SZt&ucSWgxeaUk{o%-o!H#%oTlg!u=l+L#i))^+Evrd;xwiR{ z;){Lx;iShgR%+i@0(hEPyQm~X%dzKf^aYOQT9b!QNemGDZhx?BN zKQif*G1km@1M)_7gqm#C3pI^?(FDXn+WEiZ&o#`r{3%3mp>-@G_PYpc(PzW?^4x$W zaJ&A$VS=t4fSw_5koP&oKwU|n`}9=9{C`db&B|K~m?j1&%~cRMCeo-}2Xl&8bw}US7l8 xzDP`VO!l|@a~?ZQ5sdfXWQO^#>C402`_Q=XPx%%u99Re#W+s-G%Z#1l{ts%4>tg@_ literal 0 HcmV?d00001 From 34ce887c74d2a7aae0604368a37a27badd75e8f3 Mon Sep 17 00:00:00 2001 From: jschepers Date: Tue, 30 Jul 2024 13:58:26 +0000 Subject: [PATCH 028/156] Added intro paragraph for Simulate ERP tutorial --- docs/literate/tutorials/simulateERP.jl | 27 +++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/docs/literate/tutorials/simulateERP.jl b/docs/literate/tutorials/simulateERP.jl index b3798096..c71f613d 100644 --- a/docs/literate/tutorials/simulateERP.jl +++ b/docs/literate/tutorials/simulateERP.jl @@ -1,12 +1,33 @@ +# # Simulate event-related potentials (ERPs) + +# One subfield of EEG research focuses on so-called event-related potentials (ERPs) which are defined as brain responses time-locked to a certain event e.g. stimulus onset. +# The waveform of an ERP usually consists of multiple ERP components which denote the peaks and troughs of the waveform. + +# ERP components are characterized (and named) by their timing relative to the event, their polarity (positive or negative) and their scalp topography. +# For example, the N170 describes a negative deflection which occurrs roughly 170 ms after the onset of (certain) visual stimuli. +# Often, researchers are interested how a component (e.g. its amplitude or timing) changes depending on certain experimental factors. +# For example, N170 has been shown to be related to face processing and its amplitude is modulated by whether the stimulus is a face or an object e.g. a car. +# ([Source](https://neuraldatascience.io/7-eeg/components.html)) + +# Here we will learn how to simulate a typical ERP complex with P100, N170, P300. + +# ### Setup +# ```@raw html +#
+# Click to expand +# ``` +## Load required packages using UnfoldSim using CairoMakie using Random using Unfold using UnfoldMakie -# ## ERP Complex -# Here we will learn how to simulate a typical ERP complex with P100, N170, P300. +# ```@raw html +#
+# ``` -# Let's grab a SingleSubjectDesign and add a continuous predictor +# ## Simulation +# Let's grab a `SingleSubjectDesign` and add a continuous predictor design = SingleSubjectDesign(; conditions = Dict( From b5a60bfe9aa67f5a7d46e2865bc0b59f7506626a Mon Sep 17 00:00:00 2001 From: jschepers Date: Tue, 30 Jul 2024 15:27:36 +0000 Subject: [PATCH 029/156] Improved docstrings for single- and multi-subject design --- src/design.jl | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/design.jl b/src/design.jl index 42e02328..66955bd9 100644 --- a/src/design.jl +++ b/src/design.jl @@ -1,6 +1,9 @@ """ - MultiSubjectDesign + MultiSubjectDesign <: AbstractDesign +A type for specifying the experimental design for multiple subjects (based on the given random-effects structure). + +### Fields - `n_subjects`::Int -> number of subjects - `n_items`::Int -> number of items (sometimes ≈trials) - `subjects_between` = Dict{Symbol,Vector} -> effects between subjects, e.g. young vs old @@ -8,10 +11,10 @@ - `both_within` = Dict{Symbol,Vector} -> effects completly crossed - `event_order_function` = `x->x`; # can be used to sort, or e.g. `x->shuffle(MersenneTwister(42),x)` - be sure to fix/update the rng accordingly!! -tipp: check the resulting dataframe using `generate_events(design)` - +Tip: Check the resulting dataframe using `generate_events(design)` +### Example ```julia # declaring same condition both sub-between and item-between results in a full between subject/item design design = MultiSubjectDesign(; @@ -33,6 +36,11 @@ end """ + SingleSubjectDesign <: AbstractDesign + +A type for specifying the experimental for a single subject (based on the given conditions). + +### Fields - conditions = Dict{Symbol,Vector} of conditions, e.g. `Dict(:A=>["a_small","a_big"],:B=>["b_tiny","b_large"])` - `event_order_function` = x->x; # can be used to sort, or x->shuffle(MersenneTwister(42),x) - be sure to fix/update the rng accordingly!! @@ -42,7 +50,16 @@ To increase the number of repetitions simply use `RepeatDesign(SingleSubjectDesi If conditions are omitted (or set to `nothing`), a single trial is simulated with a column `:dummy` and content `:dummy` - this is for convenience. -tipp: check the resulting dataframe using `generate_events(design)` +Tip: Check the resulting dataframe using `generate_events(design)` + +### Example +```julia +design = SingleSubjectDesign(; + conditions = Dict( + :stimulus_type => ["natural", "artificial"], + :contrast_level => range(0, 1, length = 5), +); +``` """ @with_kw struct SingleSubjectDesign <: AbstractDesign conditions::Dict{Symbol,Vector} = Dict() @@ -133,7 +150,7 @@ length(design::AbstractDesign) = *(size(design)...) # ---- """ - RepeatDesign{T} + RepeatDesign{T} <: AbstractDesign Repeat a design DataFrame multiple times to mimick repeatedly recorded trials. ```julia From b81b60556d2f154ff833c3c53c7dcb4ba3bf7e6d Mon Sep 17 00:00:00 2001 From: jschepers Date: Tue, 30 Jul 2024 16:07:24 +0000 Subject: [PATCH 030/156] Fixed simulate docstring --- src/simulation.jl | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/simulation.jl b/src/simulation.jl index 2376e8ff..945c7a67 100755 --- a/src/simulation.jl +++ b/src/simulation.jl @@ -6,9 +6,15 @@ Simulation( noisetype::AbstractNoise, ) = Simulation(design, [component], onset, noisetype) + +function simulate(design::AbstractDesign, signal, onset::AbstractOnset, args...; kwargs...) + @warn "No random generator defined, used the default (`Random.MersenneTwister(1)`) with a fixed seed. This will always return the same results and the user is strongly encouraged to provide their own random generator!" + simulate(MersenneTwister(1), design, signal, onset, args...; kwargs...) +end + """ simulate( - rng, + [rng::AbstractRNG,] design::AbstractDesign, signal, onset::AbstractOnset, @@ -31,13 +37,6 @@ Some remarks to how the noise is added: - The case `return_epoched = false` and `onset = NoOnset()` is not possible and therefore covered by an assert statement """ - - -function simulate(design::AbstractDesign, signal, onset::AbstractOnset, args...; kwargs...) - @warn "No random generator defined, used the default (`Random.MersenneTwister(1)`) with a fixed seed. This will always return the same results and the user is strongly encouraged to provide their own random generator!" - simulate(MersenneTwister(1), design, signal, onset, args...; kwargs...) -end - simulate( rng::AbstractRNG, design::AbstractDesign, From 369faff6b9cd8b0ca1d9e2baa73b7044a45d5ecc Mon Sep 17 00:00:00 2001 From: jschepers Date: Wed, 31 Jul 2024 11:37:32 +0000 Subject: [PATCH 031/156] Added cross references in docstrings --- src/design.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/design.jl b/src/design.jl index 66955bd9..e7bcdbcc 100644 --- a/src/design.jl +++ b/src/design.jl @@ -24,6 +24,7 @@ design = MultiSubjectDesign(; items_between = Dict(:cond => ["levelA", "levelB"]), ); ``` +See also [`SingleSubjectDesign`](@ref), [`RepeatDesign`](@ref) """ @with_kw struct MultiSubjectDesign <: AbstractDesign n_subjects::Int @@ -60,6 +61,7 @@ design = SingleSubjectDesign(; :contrast_level => range(0, 1, length = 5), ); ``` +See also [`MultiSubjectDesign`](@ref), [`RepeatDesign`](@ref) """ @with_kw struct SingleSubjectDesign <: AbstractDesign conditions::Dict{Symbol,Vector} = Dict() @@ -163,6 +165,7 @@ designOnce = MultiSubjectDesign(; design = RepeatDesign(designOnce,4); ``` +See also [`SingleSubjectDesign`](@ref), [`MultiSubjectDesign`](@ref) """ @with_kw struct RepeatDesign{T} <: AbstractDesign design::T From 541cf6712e01278bf6c4e4767d017f942732b0a8 Mon Sep 17 00:00:00 2001 From: jschepers Date: Wed, 31 Jul 2024 12:54:54 +0000 Subject: [PATCH 032/156] Added intro sentences, matched titles and sidebar, reordered pages and added collapsible setup blocks --- docs/literate/HowTo/multichannel.jl | 18 +++++++++++++++--- docs/literate/HowTo/newComponent.jl | 18 ++++++++++++++---- docs/literate/HowTo/newDesign.jl | 23 +++++++++++++++++------ docs/literate/HowTo/predefinedData.jl | 10 ++++++++++ docs/literate/HowTo/repeatTrials.jl | 6 ++---- docs/literate/reference/basistypes.jl | 19 ++++++++++++++----- docs/literate/reference/designtypes.jl | 2 +- docs/literate/reference/noisetypes.jl | 7 ++++++- docs/literate/reference/onsettypes.jl | 2 +- docs/literate/reference/overview.jl | 10 ++++++++++ docs/literate/tutorials/multisubject.jl | 15 ++++++++++++++- docs/literate/tutorials/poweranalysis.jl | 22 ++++++++++++++++------ docs/literate/tutorials/quickstart.jl | 4 ++-- docs/make.jl | 15 ++++++++------- docs/src/index.md | 4 ++-- 15 files changed, 132 insertions(+), 43 deletions(-) diff --git a/docs/literate/HowTo/multichannel.jl b/docs/literate/HowTo/multichannel.jl index 8aa5111f..50d4b5fa 100644 --- a/docs/literate/HowTo/multichannel.jl +++ b/docs/literate/HowTo/multichannel.jl @@ -1,10 +1,22 @@ +# # Generate multi channel data +# Here you will learn how to simulate EEG data for multiple channels/electrodes. +# The idea is to specify a signal on source level and then use a head model or a manual projection matrix to project the source signal to a number of electrodes. + +# ### Setup +# ```@raw html +#
+# Click to expand +# ``` +## Load required packages using UnfoldSim using UnfoldMakie using CairoMakie using DataFrames using Random - +# ```@raw html +#
+# ``` # ## Specifying a design @@ -17,7 +29,7 @@ c2 = LinearModelComponent(; basis = p300(), formula = @formula(0 ~ 1), β = [1]) # ## The multichannel component -# next similar to the nested design above, we can nest the component in a `MultichannelComponent`. We could either provide the projection marix manually, e.g.: +# Next, similar to the nested design above, we can nest the component in a `MultichannelComponent`. We could either provide the projection matrix manually, e.g.: mc = UnfoldSim.MultichannelComponent(c, [1, 2, -1, 3, 5, 2.3, 1]) # or maybe more convenient: use the pair-syntax: Headmodel=>Label which makes use of a headmodel (HaRTmuT is currently easily available in UnfoldSim) @@ -26,7 +38,7 @@ mc = UnfoldSim.MultichannelComponent(c, hart => "Left Postcentral Gyrus") mc2 = UnfoldSim.MultichannelComponent(c2, hart => "Right Occipital Pole") # !!! hint -# You could also specify a noise-specific component which is applied prior to projection & summing with other components +# You could also specify a noise-specific component which is applied prior to projection & summing with other components. # # finally we need to define the onsets of the signal onset = UniformOnset(; width = 20, offset = 4); diff --git a/docs/literate/HowTo/newComponent.jl b/docs/literate/HowTo/newComponent.jl index bb8bee72..91da1245 100644 --- a/docs/literate/HowTo/newComponent.jl +++ b/docs/literate/HowTo/newComponent.jl @@ -1,6 +1,12 @@ -# # New component: Duration + Shift +# # Define a new component (with variable duration and shift) -# We want a new component that changes its duration and shift depending on a column in the event-design. This is somewhat already implemented in the HRF + Pupil bases +# We want a new component that changes its duration and shift depending on a column in the event design. This is somewhat already implemented in the HRF + Pupil bases. + +# ### Setup +# ```@raw html +#
+# Click to expand +# ``` using UnfoldSim using Unfold using Random @@ -8,6 +14,9 @@ using DSP using CairoMakie, UnfoldMakie sfreq = 100; +# ```@raw html +#
+# ``` # ## Design # Let's generate a design with two columns, shift + duration @@ -19,7 +28,8 @@ design = UnfoldSim.SingleSubjectDesign(; ) -# We also need a new AbstractComponent +# ## Implement a new AbstractComponent +# We also need a new `AbstractComponent` struct TimeVaryingComponent <: AbstractComponent basisfunction::Any maxlength::Any @@ -51,7 +61,7 @@ function basis_shiftduration(evts, maxlength) end end - +# ## Simulate data with the new component type erp = UnfoldSim.simulate( MersenneTwister(1), TimeVaryingComponent(basis_shiftduration, 50), diff --git a/docs/literate/HowTo/newDesign.jl b/docs/literate/HowTo/newDesign.jl index 955044d2..ec0fc256 100644 --- a/docs/literate/HowTo/newDesign.jl +++ b/docs/literate/HowTo/newDesign.jl @@ -1,13 +1,24 @@ +# # Define a new (imbalanced) design + +# A design specifies how much data is generated, and how the event-table(s) +# should be generated. Already implemented examples are `MultiSubjectDesign` and `SingleSubjectDesign`. + +# We need 3 things for a new design: a `struct<:AbstractDesign`, a `size` and a `generate` function. + +# ### Setup +# ```@raw html +#
+# Click to expand +# ``` using UnfoldSim using StableRNGs using DataFrames using Parameters -# ## Define a new Design -# A design specifies how much data is generated, and how the event-table(s) -# should be generated. Already implemented examples are `MultiSubjectDesign` and `SingleSubjectDesign` -# -# We need 3 things for a new design: a `struct<:AbstractDesign`, a `size` and a `generate` function -# +# ```@raw html +#
+#
+# ``` + # #### 1) `type` # We need a `ImbalanceSubjectDesign` struct. You are free to implement it as you wish, as long as the other two functions are implemented # diff --git a/docs/literate/HowTo/predefinedData.jl b/docs/literate/HowTo/predefinedData.jl index 9a394e9b..5ed27f74 100644 --- a/docs/literate/HowTo/predefinedData.jl +++ b/docs/literate/HowTo/predefinedData.jl @@ -2,10 +2,20 @@ # Let's say you want to use the events data frame (containing the levels of the experimental variables and the event onsets (latencies)) from a previous study in your simulation. +# ### Setup +# ```@raw html +#
+# Click to expand +# ``` +## Load required packages using UnfoldSim using DataFrames using Random using CairoMakie # for plotting +# ```@raw html +#
+#
+# ``` # From a previous study, we (somehow, e.g. by using [pyMNE.jl](https://unfoldtoolbox.github.io/Unfold.jl/dev/HowTo/pymne/)) imported an event data frame like this: my_events = DataFrame(:condition => [:A, :B, :B, :A, :A], :latency => [7, 13, 22, 35, 41]) diff --git a/docs/literate/HowTo/repeatTrials.jl b/docs/literate/HowTo/repeatTrials.jl index 1add60e6..b85db6b0 100644 --- a/docs/literate/HowTo/repeatTrials.jl +++ b/docs/literate/HowTo/repeatTrials.jl @@ -1,12 +1,10 @@ -using UnfoldSim - - -# # [Repeating Design entries](@id howto_repeat_design) +# # [Get multiple trials with identical subject/item combinations](@id howto_repeat_design) # Sometimes we want to repeat a design, that is, have multiple trials with identical values, but it is not always straight forward to implement. # For instance, there is no way to easily modify `MultiSubjectDesign` to have multiple identical subject/item combinations, # without doing awkward repetitions of condition-levels or something. # If you struggle with this problem `RepeatDesign` is an easy tool for you: +using UnfoldSim designOnce = MultiSubjectDesign(; n_items = 2, diff --git a/docs/literate/reference/basistypes.jl b/docs/literate/reference/basistypes.jl index de4db4a6..188951bd 100644 --- a/docs/literate/reference/basistypes.jl +++ b/docs/literate/reference/basistypes.jl @@ -1,14 +1,23 @@ -using UnfoldSim -using CairoMakie -using DSP -using StableRNGs - # # Overview: Basis function (component) types # There are several basis types directly implemented. They can be easily used for the `components`. # # !!! note # You can use any arbitrary shape defined by yourself! We often make use of `hanning(50)` from the DSP.jl package. +# ### Setup +# ```@raw html +#
+# Click to expand +# ``` +## Load required packages +using UnfoldSim +using CairoMakie +using DSP +using StableRNGs +# ```@raw html +#
+# ``` + # ## EEG # By default, the EEG bases assume a sampling rate of 100, which can easily be changed by e.g. p100(; sfreq=300) f = Figure() diff --git a/docs/literate/reference/designtypes.jl b/docs/literate/reference/designtypes.jl index 3d4cb98c..7d571f47 100644 --- a/docs/literate/reference/designtypes.jl +++ b/docs/literate/reference/designtypes.jl @@ -3,7 +3,7 @@ # The experimental design specifies the experimental conditions and other variables that are supposed to have an influence on the simulated data. # Currently, there are three types of designs implemented: `SingleSubjectDesign`, `MultiSubjectDesign` and `RepeatDesign`. -# ## Setup +# ### Setup # ```@raw html #
# Click to expand diff --git a/docs/literate/reference/noisetypes.jl b/docs/literate/reference/noisetypes.jl index 26af0e1e..264e3439 100644 --- a/docs/literate/reference/noisetypes.jl +++ b/docs/literate/reference/noisetypes.jl @@ -1,5 +1,10 @@ # # Overview: Noise types -# There are several noise types directly implemented. Here is a comparison: + +# There are different types of noise signals which differ in their power spectra. +# If you are not familiar with different types/colors of noise yet, have a look at this [source](https://en.wikipedia.org/wiki/Colors_of_noise). + +# There are several noise types directly implemented in UnfoldSim.jl. Here is a comparison: + using UnfoldSim using CairoMakie diff --git a/docs/literate/reference/onsettypes.jl b/docs/literate/reference/onsettypes.jl index bbadef15..6618706b 100644 --- a/docs/literate/reference/onsettypes.jl +++ b/docs/literate/reference/onsettypes.jl @@ -2,7 +2,7 @@ # The onset types determine the distances between event onsets in the continuous EEG signal. The distances are sampled from a certain probability distribution. # Currently, there are two types of onset distributions implemented: `UniformOnset` and `LogNormalOnset`. -# ## Setup +# ### Setup # ```@raw html #
# Click to expand diff --git a/docs/literate/reference/overview.jl b/docs/literate/reference/overview.jl index 80145bcf..a4f4b611 100644 --- a/docs/literate/reference/overview.jl +++ b/docs/literate/reference/overview.jl @@ -1,7 +1,17 @@ # # Overview of functionality # UnfoldSim has many modules, here we try to collect them to provide you with an overview. + +# ### Setup +# ```@raw html +#
+# Click to expand +# ``` +## Load required packages using UnfoldSim using InteractiveUtils +# ```@raw html +#
+# ``` # ## Design # Designs define the experimental design. They can be nested, e.g. `RepeatDesign(SingleSubjectDesign,10)` would repeat the generated design-dataframe 10x. diff --git a/docs/literate/tutorials/multisubject.jl b/docs/literate/tutorials/multisubject.jl index b63e58d4..61570240 100644 --- a/docs/literate/tutorials/multisubject.jl +++ b/docs/literate/tutorials/multisubject.jl @@ -1,10 +1,23 @@ +# # Multi-subject simulation + +# In this tutorial, you will learn how to simulate data for multiple subjects. In particular, you will learn how to specify fixed and random effects and what their influence on the simulated data looks like. + +# ### Setup +# ```@raw html +#
+# Click to expand +# ``` +## Load required packages using UnfoldSim using Unfold using CairoMakie using UnfoldMakie using DataFrames +# ```@raw html +#
+#
+# ``` -# # Multi-subject simulation # Similar to the single subject case, multi-subject simulation depends on: # - `Design` (typically a `MultiSubjectDesign`) # - `Components` (typically a `MixedModelComponent`) diff --git a/docs/literate/tutorials/poweranalysis.jl b/docs/literate/tutorials/poweranalysis.jl index 5202ad66..fbc79b85 100644 --- a/docs/literate/tutorials/poweranalysis.jl +++ b/docs/literate/tutorials/poweranalysis.jl @@ -1,14 +1,24 @@ +# # Power analysis +# For a power analysis, we will repeatedly simulate data, and check whether we can find a significant effect. +# We perform the power analysis on epoched data. + +# ### Setup +# ```@raw html +#
+# Click to expand +# ``` +## Load required packages using UnfoldSim using Statistics using HypothesisTests using DataFrames using Random +# ```@raw html +#
+#
+# ``` - -# ## Simple Power analysis Script -# For a power analysis, we will repeatedly simulate data, and check whether we can find a significant effect. -# -# We perform the power analysis on epoched data. +# ## Simulation loop pvals = fill(NaN, 100) @time for seed in eachindex(pvals) ## Simulate data of 30 subjects @@ -33,5 +43,5 @@ pvals = fill(NaN, 100) pvals[seed] = pvalue(OneSampleTTest(y_big, y_small)) end -# let's calculate the power +# Let's calculate the power power = mean(pvals .< 0.05) * 100 diff --git a/docs/literate/tutorials/quickstart.jl b/docs/literate/tutorials/quickstart.jl index 66d735a9..0270a9a8 100644 --- a/docs/literate/tutorials/quickstart.jl +++ b/docs/literate/tutorials/quickstart.jl @@ -1,10 +1,10 @@ # # Quickstart # To get started with data simulation, the user needs to provide four ingredients: -# 1) an experimental design, defining which conditions exist and how many events/"trials" +# 1) an experimental design, defining which conditions and how many events/"trials" exist # 2) an event basis function, defining the simulated event-related response for every event (e.g. the ERP shape in EEG) # 3) an inter-onset event distribution, defining the distances in time of the event sequence -# 4) a noise specification, defining, well, the noise :) +# 4) a noise specification, defining the type of noise signal that is added to the simulated signal (e.g. pink noise) # !!! tip # Use `subtypes(AbstractNoise)` (or `subtypes(AbstractComponent)` etc.) to find already implemented building blocks. diff --git a/docs/make.jl b/docs/make.jl index 72dc6219..2b24ff62 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -19,7 +19,7 @@ DocMeta.setdocmeta!(UnfoldSim, :DocTestSetup, :(using UnfoldSim); recursive = tr makedocs(; modules = [UnfoldSim], - authors = "Luis Lips, Benedikt Ehinger, Judith Schepers", + authors = "Judith Schepers, Luis Lips, Maanik Marathe, Benedikt Ehinger", #repo="https://github.com/unfoldtoolbox/UnfoldSim.jl/blob/{commit}{path}#{line}", repo = Documenter.Remotes.GitHub("unfoldtoolbox", "UnfoldSim.jl"), sitename = "UnfoldSim.jl", @@ -27,13 +27,14 @@ makedocs(; prettyurls = get(ENV, "CI", "false") == "true", canonical = "https://unfoldtoolbox.github.io/UnfoldSim.jl", edit_link = "main", + sidebar_sitename = false, assets = String[], ), pages = [ "Home" => "index.md", "Tutorials" => [ "Quickstart" => "generated/tutorials/quickstart.md", - "Simulate ERPs" => "generated/tutorials/simulateERP.md", + "Simulate event-related potentials (ERPs)" => "generated/tutorials/simulateERP.md", "Power analysis" => "generated/tutorials/poweranalysis.md", "Multi-subject simulation" => "generated/tutorials/multisubject.md", ], @@ -45,13 +46,13 @@ makedocs(; "Overview: Noise types" => "./generated/reference/noisetypes.md", ], "HowTo" => [ - "Define a new, (imbalanced) design" => "./generated/HowTo/newDesign.md", - "Repeating a design" => "./generated/HowTo/repeatTrials.md", - "Define a new duration & jitter component" => "./generated/HowTo/newComponent.md", + "Define a new (imbalanced) design" => "./generated/HowTo/newDesign.md", + "Get multiple trials with identical subject/item combinations" => "./generated/HowTo/repeatTrials.md", + "Define a new component (with variable duration and shift)" => "./generated/HowTo/newComponent.md", "Generate multi channel data" => "./generated/HowTo/multichannel.md", - "Use predefined design / onsets data" => "./generated/HowTo/predefinedData.md", + "Use existing experimental designs & onsets in the simulation" => "./generated/HowTo/predefinedData.md", ], - "API / DocStrings" => "api.md", + "API / Docstrings" => "api.md", ], ) diff --git a/docs/src/index.md b/docs/src/index.md index 40ff2f14..e18f03e6 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -4,9 +4,9 @@ CurrentModule = UnfoldSim # UnfoldSim.jl -Documentation for [UnfoldSim.jl](https://github.com/unfoldtoolbox/UnfoldSim.jl). UnfoldSim.jl is a Julia package for simulating multivariate timeseries data with a special focus on EEG data. +Documentation for [UnfoldSim.jl](https://github.com/unfoldtoolbox/UnfoldSim.jl): a Julia package for simulating multivariate timeseries data with a special focus on EEG data. -## Start simulating timeseries +## Start simulating time series data We offer some predefined (EEG) signals, check them out! For instance a P1/N170/P300 complex (containing three typical ERP components). From f449083fdb72eec8b94e6eb83a5cad0c7ce2e1ce Mon Sep 17 00:00:00 2001 From: Benedikt Ehinger Date: Fri, 26 Jul 2024 13:26:21 +0200 Subject: [PATCH 033/156] Update noise.jl --- src/noise.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/noise.jl b/src/noise.jl index 28421ccd..ba709e17 100644 --- a/src/noise.jl +++ b/src/noise.jl @@ -72,7 +72,7 @@ Noise with exponential decay in AR spectrum. `noiselevel` is used to scale the noise !!! warning - Current implementation: Cholesky of NxN matrix needs to be calculated, which might need lots of RAM. + With the current implementation we try to get exponential decay over the whole AR spectrum, which is N-Samples long. This involves the inversion of a cholesky matrix of size NxN matrix, which will need lots of RAM for non-trivial problems. """ @with_kw struct ExponentialNoise <: AbstractNoise noiselevel = 1 From fe00ec2eb057f9b520cea8d056a3b7fb0df4c3b9 Mon Sep 17 00:00:00 2001 From: Benedikt Ehinger Date: Fri, 26 Jul 2024 14:59:57 +0200 Subject: [PATCH 034/156] Update src/noise.jl --- src/noise.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/noise.jl b/src/noise.jl index ba709e17..983f8270 100644 --- a/src/noise.jl +++ b/src/noise.jl @@ -72,7 +72,7 @@ Noise with exponential decay in AR spectrum. `noiselevel` is used to scale the noise !!! warning - With the current implementation we try to get exponential decay over the whole AR spectrum, which is N-Samples long. This involves the inversion of a cholesky matrix of size NxN matrix, which will need lots of RAM for non-trivial problems. + With the current implementation we try to get exponential decay over the whole autocorrelation (AR) spectrum, which is N-Samples (the total number of samples) long. This involves the inversion of a cholesky matrix of size NxN matrix, which will need lots of RAM for non-trivial problems. """ @with_kw struct ExponentialNoise <: AbstractNoise noiselevel = 1 From f93a3c19333a26bf7b95f77f9b39d22848c65cbc Mon Sep 17 00:00:00 2001 From: Benedikt Ehinger Date: Fri, 26 Jul 2024 15:00:22 +0200 Subject: [PATCH 035/156] Update src/noise.jl --- src/noise.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/noise.jl b/src/noise.jl index 983f8270..7ba1ddea 100644 --- a/src/noise.jl +++ b/src/noise.jl @@ -72,7 +72,7 @@ Noise with exponential decay in AR spectrum. `noiselevel` is used to scale the noise !!! warning - With the current implementation we try to get exponential decay over the whole autocorrelation (AR) spectrum, which is N-Samples (the total number of samples) long. This involves the inversion of a cholesky matrix of size NxN matrix, which will need lots of RAM for non-trivial problems. + With the current implementation we try to get exponential decay over the whole autoregressive (AR) spectrum, which is N-Samples (the total number of samples) long. This involves the inversion of a cholesky matrix of size NxN matrix, which will need lots of RAM for non-trivial problems. """ @with_kw struct ExponentialNoise <: AbstractNoise noiselevel = 1 From aca3fd2415221be8ca99183d2e4adf649f4de7b7 Mon Sep 17 00:00:00 2001 From: Judith Schepers Date: Wed, 31 Jul 2024 15:03:15 +0200 Subject: [PATCH 036/156] Update src/noise.jl --- src/noise.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/noise.jl b/src/noise.jl index 7ba1ddea..fcd376ec 100644 --- a/src/noise.jl +++ b/src/noise.jl @@ -72,7 +72,7 @@ Noise with exponential decay in AR spectrum. `noiselevel` is used to scale the noise !!! warning - With the current implementation we try to get exponential decay over the whole autoregressive (AR) spectrum, which is N-Samples (the total number of samples) long. This involves the inversion of a cholesky matrix of size NxN matrix, which will need lots of RAM for non-trivial problems. + With the current implementation we try to get exponential decay over the whole autoregressive (AR) spectrum, which is N samples (the total number of samples in the signal) long. This involves the inversion of a Cholesky matrix of size NxN matrix, which will need lots of RAM for non-trivial problems. """ @with_kw struct ExponentialNoise <: AbstractNoise noiselevel = 1 From e868a14f80a3ca37eb7303911d7fd8c9fa2cdb23 Mon Sep 17 00:00:00 2001 From: jschepers Date: Wed, 31 Jul 2024 13:36:38 +0000 Subject: [PATCH 037/156] add empty line for formatting reasons --- docs/literate/reference/designtypes.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/literate/reference/designtypes.jl b/docs/literate/reference/designtypes.jl index 7d571f47..eca80e26 100644 --- a/docs/literate/reference/designtypes.jl +++ b/docs/literate/reference/designtypes.jl @@ -104,4 +104,4 @@ generate_events(design_single) design_repeated = RepeatDesign(design_single, 3); generate_events(design_repeated) -# [Here](@ref howto_repeat_design) one can find another example of how to repeat design entries for multi-subject designs. \ No newline at end of file +# [Here](@ref howto_repeat_design) one can find another example of how to repeat design entries for multi-subject designs. From 43dd57c9f63f59b570c1a5c3d7eb9b0054b8a911 Mon Sep 17 00:00:00 2001 From: Judith Schepers Date: Thu, 1 Aug 2024 10:22:17 +0200 Subject: [PATCH 038/156] Update docs/literate/reference/noisetypes.jl Co-authored-by: Benedikt Ehinger --- docs/literate/reference/noisetypes.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/literate/reference/noisetypes.jl b/docs/literate/reference/noisetypes.jl index 264e3439..a14551eb 100644 --- a/docs/literate/reference/noisetypes.jl +++ b/docs/literate/reference/noisetypes.jl @@ -1,7 +1,7 @@ # # Overview: Noise types # There are different types of noise signals which differ in their power spectra. -# If you are not familiar with different types/colors of noise yet, have a look at this [source](https://en.wikipedia.org/wiki/Colors_of_noise). +# If you are not familiar with different types/colors of noise yet, have a look at the[colored noise wikipedia page](https://en.wikipedia.org/wiki/Colors_of_noise). # There are several noise types directly implemented in UnfoldSim.jl. Here is a comparison: From 68ce72283268e3f8c87dd616ffa4a03783e7ca4a Mon Sep 17 00:00:00 2001 From: Judith Schepers Date: Thu, 1 Aug 2024 10:22:27 +0200 Subject: [PATCH 039/156] Update docs/literate/reference/overview.jl Co-authored-by: Benedikt Ehinger --- docs/literate/reference/overview.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/literate/reference/overview.jl b/docs/literate/reference/overview.jl index a4f4b611..f34c7f4e 100644 --- a/docs/literate/reference/overview.jl +++ b/docs/literate/reference/overview.jl @@ -1,5 +1,5 @@ # # Overview of functionality -# UnfoldSim has many modules, here we try to collect them to provide you with an overview. +# A UnfoldSim simulation has four ingredients: Design, Component, Onset and Noise. Here we provide a short overview of the implemented functions. # ### Setup # ```@raw html From 2dfd44becaa12a35d5ad854bbc83b37bed450b89 Mon Sep 17 00:00:00 2001 From: Judith Schepers Date: Thu, 1 Aug 2024 10:36:41 +0200 Subject: [PATCH 040/156] Update docs/literate/reference/overview.jl --- docs/literate/reference/overview.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/literate/reference/overview.jl b/docs/literate/reference/overview.jl index f34c7f4e..74cf3bc1 100644 --- a/docs/literate/reference/overview.jl +++ b/docs/literate/reference/overview.jl @@ -1,5 +1,5 @@ # # Overview of functionality -# A UnfoldSim simulation has four ingredients: Design, Component, Onset and Noise. Here we provide a short overview of the implemented functions. +# A UnfoldSim simulation has four ingredients: Design, Component, Onset and Noise. Here we provide a short overview of the implemented types. # ### Setup # ```@raw html From 2c34e5e7bf9674073cf900cf0b4fff04e9289843 Mon Sep 17 00:00:00 2001 From: Judith Schepers Date: Thu, 1 Aug 2024 10:36:50 +0200 Subject: [PATCH 041/156] Update docs/literate/reference/noisetypes.jl --- docs/literate/reference/noisetypes.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/literate/reference/noisetypes.jl b/docs/literate/reference/noisetypes.jl index a14551eb..03d35b4b 100644 --- a/docs/literate/reference/noisetypes.jl +++ b/docs/literate/reference/noisetypes.jl @@ -1,7 +1,7 @@ # # Overview: Noise types # There are different types of noise signals which differ in their power spectra. -# If you are not familiar with different types/colors of noise yet, have a look at the[colored noise wikipedia page](https://en.wikipedia.org/wiki/Colors_of_noise). +# If you are not familiar with different types/colors of noise yet, have a look at the[colors of noise Wikipedia page](https://en.wikipedia.org/wiki/Colors_of_noise). # There are several noise types directly implemented in UnfoldSim.jl. Here is a comparison: From 3a3bd3d74e7c3ef5f9b75c321a318ac8763f5137 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Fri, 9 Aug 2024 07:28:13 +0000 Subject: [PATCH 042/156] added docstring --- docs/literate/HowTo/sequence.jl | 39 ++++++++++++++++++++------------- src/component.jl | 8 +++++-- src/design.jl | 31 +++++++++++++++++++++++++- 3 files changed, 60 insertions(+), 18 deletions(-) diff --git a/docs/literate/HowTo/sequence.jl b/docs/literate/HowTo/sequence.jl index 6a07d15f..986446a6 100644 --- a/docs/literate/HowTo/sequence.jl +++ b/docs/literate/HowTo/sequence.jl @@ -5,18 +5,19 @@ using StableRNGs # ## Stimulus - Response design -# let's say we want to simulate a stimulus response, followed by a button press response. +# Let's say we want to simulate a stimulus response, followed by a button press response. +# # First we generate the minimal design of the experiment by specifying our conditins (a one-condition-two-levels design in our case) -design = SingleSubjectDesign(conditions = Dict(:condition => ["one", "two"]))#|>x->RepeatDesign(x,4) +design = SingleSubjectDesign(conditions = Dict(:condition => ["one", "two"])) generate_events(design) -# next we use the `SequenceDesign` and nest our initial design in it. "SR_" is code for an "S" event and an "R" event - only single letter events are supported! The `_` is a signal for the Onset generator to generate a bigger pause - no overlap between adjacend `SR` pairs -design = SequenceDesign(design, "SR{1,2}_", 0, StableRNG(1)) +# Next we use the `SequenceDesign` and nest our initial design in it. "`SR_`" is code for an "`S`" event and an "`R`" event - only single letter events are supported! The "`_`" is a signal for the Onset generator to generate a bigger pause - no overlap between adjacend "`SR`" pairs +design = SequenceDesign(design, "SR_", StableRNG(1)) generate_events(design) # The main thing that happened is that the design was repeated for every event (each 'letter') of the sequence, and an `eventtype` column was added. # !!! hint -# more advaned sequences are possible as well, like "SR{1,3}", or "A[BC]". Infinite sequences are not possible like "AB*" +# more advaned sequences are possible as well, like "SR{1,3}", or "A[BC]". Infinite sequences are **not** possible like "AB*" -# Finally, let's repeat the design 2 times - because we can +# Finally, let's repeat the current design 4 times design = RepeatDesign(design, 4) generate_events(design) @@ -24,36 +25,39 @@ generate_events(design) #generate_events(design) -# This results in 12 trials that nicely follow our sequence +# This results in 16 trials that nicely follow our sequence # Next we have to specify for both events `S` and `R` what the responses should look like. - p1 = LinearModelComponent(; basis = p100(), formula = @formula(0 ~ 1 + condition), β = [1, 0.5], -); +) n1 = LinearModelComponent(; basis = n170(), formula = @formula(0 ~ 1 + condition), β = [1, 0.5], -); +) + p3 = LinearModelComponent(; basis = UnfoldSim.hanning(Int(0.5 * 100)), # sfreq = 100 for the other bases formula = @formula(0 ~ 1 + condition), β = [1, 0], -); +) resp = LinearModelComponent(; basis = UnfoldSim.hanning(Int(0.5 * 100)), # sfreq = 100 for the other bases formula = @formula(0 ~ 1 + condition), β = [1, 2], offset = -10, -); +) +nothing ## hide + +# We combine them into a dictionary with a sequence-`Char` as key and simulate components = Dict('S' => [p1, n1, p3], 'R' => [resp]) -#components = [p1, n1, resp] + data, evts = simulate( StableRNG(1), design, @@ -61,8 +65,13 @@ data, evts = simulate( UniformOnset(offset = 40, width = 10), NoNoise(), ) +nothing ## hide +# Finally we can plot the results lines(data) -vlines!(evts.latency, color = (:gray, 0.5)) +vlines!(evts.latency[evts.event.=='S'], color = (:darkblue, 0.5)) +vlines!(evts.latency[evts.event.=='R'], color = (:darkred, 0.5)) xlims!(0, 500) -current_figure() \ No newline at end of file +current_figure() + +# As visible, the `R` response always follows the `S` response. Due to the "`_`" we have large breaks between the individual sequences. \ No newline at end of file diff --git a/src/component.jl b/src/component.jl index d3a0c5e7..65ef5e38 100644 --- a/src/component.jl +++ b/src/component.jl @@ -21,6 +21,7 @@ MixedModelComponent(; ``` """ +# backwards compatability after introducing the `offset` field` @with_kw struct MixedModelComponent <: AbstractComponent basis::Any formula::Any # e.g. 0~1+cond @@ -29,7 +30,8 @@ MixedModelComponent(; contrasts::Dict = Dict() offset::Int = 0 end - +MixedModelComponent(basis, formula, β, σs, contrasts) = + MixedModelComponent(basis, formula, β, σs, contrasts, 0) """ A multiple regression component for one subject @@ -51,6 +53,7 @@ LinearModelComponent(; ``` """ +# backwards compatability after introducing the `offset` field @with_kw struct LinearModelComponent <: AbstractComponent basis::Any formula::Any # e.g. 0~1+cond - left side must be "0" @@ -59,7 +62,8 @@ LinearModelComponent(; offset::Int = 0 end - +LinearModelComponent(basis, formula, β, contrasts) = + LinearModelComponent(basis, formula, β, contrasts, 0) """ offset(AbstractComponent) diff --git a/src/design.jl b/src/design.jl index d35ea440..740217bb 100644 --- a/src/design.jl +++ b/src/design.jl @@ -178,6 +178,34 @@ function check_sequence(s::String) @assert length(blankfind) <= 1 && (length(blankfind) == 0 || length(s) == blankfind[1]) "the blank-indicator '_' has to be the last sequence element" return s end + + +""" + SequenceDesign{T} <: AbstractDesign +Enforce a sequence of events for each entry of a provided `AbstractDesign`. +The sequence string can contain any number of `char`, but the `_` character is used to indicate a break between events without any overlap. + + +```julia +design = SingleSubjectDesign(conditions = Dict(:condition => ["one", "two"])) +design = SequenceDesign(design, "SCR_", StableRNG(1)) +``` +Would result in a `generate_events(design)` +```repl +6×2 DataFrame + Row │ condition event + │ String Char +─────┼────────────────── + 1 │ one S + 2 │ one C + 3 │ one R + 4 │ two S + 5 │ two C + 6 │ two R +``` + +See also [`SingleSubjectDesign`](@ref), [`MultiSubjectDesign`](@ref), [`RepeatDesign`](@ref) +""" @with_kw struct SequenceDesign{T} <: AbstractDesign design::T sequence::String = "" @@ -187,7 +215,8 @@ end SequenceDesign{T}(d, s, sl, r) where {T<:AbstractDesign} = new(d, check_sequence(s), sl, r) end - +SequenceDesign(design, sequence, rng::AbstractRNG) = + SequenceDesign(design = design, sequence = sequence, rng = rng) SequenceDesign(design, sequence) = SequenceDesign(design = design, sequence = sequence) generate_events(design::SequenceDesign{MultiSubjectDesign}) = error("not yet implemented") From b8d85b92ec408efd598015cc0af0bec766310094 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Fri, 9 Aug 2024 11:18:09 +0000 Subject: [PATCH 043/156] renamed to have the formula at the end --- docs/literate/reference/onsettypes.jl | 6 +++--- src/onset.jl | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/literate/reference/onsettypes.jl b/docs/literate/reference/onsettypes.jl index e9967316..9871ef58 100644 --- a/docs/literate/reference/onsettypes.jl +++ b/docs/literate/reference/onsettypes.jl @@ -286,10 +286,10 @@ end -# ## Design-dependent `FormulaXOnset` +# ## Design-dependent `X-OnsetFormula` -# For additional control we provide `FormulaUniformOnset` and `FormulaLogNormalOnset` types, that allow to control all parameters by specifying formulas -o = UnfoldSim.FormulaUniformOnset( +# For additional control we provide `UniformOnsetFormula` and `LogNormalOnsetFormula` types, that allow to control all parameters by specifying formulas +o = UnfoldSim.UniformOnsetFormula( width_formula = @formula(0 ~ 1 + cond), width_β = [50, 20], ) diff --git a/src/onset.jl b/src/onset.jl index a8335552..f00d7871 100644 --- a/src/onset.jl +++ b/src/onset.jl @@ -8,7 +8,7 @@ Provide a Uniform Distribution of the inter-event-distances. `width` is the width of the uniform distribution (=> the jitter). Since the lower bound is 0, `width` is also the upper bound. `offset` is the minimal distance. The maximal distance is `offset + width`. -For a more advanced parameter specification, see `FormulaUniformOnset``, which allows to specify the onset-parameters depending on the `Design` employed via a linear regression model +For a more advanced parameter specification, see `UniformOnsetFormula``, which allows to specify the onset-parameters depending on the `Design` employed via a linear regression model """ @with_kw struct UniformOnset <: AbstractOnset width = 50 # how many samples jitter? @@ -20,7 +20,7 @@ Log-normal inter-event distances using the `Distributions.jl` truncated LogNorma Be careful with large `μ` and `σ` values, as they are on logscale. σ>8 can quickly give you out-of-memory sized signals! -For a more advanced parameter specification, see `FormulaLogNormalOnset, which allows to specify the onset-parameters depending on the `Design` employed via linear regression model +For a more advanced parameter specification, see `LogNormalOnsetFormula, which allows to specify the onset-parameters depending on the `Design` employed via linear regression model """ @with_kw struct LogNormalOnset <: AbstractOnset μ::Any # mean @@ -38,8 +38,8 @@ struct NoOnset <: AbstractOnset end """ simulate_interonset_distances(rng, onset::UniformOnset, design::AbstractDesign) simulate_interonset_distances(rng, onset::LogNormalOnset, design::AbstractDesign) - simulate_interonset_distances(rng, onset::FormulaUniformOnset, design::AbstractDesign) - simulate_interonset_distances(rng, onset::FormulaLogNormalOnset, design::AbstractDesign) + simulate_interonset_distances(rng, onset::UniformOnsetFormula, design::AbstractDesign) + simulate_interonset_distances(rng, onset::LogNormalOnsetFormula, design::AbstractDesign) Generate the inter-event-onset vector in samples (returns Int). """ @@ -80,7 +80,7 @@ function simulate_onsets(rng, onset::AbstractOnset, simulation::Simulation) end """ - FormulaUniformOnset <: AbstractOnset + UniformOnsetFormula <: AbstractOnset provide a Uniform Distribution of the inter-event-distances, but with regression formulas. This is helpful if your overlap/event-distribution should be dependend on some condition, e.g. more overlap in cond='A' than cond='B'. @@ -95,7 +95,7 @@ This is helpful if your overlap/event-distribution should be dependend on some c See `UniformOnset` for a simplified version without linear regression specifications """ -@with_kw struct FormulaUniformOnset <: AbstractOnset +@with_kw struct UniformOnsetFormula <: AbstractOnset width_formula = @formula(0 ~ 1) width_β::Vector = [50] width_contrasts::Dict = Dict() @@ -105,7 +105,7 @@ See `UniformOnset` for a simplified version without linear regression specificat end -function simulate_interonset_distances(rng, o::FormulaUniformOnset, design::AbstractDesign) +function simulate_interonset_distances(rng, o::UniformOnsetFormula, design::AbstractDesign) events = generate_events(design) widths = UnfoldSim.generate_designmatrix(o.width_formula, events, o.width_contrasts) * @@ -120,7 +120,7 @@ function simulate_interonset_distances(rng, o::FormulaUniformOnset, design::Abst end -@with_kw struct FormulaLogNormalOnset <: AbstractOnset +@with_kw struct LogNormalOnsetFormula <: AbstractOnset μ_formula = @formula(0 ~ 1) μ_β::Vector = [0] μ_contrasts::Dict = Dict() @@ -135,7 +135,7 @@ end function simulate_interonset_distances( rng, - o::FormulaLogNormalOnset, + o::LogNormalOnsetFormula, design::AbstractDesign, ) events = generate_events(design) From e05a9c6000eefb2d1918b9f792fcf04b7d1078a1 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Fri, 9 Aug 2024 11:39:30 +0000 Subject: [PATCH 044/156] merge fix, double definition of function --- src/simulation.jl | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/simulation.jl b/src/simulation.jl index e5e85531..0d104da2 100755 --- a/src/simulation.jl +++ b/src/simulation.jl @@ -7,10 +7,6 @@ Simulation( ) = Simulation(design, [component], onset, noisetype) -function simulate(design::AbstractDesign, signal, onset::AbstractOnset, args...; kwargs...) - @warn "No random generator defined, used the default (`Random.MersenneTwister(1)`) with a fixed seed. This will always return the same results and the user is strongly encouraged to provide their own random generator!" - simulate(MersenneTwister(1), design, signal, onset, args...; kwargs...) -end """ simulate( @@ -40,7 +36,6 @@ Some remarks to how the noise is added: """ - function simulate(design::AbstractDesign, signal, onset::AbstractOnset, args...; kwargs...) @warn "No random generator defined, used the default (`Random.MersenneTwister(1)`) with a fixed seed. This will always return the same results and the user is strongly encouraged to provide their own random generator!" simulate(MersenneTwister(1), design, signal, onset, args...; kwargs...) From 8aa75912e4a0853d9de797107cb954db69378c01 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Fri, 9 Aug 2024 12:05:00 +0000 Subject: [PATCH 045/156] component function test + docstring --- docs/literate/HowTo/componentfunction.jl | 4 ++-- src/component.jl | 18 ++++++++++---- test/component.jl | 30 ++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/docs/literate/HowTo/componentfunction.jl b/docs/literate/HowTo/componentfunction.jl index 7d36c62f..a0db3773 100644 --- a/docs/literate/HowTo/componentfunction.jl +++ b/docs/literate/HowTo/componentfunction.jl @@ -23,8 +23,8 @@ design = UnfoldSim.SingleSubjectDesign(; # !!! important # because any function depending on `design` can be used, two things have to be taken care of: # -# 1. in case a random component exist, specify a `RNG`, the basis might be evaluated multiple times inside `simulate` -# 2. a `maxlength` has to be specified via a tuple `(function.maxlength)`` +# 1. in case a random component exist in the function, specify a `<:AbstractRNG` within the function call , the basis might be evaluated multiple times inside `simulate` +# 2. a `maxlength` has to be specified via a tuple `(function,maxlength)`` mybasisfun = design -> hanning.(generate_events(design).duration) signal = LinearModelComponent(; basis = (mybasisfun, 100), diff --git a/src/component.jl b/src/component.jl index ed1f92bc..4c1828f4 100644 --- a/src/component.jl +++ b/src/component.jl @@ -35,15 +35,16 @@ MixedModelComponent(basis, formula, β, σs, contrasts) = """ A multiple regression component for one subject -- `basis`: an object, if accessed, provides a 'basis-function', e.g. `hanning(40)`, this defines the response at a single event. It will be weighted by the model-prediction +- `basis`: an object, if accessed, provides a 'basis-function', e.g. `hanning(40)`, this defines the response at a single event. It will be weighted by the model-prediction. Can also be a tuple `(fun::Function,maxlength::Int)` with a function `fun` that either generates a matrix `size = (maxlength,size(design,1))` or a vector of vectors. If a larger matrix is generated, it is automatically cutoff at `maxlength` - `formula`: StatsModels Formula-Object `@formula 0~1+cond` (left side must be 0) - `β` Vector of betas, must fit the formula - `contrasts`: Dict. Default is empty, e.g. `Dict(:condA=>EffectsCoding())` - -All arguments can be named, in that case `contrasts` is optional +- `offset`: Int. Default is 0. Can be used to shift the basis function in time +All arguments can be named, in that case `contrasts` and `offset` are optional. Works best with `SingleSubjectDesign` ```julia +# use a hanning window of size 40 as the component basis LinearModelComponent(; basis=hanning(40), formula=@formula(0~1+cond), @@ -51,9 +52,17 @@ LinearModelComponent(; contrasts=Dict(:cond=>EffectsCoding()) ) +# define a function returning random numbers as the component basis +maxlength = 15 +my_signal_function = d->rand(StableRNG(1),maxlength,length(d)) +LinearModelComponent(; + basis=(my_signal_function,maxlength), + formula=@formula(0~1), + β = [1.], +) + ``` """ -# backwards compatability after introducing the `offset` field @with_kw struct LinearModelComponent <: AbstractComponent basis::Union{Tuple{Function,Int},Array} formula::FormulaTerm # e.g. 0~1+cond - left side must be "0" @@ -69,6 +78,7 @@ LinearModelComponent(; new(basis, formula, β, contrasts, offset) end +# backwards compatability after introducing the `offset` field LinearModelComponent(basis, formula, β, contrasts) = LinearModelComponent(basis, formula, β, contrasts, 0) """ diff --git a/test/component.jl b/test/component.jl index 3893a391..1b48257d 100644 --- a/test/component.jl +++ b/test/component.jl @@ -1,5 +1,35 @@ @testset "component" begin + @testset "componentfunction" begin + design = UnfoldSim.SingleSubjectDesign(; conditions = Dict(:duration => 10:-1:5)) + + mybasisfun = design -> (collect.(range.(1, generate_events(design).duration))) + signal = LinearModelComponent(; + basis = (mybasisfun, 15), + formula = @formula(0 ~ 1), + β = [1], + ) + + erp = UnfoldSim.simulate_component(StableRNG(1), signal, design) + + @test size(erp) == (15, 6) + @test all(erp[11:15, :] .== 0) + @test erp[1:9, 2] == collect(1.0:9) + + # test shorter cut + signal = LinearModelComponent(; + basis = (mybasisfun, 5), + formula = @formula(0 ~ 1), + β = [1], + ) + + erp = UnfoldSim.simulate_component(StableRNG(1), signal, design) + @test size(erp) == (5, 6) + @test !any(erp .== 0) + + + + end @testset "LMM" begin @test UnfoldSim.weight_σs(Dict(:subj => [1, 2]), 0.5, 1.0).subj == LowerTriangular([0.5 0; 0 1.0]) From 395b5ee527ea47962a1e134c9112348852455647 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Fri, 9 Aug 2024 12:54:26 +0000 Subject: [PATCH 046/156] better docs --- docs/literate/HowTo/componentfunction.jl | 22 ++++++++++- docs/literate/HowTo/sequence.jl | 8 ++-- src/design.jl | 49 ++++++++++++++++++++++++ test/sequence.jl | 39 +++++++++++++++++-- 4 files changed, 110 insertions(+), 8 deletions(-) diff --git a/docs/literate/HowTo/componentfunction.jl b/docs/literate/HowTo/componentfunction.jl index a0db3773..6b62790c 100644 --- a/docs/literate/HowTo/componentfunction.jl +++ b/docs/literate/HowTo/componentfunction.jl @@ -36,4 +36,24 @@ erp = UnfoldSim.simulate_component(MersenneTwister(1), signal, design); # Finally, let's plot it, sorted by duration -plot_erpimage(erp, sortvalues = generate_events(design).duration) + +f = Figure() +df = UnfoldMakie.eeg_array_to_dataframe(erp') +df.duration = repeat(generate_events(design).duration, inner = size(erp, 1)) +plot_erp!( + f[1, 1], + df, + mapping = (; group = :duration, color = :duration), + categorical_color = false, + categorical_group = true, + layout = (; legend_position = :left), +) +plot_erpimage!( + f[2, 1], + erp, + sortvalues = generate_events(design).duration, + layout = (; legend_position = :bottom), +) +f + +# The scaling by the two `condition`` effect levels and the modified event duration by the `duration` are clearly visible diff --git a/docs/literate/HowTo/sequence.jl b/docs/literate/HowTo/sequence.jl index 986446a6..3f280f26 100644 --- a/docs/literate/HowTo/sequence.jl +++ b/docs/literate/HowTo/sequence.jl @@ -15,17 +15,17 @@ design = SequenceDesign(design, "SR_", StableRNG(1)) generate_events(design) # The main thing that happened is that the design was repeated for every event (each 'letter') of the sequence, and an `eventtype` column was added. # !!! hint -# more advaned sequences are possible as well, like "SR{1,3}", or "A[BC]". Infinite sequences are **not** possible like "AB*" +# more advaned sequences are possible as well, like "SR{1,3}", or "A[BC]". Infinite sequences are **not** possible like "AB*". # Finally, let's repeat the current design 4 times design = RepeatDesign(design, 4) generate_events(design) -#design = UnfoldSim.AddSaccadeAmplitudeDesign4(design,:rt,Normal(0,1),MersenneTwister(1)) -#generate_events(design) +# This results in 16 trials that nicely follow our sequence +# !!! hint +# There is a difference between `SequenceDesign(RepeatDesign)` and `RepeatDesign(SequenceDesign)` for variable sequences e.g. "A[BC]", where in the former case, one sequence is drawn e.g. "AC" and applied to all repeated rows, in the latter, one sequence for each repeat is drawn. -# This results in 16 trials that nicely follow our sequence # Next we have to specify for both events `S` and `R` what the responses should look like. p1 = LinearModelComponent(; diff --git a/src/design.jl b/src/design.jl index 740217bb..98e3ca2a 100644 --- a/src/design.jl +++ b/src/design.jl @@ -185,6 +185,11 @@ end Enforce a sequence of events for each entry of a provided `AbstractDesign`. The sequence string can contain any number of `char`, but the `_` character is used to indicate a break between events without any overlap. +It is also possible to define variable length sequences using `{}`. For example, `A{10,20}` would result in a sequence of 10 to 20 `A`'s. + +Another variable sequence is defined using `[]`. For example, `S[ABC]` would result in any one sequence `SA`, `SB`, `SC`. + +Important: The exact same variable sequence is used for current rows of a design. Only, if you later nest in a `RepeatDesign` then each `RepeatDesign` repetition will gain a new variable sequence. If you need imbalanced designs, please refer to the `ImbalancedDesign` tutorial ```julia design = SingleSubjectDesign(conditions = Dict(:condition => ["one", "two"])) @@ -204,6 +209,50 @@ Would result in a `generate_events(design)` 6 │ two R ``` +## Example for Sequence -> Repeat vs. Repeat -> Sequence + +### Sequence -> Repeat +```julia +design = SingleSubjectDesign(conditions = Dict(:condition => ["one", "two"])) +design = SequenceDesign(design, "[AB]", StableRNG(1)) +design = RepeatDesign(design,2) +generate_events(design) +``` + + +```repl +4×2 DataFrame + Row │ condition event + │ String Char +─────┼────────────────── + 1 │ one A + 2 │ two A + 3 │ one B + 4 │ two B +``` +Sequence -> Repeat: a sequence design is repeated, then for each repetition a sequence is generated and applied. Events have different values + +### Repeat -> Sequence +```julia +design = SingleSubjectDesign(conditions = Dict(:condition => ["one", "two"])) +design = RepeatDesign(design,2) +design = SequenceDesign(design, "[AB]", StableRNG(1)) +generate_events(design) +``` + +```repl +4×2 DataFrame + Row │ condition event + │ String Char +─────┼────────────────── + 1 │ one A + 2 │ two A + 3 │ one A + 4 │ two A +``` +Repeat -> Sequence: the design is first repeated, then for that design one sequence generated and applied. All events are the same + + See also [`SingleSubjectDesign`](@ref), [`MultiSubjectDesign`](@ref), [`RepeatDesign`](@ref) """ @with_kw struct SequenceDesign{T} <: AbstractDesign diff --git a/test/sequence.jl b/test/sequence.jl index e4a9ace0..342033f9 100644 --- a/test/sequence.jl +++ b/test/sequence.jl @@ -5,7 +5,40 @@ @test_throws AssertionError UnfoldSim.check_sequence("b_la") @test_throws AssertionError UnfoldSim.check_sequence("_bla") + @test length(UnfoldSim.sequencestring(StableRNG(1), "A{10,10}")) == 10 + @test length(UnfoldSim.sequencestring(StableRNG(1), "A{10,10}B")) == 11 + @test length(UnfoldSim.sequencestring(StableRNG(1), "A{10,20}")) >= 10 +end + +@testset "Simulate Sequences" begin + + + + design = SingleSubjectDesign(conditions = Dict(:condition => ["one", "two"])) + design = SequenceDesign(design, "SCR_", StableRNG(1)) + evt = generate_events(design) + @test size(evt, 1) == 6 + @test evt.event == ['S', 'C', 'R', 'S', 'C', 'R'] + + design = RepeatDesign(design, 2) + evt = generate_events(design) + @test size(evt, 1) == 12 + @test evt.event == ['S', 'C', 'R', 'S', 'C', 'R', 'S', 'C', 'R', 'S', 'C', 'R'] + + + # repeat first, then sequence => same sequence + design = SingleSubjectDesign(conditions = Dict(:condition => ["A", "B"])) + design = RepeatDesign(design, 2) + design = SequenceDesign(design, "S[ABCD]", StableRNG(2)) + evt = generate_events(design) + + @test all(evt.event[2:2:end] .== 'B') + + + # sequence first, then repeat => different sequence for each repetition + design = SingleSubjectDesign(conditions = Dict(:condition => ["A", "B"])) + design = SequenceDesign(design, "S[ABCD]", StableRNG(2)) + design = RepeatDesign(design, 2) + evt = generate_events(design) + @test !all(evt.event[2:2:end] .== 'B') end -@test length(UnfoldSim.sequencestring(StableRNG(1), "A{10,10}")) == 10 -@test length(UnfoldSim.sequencestring(StableRNG(1), "A{10,10}B")) == 11 -@test length(UnfoldSim.sequencestring(StableRNG(1), "A{10,20}")) >= 10 \ No newline at end of file From da51a16150f570cf7d2be36412b48910ab94c423 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Fri, 9 Aug 2024 13:16:38 +0000 Subject: [PATCH 047/156] added unittests --- src/UnfoldSim.jl | 2 +- src/onset.jl | 55 ++++++++++++++++++++++++++++++++++++------------ test/onset.jl | 43 +++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 14 deletions(-) diff --git a/src/UnfoldSim.jl b/src/UnfoldSim.jl index 126264b8..e5fccd49 100644 --- a/src/UnfoldSim.jl +++ b/src/UnfoldSim.jl @@ -66,7 +66,7 @@ export simulate, export pad_array, convert # export Offsets -export UniformOnset, LogNormalOnset, NoOnset +export UniformOnset, LogNormalOnset, NoOnset, UniformOnsetFormula, LogNormalOnsetFormula # re-export StatsModels export DummyCoding, EffectsCoding diff --git a/src/onset.jl b/src/onset.jl index a26c4a5c..38ce857f 100644 --- a/src/onset.jl +++ b/src/onset.jl @@ -108,20 +108,23 @@ end provide a Uniform Distribution of the inter-event-distances, but with regression formulas. This is helpful if your overlap/event-distribution should be dependend on some condition, e.g. more overlap in cond='A' than cond='B'. - - `width`: is the width of the uniform distribution (=> the jitter). Since the lower bound is 0, `width` is also the upper bound. - -`width_formula`: choose a formula depending on your `Design` - -`width_β`: Choose a vector of betas, number needs to fit the formula chosen - -`width_contrasts` (optional): Choose a contrasts-dictionary according to the StatsModels specifications - `offset` is the minimal distance. The maximal distance is `offset + width`. - -`offset_formula`: choose a formula depending on your `design` - -`offset_β`: Choose a vector of betas, number needs to fit the formula chosen - -`offset_contrasts` (optional): Choose a contrasts-dictionary according to the StatsModels specifications +**width** + + -`width_formula`: choose a formula depending on your `Design`, default `@formula(0~1)` + -`width_β`: Choose a `Vector` of betas, number needs to fit the formula chosen, no default. + -`width_contrasts` (optional): Choose a contrasts-`Dict`ionary according to the StatsModels specifications, default `Dict()`` + +**offset** is the minimal distance. The maximal distance is `offset + width`. + + -`offset_formula`: choose a formula depending on your `design`, default `@formula(0~1)`` + -`offset_β`: Choose a `Vector` of betas, number needs to fit the formula chosen, default `[0]` + -`offset_contrasts` (optional): Choose a contrasts-`Dict`ionary according to the StatsModels specifications, default `Dict()` See `UniformOnset` for a simplified version without linear regression specifications """ @with_kw struct UniformOnsetFormula <: AbstractOnset width_formula = @formula(0 ~ 1) - width_β::Vector = [50] + width_β::Vector width_contrasts::Dict = Dict() offset_formula = @formula(0 ~ 1) offset_β::Vector = [0] @@ -144,12 +147,37 @@ function simulate_interonset_distances(rng, o::UniformOnsetFormula, design::Abst end +""" + LogNormalOnsetFormula <: AbstractOnset +provide a LogNormal Distribution of the inter-event-distances, but with regression formulas. +This is helpful if your overlap/event-distribution should be dependend on some condition, e.g. more overlap in cond='A' than cond='B'. + +**μ** + + -`μ_formula`: choose a formula depending on your `Design`, default `@formula(0~1)` + -`μ_β`: Choose a `Vector` of betas, number needs to fit the formula chosen, default `[0]` + -`μ_contrasts` (optional): Choose a contrasts-`Dict`ionary according to the StatsModels specifications, default `Dict()`` + + -`σ_formula`: choose a formula depending on your `Design`, default `@formula(0~1)` + -`σ_β`: Choose a `Vector` of betas, number needs to fit the formula chosen, default `[0]` + -`σ_contrasts` (optional): Choose a contrasts-`Dict`ionary according to the StatsModels specifications, default `Dict()`` + +**offset** is the minimal distance. The maximal distance is `offset + width`. + + -`offset_formula`: choose a formula depending on your `design`, default `@formula(0~1)`` + -`offset_β`: Choose a `Vector` of betas, number needs to fit the formula chosen, default `[0]` + -`offset_contrasts` (optional): Choose a contrasts-`Dict`ionary according to the StatsModels specifications, default `Dict()` + +`truncate_upper` - truncate at some sample, default nothing + +See `LogNormalOnset` for a simplified version without linear regression specifications +""" @with_kw struct LogNormalOnsetFormula <: AbstractOnset μ_formula = @formula(0 ~ 1) - μ_β::Vector = [0] + μ_β::Vector μ_contrasts::Dict = Dict() σ_formula = @formula(0 ~ 1) - σ_β::Vector = [0] + σ_β::Vector σ_contrasts::Dict = Dict() offset_formula = @formula(0 ~ 1) offset_β::Vector = [0] @@ -174,9 +202,10 @@ function simulate_interonset_distances( funs = LogNormal.(μs, σs) if !isnothing(o.truncate_upper) - fun = truncated.(fun; upper = o.truncate_upper) + funs = truncated.(funs; upper = o.truncate_upper) end - return Int.(round.(offsets .+ rand.(deepcopy(rng), funs, 1))) + #@debug reduce(hcat, rand.(deepcopy(rng), funs, 1)) + return Int.(round.(offsets .+ reduce(vcat, rand.(deepcopy(rng), funs, 1)))) end diff --git a/test/onset.jl b/test/onset.jl index 0e6ec715..d2620c5a 100644 --- a/test/onset.jl +++ b/test/onset.jl @@ -60,4 +60,47 @@ @test accumulated_onset[1] >= 1 end + @testset "OnsetFormula" begin + + design = + SingleSubjectDesign(conditions = Dict(:cond => ["A", "B"])) |> + x -> RepeatDesign(x, 10000) + + + o = UniformOnsetFormula(width_formula = @formula(0 ~ 1 + cond), width_β = [50, 20]) + events = generate_events(design) + onsets = UnfoldSim.simulate_interonset_distances(StableRNG(1), o, design) + @test minimum(onsets[1:2:end]) == 0 + @test maximum(onsets[1:2:end]) == 50 + @test minimum(onsets[2:2:end]) == 0 + @test maximum(onsets[2:2:end]) == 70 + + o = UniformOnsetFormula( + offset_formula = @formula(0 ~ 1 + cond), + offset_β = [50, 20], + width_β = [50], + ) + events = generate_events(design) + onsets = UnfoldSim.simulate_interonset_distances(StableRNG(1), o, design) + @test minimum(onsets[1:2:end]) == 50 + @test maximum(onsets[1:2:end]) == 100 + @test minimum(onsets[2:2:end]) == 70 + @test maximum(onsets[2:2:end]) == 120 + + + o = LogNormalOnsetFormula( + μ_formula = @formula(0 ~ 1 + cond), + μ_β = [1, 1], + σ_β = [1], + ) + events = generate_events(design) + onsets = UnfoldSim.simulate_interonset_distances(StableRNG(1), o, design) + @test minimum(onsets[1:2:end]) == 0 + @test maximum(onsets[1:2:end]) < 150 + @test minimum(onsets[2:2:end]) == 0 + @test maximum(onsets[2:2:end]) > 300 + + + + end end From 9603df9aa2d55b099d4528eec92ed9a57362e85d Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Tue, 3 Sep 2024 13:51:20 +0000 Subject: [PATCH 048/156] fixed small wording --- docs/literate/HowTo/componentfunction.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/literate/HowTo/componentfunction.jl b/docs/literate/HowTo/componentfunction.jl index 6b62790c..236362db 100644 --- a/docs/literate/HowTo/componentfunction.jl +++ b/docs/literate/HowTo/componentfunction.jl @@ -19,7 +19,7 @@ design = UnfoldSim.SingleSubjectDesign(; ); -# Instead of defining a boring vector basis function e.g. `[0,0,1,2,3,3,2,1,0,0,0]`, let's use function, generating random values for now. +# Instead of defining a boring vector basis function e.g. `[0,0,1,2,3,3,2,1,0,0,0]`, let's use function, modulating a hanning windows by the experimental design's duration. # !!! important # because any function depending on `design` can be used, two things have to be taken care of: # From 612786aefe485ce80a05dff6344f028a8b7480c4 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Tue, 3 Sep 2024 13:52:56 +0000 Subject: [PATCH 049/156] fix tutorial with v0.3 renaming of simulate to simulate_component --- docs/literate/HowTo/newComponent.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/literate/HowTo/newComponent.jl b/docs/literate/HowTo/newComponent.jl index 97f4699b..de6297b8 100644 --- a/docs/literate/HowTo/newComponent.jl +++ b/docs/literate/HowTo/newComponent.jl @@ -42,7 +42,7 @@ end Base.length(c::TimeVaryingComponent) = length(c.maxlength) # While we could have put the TimeVaryingComponent.basisfunction directly into the simulate function, I thought this is a bit more modular -function UnfoldSim.simulate(rng, c::TimeVaryingComponent, design::AbstractDesign) +function UnfoldSim.simulate_component(rng, c::TimeVaryingComponent, design::AbstractDesign) evts = generate_events(design) return c.basisfunction(evts, c.maxlength) end From 272eb02d5e168957e9d0aea2cccf448cb453c53e Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Tue, 3 Sep 2024 14:17:55 +0000 Subject: [PATCH 050/156] fix newComponent tutorial --- docs/literate/HowTo/newComponent.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/literate/HowTo/newComponent.jl b/docs/literate/HowTo/newComponent.jl index de6297b8..aaf909e9 100644 --- a/docs/literate/HowTo/newComponent.jl +++ b/docs/literate/HowTo/newComponent.jl @@ -11,6 +11,7 @@ # Click to expand # ``` using UnfoldSim +import UnfoldSim.simulate_component using Unfold using Random using DSP @@ -65,7 +66,7 @@ function basis_shiftduration(evts, maxlength) end # ## Simulate data with the new component type -erp = UnfoldSim.simulate( +erp = UnfoldSim.simulate_component( MersenneTwister(1), TimeVaryingComponent(basis_shiftduration, 50), design, From 360b0eb5983d09c2d008fb03dd713161f12f4e3e Mon Sep 17 00:00:00 2001 From: ReneSkukies Date: Wed, 16 Oct 2024 09:38:26 +0000 Subject: [PATCH 051/156] add GroundTruth desing and needed functions --- src/design.jl | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/design.jl b/src/design.jl index e7bcdbcc..393f0387 100644 --- a/src/design.jl +++ b/src/design.jl @@ -188,3 +188,50 @@ end Base.size(design::RepeatDesign{MultiSubjectDesign}) = size(design.design) .* (design.repeat, 1) Base.size(design::RepeatDesign{SingleSubjectDesign}) = size(design.design) .* design.repeat + + +# --- +# Ground Truth + + +struct GroundTruthDesign + design::AbstractDesign + effectsdict::Dict +end + +""" + expand_grid(design) + +Used to expand effects grid. Copied from Effects.jl +""" +function expand_grid(design) + colnames = tuple(keys(design)...) + rowtab = NamedTuple{colnames}.(product(values(design)...)) + + return DataFrame(vec(rowtab)) +end + +typical_value(v::Vector{<:Number}) = [mean(v)] +typical_value(v) = unique(v) + +""" + UnfoldSim.generate_events(design::GroundTruthDesign) + +Generates events to simulate ground truth data using an effects dictionary. Every covariate that is in the `GroundTruthDesign` but not in the `effects_dict` will be set to a `typical_value` (i.e. the mean) + +```julia +# Example + +effects_dict = Dict{Symbol,Union{<:Number,<:String}}(:conditionA=>[0,1]) +SingleSubjectDesign(...) |> x-> GroundTruthDesign(x,effects_dict) +``` +""" +function UnfoldSim.generate_events(t::GroundTruthDesign) + effects_dict = Dict{Any,Any}(t.effects_dict) + current_design = generate_events(t.des) + to_be_added = setdiff(names(current_design), keys(effects_dict)) + for tba in to_be_added + effects_dict[tba] = typical_value(current_design[:, tba]) + end + return expand_grid(effects_grid) +end \ No newline at end of file From f082ce41e606923df3f4c945dbebb6f99d55b8f9 Mon Sep 17 00:00:00 2001 From: ReneSkukies Date: Mon, 28 Oct 2024 09:45:23 +0100 Subject: [PATCH 052/156] implement EffectsDesign; implement Tests; export Design --- src/UnfoldSim.jl | 3 +-- src/design.jl | 36 ++++++++++++++++++++++++------------ test/design.jl | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 14 deletions(-) diff --git a/src/UnfoldSim.jl b/src/UnfoldSim.jl index cd1b0287..5b46368d 100644 --- a/src/UnfoldSim.jl +++ b/src/UnfoldSim.jl @@ -14,7 +14,6 @@ using LinearAlgebra using ToeplitzMatrices # for AR Expo. Noise "Circulant" using StatsModels using HDF5, Artifacts, FileIO - using LinearAlgebra # headmodel import DSP.hanning @@ -46,7 +45,7 @@ export Simulation export MixedModelComponent, LinearModelComponent # export designs -export MultiSubjectDesign, SingleSubjectDesign, RepeatDesign +export MultiSubjectDesign, SingleSubjectDesign, RepeatDesign, EffectsDesign # noise functions export PinkNoise, RedNoise, WhiteNoise, NoNoise, ExponentialNoise #,RealNoise (not implemented yet) diff --git a/src/design.jl b/src/design.jl index 393f0387..1eea7261 100644 --- a/src/design.jl +++ b/src/design.jl @@ -191,13 +191,24 @@ Base.size(design::RepeatDesign{SingleSubjectDesign}) = size(design.design) .* de # --- -# Ground Truth +# Effects - -struct GroundTruthDesign +""" + EffectsDesign <: AbstractDesign +Design to obtain ground truth simulation. + +## Fields +- `design::AbstractDesign` + The design of your (main) simulation. +- `èffects_dict::Dict` + Effects.jl style dictionary specifying variable effects. See also [Unfold.jl marginalized effects](https://unfoldtoolbox.github.io/Unfold.jl/stable/generated/HowTo/effects/) +""" +struct EffectsDesign <: AbstractDesign design::AbstractDesign - effectsdict::Dict + effects_dict::Dict end +EffectsDesign(design::MultiSubjectDesign,effects_dict::Dict) = error("not yet implemented") +UnfoldSim.size(t::EffectsDesign) = size(generate_events(t)) """ expand_grid(design) @@ -205,8 +216,8 @@ end Used to expand effects grid. Copied from Effects.jl """ function expand_grid(design) - colnames = tuple(keys(design)...) - rowtab = NamedTuple{colnames}.(product(values(design)...)) + colnames = tuple(Symbol.(keys(design))...) + rowtab = NamedTuple{colnames}.(Base.Iterators.product(values(design)...)) return DataFrame(vec(rowtab)) end @@ -215,7 +226,7 @@ typical_value(v::Vector{<:Number}) = [mean(v)] typical_value(v) = unique(v) """ - UnfoldSim.generate_events(design::GroundTruthDesign) + UnfoldSim.generate_events(design::EffectsDesign) Generates events to simulate ground truth data using an effects dictionary. Every covariate that is in the `GroundTruthDesign` but not in the `effects_dict` will be set to a `typical_value` (i.e. the mean) @@ -223,15 +234,16 @@ Generates events to simulate ground truth data using an effects dictionary. Ever # Example effects_dict = Dict{Symbol,Union{<:Number,<:String}}(:conditionA=>[0,1]) -SingleSubjectDesign(...) |> x-> GroundTruthDesign(x,effects_dict) +SingleSubjectDesign(...) |> x-> EffectsDesign(x,effects_dict) ``` """ -function UnfoldSim.generate_events(t::GroundTruthDesign) +function UnfoldSim.generate_events(t::EffectsDesign) effects_dict = Dict{Any,Any}(t.effects_dict) - current_design = generate_events(t.des) - to_be_added = setdiff(names(current_design), keys(effects_dict)) + #effects_dict = t.effects_dict + current_design = generate_events(t.design) + to_be_added = setdiff(names(current_design), string.(keys(effects_dict))) for tba in to_be_added effects_dict[tba] = typical_value(current_design[:, tba]) end - return expand_grid(effects_grid) + return expand_grid(effects_dict) end \ No newline at end of file diff --git a/test/design.jl b/test/design.jl index 02efd8dc..aaafd7b3 100644 --- a/test/design.jl +++ b/test/design.jl @@ -162,4 +162,39 @@ @test length(unique(i1.cond)) == 1 end + @testset "Effects Design" begin + # Begin with simulation design + design = SingleSubjectDesign(; + conditions = Dict( + :condition => ["car", "face"], + :continuous => range(0, 5, length = 10), + ), + ) + + # Effects dictionary + effects_dict_1 = Dict(:condition => ["car", "face"]) + effects_dict_2 = Dict(:condition => ["car", "face"], :continuous => [2, 3, 4]) + + # Generate effects design + ef_design_1 = UnfoldSim.EffectsDesign(design, effects_dict_1) + ef_design_2 = UnfoldSim.EffectsDesign(design, effects_dict_2) + + # Generate events + ef_events_1 = generate_events(ef_design_1) + ef_events_2 = generate_events(ef_design_2) + + # SingleSubject tests + @test size(ef_events_1, 1) == 2 # Test correct length of events df + @test ef_events_1[1].contiunous == mean(range(0, 5, length = 10)) # Test that average is calculated correctly + @test size(ef_events_2, 1) == 6 # Test correct length of events df when continuous variable is marginalizes + + # MultiSubjectDesign -> not implemented yet, so should error + design = MultiSubjectDesign( + n_subjects = 20, + n_items = 8, + items_between = Dict(:condition => ["car", "face"], :continuous => [1, 2]), + ) + @test_throws ErrorException UnfoldSim.EffectsDesign(design, effects_dict_1) + + end end \ No newline at end of file From ba10e1d1de3cd804b57c22aed79c39c4d0e56de5 Mon Sep 17 00:00:00 2001 From: ReneSkukies Date: Mon, 28 Oct 2024 10:28:27 +0100 Subject: [PATCH 053/156] fix size(EffectsDesign) bug --- src/design.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/design.jl b/src/design.jl index 1eea7261..8505a6fe 100644 --- a/src/design.jl +++ b/src/design.jl @@ -208,7 +208,7 @@ struct EffectsDesign <: AbstractDesign effects_dict::Dict end EffectsDesign(design::MultiSubjectDesign,effects_dict::Dict) = error("not yet implemented") -UnfoldSim.size(t::EffectsDesign) = size(generate_events(t)) +UnfoldSim.size(t::EffectsDesign) = size(generate_events(t),1) """ expand_grid(design) From f5facfe1c49ec9b59e049a2c00a4b24ded570ec3 Mon Sep 17 00:00:00 2001 From: ReneSkukies Date: Mon, 28 Oct 2024 11:08:55 +0100 Subject: [PATCH 054/156] fix bug in EffectsDesign test --- test/design.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/design.jl b/test/design.jl index aaafd7b3..0ea9574b 100644 --- a/test/design.jl +++ b/test/design.jl @@ -185,7 +185,7 @@ # SingleSubject tests @test size(ef_events_1, 1) == 2 # Test correct length of events df - @test ef_events_1[1].contiunous == mean(range(0, 5, length = 10)) # Test that average is calculated correctly + @test unique(ef_events_1[!,:continuous])[1] ≈ mean(range(0, 5, length = 10)) # Test that average is calculated correctly and only one value is present in df @test size(ef_events_2, 1) == 6 # Test correct length of events df when continuous variable is marginalizes # MultiSubjectDesign -> not implemented yet, so should error From 7a0fffc9bba1ebb034dd4e56bb549384fa0f9b76 Mon Sep 17 00:00:00 2001 From: ReneSkukies Date: Tue, 29 Oct 2024 13:58:01 +0100 Subject: [PATCH 055/156] Add tutorial --- docs/literate/HowTo/getGroundTruth.jl | 85 +++++++++++++++++++++++++++ docs/make.jl | 1 + 2 files changed, 86 insertions(+) create mode 100644 docs/literate/HowTo/getGroundTruth.jl diff --git a/docs/literate/HowTo/getGroundTruth.jl b/docs/literate/HowTo/getGroundTruth.jl new file mode 100644 index 00000000..cc22f68f --- /dev/null +++ b/docs/literate/HowTo/getGroundTruth.jl @@ -0,0 +1,85 @@ +# # Get ground truth via EffectsDesign + +# Usually, to test a method, you want to compare your results to a known ground truth. In UnfoldSim you can obtain your ground truth via the `EffectsDesign`. +# Doing it this way let's you marginalize any effects/ variables of your original design. You can find more on what marginalized effects are here in the [Unfold.jl documentation](https://unfoldtoolbox.github.io/Unfold.jl/dev/generated/HowTo/effects/) + +# ## Setup +using UnfoldSim +using Unfold +using CairoMakie +using Random + +# ## Simulation +# First let's make up a SingleSubject simulation + +# !!! note +# Getting a ground truth for a MultiSubjectDesign is not implemented yet + +design = + SingleSubjectDesign(; + conditions = Dict( + :condition => ["bike", "face"], + :continuous => range(0, 5, length = 10), + ), + ) |> x -> RepeatDesign(x, 100); + +# **n170** has a condition effect, faces are more negative than bikes +n1 = LinearModelComponent(; + basis = n170(), + formula = @formula(0 ~ 1 + condition), + β = [5, 3], +); +# **p300** has a continuous effect, higher continuous values will result in larger P300's. +# We include both a linear and a quadratic effect of the continuous variable. +p3 = LinearModelComponent(; + basis = p300(), + formula = @formula(0 ~ 1 + continuous + continuous^2), + β = [5, 1, 0.2], +); + +components = [n1, p3] +data, evts = simulate( + MersenneTwister(1), + design, + components, + UniformOnset(; width = 0, offset = 1000), + PinkNoise(), +); + +# ## GroundTruthDesign +# To marginalize effects we first have to specify an effects dictionary and subsequently hand this dict plus the original design to `EffectsDesign()` + +effects_dict = Dict(:condition => ["bike", "face"]) + +effects_design = EffectsDesign(design, effects_dict) + +# !!! note +# We only specified the condition levels here, by default every unspecified variable will be set to a "typical" (i.e. the mean) value. + +# And finally we can simulate our ground truth ERP with marginalized effects + +gt_data, gt_events = simulate( + MersenneTwister(1), + effects_design, + components, + UniformOnset(; width = 0, offset = 1000), + NoNoise(), + return_epoched = true +); + +# ## Compare with Unfold.jl results + +m = fit( + UnfoldModel, + Dict( + Any => ( + @formula(0 ~ 1 + condition + spl(continuous, 4)), + firbasis(τ = [-0.1, 1], sfreq = 100, name = "basis"), + ), + ), + evts, + data, +); + +eff = effects(effects_dict, m) + diff --git a/docs/make.jl b/docs/make.jl index c800551b..3f4dc489 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -51,6 +51,7 @@ makedocs(; "Define a new component (with variable duration and shift)" => "./generated/HowTo/newComponent.md", "Generate multi channel data" => "./generated/HowTo/multichannel.md", "Use existing experimental designs & onsets in the simulation" => "./generated/HowTo/predefinedData.md", + "Get ground truth via EffectsDesign" => "./generated/HowTo/getGroundTruth.md", ], "API / Docstrings" => "api.md", ], From 0e7df888f4b2d7094cea06acca240e3306928e64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Skukies?= <57703446+ReneSkukies@users.noreply.github.com> Date: Tue, 29 Oct 2024 14:37:13 +0100 Subject: [PATCH 056/156] Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/literate/HowTo/getGroundTruth.jl | 3 +-- src/design.jl | 4 ++-- test/design.jl | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/literate/HowTo/getGroundTruth.jl b/docs/literate/HowTo/getGroundTruth.jl index cc22f68f..4738aed3 100644 --- a/docs/literate/HowTo/getGroundTruth.jl +++ b/docs/literate/HowTo/getGroundTruth.jl @@ -64,7 +64,7 @@ gt_data, gt_events = simulate( components, UniformOnset(; width = 0, offset = 1000), NoNoise(), - return_epoched = true + return_epoched = true, ); # ## Compare with Unfold.jl results @@ -82,4 +82,3 @@ m = fit( ); eff = effects(effects_dict, m) - diff --git a/src/design.jl b/src/design.jl index 8505a6fe..22a957e7 100644 --- a/src/design.jl +++ b/src/design.jl @@ -207,8 +207,8 @@ struct EffectsDesign <: AbstractDesign design::AbstractDesign effects_dict::Dict end -EffectsDesign(design::MultiSubjectDesign,effects_dict::Dict) = error("not yet implemented") -UnfoldSim.size(t::EffectsDesign) = size(generate_events(t),1) +EffectsDesign(design::MultiSubjectDesign, effects_dict::Dict) = error("not yet implemented") +UnfoldSim.size(t::EffectsDesign) = size(generate_events(t), 1) """ expand_grid(design) diff --git a/test/design.jl b/test/design.jl index 0ea9574b..88f3629e 100644 --- a/test/design.jl +++ b/test/design.jl @@ -185,7 +185,7 @@ # SingleSubject tests @test size(ef_events_1, 1) == 2 # Test correct length of events df - @test unique(ef_events_1[!,:continuous])[1] ≈ mean(range(0, 5, length = 10)) # Test that average is calculated correctly and only one value is present in df + @test unique(ef_events_1[!, :continuous])[1] ≈ mean(range(0, 5, length = 10)) # Test that average is calculated correctly and only one value is present in df @test size(ef_events_2, 1) == 6 # Test correct length of events df when continuous variable is marginalizes # MultiSubjectDesign -> not implemented yet, so should error From a10d5a9573209309982a0a759a135a606dfc6afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Skukies?= <57703446+ReneSkukies@users.noreply.github.com> Date: Tue, 29 Oct 2024 15:04:26 +0100 Subject: [PATCH 057/156] Update getGroundTruth.jl tutorial --- docs/literate/HowTo/getGroundTruth.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/literate/HowTo/getGroundTruth.jl b/docs/literate/HowTo/getGroundTruth.jl index 4738aed3..fd2b31d8 100644 --- a/docs/literate/HowTo/getGroundTruth.jl +++ b/docs/literate/HowTo/getGroundTruth.jl @@ -66,6 +66,7 @@ gt_data, gt_events = simulate( NoNoise(), return_epoched = true, ); +@show gt_events # ## Compare with Unfold.jl results @@ -81,4 +82,4 @@ m = fit( data, ); -eff = effects(effects_dict, m) +eff = effects(effects_dict, m); From 523f90b70ca3cc373897ff9d9c74add0da486669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Skukies?= <57703446+ReneSkukies@users.noreply.github.com> Date: Thu, 7 Nov 2024 14:44:01 +0100 Subject: [PATCH 058/156] Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src/design.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/design.jl b/src/design.jl index b7c9f00d..4bc2abb7 100644 --- a/src/design.jl +++ b/src/design.jl @@ -414,7 +414,6 @@ function UnfoldSim.generate_events(t::EffectsDesign) end - #Base.size(design::SequenceDesign) = #size(design.design) .* length(replace(design.sequence, "_" => "",r"\{.*\}"=>"")) @@ -426,4 +425,3 @@ end Base.size( design::Union{<:SequenceDesign,<:SubselectDesign,<:RepeatDesign{<:SequenceDesign}}, ) = size(generate_events(design), 1) - From 5fb95bf83bb792605c809016a7258790e7d3dc6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Skukies?= <57703446+ReneSkukies@users.noreply.github.com> Date: Thu, 7 Nov 2024 14:59:24 +0100 Subject: [PATCH 059/156] Update design.jl with empty line in the end --- test/design.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/design.jl b/test/design.jl index e60b6222..b118ff9f 100644 --- a/test/design.jl +++ b/test/design.jl @@ -223,4 +223,4 @@ @test_throws ErrorException UnfoldSim.EffectsDesign(design, effects_dict_1) end -end \ No newline at end of file +end From 5574425a836ce78545531e726231c7fd95f538c5 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Thu, 28 Nov 2024 13:49:24 +0100 Subject: [PATCH 060/156] fix sequence design rng --- src/simulation.jl | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/simulation.jl b/src/simulation.jl index c290b3db..83401106 100755 --- a/src/simulation.jl +++ b/src/simulation.jl @@ -37,8 +37,7 @@ Some remarks to how the noise is added: - If `return_epoched = true` and `onset =NoOnset()` the noise is added to the epoched data matrix - If `onset` is not `NoOnset`, a continuous signal is created and the noise is added to this i.e. this means that the noise won't be the same as in the `onset = NoOnset()` case even if `return_epoched = true`. - The case `return_epoched = false` and `onset = NoOnset()` is not possible and therefore covered by an assert statement - - `simulate(rng,design::SequenceDesign,...)` - If no `design.rng` was defined for `SequenceDesign`, we replace it with the `simulation`-function call `rng` object + """ @@ -52,26 +51,10 @@ function simulate( kwargs..., ) - if is_SequenceDesign(design) - design = sequencedesign_add_rng(rng, design) - end simulate(rng, Simulation(design, signal, onset, noise); kwargs...) end -sequencedesign_add_rng(rng, design::AbstractDesign) = design -sequencedesign_add_rng(rng, design::RepeatDesign) = - RepeatDesign(sequencedesign_add_rng(rng, design.design), design.repeat) -sequencedesign_add_rng(rng, design::SequenceDesign) = - isnothing(design.rng) ? - SequenceDesign(design.design, design.sequence, design.sequencelength, rng) : design - - -is_SequenceDesign(d::AbstractDesign) = false -is_SequenceDesign(d::RepeatDesign) = is_SequenceDesign(d.design) -is_SequenceDesign(d::SequenceDesign) = true - - function simulate(rng::AbstractRNG, simulation::Simulation; return_epoched::Bool = false) (; design, components, onset, noisetype) = simulation From 1966fbe807f0657c33547ce314f5bffe7fdd09fc Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Thu, 28 Nov 2024 13:57:32 +0100 Subject: [PATCH 061/156] fix sequence test --- test/sequence.jl | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/test/sequence.jl b/test/sequence.jl index 342033f9..c60010e1 100644 --- a/test/sequence.jl +++ b/test/sequence.jl @@ -15,13 +15,13 @@ end design = SingleSubjectDesign(conditions = Dict(:condition => ["one", "two"])) - design = SequenceDesign(design, "SCR_", StableRNG(1)) - evt = generate_events(design) + design = SequenceDesign(design, "SCR_") + evt = generate_events(StableRNG(1),design) @test size(evt, 1) == 6 @test evt.event == ['S', 'C', 'R', 'S', 'C', 'R'] design = RepeatDesign(design, 2) - evt = generate_events(design) + evt = generate_events(StableRNG(1),design) @test size(evt, 1) == 12 @test evt.event == ['S', 'C', 'R', 'S', 'C', 'R', 'S', 'C', 'R', 'S', 'C', 'R'] @@ -29,16 +29,29 @@ end # repeat first, then sequence => same sequence design = SingleSubjectDesign(conditions = Dict(:condition => ["A", "B"])) design = RepeatDesign(design, 2) - design = SequenceDesign(design, "S[ABCD]", StableRNG(2)) - evt = generate_events(design) + design = SequenceDesign(design, "S[ABCD]") + evt = generate_events(StableRNG(2),design) @test all(evt.event[2:2:end] .== 'B') # sequence first, then repeat => different sequence for each repetition design = SingleSubjectDesign(conditions = Dict(:condition => ["A", "B"])) - design = SequenceDesign(design, "S[ABCD]", StableRNG(2)) + design = SequenceDesign(design, "S[ABCD]") design = RepeatDesign(design, 2) - evt = generate_events(design) + evt = generate_events(StableRNG(2),design) @test !all(evt.event[2:2:end] .== 'B') end + +@testset "simulate_sequence" +design = SingleSubjectDesign(conditions = Dict(:condition => ["one", "two"])) +design = SequenceDesign(design, "SCR_") +c = LinearModelComponent(; + basis=UnfoldSim.hanning(40), + formula=@formula(0~1+condition), + β = [1.,2.], + contrasts=Dict(:cond=>EffectsCoding()) +) +s,e = simulate(design,c,NoOnset();return_epoched=true) +@test size(s) == (40,6) +end \ No newline at end of file From fa6f78b46924bf8691e97310b8a69d354865bdf5 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Thu, 28 Nov 2024 13:58:34 +0100 Subject: [PATCH 062/156] fix rng docs --- docs/literate/HowTo/sequence.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/literate/HowTo/sequence.jl b/docs/literate/HowTo/sequence.jl index 3f280f26..323f293c 100644 --- a/docs/literate/HowTo/sequence.jl +++ b/docs/literate/HowTo/sequence.jl @@ -11,15 +11,15 @@ using StableRNGs design = SingleSubjectDesign(conditions = Dict(:condition => ["one", "two"])) generate_events(design) # Next we use the `SequenceDesign` and nest our initial design in it. "`SR_`" is code for an "`S`" event and an "`R`" event - only single letter events are supported! The "`_`" is a signal for the Onset generator to generate a bigger pause - no overlap between adjacend "`SR`" pairs -design = SequenceDesign(design, "SR_", StableRNG(1)) -generate_events(design) +design = SequenceDesign(design, "SR_") +generate_events(StableRNG(1), design) # The main thing that happened is that the design was repeated for every event (each 'letter') of the sequence, and an `eventtype` column was added. # !!! hint # more advaned sequences are possible as well, like "SR{1,3}", or "A[BC]". Infinite sequences are **not** possible like "AB*". # Finally, let's repeat the current design 4 times design = RepeatDesign(design, 4) -generate_events(design) +generate_events(StableRNG(1), design) # This results in 16 trials that nicely follow our sequence From b9cde99d6ef1b064c060dff455c51ee6dff7d007 Mon Sep 17 00:00:00 2001 From: ReneSkukies Date: Fri, 29 Nov 2024 10:57:51 +0100 Subject: [PATCH 063/156] finish gt tutorial --- docs/literate/HowTo/getGroundTruth.jl | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/docs/literate/HowTo/getGroundTruth.jl b/docs/literate/HowTo/getGroundTruth.jl index fd2b31d8..7ed17664 100644 --- a/docs/literate/HowTo/getGroundTruth.jl +++ b/docs/literate/HowTo/getGroundTruth.jl @@ -68,18 +68,33 @@ gt_data, gt_events = simulate( ); @show gt_events +# Additionally, we can get the simulated effects into a tidy dataframe using Unfold's `result_to_table`. +# Note that the data has to be reshaped into a channel X times X predictor form. (In our one channel example `size(gt_data) = (45,2)`, missing the channel dimension) + +g = reshape(gt_data,1,size(gt_data)...) +times = range(1, 45); +gt_effects = Unfold.result_to_table([g], [gt_events], [times], ["effects"]) +first(gt_effects, 5) + + # ## Compare with Unfold.jl results m = fit( UnfoldModel, - Dict( + [ Any => ( @formula(0 ~ 1 + condition + spl(continuous, 4)), firbasis(τ = [-0.1, 1], sfreq = 100, name = "basis"), ), - ), + ], evts, data, ); -eff = effects(effects_dict, m); +ef = effects(effects_dict, m); + +# Display ground truth and effects, note that the ground truth will be shorter because of the missing baseline. +# If you want to actually compare results with the ground truth, you could either us `UnfoldSim.pad_array()` or forgo the baseline of your estimates. +lines(ef.yhat) +lines!(gt_effects.yhat) +current_figure() \ No newline at end of file From 60b8941ee03eac0c5ab11656fc1e93ca210b45c8 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Fri, 13 Dec 2024 16:37:32 +0100 Subject: [PATCH 064/156] fix #124, explicitly cast the dict --- src/types.jl | 8 ++++++++ test/simulation.jl | 26 ++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/types.jl b/src/types.jl index 7154340c..0a266eec 100644 --- a/src/types.jl +++ b/src/types.jl @@ -20,3 +20,11 @@ struct Simulation onset::AbstractOnset noisetype::AbstractNoise end + + +Simulation( + design::AbstractDesign, + components::Dict{<:Char,<:Vector}, + onset::AbstractOnset, + noisetype::AbstractNoise, +) = Simulation(design, Dict{Char,Vector{<:AbstractComponent}}(components), onset, noisetype) diff --git a/test/simulation.jl b/test/simulation.jl index 7316e776..d7f6ac1f 100644 --- a/test/simulation.jl +++ b/test/simulation.jl @@ -1,3 +1,4 @@ +using Base: AbstractCartesianIndex @testset "simulation" begin @testset "general_test_simulate" begin @@ -215,4 +216,29 @@ end end + @testset "multi-component sequence #124" begin + struct MyLinearModelComponent1 <: AbstractComponent + comp::Any + end + MyLinearModelComponent1(b, f, β) = + MyLinearModelComponent1(LinearModelComponent(; basis = b, formula = f, β)) + UnfoldSim.simulate_component(rng, c::MyLinearModelComponent1, design) = + simulate_component(rng, c.comp, design) + Simulation( + SingleSubjectDesign(), + Dict( + 'A' => [ + LinearModelComponent( + basis = p100(), + formula = @formula(0 ~ 1), + β = [0], + ), + ], + 'B' => [MyLinearModelComponent1(p100(), @formula(0 ~ 1), [0])], + ), + NoOnset(), + NoNoise(), + ) + end + end From 01a1e443e020b74a3a38c0af209539d90d3a626b Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Fri, 13 Dec 2024 16:59:27 +0100 Subject: [PATCH 065/156] not sure where this error came in... --- test/bases.jl | 7 +++++++ test/sequence.jl | 32 ++++++++++++++++---------------- 2 files changed, 23 insertions(+), 16 deletions(-) create mode 100644 test/bases.jl diff --git a/test/bases.jl b/test/bases.jl new file mode 100644 index 00000000..1ac947a2 --- /dev/null +++ b/test/bases.jl @@ -0,0 +1,7 @@ +using UnfoldSim +@testset "hanning" begin + @test UnfoldSim.hanning(0.021, 0.04, 1000)[40] == 1.0 + @test UnfoldSim.hanning(0.011, 0.04, 1000)[40] == 1.0 + @test UnfoldSim.hanning(0.011, 0.02, 1000)[20] == 1.0 + @test_throws Exception UnfoldSim.hanning(0.011, 0.0, 1000) +end \ No newline at end of file diff --git a/test/sequence.jl b/test/sequence.jl index c60010e1..98aa6a0c 100644 --- a/test/sequence.jl +++ b/test/sequence.jl @@ -16,12 +16,12 @@ end design = SingleSubjectDesign(conditions = Dict(:condition => ["one", "two"])) design = SequenceDesign(design, "SCR_") - evt = generate_events(StableRNG(1),design) + evt = generate_events(StableRNG(1), design) @test size(evt, 1) == 6 @test evt.event == ['S', 'C', 'R', 'S', 'C', 'R'] design = RepeatDesign(design, 2) - evt = generate_events(StableRNG(1),design) + evt = generate_events(StableRNG(1), design) @test size(evt, 1) == 12 @test evt.event == ['S', 'C', 'R', 'S', 'C', 'R', 'S', 'C', 'R', 'S', 'C', 'R'] @@ -30,7 +30,7 @@ end design = SingleSubjectDesign(conditions = Dict(:condition => ["A", "B"])) design = RepeatDesign(design, 2) design = SequenceDesign(design, "S[ABCD]") - evt = generate_events(StableRNG(2),design) + evt = generate_events(StableRNG(2), design) @test all(evt.event[2:2:end] .== 'B') @@ -39,19 +39,19 @@ end design = SingleSubjectDesign(conditions = Dict(:condition => ["A", "B"])) design = SequenceDesign(design, "S[ABCD]") design = RepeatDesign(design, 2) - evt = generate_events(StableRNG(2),design) + evt = generate_events(StableRNG(2), design) @test !all(evt.event[2:2:end] .== 'B') end -@testset "simulate_sequence" -design = SingleSubjectDesign(conditions = Dict(:condition => ["one", "two"])) -design = SequenceDesign(design, "SCR_") -c = LinearModelComponent(; - basis=UnfoldSim.hanning(40), - formula=@formula(0~1+condition), - β = [1.,2.], - contrasts=Dict(:cond=>EffectsCoding()) -) -s,e = simulate(design,c,NoOnset();return_epoched=true) -@test size(s) == (40,6) -end \ No newline at end of file +@testset "simulate_sequence" begin + design = SingleSubjectDesign(conditions = Dict(:condition => ["one", "two"])) + design = SequenceDesign(design, "SCR_") + c = LinearModelComponent(; + basis = UnfoldSim.hanning(40), + formula = @formula(0 ~ 1 + condition), + β = [1.0, 2.0], + contrasts = Dict(:cond => EffectsCoding()), + ) + s, e = simulate(design, c, NoOnset(); return_epoched = true) + @test size(s) == (40, 6) +end From 33bc2b666e13dc01ed5cc7a89643f69b73cb428a Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Fri, 13 Dec 2024 17:05:40 +0100 Subject: [PATCH 066/156] hanning upgrade --- src/bases.jl | 15 ++++++++++----- test/bases.jl | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/bases.jl b/src/bases.jl index 2dbeffc9..fa8770b5 100644 --- a/src/bases.jl +++ b/src/bases.jl @@ -28,13 +28,18 @@ n400(; sfreq = 100) = -hanning(0.4, 0.4, sfreq) """ generate a hanning window -duration: in s -offset: in s, defines hanning peak +width: in s +offset: in s, defines hanning peak, must be > round(width/2) sfreq: sampling rate in Hz """ -function DSP.hanning(duration, offset, sfreq) - signal = hanning(Int(round(duration * sfreq))) - return pad_array(signal, -Int(round(offset * sfreq / 2)), 0) +function DSP.hanning(width, offset, sfreq) + width = width * sfreq + offset = offset * sfreq + signal = hanning(Int(round(width))) + pad_by = Int(round(offset - length(signal) / 2)) + + pad_by < 0 ? error("offset has to be > round(width/2)") : "" + return pad_array(signal, -pad_by, 0) end ## pupil diff --git a/test/bases.jl b/test/bases.jl index 1ac947a2..ea153c1a 100644 --- a/test/bases.jl +++ b/test/bases.jl @@ -1,6 +1,6 @@ using UnfoldSim @testset "hanning" begin - @test UnfoldSim.hanning(0.021, 0.04, 1000)[40] == 1.0 + @test UnfoldSim.hanning(0.021, 0.04, 1000)[41] == 1.0 # why 41 not 40? beacuse round(0.5) = 0 and round(1.5) = 2 -- and we are living on the edge! @test UnfoldSim.hanning(0.011, 0.04, 1000)[40] == 1.0 @test UnfoldSim.hanning(0.011, 0.02, 1000)[20] == 1.0 @test_throws Exception UnfoldSim.hanning(0.011, 0.0, 1000) From 5d383114e59fa747ebb5d6ca6f6c0fc0958a8507 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Fri, 13 Dec 2024 17:08:37 +0100 Subject: [PATCH 067/156] checking stuff --- test/bases.jl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/bases.jl b/test/bases.jl index ea153c1a..6b0ae558 100644 --- a/test/bases.jl +++ b/test/bases.jl @@ -4,4 +4,12 @@ using UnfoldSim @test UnfoldSim.hanning(0.011, 0.04, 1000)[40] == 1.0 @test UnfoldSim.hanning(0.011, 0.02, 1000)[20] == 1.0 @test_throws Exception UnfoldSim.hanning(0.011, 0.0, 1000) +end + +@testset "p100,N170,p300,n400" begin + sfreq = 1000 + @test argmax(p100(; sfreq)) == 0.1 * sfreq + @test argmin(n170(; sfreq)) == 0.17 * sfreq + @test argmax(p300(; sfreq)) == 0.3 * sfreq + @test argmin(n400(; sfreq)) == 0.4 * sfreq end \ No newline at end of file From 25009e493e1077aca5ad0607dcf86ec7c83ae87c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Skukies?= <57703446+ReneSkukies@users.noreply.github.com> Date: Fri, 17 Jan 2025 11:22:27 +0100 Subject: [PATCH 068/156] Apply suggestions from code review Co-authored-by: Benedikt Ehinger --- docs/make.jl | 2 +- src/design.jl | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index ef696365..645b4871 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -53,7 +53,7 @@ makedocs(; "Define a new component (with variable duration and shift)" => "./generated/HowTo/newComponent.md", "Generate multi channel data" => "./generated/HowTo/multichannel.md", "Use existing experimental designs & onsets in the simulation" => "./generated/HowTo/predefinedData.md", - "Get ground truth via EffectsDesign" => "./generated/HowTo/getGroundTruth.md", + "Simulated marginal effects" => "./generated/HowTo/getGroundTruth.md", "Produce specific sequences of events" => "./generated/HowTo/sequence.md", ], "API / Docstrings" => "api.md", diff --git a/src/design.jl b/src/design.jl index 4bc2abb7..29048c3b 100644 --- a/src/design.jl +++ b/src/design.jl @@ -365,7 +365,7 @@ Design to obtain ground truth simulation. ## Fields - `design::AbstractDesign` The design of your (main) simulation. -- `èffects_dict::Dict` +- `effects_dict::Dict` Effects.jl style dictionary specifying variable effects. See also [Unfold.jl marginalized effects](https://unfoldtoolbox.github.io/Unfold.jl/stable/generated/HowTo/effects/) """ struct EffectsDesign <: AbstractDesign @@ -378,7 +378,7 @@ UnfoldSim.size(t::EffectsDesign) = size(generate_events(t), 1) """ expand_grid(design) -Used to expand effects grid. Copied from Effects.jl +calculate all possible combinations of the key/value pairs of the design-dict. Copied from Effects.jl """ function expand_grid(design) colnames = tuple(Symbol.(keys(design))...) @@ -393,7 +393,7 @@ typical_value(v) = unique(v) """ UnfoldSim.generate_events(design::EffectsDesign) -Generates events to simulate ground truth data using an effects dictionary. Every covariate that is in the `GroundTruthDesign` but not in the `effects_dict` will be set to a `typical_value` (i.e. the mean) +Generates events to simulate marginalized effects using an Effects.jl reference-grid dictionary. Every covariate that is in the `EffectsDesign` but not in the `effects_dict` will be set to a `typical_value` (i.e. the mean) ```julia # Example From 7996783a3a0b837b1771a56ca5e7a9f1cfb5d02d Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Thu, 27 Feb 2025 14:31:18 +0100 Subject: [PATCH 069/156] rename offset/basis -> get_offset/get_basis; fix test ommitted begin --- src/component.jl | 49 +++++++++++++++++++++++++---------------------- src/simulation.jl | 5 ++++- test/sequence.jl | 30 ++++++++++++++--------------- 3 files changed, 45 insertions(+), 39 deletions(-) diff --git a/src/component.jl b/src/component.jl index 403d0528..94b37355 100644 --- a/src/component.jl +++ b/src/component.jl @@ -94,18 +94,20 @@ end LinearModelComponent(basis, formula, β, contrasts) = LinearModelComponent(basis, formula, β, contrasts, 0) """ - offset(AbstractComponent) + get_offset(AbstractComponent) Should the `basis` be shifted? Returns c.offset for most components, if not implemented for a type, returns 0. Can be positive or negative, but has to be Integer """ -offset(c::AbstractComponent)::Int = 0 -offset(c::LinearModelComponent)::Int = c.offset -offset(c::MixedModelComponent)::Int = c.offset +get_offset(c::AbstractComponent)::Int = 0 +get_offset(c::LinearModelComponent)::Int = c.offset +get_offset(c::MixedModelComponent)::Int = c.offset -maxoffset(c::Vector{<:AbstractComponent}) = maximum(offset.(c)) -maxoffset(d::Dict{<:Char,<:Vector{<:AbstractComponent}}) = maximum(maxoffset.(values(d))) -minoffset(c::Vector{<:AbstractComponent}) = minimum(offset.(c)) -minoffset(d::Dict{<:Char,<:Vector{<:AbstractComponent}}) = minimum(minoffset.(values(d))) +maxoffset(c::Vector{<:AbstractComponent}) = maximum(get_offset.(c)) +maxoffset(d::Dict{<:Char,<:Vector{<:AbstractComponent}}) = + maximum(maxget_offset.(values(d))) +minoffset(c::Vector{<:AbstractComponent}) = minimum(get_offset.(c)) +minoffset(d::Dict{<:Char,<:Vector{<:AbstractComponent}}) = + minimum(minget_offset.(values(d))) @@ -209,21 +211,21 @@ end """ - basis(c::AbstractComponent) + get_basis(c::AbstractComponent) returns the basis of the component (typically `c.basis`) """ -basis(c::AbstractComponent) = c.basis +get_basis(c::AbstractComponent) = c.basis """ - basis(c::AbstractComponent,design) + get_basis(c::AbstractComponent,design) evaluates the basis, if basis is a vector, directly returns it. if basis is a tuple `(f::Function,maxlength::Int)`, evaluates the function with input `design`. Cuts the resulting vector or Matrix at `maxlength` """ -basis(c::AbstractComponent, design) = basis(basis(c), design) -basis(b::AbstractVector, design) = b +get_basis(c::AbstractComponent, design) = get_basis(get_basis(c), design) +get_basis(b::AbstractVector, design) = b -function basis(basis::Tuple{Function,Int}, design) +function get_basis(basis::Tuple{Function,Int}, design) f = basis[1] maxlength = basis[2] basis_out = f(design) @@ -255,7 +257,8 @@ end limit_basis(b::AbstractVector{<:Number}, maxlength) = b[1:min(length(b), maxlength)] limit_basis(b::AbstractMatrix, maxlength) = b[1:min(length(b), maxlength), :] -Base.length(c::AbstractComponent) = isa(basis(c), Tuple) ? basis(c)[2] : length(basis(c)) +Base.length(c::AbstractComponent) = + isa(get_basis(c), Tuple) ? get_basis(c)[2] : length(get_basis(c)) @@ -282,7 +285,7 @@ simulate_component(rng, c::AbstractComponent, simulation::Simulation) = Generate a linear model design matrix, weight it by the coefficients `c.β` and multiply the result with the given basis vector. # Returns -- `Matrix{Float64}`: Simulated component for each event in the events data frame. The output dimensions are `length(basis(basis)) x length(design)`. +- `Matrix{Float64}`: Simulated component for each event in the events data frame. The output dimensions are `length(get_basis(c.basis)) x length(design)`. # Examples ```julia-repl @@ -305,7 +308,7 @@ function simulate_component(rng, c::LinearModelComponent, design::AbstractDesign X = generate_designmatrix(c.formula, events, c.contrasts) y = X * c.β - return y' .* basis(c, design) + return y' .* get_basis(c, design) end @@ -338,7 +341,7 @@ Generate a MixedModel and simulate data according to the given parameters `c.β` - `return_parameters::Bool = false`: Can be used to return the per-event parameters used to weight the basis function. Sometimes useful to inspect what is simulated. # Returns -- `Matrix{Float64}`: Simulated component for each event in the events data frame. The output dimensions are `length(basis(basis)) x length(design)`. +- `Matrix{Float64}`: Simulated component for each event in the events data frame. The output dimensions are `length(get_basis(basis)) x length(design)`. # Notes 1) MixedModels/Sim does not allow simulation of data without white noise of the residuals. Because we want our own noise, we use the following trick to remove the MixedModels-Noise: @@ -413,9 +416,9 @@ function simulate_component( rethrow(e) end - @debug size(basis(c, design)) + @debug size(get_basis(c, design)) # in case the parameters are of interest, we will return those, not them weighted by basis - b = return_parameters ? [1.0] : basis(c, design) + b = return_parameters ? [1.0] : get_basis(c, design) @debug :b, typeof(b), size(b), :m, size(m.y') if isa(b, AbstractMatrix) epoch_data_component = ((m.y' .* b)) @@ -457,7 +460,7 @@ end Return the projection of a `MultichannelComponent c` from "source" to "sensor" space. # Returns -- `Array{Float64,3}`: Projected simulated component for each event in the events data frame. The output dimensions are `length(c.projection) x length(basis(c)) x length(design)`. +- `Array{Float64,3}`: Projected simulated component for each event in the events data frame. The output dimensions are `length(c.projection) x length(get_basis(c)) x length(design)`. # Examples ```julia-repl @@ -676,7 +679,7 @@ function simulate_and_add!( ) @debug "matrix" - off = offset(component) - minoffset(simulation.components) + off = get_offset(component) - minoffset(simulation.components) @views epoch_data[1+off:length(component)+off, :] .+= @@ -689,7 +692,7 @@ function simulate_and_add!( rng, ) @debug "3D Array" - off = offset(component) - minoffset(simulation.components) + off = get_offset(component) - minoffset(simulation.components) @views epoch_data[:, 1+off:length(component)+off, :] .+= simulate_component(rng, component, simulation) end diff --git a/src/simulation.jl b/src/simulation.jl index 322baa0c..c7718b98 100755 --- a/src/simulation.jl +++ b/src/simulation.jl @@ -128,7 +128,10 @@ function simulate( onset::AbstractOnset, noise::AbstractNoise = NoNoise(); kwargs..., -) = simulate(rng, Simulation(design, components, onset, noise); kwargs...) +) + simulate(rng, Simulation(design, components, onset, noise); kwargs...) +end + function simulate(rng::AbstractRNG, simulation::Simulation; return_epoched::Bool = false) diff --git a/test/sequence.jl b/test/sequence.jl index c60010e1..0870149f 100644 --- a/test/sequence.jl +++ b/test/sequence.jl @@ -16,12 +16,12 @@ end design = SingleSubjectDesign(conditions = Dict(:condition => ["one", "two"])) design = SequenceDesign(design, "SCR_") - evt = generate_events(StableRNG(1),design) + evt = generate_events(StableRNG(1), design) @test size(evt, 1) == 6 @test evt.event == ['S', 'C', 'R', 'S', 'C', 'R'] design = RepeatDesign(design, 2) - evt = generate_events(StableRNG(1),design) + evt = generate_events(StableRNG(1), design) @test size(evt, 1) == 12 @test evt.event == ['S', 'C', 'R', 'S', 'C', 'R', 'S', 'C', 'R', 'S', 'C', 'R'] @@ -30,7 +30,7 @@ end design = SingleSubjectDesign(conditions = Dict(:condition => ["A", "B"])) design = RepeatDesign(design, 2) design = SequenceDesign(design, "S[ABCD]") - evt = generate_events(StableRNG(2),design) + evt = generate_events(StableRNG(2), design) @test all(evt.event[2:2:end] .== 'B') @@ -39,19 +39,19 @@ end design = SingleSubjectDesign(conditions = Dict(:condition => ["A", "B"])) design = SequenceDesign(design, "S[ABCD]") design = RepeatDesign(design, 2) - evt = generate_events(StableRNG(2),design) + evt = generate_events(StableRNG(2), design) @test !all(evt.event[2:2:end] .== 'B') end -@testset "simulate_sequence" -design = SingleSubjectDesign(conditions = Dict(:condition => ["one", "two"])) -design = SequenceDesign(design, "SCR_") -c = LinearModelComponent(; - basis=UnfoldSim.hanning(40), - formula=@formula(0~1+condition), - β = [1.,2.], - contrasts=Dict(:cond=>EffectsCoding()) -) -s,e = simulate(design,c,NoOnset();return_epoched=true) -@test size(s) == (40,6) +@testset "simulate_sequence" begin + design = SingleSubjectDesign(conditions = Dict(:condition => ["one", "two"])) + design = SequenceDesign(design, "SCR_") + c = LinearModelComponent(; + basis = UnfoldSim.hanning(40), + formula = @formula(0 ~ 1 + condition), + β = [1.0, 2.0], + contrasts = Dict(:cond => EffectsCoding()), + ) + s, e = simulate(design, c, NoOnset(); return_epoched = true) + @test size(s) == (40, 6) end \ No newline at end of file From d5c7b4dac3264daef9b0de1eeee443de874a33c2 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Thu, 27 Feb 2025 14:37:10 +0100 Subject: [PATCH 070/156] fix: code duplications --- src/design.jl | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/src/design.jl b/src/design.jl index 119ccd71..eb4da761 100644 --- a/src/design.jl +++ b/src/design.jl @@ -354,27 +354,6 @@ end # ---- -""" - RepeatDesign{T} <: AbstractDesign -Repeat a design DataFrame multiple times to mimick repeatedly recorded trials. - -```julia -designOnce = MultiSubjectDesign(; - n_items=2, - n_subjects = 2, - subjects_between =Dict(:cond=>["levelA","levelB"]), - items_between =Dict(:cond=>["levelA","levelB"]), - ); - -design = RepeatDesign(designOnce,4); -``` -See also [`SingleSubjectDesign`](@ref), [`MultiSubjectDesign`](@ref) -""" -@with_kw struct RepeatDesign{T} <: AbstractDesign - design::T - repeat::Int = 1 -end - function check_sequence(s::String) blankfind = findall('_', s) @@ -525,10 +504,6 @@ function generate_events(design::SubselectDesign) end -Base.size(design::RepeatDesign{MultiSubjectDesign}) = - size(design.design) .* (design.repeat, 1) -Base.size(design::RepeatDesign{SingleSubjectDesign}) = size(design.design) .* design.repeat - # --- # Effects From c528f6ac5dce6508a40e95c677bb03d88aa3e027 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Thu, 27 Feb 2025 14:46:12 +0100 Subject: [PATCH 071/156] refactor: get_basis, fix basis docstring --- src/component.jl | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/component.jl b/src/component.jl index 94b37355..3d6bf1c2 100644 --- a/src/component.jl +++ b/src/component.jl @@ -6,7 +6,7 @@ A component that adds a hierarchical relation between parameters according to a All fields can be named. Works best with [`MultiSubjectDesign`](@ref). # Fields -- `basis::Any`: an object, if accessed, provides a 'basis function', e.g. `hanning(40)::Vector`, this defines the response at a single event. It will be weighted by the model prediction. It is possible to also provide a function that evaluates to an `Vector`, with the `design` as input to the function, in that case, one has to specify the `maxlength` as well in a tuple. E.g. `basis=(myfun,40)`, which would automatically cut the output of `myfun` to 40 samples. +- `basis::Any`: an object, if accessed, provides a 'basis function', e.g. `hanning(40)::Vector`, this defines the response at a single event. It will be weighted by the model prediction. It is also possible to provide a function that evaluates to an `Vector` of `Vectors`, with the `design` as input to the function, the outer vector has to have `nrows(design)`, one for each event. The inner vector represents the basis functions which can be of different size (a ragged array). Alternatively, one can also return a Matrix with the second dimension representing `nrows(design)`. In the case of providing a function, one has to specify the `maxlength` as well in a tuple. E.g. `basis=(myfun,40)`, which would automatically cut the output of `myfun` to 40 samples. - `formula::Any`: Formula-object in the style of MixedModels.jl e.g. `@formula 0 ~ 1 + cond + (1|subject)`. The left-hand side is ignored. - `β::Vector` Vector of betas (fixed effects), must fit the formula. - `σs::Dict` Dict of random effect variances, e.g. `Dict(:subject => [0.5, 0.4])` or to specify correlation matrix `Dict(:subject=>[0.5,0.4,I(2,2)],...)`. Technically, this will be passed to the MixedModels.jl `create_re` function, which creates the θ matrices. @@ -225,18 +225,18 @@ get_basis(c::AbstractComponent, design) = get_basis(get_basis(c), design) get_basis(b::AbstractVector, design) = b +_get_basis_length(basis_out::AbstractMatrix) = size(basis_out, 2) +_get_basis_length(basis_out::AbstractVector{<:AbstractVector}) = length(basis_out) +_get_basis_length(basis_out) = error( + "Component basis function needs to either return a Vector of vectors or a Matrix ", +) function get_basis(basis::Tuple{Function,Int}, design) f = basis[1] maxlength = basis[2] basis_out = f(design) - if isa(basis_out, AbstractVector{<:AbstractVector}) || isa(basis_out, AbstractMatrix) - if isa(basis_out, AbstractMatrix) - l = size(basis_out, 2) - else - l = length(basis_out) # vector of vector case - end - @assert l == size(generate_events(design))[1] "Component basis function needs to either return a Vector of vectors or a Matrix with dim(2) == size(design,1) [l / $(size(design,1))], or a Vector of Vectors with length(b) == size(design,1) [$l / $(size(design,1))]. " - end + l = _get_basis_length(basis_out) + + @assert l == size(generate_events(design))[1] "Component basis function needs to either return a Vector of vectors or a Matrix with dim(2) == size(design,1) [$l / $(size(design,1))], or a Vector of Vectors with length(b) == size(design,1) [$l / $(size(design,1))]. " limit_basis(basis_out, maxlength) end From 73409f47f1fd308c8b978640b4ee1b9f7eea6176 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Thu, 27 Feb 2025 15:15:21 +0100 Subject: [PATCH 072/156] fix: format docstring; fix: remove size(design) replace with length(design); fix remaining generate_events without rng --- src/component.jl | 19 +++++++++++-------- src/design.jl | 12 ++++++------ src/onset.jl | 6 +++--- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/component.jl b/src/component.jl index 3d6bf1c2..0f2d5d58 100644 --- a/src/component.jl +++ b/src/component.jl @@ -236,7 +236,7 @@ function get_basis(basis::Tuple{Function,Int}, design) basis_out = f(design) l = _get_basis_length(basis_out) - @assert l == size(generate_events(design))[1] "Component basis function needs to either return a Vector of vectors or a Matrix with dim(2) == size(design,1) [$l / $(size(design,1))], or a Vector of Vectors with length(b) == size(design,1) [$l / $(size(design,1))]. " + @assert l == length(design) "Component basis function needs to either return a Vector of vectors or a Matrix with dim(2) == length(design) [$l / $(length(design))], or a Vector of Vectors with length(b) == length(design) [$l / $(length(design))]. " limit_basis(basis_out, maxlength) end @@ -251,7 +251,6 @@ function limit_basis(b::AbstractVector{<:AbstractVector}, maxlength) b = pad_array.(b, Δlengths, 0) basis_out = reduce(hcat, b) - return basis_out end limit_basis(b::AbstractVector{<:Number}, maxlength) = b[1:min(length(b), maxlength)] @@ -260,17 +259,15 @@ limit_basis(b::AbstractMatrix, maxlength) = b[1:min(length(b), maxlength), :] Base.length(c::AbstractComponent) = isa(get_basis(c), Tuple) ? get_basis(c)[2] : length(get_basis(c)) - - """ maxlength(c::Vector{<:AbstractComponent}) = maximum(length.(c)) maxlength(components::Dict) -maximum of individual component lengths + +Maximum of individual component lengths """ maxlength(c::Vector{<:AbstractComponent}) = maximum(length.(c)) - - maxlength(components::Dict) = maximum([maximum(length.(c)) for c in values(components)]) + """ simulate_component(rng, c::AbstractComponent, simulation::Simulation) @@ -610,6 +607,12 @@ function simulate_responses!( end return epoch_data end + + +""" +Initializes an Array with zeros. Returns either a 2-dimensional for component-length x length(design), or a 3-D for channels x component-length x length(design) + +""" function init_epoch_data(components, design) max_offset = maxoffset(components) min_offset = minoffset(components) @@ -630,7 +633,7 @@ function simulate_responses(rng, event_component_dict::Dict, s::Simulation) #@debug rng.state epoch_data = init_epoch_data(event_component_dict, s.design) #@debug rng.state - evts = generate_events(s.design) + evts = generate_events(deepcopy(rng), s.design) #@debug rng.state @debug size(epoch_data), size(evts) multichannel = n_channels(event_component_dict) > 1 diff --git a/src/design.jl b/src/design.jl index eb4da761..1f06bbc3 100644 --- a/src/design.jl +++ b/src/design.jl @@ -453,7 +453,7 @@ generate_events(rng, design::SequenceDesign{MultiSubjectDesign}) = generate_events(rng, design::AbstractDesign) = generate_events(design) function generate_events(rng, design::SequenceDesign) - df = generate_events(design.design) + df = generate_events(deepcopy(rng), design.design) nrows_df = size(df, 1) # @debug design.sequence @@ -499,8 +499,8 @@ struct SubselectDesign{T} <: AbstractDesign key::Char end -function generate_events(design::SubselectDesign) - return subset(generate_events(design.design), :event => x -> x .== design.key) +function generate_events(rng, design::SubselectDesign) + return subset(generate_events(rng, design.design), :event => x -> x .== design.key) end @@ -541,7 +541,7 @@ typical_value(v::Vector{<:Number}) = [mean(v)] typical_value(v) = unique(v) """ - UnfoldSim.generate_events(design::EffectsDesign) + UnfoldSim.generate_events(rng,design::EffectsDesign) Generates events to simulate marginalized effects using an Effects.jl reference-grid dictionary. Every covariate that is in the `EffectsDesign` but not in the `effects_dict` will be set to a `typical_value` (i.e. the mean) @@ -552,10 +552,10 @@ effects_dict = Dict{Symbol,Union{<:Number,<:String}}(:conditionA=>[0,1]) SingleSubjectDesign(...) |> x-> EffectsDesign(x,effects_dict) ``` """ -function UnfoldSim.generate_events(t::EffectsDesign) +function UnfoldSim.generate_events(rng, t::EffectsDesign) effects_dict = Dict{Any,Any}(t.effects_dict) #effects_dict = t.effects_dict - current_design = generate_events(t.design) + current_design = generate_events(deepcopy(rng), t.design) to_be_added = setdiff(names(current_design), string.(keys(effects_dict))) for tba in to_be_added effects_dict[tba] = typical_value(current_design[:, tba]) diff --git a/src/onset.jl b/src/onset.jl index 1e07085f..095b55d8 100644 --- a/src/onset.jl +++ b/src/onset.jl @@ -202,7 +202,7 @@ function simulate_onsets(rng, onset::AbstractOnset, simulation::Simulation) if !isnothing(findfirst("_", currentsequence)) @assert currentsequence[end] == '_' "the blank-indicator '_' has to be the last sequence element" - df = generate_events(simulation.design) + df = generate_events(deepcopy(rng), simulation.design) nrows_df = size(df, 1) stepsize = length(currentsequence) - 1 # add to every stepsize onset the maxlength of the response @@ -252,7 +252,7 @@ end function simulate_interonset_distances(rng, o::UniformOnsetFormula, design::AbstractDesign) - events = generate_events(design) + events = generate_events(deepcopy(rng), design) widths = UnfoldSim.generate_designmatrix(o.width_formula, events, o.width_contrasts) * o.width_β @@ -309,7 +309,7 @@ function simulate_interonset_distances( o::LogNormalOnsetFormula, design::AbstractDesign, ) - events = generate_events(design) + events = generate_events(deepcopy(rng), design) μs = UnfoldSim.generate_designmatrix(o.μ_formula, events, o.μ_contrasts) * o.μ_β From 08b0738dd023704fd1d8525666090f449f68d6e1 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Thu, 27 Feb 2025 15:34:38 +0100 Subject: [PATCH 073/156] relax compat --- Project.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index 8775333b..96d86d6b 100644 --- a/Project.toml +++ b/Project.toml @@ -24,7 +24,7 @@ ToeplitzMatrices = "c751599d-da0a-543b-9d20-d0a503d91d24" [compat] Artifacts = "1" -DSP = "0.7" +DSP = "0.7,0.8" DataFrames = "1" Distributions = "0.25" FileIO = "1" @@ -35,7 +35,7 @@ MixedModels = "4" MixedModelsSim = "0.2" Parameters = "0.12" Random = "1" -SignalAnalysis = "0.4, 0.5,0.6,0.7,0.8" +SignalAnalysis = "0.4, 0.5,0.6,0.7,0.8,0.9,0.10" Statistics = "1" StatsModels = "0.6,0.7" ToeplitzMatrices = "0.7, 0.8" From ddbafdf997e73bb135c08ac08fb7a0630b1a0cfa Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Thu, 27 Feb 2025 15:34:56 +0100 Subject: [PATCH 074/156] remove unused type-field --- src/design.jl | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/design.jl b/src/design.jl index 1f06bbc3..482c0bd1 100644 --- a/src/design.jl +++ b/src/design.jl @@ -440,12 +440,9 @@ See also [`SingleSubjectDesign`](@ref), [`MultiSubjectDesign`](@ref), [`RepeatDe @with_kw struct SequenceDesign{T} <: AbstractDesign design::T sequence::String = "" - sequencelength::Int = 0 - SequenceDesign{T}(d, s, sl) where {T<:AbstractDesign} = new(d, check_sequence(s), sl) + SequenceDesign{T}(d, s) where {T<:AbstractDesign} = new(d, check_sequence(s)) end -SequenceDesign(design, sequence) = SequenceDesign(design = design, sequence = sequence) - generate_events(rng, design::SequenceDesign{MultiSubjectDesign}) = error("not yet implemented") From 1de21861b93bad18536dd16224704e0cfa1393f8 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Thu, 27 Feb 2025 15:47:38 +0100 Subject: [PATCH 075/156] added docstring warning for irregular size --- src/design.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/design.jl b/src/design.jl index 482c0bd1..905bf083 100644 --- a/src/design.jl +++ b/src/design.jl @@ -367,12 +367,15 @@ end Enforce a sequence of events for each entry of a provided `AbstractDesign`. The sequence string can contain any number of `char`, but the `_` character is used to indicate a break between events without any overlap. -It is also possible to define variable length sequences using `{}`. For example, `A{10,20}` would result in a sequence of 10 to 20 `A`'s. + Another variable sequence is defined using `[]`. For example, `S[ABC]` would result in any one sequence `SA`, `SB`, `SC`. Important: The exact same variable sequence is used for current rows of a design. Only, if you later nest in a `RepeatDesign` then each `RepeatDesign` repetition will gain a new variable sequence. If you need imbalanced designs, please refer to the `ImbalancedDesign` tutorial + +Experimental: It is also possible to define variable length sequences using `{}`. For example, `A{10,20}` would result in a sequence of 10 to 20 `A`'s. Because the number of trials is not defined before actually executing the design, this can lead to problems down the road, if functions require to know the number of trials before generation of the design. + ```julia design = SingleSubjectDesign(conditions = Dict(:condition => ["one", "two"])) design = SequenceDesign(design, "SCR_") From 36b192042484e1c19b65a26cfb336850973d1ae3 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Thu, 20 Mar 2025 12:39:38 +0100 Subject: [PATCH 076/156] fix maxget_offset bug & get_basis with rng --- src/component.jl | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/component.jl b/src/component.jl index 0f2d5d58..108681d1 100644 --- a/src/component.jl +++ b/src/component.jl @@ -103,11 +103,9 @@ get_offset(c::LinearModelComponent)::Int = c.offset get_offset(c::MixedModelComponent)::Int = c.offset maxoffset(c::Vector{<:AbstractComponent}) = maximum(get_offset.(c)) -maxoffset(d::Dict{<:Char,<:Vector{<:AbstractComponent}}) = - maximum(maxget_offset.(values(d))) +maxoffset(d::Dict{<:Char,<:Vector{<:AbstractComponent}}) = maximum(max_offset.(values(d))) minoffset(c::Vector{<:AbstractComponent}) = minimum(get_offset.(c)) -minoffset(d::Dict{<:Char,<:Vector{<:AbstractComponent}}) = - minimum(minget_offset.(values(d))) +minoffset(d::Dict{<:Char,<:Vector{<:AbstractComponent}}) = minimum(min_offset.(values(d))) @@ -217,12 +215,24 @@ returns the basis of the component (typically `c.basis`) """ get_basis(c::AbstractComponent) = c.basis + +""" + get_basis(c::AbstractComponent) + +returns the basis of the component (typically `c.basis`) """ - get_basis(c::AbstractComponent,design) +get_basis(rng::AbstractRNG, c::AbstractComponent) = get_basis(c) + + +""" + get_basis([rng],c::AbstractComponent,design) evaluates the basis, if basis is a vector, directly returns it. if basis is a tuple `(f::Function,maxlength::Int)`, evaluates the function with input `design`. Cuts the resulting vector or Matrix at `maxlength` + +To ensure the same design can be generated, rng should be the global simulation rng. If not specified, we use `MersenneTwister(1)` """ -get_basis(c::AbstractComponent, design) = get_basis(get_basis(c), design) -get_basis(b::AbstractVector, design) = b +get_basis(basis, design) = get_basis(MersenneTwister(1), basis, design) +get_basis(rng, c::AbstractComponent, design) = get_basis(rng, get_basis(c), design) +get_basis(rng, b::AbstractVector, design) = b _get_basis_length(basis_out::AbstractMatrix) = size(basis_out, 2) @@ -230,10 +240,11 @@ _get_basis_length(basis_out::AbstractVector{<:AbstractVector}) = length(basis_ou _get_basis_length(basis_out) = error( "Component basis function needs to either return a Vector of vectors or a Matrix ", ) -function get_basis(basis::Tuple{Function,Int}, design) + +function get_basis(rng::AbstractRNG, basis::Tuple{Function,Int}, design) f = basis[1] maxlength = basis[2] - basis_out = f(design) + basis_out = applicable(f, 2) ? f(rng, design) : f(design) l = _get_basis_length(basis_out) @assert l == length(design) "Component basis function needs to either return a Vector of vectors or a Matrix with dim(2) == length(design) [$l / $(length(design))], or a Vector of Vectors with length(b) == length(design) [$l / $(length(design))]. " @@ -305,7 +316,7 @@ function simulate_component(rng, c::LinearModelComponent, design::AbstractDesign X = generate_designmatrix(c.formula, events, c.contrasts) y = X * c.β - return y' .* get_basis(c, design) + return y' .* get_basis(deepcopy(rng), c, design) end @@ -413,9 +424,9 @@ function simulate_component( rethrow(e) end - @debug size(get_basis(c, design)) + @debug size(get_basis(deepcopy(rng), c, design)) # in case the parameters are of interest, we will return those, not them weighted by basis - b = return_parameters ? [1.0] : get_basis(c, design) + b = return_parameters ? [1.0] : get_basis(deepcopy(rng), c, design) @debug :b, typeof(b), size(b), :m, size(m.y') if isa(b, AbstractMatrix) epoch_data_component = ((m.y' .* b)) From e6100416ad0440dd798c5556512669ce644c9c83 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Thu, 20 Mar 2025 12:50:02 +0100 Subject: [PATCH 077/156] test: get_basis --- src/component.jl | 2 +- test/component.jl | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/component.jl b/src/component.jl index 108681d1..a55e16d4 100644 --- a/src/component.jl +++ b/src/component.jl @@ -244,7 +244,7 @@ _get_basis_length(basis_out) = error( function get_basis(rng::AbstractRNG, basis::Tuple{Function,Int}, design) f = basis[1] maxlength = basis[2] - basis_out = applicable(f, 2) ? f(rng, design) : f(design) + basis_out = applicable(f, rng, design) ? f(rng, design) : f(design) l = _get_basis_length(basis_out) @assert l == length(design) "Component basis function needs to either return a Vector of vectors or a Matrix with dim(2) == length(design) [$l / $(length(design))], or a Vector of Vectors with length(b) == length(design) [$l / $(length(design))]. " diff --git a/test/component.jl b/test/component.jl index 1b48257d..1db10b35 100644 --- a/test/component.jl +++ b/test/component.jl @@ -44,5 +44,29 @@ @test UnfoldSim.weight_σs(Dict(:subj => [1, 2, [1 0.5; 0.5 1]]), 1.0, 2.0).subj == create_re(1, 2; corrmat = [1 0.5; 0.5 1]) ./ 2 end + @testset "get_basis" begin + rng = StableRNG(1) + design = UnfoldSim.SingleSubjectDesign(; conditions = Dict(:duration => 10:-1:5)) + mybasisfun = + (rng, design) -> (collect.(range.(1, generate_events(rng, design).duration))) + signal = LinearModelComponent(; + basis = (mybasisfun, 15), + formula = @formula(0 ~ 1), + β = [1], + ) + @test UnfoldSim.get_basis(deepcopy(rng), signal, design) == + UnfoldSim.get_basis(signal, design) + + shuffle_design = UnfoldSim.SingleSubjectDesign(; + conditions = Dict(:duration => 10:-1:5), + event_order_function = shuffle, + ) + # with same seed => equal result + @test UnfoldSim.get_basis(StableRNG(1), signal, shuffle_design) == + UnfoldSim.get_basis(StableRNG(1), signal, shuffle_design) + # with different seed => unequal result + @test UnfoldSim.get_basis(StableRNG(1), signal, shuffle_design) != + UnfoldSim.get_basis(StableRNG(2), signal, shuffle_design) + end end From 2cbad0b455b0c430eeb27fb1ba6954be99e911a8 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Thu, 20 Mar 2025 12:55:18 +0100 Subject: [PATCH 078/156] added docstring with rng --- src/component.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/component.jl b/src/component.jl index a55e16d4..a6744888 100644 --- a/src/component.jl +++ b/src/component.jl @@ -6,7 +6,7 @@ A component that adds a hierarchical relation between parameters according to a All fields can be named. Works best with [`MultiSubjectDesign`](@ref). # Fields -- `basis::Any`: an object, if accessed, provides a 'basis function', e.g. `hanning(40)::Vector`, this defines the response at a single event. It will be weighted by the model prediction. It is also possible to provide a function that evaluates to an `Vector` of `Vectors`, with the `design` as input to the function, the outer vector has to have `nrows(design)`, one for each event. The inner vector represents the basis functions which can be of different size (a ragged array). Alternatively, one can also return a Matrix with the second dimension representing `nrows(design)`. In the case of providing a function, one has to specify the `maxlength` as well in a tuple. E.g. `basis=(myfun,40)`, which would automatically cut the output of `myfun` to 40 samples. +- `basis::Any`: an object, if accessed, provides a 'basis function', e.g. `hanning(40)::Vector`, this defines the response at a single event. It will be weighted by the model prediction. It is also possible to provide a function that evaluates to an `Vector` of `Vectors`, with the `design` as input to the function, the outer vector has to have `nrows(design)`, one for each event. The inner vector represents the basis functions which can be of different size (a ragged array). Alternatively, one can also return a Matrix with the second dimension representing `nrows(design)`. In the case of providing a function, one has to specify the `maxlength` as well in a tuple. E.g. `basis=(myfun,40)`, which would automatically cut the output of `myfun` to 40 samples. If your design depends on `rng`, e.g. because of `event_order_function=shuffle` or some special `SequenceDesign`, then you can provide a two-arguments function `(rng,design)->...` - `formula::Any`: Formula-object in the style of MixedModels.jl e.g. `@formula 0 ~ 1 + cond + (1|subject)`. The left-hand side is ignored. - `β::Vector` Vector of betas (fixed effects), must fit the formula. - `σs::Dict` Dict of random effect variances, e.g. `Dict(:subject => [0.5, 0.4])` or to specify correlation matrix `Dict(:subject=>[0.5,0.4,I(2,2)],...)`. Technically, this will be passed to the MixedModels.jl `create_re` function, which creates the θ matrices. @@ -31,7 +31,6 @@ MixedModelComponent See also [`LinearModelComponent`](@ref), [`MultichannelComponent`](@ref). """ -# backwards compatability after introducing the `offset` field` @with_kw struct MixedModelComponent <: AbstractComponent basis::Any formula::Any # e.g. 0~1+cond @@ -50,7 +49,7 @@ A multiple regression component for one subject. All fields can be named. Works best with [`SingleSubjectDesign`](@ref). # Fields -- `basis::Any`: an object, if accessed, provides a 'basis function', e.g. `hanning(40)::Vector`, this defines the response at a single event. It will be weighted by the model prediction. Future versions will allow for functions, as of v0.3 this is restricted to array-like objects +- `basis::Any`: an object, if accessed, provides a 'basis function', e.g. `hanning(40)::Vector`, this defines the response at a single event. It will be weighted by the model prediction. It is also possible to provide a function that evaluates to an `Vector` of `Vectors`, with the `design` as input to the function, the outer vector has to have `nrows(design)`, one for each event. The inner vector represents the basis functions which can be of different size (a ragged array). Alternatively, one can also return a Matrix with the second dimension representing `nrows(design)`. In the case of providing a function, one has to specify the `maxlength` as well in a tuple. E.g. `basis=(myfun,40)`, which would automatically cut the output of `myfun` to 40 samples. If your design depends on `rng`, e.g. because of `event_order_function=shuffle` or some special `SequenceDesign`, then you can provide a two-arguments function `(rng,design)->...` - `formula::Any`: StatsModels `formula` object, e.g. `@formula 0 ~ 1 + cond` (left-hand side must be 0). - `β::Vector` Vector of betas/coefficients, must fit the formula. - `contrasts::Dict` (optional): Determines which coding scheme to use for which categorical variables. Default is empty which corresponds to dummy coding. @@ -58,6 +57,7 @@ All fields can be named. Works best with [`SingleSubjectDesign`](@ref). For more information see . + # Examples ```julia-repl julia> LinearModelComponent(; From f49004db57ee5c0dcb4c1b22e396ef6f62fdc3b3 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Thu, 20 Mar 2025 13:00:40 +0100 Subject: [PATCH 079/156] fix: added a comment on the basisfunction-rng two inputs --- docs/literate/HowTo/componentfunction.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/literate/HowTo/componentfunction.jl b/docs/literate/HowTo/componentfunction.jl index 236362db..779c8f7b 100644 --- a/docs/literate/HowTo/componentfunction.jl +++ b/docs/literate/HowTo/componentfunction.jl @@ -21,10 +21,10 @@ design = UnfoldSim.SingleSubjectDesign(; # Instead of defining a boring vector basis function e.g. `[0,0,1,2,3,3,2,1,0,0,0]`, let's use function, modulating a hanning windows by the experimental design's duration. # !!! important -# because any function depending on `design` can be used, two things have to be taken care of: -# -# 1. in case a random component exist in the function, specify a `<:AbstractRNG` within the function call , the basis might be evaluated multiple times inside `simulate` +# Two things have to be taken care of: +# 1. in case a rng is required to e.g. generate the design, or your absisfunction depends on it, you have to specify a two-argument basis-function: `(rng,design)->...` # 2. a `maxlength` has to be specified via a tuple `(function,maxlength)`` + mybasisfun = design -> hanning.(generate_events(design).duration) signal = LinearModelComponent(; basis = (mybasisfun, 100), From 9be737640c338e93b2e94a6f2784cfd9bde3383f Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Thu, 20 Mar 2025 13:41:07 +0100 Subject: [PATCH 080/156] tests+fix: min/maxoffset, negative minoffset bug --- src/component.jl | 15 ++++----------- src/onset.jl | 1 + test/component.jl | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/component.jl b/src/component.jl index a6744888..fc0b14a7 100644 --- a/src/component.jl +++ b/src/component.jl @@ -103,9 +103,9 @@ get_offset(c::LinearModelComponent)::Int = c.offset get_offset(c::MixedModelComponent)::Int = c.offset maxoffset(c::Vector{<:AbstractComponent}) = maximum(get_offset.(c)) -maxoffset(d::Dict{<:Char,<:Vector{<:AbstractComponent}}) = maximum(max_offset.(values(d))) +maxoffset(d::Dict{<:Char,<:Vector{<:AbstractComponent}}) = maximum(maxoffset.(values(d))) minoffset(c::Vector{<:AbstractComponent}) = minimum(get_offset.(c)) -minoffset(d::Dict{<:Char,<:Vector{<:AbstractComponent}}) = minimum(min_offset.(values(d))) +minoffset(d::Dict{<:Char,<:Vector{<:AbstractComponent}}) = minimum(minoffset.(values(d))) @@ -209,18 +209,11 @@ end """ - get_basis(c::AbstractComponent) + get_basis([rng],c::AbstractComponent) -returns the basis of the component (typically `c.basis`) +returns the basis of the component (typically `c.basis`). rng is optional and ignored, but exists to have the same interface as `get_basis(c,design)` """ get_basis(c::AbstractComponent) = c.basis - - -""" - get_basis(c::AbstractComponent) - -returns the basis of the component (typically `c.basis`) -""" get_basis(rng::AbstractRNG, c::AbstractComponent) = get_basis(c) diff --git a/src/onset.jl b/src/onset.jl index 095b55d8..1d1927b4 100644 --- a/src/onset.jl +++ b/src/onset.jl @@ -218,6 +218,7 @@ function simulate_onsets(rng, onset::AbstractOnset, simulation::Simulation) end # accumulate them onsets_accum = accumulate(+, onsets, dims = 1, init = 1) + onsets_accum = onsets_accum .- minoffset(simulation.components) return onsets_accum end diff --git a/test/component.jl b/test/component.jl index 1db10b35..19944924 100644 --- a/test/component.jl +++ b/test/component.jl @@ -68,5 +68,41 @@ # with different seed => unequal result @test UnfoldSim.get_basis(StableRNG(1), signal, shuffle_design) != UnfoldSim.get_basis(StableRNG(2), signal, shuffle_design) + + end + + @testset "max/min offset" begin + # test max/min offset + smin10 = LinearModelComponent(; + basis = [1, 2, 3], + formula = @formula(0 ~ 1), + β = [1], + offset = -10, + ) + splus5 = LinearModelComponent(; + basis = [1, 2, 3], + formula = @formula(0 ~ 1), + β = [1], + offset = 5, + ) + @test UnfoldSim.get_offset(smin10) == -10 + @test UnfoldSim.maxoffset([smin10, splus5]) == 5 + @test UnfoldSim.minoffset([smin10, splus5]) == -10 + @test UnfoldSim.minoffset(Dict('A' => [smin10, splus5])) == -10 + @test UnfoldSim.maxoffset(Dict('A' => [smin10, smin10], 'B' => [splus5, splus5])) == + 5 + # test that you can have a super large negative offset and dont run into errors (e.g. an event cannot even run in the issue to start before simulation time = 0) + + smin10000 = LinearModelComponent(; + basis = [1, 2, 3], + formula = @formula(0 ~ 1), + β = [1], + offset = -10_000, + ) + design = UnfoldSim.SingleSubjectDesign(; conditions = Dict(:duration => 10:-1:5)) + d, e = simulate(design, smin10000, UniformOnset(50, 0)) + @test length(d) > 10_000 + @test e.latency[1] > 10_000 + end end From d1defe0617b65677580bb7e37d84c2117e1ab2f4 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Thu, 27 Mar 2025 10:53:43 +0100 Subject: [PATCH 081/156] fix docustring simulation missing --- src/simulation.jl | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/simulation.jl b/src/simulation.jl index c7718b98..62717da4 100755 --- a/src/simulation.jl +++ b/src/simulation.jl @@ -119,20 +119,14 @@ Additional remarks on the overlap of adjacent signals when `return_epoched = tru - If `onset = NoOnset()` there will not be any overlapping signals in the data because the onset calculation and conversion to a continuous signal is skipped. - If an inter-onset distance distribution is given, a continuous signal(potentially with overlap) is constructed and partitioned into epochs afterwards. """ - - -function simulate( +simulate( rng::AbstractRNG, design::AbstractDesign, components, onset::AbstractOnset, noise::AbstractNoise = NoNoise(); kwargs..., -) - simulate(rng, Simulation(design, components, onset, noise); kwargs...) -end - - +) = simulate(rng, Simulation(design, components, onset, noise); kwargs...) function simulate(rng::AbstractRNG, simulation::Simulation; return_epoched::Bool = false) (; design, components, onset, noisetype) = simulation From c23c317e47857891529cecad76c49084829e264e Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Thu, 27 Mar 2025 11:00:10 +0100 Subject: [PATCH 082/156] fix mention onsetformulas at top --- docs/literate/reference/onsettypes.jl | 2 +- docs/make.jl | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/literate/reference/onsettypes.jl b/docs/literate/reference/onsettypes.jl index fb26f11f..e035ab40 100644 --- a/docs/literate/reference/onsettypes.jl +++ b/docs/literate/reference/onsettypes.jl @@ -1,6 +1,6 @@ # # Overview: Onset types # The onset types determine the distances between event onsets in the continuous EEG signal. The distances are sampled from a certain probability distribution. -# Currently, there are two types of onset distributions implemented: `UniformOnset` and `LogNormalOnset`. +# Currently, there are two types of onset distributions implemented: `UniformOnset` and `LogNormalOnset`. Both are accompanied by their `UniformOnsetFormula` and `LogNormalOnsetFormula` conterparts, which allows to modify the overlap based on the design. # ### Setup # ```@raw html diff --git a/docs/make.jl b/docs/make.jl index 050dede2..1c67c418 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -49,9 +49,8 @@ makedocs(; "Overview: Noise types" => "./generated/reference/noisetypes.md", ], "HowTo" => [ - "Define a new (imbalanced) design" => "./generated/HowTo/newDesign.md", - "Use a component-basis-function (duration)" => "./generated/HowTo/componentfunction.md", + "Use a component basisfunction (duration)" => "./generated/HowTo/componentfunction.md", "Get multiple trials with identical subject/item combinations" => "./generated/HowTo/repeatTrials.md", "Define a new component (with variable duration and shift)" => "./generated/HowTo/newComponent.md", "Generate multi channel data" => "./generated/HowTo/multichannel.md", From 6ccd668046bfbe4e2e8fd28310712fee793b9ad5 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Thu, 27 Mar 2025 11:01:18 +0100 Subject: [PATCH 083/156] fix better assert msg --- src/design.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/design.jl b/src/design.jl index 905bf083..389bdca9 100644 --- a/src/design.jl +++ b/src/design.jl @@ -357,7 +357,7 @@ end function check_sequence(s::String) blankfind = findall('_', s) - @assert length(blankfind) <= 1 && (length(blankfind) == 0 || length(s) == blankfind[1]) "the blank-indicator '_' has to be the last sequence element" + @assert length(blankfind) <= 1 && (length(blankfind) == 0 || length(s) == blankfind[1]) "the blank-indicator '_' has to be the last sequence element, and only one can exist" return s end From c28fbc196c01e35e7244d9ed14655dcb9bc44ce4 Mon Sep 17 00:00:00 2001 From: Benedikt Ehinger Date: Thu, 27 Mar 2025 11:03:11 +0100 Subject: [PATCH 084/156] Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Judith Schepers --- docs/literate/HowTo/getGroundTruth.jl | 2 +- docs/literate/HowTo/newComponent.jl | 1 - docs/literate/HowTo/sequence.jl | 4 ++-- docs/literate/reference/onsettypes.jl | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/literate/HowTo/getGroundTruth.jl b/docs/literate/HowTo/getGroundTruth.jl index 7ed17664..e4cc6176 100644 --- a/docs/literate/HowTo/getGroundTruth.jl +++ b/docs/literate/HowTo/getGroundTruth.jl @@ -71,7 +71,7 @@ gt_data, gt_events = simulate( # Additionally, we can get the simulated effects into a tidy dataframe using Unfold's `result_to_table`. # Note that the data has to be reshaped into a channel X times X predictor form. (In our one channel example `size(gt_data) = (45,2)`, missing the channel dimension) -g = reshape(gt_data,1,size(gt_data)...) +g = reshape(gt_data, 1, size(gt_data)...) times = range(1, 45); gt_effects = Unfold.result_to_table([g], [gt_events], [times], ["effects"]) first(gt_effects, 5) diff --git a/docs/literate/HowTo/newComponent.jl b/docs/literate/HowTo/newComponent.jl index 14f6c254..3a8e9df0 100644 --- a/docs/literate/HowTo/newComponent.jl +++ b/docs/literate/HowTo/newComponent.jl @@ -68,7 +68,6 @@ end # ## Simulate data with the new component type erp = UnfoldSim.simulate_component( - MersenneTwister(1), TimeVaryingComponent(basis_shiftduration, 50), design, diff --git a/docs/literate/HowTo/sequence.jl b/docs/literate/HowTo/sequence.jl index 323f293c..b2426fc2 100644 --- a/docs/literate/HowTo/sequence.jl +++ b/docs/literate/HowTo/sequence.jl @@ -10,12 +10,12 @@ using StableRNGs # First we generate the minimal design of the experiment by specifying our conditins (a one-condition-two-levels design in our case) design = SingleSubjectDesign(conditions = Dict(:condition => ["one", "two"])) generate_events(design) -# Next we use the `SequenceDesign` and nest our initial design in it. "`SR_`" is code for an "`S`" event and an "`R`" event - only single letter events are supported! The "`_`" is a signal for the Onset generator to generate a bigger pause - no overlap between adjacend "`SR`" pairs +# Next we use the `SequenceDesign` and nest our initial design in it. "`SR_`" is code for an "`S`" (stimulus) event and an "`R`" (response) event - only single letter events are supported! The "`_`" is a signal for the onset generator to generate a bigger pause - no overlap between adjacent "`SR`" pairs. design = SequenceDesign(design, "SR_") generate_events(StableRNG(1), design) # The main thing that happened is that the design was repeated for every event (each 'letter') of the sequence, and an `eventtype` column was added. # !!! hint -# more advaned sequences are possible as well, like "SR{1,3}", or "A[BC]". Infinite sequences are **not** possible like "AB*". +# More advanced sequences are possible as well, like "SR{1,3}", or "A[BC]". Infinite sequences are **not** possible like "AB*". # Finally, let's repeat the current design 4 times design = RepeatDesign(design, 4) diff --git a/docs/literate/reference/onsettypes.jl b/docs/literate/reference/onsettypes.jl index e035ab40..4c2c3b1d 100644 --- a/docs/literate/reference/onsettypes.jl +++ b/docs/literate/reference/onsettypes.jl @@ -303,4 +303,4 @@ hist!(ax, onsets[events.cond.=="B"], bins = range(0, 100, step = 1), label = "co axislegend(ax) f -# Voila - the inter-onset intervals are `20` samples longer for condition `B`, exactly as specified.` \ No newline at end of file +# Voila - the inter-onset intervals are `20` samples longer for condition `B`, exactly as specified. \ No newline at end of file From 89c3454708d4253da4791438c4b5c7dffe27bb67 Mon Sep 17 00:00:00 2001 From: Benedikt Ehinger Date: Thu, 27 Mar 2025 11:03:55 +0100 Subject: [PATCH 085/156] Update src/component.jl Co-authored-by: Judith Schepers --- src/component.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/component.jl b/src/component.jl index fc0b14a7..f338b76a 100644 --- a/src/component.jl +++ b/src/component.jl @@ -221,7 +221,7 @@ get_basis(rng::AbstractRNG, c::AbstractComponent) = get_basis(c) get_basis([rng],c::AbstractComponent,design) evaluates the basis, if basis is a vector, directly returns it. if basis is a tuple `(f::Function,maxlength::Int)`, evaluates the function with input `design`. Cuts the resulting vector or Matrix at `maxlength` -To ensure the same design can be generated, rng should be the global simulation rng. If not specified, we use `MersenneTwister(1)` +To ensure the same design can be generated, `rng` should be the global simulation `rng`. If not specified, `MersenneTwister(1)` is used. """ get_basis(basis, design) = get_basis(MersenneTwister(1), basis, design) get_basis(rng, c::AbstractComponent, design) = get_basis(rng, get_basis(c), design) From 157932f8d7c93cd43e07ad6d262594b275fba287 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Thu, 27 Mar 2025 11:06:05 +0100 Subject: [PATCH 086/156] fix n_channel docstring --- src/component.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/component.jl b/src/component.jl index fc0b14a7..eb4c4a54 100644 --- a/src/component.jl +++ b/src/component.jl @@ -191,8 +191,9 @@ n_channels(c::MultichannelComponent) = length(c.projection) """ n_channels(c::Vector{<:AbstractComponent}) + n_channels(c::Dict) -For a vector of `MultichannelComponent`s, return the number of channels for the first component but assert all are of equal length. +For a vector of `MultichannelComponent`s or a Dict of components, return the number of channels for the first component but assert all are of equal length. """ function n_channels(c::Vector{<:AbstractComponent}) all_channels = n_channels.(c) From cfb46b81e817865aad06be6cd732e3556de0b50b Mon Sep 17 00:00:00 2001 From: Benedikt Ehinger Date: Thu, 27 Mar 2025 11:06:47 +0100 Subject: [PATCH 087/156] Update src/component.jl Co-authored-by: Judith Schepers --- src/component.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/component.jl b/src/component.jl index 3c14d504..38a2bacb 100644 --- a/src/component.jl +++ b/src/component.jl @@ -219,8 +219,10 @@ get_basis(rng::AbstractRNG, c::AbstractComponent) = get_basis(c) """ - get_basis([rng],c::AbstractComponent,design) -evaluates the basis, if basis is a vector, directly returns it. if basis is a tuple `(f::Function,maxlength::Int)`, evaluates the function with input `design`. Cuts the resulting vector or Matrix at `maxlength` + get_basis(c::AbstractComponent, design) + get_basis(rng, c::AbstractComponent, design) + +Evaluate the basis, if basis is a vector, directly returns it. If basis is a tuple `(f::Function, maxlength::Int)`, evaluate the function with input `design`. Cut the resulting vector or Matrix at `maxlength`. To ensure the same design can be generated, `rng` should be the global simulation `rng`. If not specified, `MersenneTwister(1)` is used. """ From 0bb23fcfbf9363f11aa1767f7b6040d5a4748469 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Thu, 27 Mar 2025 11:14:53 +0100 Subject: [PATCH 088/156] removed unnecessary interface --- src/design.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/design.jl b/src/design.jl index 389bdca9..19d2b834 100644 --- a/src/design.jl +++ b/src/design.jl @@ -450,8 +450,6 @@ generate_events(rng, design::SequenceDesign{MultiSubjectDesign}) = error("not yet implemented") -generate_events(rng, design::AbstractDesign) = generate_events(design) - function generate_events(rng, design::SequenceDesign) df = generate_events(deepcopy(rng), design.design) nrows_df = size(df, 1) From 96c3fc5abf04dd8e10a210eb3c3860296b26a24d Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Thu, 27 Mar 2025 11:46:59 +0100 Subject: [PATCH 089/156] fix component fucntion tutorial issues --- docs/literate/HowTo/componentfunction.jl | 32 ++++++++++++++++-------- docs/make.jl | 2 +- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/docs/literate/HowTo/componentfunction.jl b/docs/literate/HowTo/componentfunction.jl index 779c8f7b..6ebcb65c 100644 --- a/docs/literate/HowTo/componentfunction.jl +++ b/docs/literate/HowTo/componentfunction.jl @@ -1,11 +1,22 @@ -# # Component Functions -# HowTo put arbitrary functions into components +# # Component Basisfunctions +# HowTo use functions that depend on the `design` and return per-event basis-vectors, instead of the same basis vector for all events. + +# ### Setup +# ```@raw html +#
+# Click to expand +# ``` +## Load required packages using UnfoldSim using Unfold using Random using DSP using CairoMakie, UnfoldMakie +# ```@raw html +#
+# ``` + sfreq = 100; @@ -19,10 +30,10 @@ design = UnfoldSim.SingleSubjectDesign(; ); -# Instead of defining a boring vector basis function e.g. `[0,0,1,2,3,3,2,1,0,0,0]`, let's use function, modulating a hanning windows by the experimental design's duration. +# Instead of defining a "boring" vector basis function e.g. `[0,0,1,2,3,3,2,1,0,0,0]`, let's use function - in our case a hanning window with the size depending on the experimental design's duration. # !!! important # Two things have to be taken care of: -# 1. in case a rng is required to e.g. generate the design, or your absisfunction depends on it, you have to specify a two-argument basis-function: `(rng,design)->...` +# 1. in case a rng is required to e.g. generate the design, or your basisfunction depends on it, you have to specify a two-argument basis-function: `(rng,design)->...` # 2. a `maxlength` has to be specified via a tuple `(function,maxlength)`` mybasisfun = design -> hanning.(generate_events(design).duration) @@ -34,19 +45,18 @@ signal = LinearModelComponent(; erp = UnfoldSim.simulate_component(MersenneTwister(1), signal, design); - -# Finally, let's plot it, sorted by duration - +# After simulation, we are ready to plot it. We expect that the simulated responses are scaled by the design's duration. To show it more effectively, we sort by duration. +##--- f = Figure() df = UnfoldMakie.eeg_array_to_dataframe(erp') df.duration = repeat(generate_events(design).duration, inner = size(erp, 1)) +df.category = repeat(generate_events(design).category, inner = size(erp, 1)) plot_erp!( f[1, 1], df, - mapping = (; group = :duration, color = :duration), - categorical_color = false, - categorical_group = true, + mapping = (; group = :group => nonnumeric, color = :duration, col = :category), layout = (; legend_position = :left), + colorbar = (; label = "Duration"), ) plot_erpimage!( f[2, 1], @@ -56,4 +66,4 @@ plot_erpimage!( ) f -# The scaling by the two `condition`` effect levels and the modified event duration by the `duration` are clearly visible +# The scaling by the two `condition` effect levels and the modified event duration by the `duration` are clearly visible diff --git a/docs/make.jl b/docs/make.jl index 1c67c418..9aceb050 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -50,7 +50,7 @@ makedocs(; ], "HowTo" => [ "Define a new (imbalanced) design" => "./generated/HowTo/newDesign.md", - "Use a component basisfunction (duration)" => "./generated/HowTo/componentfunction.md", + "Component basisfunction (duration-dependent)" => "./generated/HowTo/componentfunction.md", "Get multiple trials with identical subject/item combinations" => "./generated/HowTo/repeatTrials.md", "Define a new component (with variable duration and shift)" => "./generated/HowTo/newComponent.md", "Generate multi channel data" => "./generated/HowTo/multichannel.md", From ea7b2881b2466b40c7e83aa22d76cf770bb110ac Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Thu, 27 Mar 2025 14:41:19 +0100 Subject: [PATCH 090/156] improved sequence-tutorial --- docs/literate/HowTo/sequence.jl | 34 +++++++++++++++++++++++---------- docs/make.jl | 2 +- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/docs/literate/HowTo/sequence.jl b/docs/literate/HowTo/sequence.jl index b2426fc2..6a6e6dd6 100644 --- a/docs/literate/HowTo/sequence.jl +++ b/docs/literate/HowTo/sequence.jl @@ -1,13 +1,22 @@ -using Base: add_sum +# # Sequence of events (e.g. SCR) + +# In this HoWTo we learn to simulate a "SR"-Sequence, a stimulus response, followed by a button press response. +# ### Setup +# ```@raw html +#
+# Click to expand +# ``` +## Load required packages using UnfoldSim using CairoMakie using StableRNGs +# ```@raw html +#
+# ``` -# ## Stimulus - Response design -# Let's say we want to simulate a stimulus response, followed by a button press response. # -# First we generate the minimal design of the experiment by specifying our conditins (a one-condition-two-levels design in our case) +# First we generate the minimal design of the experiment by specifying our conditions (a one-condition-two-levels design in our case) design = SingleSubjectDesign(conditions = Dict(:condition => ["one", "two"])) generate_events(design) # Next we use the `SequenceDesign` and nest our initial design in it. "`SR_`" is code for an "`S`" (stimulus) event and an "`R`" (response) event - only single letter events are supported! The "`_`" is a signal for the onset generator to generate a bigger pause - no overlap between adjacent "`SR`" pairs. @@ -68,10 +77,15 @@ data, evts = simulate( nothing ## hide # Finally we can plot the results -lines(data) -vlines!(evts.latency[evts.event.=='S'], color = (:darkblue, 0.5)) -vlines!(evts.latency[evts.event.=='R'], color = (:darkred, 0.5)) -xlims!(0, 500) -current_figure() +f, ax, h = lines(data) +vlines!(ax, evts.latency[evts.event.=='S'], color = (:darkblue, 0.5)) +vlines!(ax, evts.latency[evts.event.=='R'], color = (:darkred, 0.5)) +ax.xlabel = "Time [samples]" +ax.ylabel = "EEG [a.u]" +xlims!(ax, 0, 500) +f + +# As visible, the `R` response always follows the `S` response. Due to the "`_`" we have large breaks between the individual sequences. + + -# As visible, the `R` response always follows the `S` response. Due to the "`_`" we have large breaks between the individual sequences. \ No newline at end of file diff --git a/docs/make.jl b/docs/make.jl index 9aceb050..f5a9c249 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -56,7 +56,7 @@ makedocs(; "Generate multi channel data" => "./generated/HowTo/multichannel.md", "Use existing experimental designs & onsets in the simulation" => "./generated/HowTo/predefinedData.md", "Simulated marginal effects" => "./generated/HowTo/getGroundTruth.md", - "Produce specific sequences of events" => "./generated/HowTo/sequence.md", + "Sequence of events (e.g. SCR)" => "./generated/HowTo/sequence.md", ], "Developer documentation" => "developer_docs.md", "API / Docstrings" => "api.md", From ad1373491cbe675cba8629083de292fc3de2e9bb Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Thu, 27 Mar 2025 15:49:42 +0100 Subject: [PATCH 091/156] better docfix --- src/onset.jl | 105 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 76 insertions(+), 29 deletions(-) diff --git a/src/onset.jl b/src/onset.jl index 1d1927b4..1c03e500 100644 --- a/src/onset.jl +++ b/src/onset.jl @@ -225,26 +225,41 @@ end """ UniformOnsetFormula <: AbstractOnset -provide a Uniform Distribution of the inter-event-distances, but with regression formulas. -This is helpful if your overlap/event-distribution should be dependend on some condition, e.g. more overlap in cond='A' than cond='B'. -**width** +Provides a Uniform Distribution of the inter-event-distances, but with regression formulas. - -`width_formula`: choose a formula depending on your `Design`, default `@formula(0~1)` - -`width_β`: Choose a `Vector` of betas, number needs to fit the formula chosen, no default. - -`width_contrasts` (optional): Choose a contrasts-`Dict`ionary according to the StatsModels specifications, default `Dict()`` - -**offset** is the minimal distance. The maximal distance is `offset + width`. +This is helpful if your overlap/event-distribution should be dependent on some condition, e.g. more overlap in cond='A' than cond='B'. +**Offset** affects the minimal distance. The maximal distance is `offset + width`. + +# Fields + +-`offset_formula= `@formula(0~1)`: choose a formula depending on your `design`, default `@formula(0~1)`` +-`offset_β=[0] (optional)`: Choose a `Vector` of betas, number needs to fit the formula chosen, default `[0]` +-`offset_contrasts` (optional): Choose a contrasts-`Dict`ionary according to the StatsModels specifications, default `Dict()` +-`width_formula = `@formula(0~1)`: choose a formula depending on your `Design`, +-`width_β=[0] (optional)`: Choose a `Vector` of betas, number needs to fit the formula chosen. +-`width_contrasts::Dict = Dict()` (optional) : Choose a contrasts-`Dict`ionary according to the StatsModels specifications + +# Combined with ShiftOnsetByOne +Sometimes one wants to bias not the inter-onset-distance prior to the current event, but after the current event. +This is possible by using `ShiftOnsetByOne(UniformOnset(...))`, effectively shifting the inter-onset-distance vector by one. See `?ShiftOnsetByOne` for a visualization. - -`offset_formula`: choose a formula depending on your `design`, default `@formula(0~1)`` - -`offset_β`: Choose a `Vector` of betas, number needs to fit the formula chosen, default `[0]` - -`offset_contrasts` (optional): Choose a contrasts-`Dict`ionary according to the StatsModels specifications, default `Dict()` -See `UniformOnset` for a simplified version without linear regression specifications + +# Examples +```julia-repl +julia> o = UnfoldSim.UniformOnsetFormula( + width_formula = @formula(0 ~ 1 + cond), + width_β = [50, 20], +) + +``` + +See also [`UniformOnset`](@ref) for a simplified version without linear regression specifications """ @with_kw struct UniformOnsetFormula <: AbstractOnset width_formula = @formula(0 ~ 1) - width_β::Vector + width_β::Vector = [0] width_contrasts::Dict = Dict() offset_formula = @formula(0 ~ 1) offset_β::Vector = [0] @@ -268,29 +283,40 @@ end """ + LogNormalOnsetFormula <: AbstractOnset + provide a LogNormal Distribution of the inter-event-distances, but with regression formulas. -This is helpful if your overlap/event-distribution should be dependend on some condition, e.g. more overlap in cond='A' than cond='B'. +This is helpful if your overlap/event-distribution should be dependent on some condition, e.g. more overlap in cond='A' than cond='B'. + +# Fields -**μ** +-`μ_formula= `@formula(0~1)`: choose a formula depending on your `design` +-`μ_β`: Choose a `Vector` of betas, number needs to fit the formula chosen. No reasonable default is available. +-`μ_contrasts` (optional): Choose a contrasts-`Dict`ionary according to the StatsModels specifications. +-`σ_formula = `@formula(0~1)`: choose a formula depending on your `Design`, +-`σ_β`: Choose a `Vector` of betas, number needs to fit the formula chosen. No reasonable default is available here +-`σ_contrasts::Dict = Dict()` (optional) : Choose a contrasts-`Dict`ionary according to the StatsModels specifications - -`μ_formula`: choose a formula depending on your `Design`, default `@formula(0~1)` - -`μ_β`: Choose a `Vector` of betas, number needs to fit the formula chosen, default `[0]` - -`μ_contrasts` (optional): Choose a contrasts-`Dict`ionary according to the StatsModels specifications, default `Dict()`` - - -`σ_formula`: choose a formula depending on your `Design`, default `@formula(0~1)` - -`σ_β`: Choose a `Vector` of betas, number needs to fit the formula chosen, default `[0]` - -`σ_contrasts` (optional): Choose a contrasts-`Dict`ionary according to the StatsModels specifications, default `Dict()`` - -**offset** is the minimal distance. The maximal distance is `offset + width`. +# Combined with ShiftOnsetByOne - -`offset_formula`: choose a formula depending on your `design`, default `@formula(0~1)`` - -`offset_β`: Choose a `Vector` of betas, number needs to fit the formula chosen, default `[0]` - -`offset_contrasts` (optional): Choose a contrasts-`Dict`ionary according to the StatsModels specifications, default `Dict()` +Sometimes one wants to bias not the inter-onset-distance prior to the current event, but after the current event. +This is possible by using `ShiftOnsetByOne(LogNormalOnset(...))`, effectively shifting the inter-onset-distance vector by one. See `?ShiftOnsetByOne` for a visualization. -`truncate_upper` - truncate at some sample, default nothing -See `LogNormalOnset` for a simplified version without linear regression specifications +# Examples +```julia-repl +julia> o = UnfoldSim.LogNormalOnsetFormula( + σ_formula = @formula(0 ~ 1 + cond), + σ_β = [0.25, 0.5], + μ_β = [2], +) + +``` + +See also [`LogNormalOnset`](@ref) for a simplified version without linear regression specifications + + """ @with_kw struct LogNormalOnsetFormula <: AbstractOnset μ_formula = @formula(0 ~ 1) @@ -329,3 +355,24 @@ function simulate_interonset_distances( end +""" + ShiftOnsetByOne <:AbstractOnset +This container AbstractOnset shifts the ShiftOnsetByOne.onset::AbstractOnset inter-onset-distance vector by one, adding a `0` to the front and removing the last `inter-onset-distance`. + +This is helpful in combination with `LogNormalOnsetFormula` or `UniformOnsetFormula`, to generate biased distances not of the previous, but of the next Event. + +Visualized: + +|__1__| A |__2__| B |__3__| C +Right now, the interonset-distances are assigned in the order 1,2,3 inbetween the events A,B,C. After ShiftOnsetByOne we would have + +|__0__| A |__1__| B |__2__| C + +with 0 being a new distance of `0`, and the 3 removed (it would describe the distance after C, because there is nothing coming, the signal is not further prolonged) + +""" +struct ShiftOnsetByOne <: AbstractOnset + onset::AbstractOnset +end +UnfoldSim.simulate_interonset_distances(rng, onsets::ShiftOnsetByOne, design) = + vcat(0, UnfoldSim.simulate_interonset_distances(rng, onsets.onset, design)[1:end-1]) \ No newline at end of file From 755daaae8d6ddf1ed07345d90154a8b496384ae5 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Mon, 31 Mar 2025 23:22:56 +0200 Subject: [PATCH 092/156] typo docstring --- src/component.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/component.jl b/src/component.jl index 38a2bacb..e8f58b6e 100644 --- a/src/component.jl +++ b/src/component.jl @@ -212,7 +212,7 @@ end """ get_basis([rng],c::AbstractComponent) -returns the basis of the component (typically `c.basis`). rng is optional and ignored, but exists to have the same interface as `get_basis(c,design)` +Return the basis of the component (typically `c.basis`). rng is optional and ignored, but exists to have the same interface as `get_basis(c,design)`. """ get_basis(c::AbstractComponent) = c.basis get_basis(rng::AbstractRNG, c::AbstractComponent) = get_basis(c) From 85131847847f493c38f9166f90d17a87c539239b Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Mon, 31 Mar 2025 23:32:11 +0200 Subject: [PATCH 093/156] better effects design docstring --- src/design.jl | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/design.jl b/src/design.jl index 19d2b834..64501be4 100644 --- a/src/design.jl +++ b/src/design.jl @@ -368,13 +368,17 @@ Enforce a sequence of events for each entry of a provided `AbstractDesign`. The sequence string can contain any number of `char`, but the `_` character is used to indicate a break between events without any overlap. +Important: The exact same variable sequence is used for current rows of a design. Only, if you later nest in a `RepeatDesign` then each `RepeatDesign` repetition will gain a new variable sequence. If you need imbalanced designs, please refer to the `ImbalancedDesign` tutorial -Another variable sequence is defined using `[]`. For example, `S[ABC]` would result in any one sequence `SA`, `SB`, `SC`. -Important: The exact same variable sequence is used for current rows of a design. Only, if you later nest in a `RepeatDesign` then each `RepeatDesign` repetition will gain a new variable sequence. If you need imbalanced designs, please refer to the `ImbalancedDesign` tutorial +# Fields +- `design::AbstractDesign`: The design that is generated for every sequence-event +- `sequence::String = ""` (optional): A string of characters depicting sequences. + A variable sequence is defined using `[]`. For example, `S[ABC]` could result in any one sequence `SA`, `SB`, `SC`. + Experimental: It is also possible to define variable length sequences using `{}`. For example, `A{10,20}` would result in a sequence of 10 to 20 `A`'s. -Experimental: It is also possible to define variable length sequences using `{}`. For example, `A{10,20}` would result in a sequence of 10 to 20 `A`'s. Because the number of trials is not defined before actually executing the design, this can lead to problems down the road, if functions require to know the number of trials before generation of the design. +# Examples ```julia design = SingleSubjectDesign(conditions = Dict(:condition => ["one", "two"])) @@ -543,11 +547,19 @@ typical_value(v) = unique(v) Generates events to simulate marginalized effects using an Effects.jl reference-grid dictionary. Every covariate that is in the `EffectsDesign` but not in the `effects_dict` will be set to a `typical_value` (i.e. the mean) -```julia # Example - -effects_dict = Dict{Symbol,Union{<:Number,<:String}}(:conditionA=>[0,1]) -SingleSubjectDesign(...) |> x-> EffectsDesign(x,effects_dict) +```julia +effects_dict = Dict(:conditionA=>[0,1]) +design = SingleSubjectDesign(; conditions = Dict(:conditionA => [0,1,2])) +eff_design = EffectsDesign(design,effects_dict) +generate_events(MersenneTwister(1),eff_design) + +2×1 DataFrame + Row │ conditionA + │ Int64 +─────┼──────────── + 1 │ 0 + 2 │ 1 ``` """ function UnfoldSim.generate_events(rng, t::EffectsDesign) From ef9556d6982593f8b43ec250eef60db1d92ee9af Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Mon, 31 Mar 2025 23:35:09 +0200 Subject: [PATCH 094/156] empty line + documented 2*maxlength --- src/onset.jl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/onset.jl b/src/onset.jl index 1d1927b4..f8875efc 100644 --- a/src/onset.jl +++ b/src/onset.jl @@ -139,9 +139,6 @@ function simulate_interonset_distances(rng, onset::LogNormalOnset, design::Abstr end -#function simulate_interonset_distances(rng, onset::AbstractOnset,design::) - - contains_design(d::AbstractDesign, target::Type) = false contains_design(d::Union{RepeatDesign,SequenceDesign,SubselectDesign}, target::Type) = d.design isa target ? true : contains_design(d.design, target) @@ -155,6 +152,8 @@ Call `simulate_interonset_distances` to generate distances between events and th Please note that this function is mainly for internal use in the context of `simulate` function calls. \n Also note that the accumulation of onsets starts at 1 to avoid indexing problems in the case that the first sampled onset is 0. +In case of a SequenceDesign with a '_' no-overlap indicator, we use twice the `maxlength(components)` as the distance following that sequence character. + # Arguments - `rng`: Random number generator (RNG) to make the process reproducible. - `onset::AbstractOnset`: Inter-onset distance distribution which is passed to `simulate_interonset_distances`. From c6b719f39416e77f6000e756de6448b2f50c3932 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Mon, 31 Mar 2025 23:53:13 +0200 Subject: [PATCH 095/156] added docstrings --- src/sequence.jl | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/sequence.jl b/src/sequence.jl index 8322455f..24a869e0 100644 --- a/src/sequence.jl +++ b/src/sequence.jl @@ -1,3 +1,21 @@ +""" + rand_re(rng::AbstractRNG, machine::Automa.Machine) + +Mimicks a reverse-regex, generating strings from regex instead of matching. Based on Automata.jl + +# Arguments +- `machine::Automa.Machine`: A Automa.Machine, typically output of `Automa.Compile(RE("mystring"))` + +# Returns +- `result::String` : A string following the rules in `Automa.Machine`. `{}` are not supported, but e.g. `+`, `*` + +# Examples +```julia-repl +julia> using Automa +julia> machine = Automa.Compile(Automa.RegExp.RE("b+l+a+)) +julia> rand_re(MersenneTwister(2),machine) +"bbbbblllaaa" +""" function rand_re(rng::AbstractRNG, machine::Automa.Machine) out = IOBuffer() node = machine.start @@ -17,6 +35,31 @@ end sequencestring(rng, d::SequenceDesign) = sequencestring(rng, d.sequence) + +""" + sequencestring(rng, str::String) + sequencestring(rng, dS::SequenceDesign) + sequencestring(rng, dR::RepeatDesign) + +Generates a sequence based on the reverse regex style string in `str`, `dS.sequence` or `dR.design.sequence`. + +Directly converting to Automa.Compileis not possible, as we first need to match & evaluate the curly brackets. We simply detect and expand them. + +# Arguments +- `str::String`: a string mimicking a regex, e.g. "b+l*a{3,4}" should evaluate to "bbbbaaa" or "bllllllllllllaaaa" - but right now we disallow `+` and `*` - we should revisit why exactly though. + +# Returns +- `result::String` : a simulated string + +# Examples +```julia-repl +julia> sequencestring(MersenneTwister(1),"bla{3,4}") +"blaaaa" + +``` + +See also [`rand_re`](@ref) +""" function sequencestring(rng, str::String) #match curly brackets and replace them @assert isnothing(findfirst("*", str)) && isnothing(findfirst("+", str)) "'infinite' sequences currently not supported" From adc225d1ea9177e4acd8d88e6a06fc0623ac2303 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Mon, 31 Mar 2025 23:55:31 +0200 Subject: [PATCH 096/156] slight fix --- src/sequence.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sequence.jl b/src/sequence.jl index 24a869e0..50a78f69 100644 --- a/src/sequence.jl +++ b/src/sequence.jl @@ -46,7 +46,7 @@ Generates a sequence based on the reverse regex style string in `str`, `dS.seque Directly converting to Automa.Compileis not possible, as we first need to match & evaluate the curly brackets. We simply detect and expand them. # Arguments -- `str::String`: a string mimicking a regex, e.g. "b+l*a{3,4}" should evaluate to "bbbbaaa" or "bllllllllllllaaaa" - but right now we disallow `+` and `*` - we should revisit why exactly though. +- `str::String`: a string mimicking a regex, e.g. "b[lL]{3,4}a" should evaluate to e.g. "bLlLLa". E.g. "b+l*a{3,4}" should in principle evaluate to "bbbbaaa" or "bllllllllllllaaaa" - but right now we disallow `+` and `*` - we should revisit why exactly though. # Returns - `result::String` : a simulated string From f0037f7f554b9caf788b95302cd5862699be82a0 Mon Sep 17 00:00:00 2001 From: Benedikt Ehinger Date: Mon, 31 Mar 2025 23:56:51 +0200 Subject: [PATCH 097/156] Update src/design.jl Co-authored-by: Judith Schepers --- src/design.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/design.jl b/src/design.jl index 64501be4..4b162cb8 100644 --- a/src/design.jl +++ b/src/design.jl @@ -365,7 +365,7 @@ end """ SequenceDesign{T} <: AbstractDesign Enforce a sequence of events for each entry of a provided `AbstractDesign`. -The sequence string can contain any number of `char`, but the `_` character is used to indicate a break between events without any overlap. +The sequence string can contain any number of `char`, but the `_` character is used to indicate a break between events without any overlap and has to be at the end of the sequence string. There can only be one `_` character in a sequence string. Important: The exact same variable sequence is used for current rows of a design. Only, if you later nest in a `RepeatDesign` then each `RepeatDesign` repetition will gain a new variable sequence. If you need imbalanced designs, please refer to the `ImbalancedDesign` tutorial From 2b7930c9a11259724b4fd19251848a89c9ee18de Mon Sep 17 00:00:00 2001 From: Benedikt Ehinger Date: Fri, 11 Apr 2025 09:56:56 +0200 Subject: [PATCH 098/156] Update test/design.jl Co-authored-by: Judith Schepers --- test/design.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/design.jl b/test/design.jl index b118ff9f..919aa9c7 100644 --- a/test/design.jl +++ b/test/design.jl @@ -212,7 +212,7 @@ # SingleSubject tests @test size(ef_events_1, 1) == 2 # Test correct length of events df @test unique(ef_events_1[!, :continuous])[1] ≈ mean(range(0, 5, length = 10)) # Test that average is calculated correctly and only one value is present in df - @test size(ef_events_2, 1) == 6 # Test correct length of events df when continuous variable is marginalizes + @test size(ef_events_2, 1) == 6 # Test correct length of events df when one inputs values for continuous variable # MultiSubjectDesign -> not implemented yet, so should error design = MultiSubjectDesign( From 34be071d8fba9a1223e73aec3b54fa8db35c0869 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Fri, 11 Apr 2025 10:33:24 +0200 Subject: [PATCH 099/156] adapt to UnfoldMixedModels --- docs/literate/tutorials/multisubject.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/literate/tutorials/multisubject.jl b/docs/literate/tutorials/multisubject.jl index 61570240..6150c3e8 100644 --- a/docs/literate/tutorials/multisubject.jl +++ b/docs/literate/tutorials/multisubject.jl @@ -10,6 +10,7 @@ ## Load required packages using UnfoldSim using Unfold +using UnfoldMixedModels using CairoMakie using UnfoldMakie using DataFrames From 5908840f282703698a0cd1c7b9119d92b16115d2 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Fri, 11 Apr 2025 10:34:03 +0200 Subject: [PATCH 100/156] remove compat, add unfoldmixedmodels, fix-up groundtrutheffectsdesign tutorial --- docs/Project.toml | 4 +--- docs/literate/HowTo/getGroundTruth.jl | 22 +++++++++++++--------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/docs/Project.toml b/docs/Project.toml index c3cb1bdf..7257e3f5 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -15,7 +15,5 @@ StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f" Unfold = "181c99d8-e21b-4ff3-b70b-c233eddec679" UnfoldMakie = "69a5ce3b-64fb-4f22-ae69-36dd4416af2a" +UnfoldMixedModels = "019ae9e0-8363-565c-86e5-97a5a2fe84f4" UnfoldSim = "ed8ae6d2-84d3-44c6-ab46-0baf21700804" - -[compat] -Unfold = "0.7" \ No newline at end of file diff --git a/docs/literate/HowTo/getGroundTruth.jl b/docs/literate/HowTo/getGroundTruth.jl index e4cc6176..7f117129 100644 --- a/docs/literate/HowTo/getGroundTruth.jl +++ b/docs/literate/HowTo/getGroundTruth.jl @@ -1,12 +1,13 @@ -# # Get ground truth via EffectsDesign +# # Simulate ground truth marginalized Effects -# Usually, to test a method, you want to compare your results to a known ground truth. In UnfoldSim you can obtain your ground truth via the `EffectsDesign`. -# Doing it this way let's you marginalize any effects/ variables of your original design. You can find more on what marginalized effects are here in the [Unfold.jl documentation](https://unfoldtoolbox.github.io/Unfold.jl/dev/generated/HowTo/effects/) +# Often when testing some algorithm, we want to compare our results to a known ground truth. In the case of marginalized effects via the `Unfold.effects`/ `Effects.jl` interface, we can do this using an `EffectsDesign`. +# You can find more on what marginalized effects are here in the [Unfold.jl documentation](https://unfoldtoolbox.github.io/Unfold.jl/dev/generated/HowTo/effects/) # ## Setup using UnfoldSim using Unfold using CairoMakie +using UnfoldMakie using Random # ## Simulation @@ -46,7 +47,7 @@ data, evts = simulate( PinkNoise(), ); -# ## GroundTruthDesign +# ## Simulate marginalized effects directly # To marginalize effects we first have to specify an effects dictionary and subsequently hand this dict plus the original design to `EffectsDesign()` effects_dict = Dict(:condition => ["bike", "face"]) @@ -56,13 +57,13 @@ effects_design = EffectsDesign(design, effects_dict) # !!! note # We only specified the condition levels here, by default every unspecified variable will be set to a "typical" (i.e. the mean) value. -# And finally we can simulate our ground truth ERP with marginalized effects +# And finally we can simulate our ground truth marginal effects gt_data, gt_events = simulate( MersenneTwister(1), effects_design, components, - UniformOnset(; width = 0, offset = 1000), + NoOnset(), NoNoise(), return_epoched = true, ); @@ -93,8 +94,11 @@ m = fit( ef = effects(effects_dict, m); -# Display ground truth and effects, note that the ground truth will be shorter because of the missing baseline. -# If you want to actually compare results with the ground truth, you could either us `UnfoldSim.pad_array()` or forgo the baseline of your estimates. +# !!! note +# The ground truth is shorter because the ground truth typically returns values between `[0 maxlength(components)]`, whereas in our unfold-model we included a baseline period of 0.1s. +# If you want to actually compare results with the ground truth, you could either us `UnfoldSim.pad_array()` or set the Unfold modelling window to `τ=[0,1]` + +plot_erp(ef) lines(ef.yhat) -lines!(gt_effects.yhat) +lines!(UnfoldSim.pad_array(gt_effects.yhat, (Int(-0.1 * 100), 65), 0)) current_figure() \ No newline at end of file From 37539f263193508a71bb7e2f30ff56fee2f038b6 Mon Sep 17 00:00:00 2001 From: Judith Schepers Date: Fri, 11 Apr 2025 11:02:21 +0200 Subject: [PATCH 101/156] JuliaFormatter: Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/literate/reference/onsettypes.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/literate/reference/onsettypes.jl b/docs/literate/reference/onsettypes.jl index 4c2c3b1d..3eee6cd8 100644 --- a/docs/literate/reference/onsettypes.jl +++ b/docs/literate/reference/onsettypes.jl @@ -298,8 +298,8 @@ onsets = UnfoldSim.simulate_interonset_distances(MersenneTwister(42), o, design) f = Figure() ax = f[1, 1] = Axis(f) -hist!(ax, onsets[events.cond.=="A"], bins = range(0, 100, step = 1), label = "cond: A") -hist!(ax, onsets[events.cond.=="B"], bins = range(0, 100, step = 1), label = "cond: B") +hist!(ax, onsets[events.cond .== "A"], bins = range(0, 100, step = 1), label = "cond: A") +hist!(ax, onsets[events.cond .== "B"], bins = range(0, 100, step = 1), label = "cond: B") axislegend(ax) f From 2b8f60098803d44887dda5b4c13fda7e8ccd5b68 Mon Sep 17 00:00:00 2001 From: Judith Schepers Date: Fri, 11 Apr 2025 11:06:24 +0200 Subject: [PATCH 102/156] JuliaFormatter: Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src/onset.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/onset.jl b/src/onset.jl index f8875efc..ae99eee3 100644 --- a/src/onset.jl +++ b/src/onset.jl @@ -326,5 +326,3 @@ function simulate_interonset_distances( #@debug reduce(hcat, rand.(deepcopy(rng), funs, 1)) return Int.(round.(offsets .+ reduce(vcat, rand.(deepcopy(rng), funs, 1)))) end - - From c9afd8a8de293de9b747e7620856d49c346d2a57 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Fri, 11 Apr 2025 11:12:37 +0200 Subject: [PATCH 103/156] fix up ground truth marginal effects tutorial --- docs/literate/HowTo/getGroundTruth.jl | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/literate/HowTo/getGroundTruth.jl b/docs/literate/HowTo/getGroundTruth.jl index 7f117129..16d241b4 100644 --- a/docs/literate/HowTo/getGroundTruth.jl +++ b/docs/literate/HowTo/getGroundTruth.jl @@ -22,7 +22,7 @@ design = :condition => ["bike", "face"], :continuous => range(0, 5, length = 10), ), - ) |> x -> RepeatDesign(x, 100); + ) |> x -> RepeatDesign(x, 5); # **n170** has a condition effect, faces are more negative than bikes n1 = LinearModelComponent(; @@ -98,7 +98,12 @@ ef = effects(effects_dict, m); # The ground truth is shorter because the ground truth typically returns values between `[0 maxlength(components)]`, whereas in our unfold-model we included a baseline period of 0.1s. # If you want to actually compare results with the ground truth, you could either us `UnfoldSim.pad_array()` or set the Unfold modelling window to `τ=[0,1]` -plot_erp(ef) -lines(ef.yhat) -lines!(UnfoldSim.pad_array(gt_effects.yhat, (Int(-0.1 * 100), 65), 0)) -current_figure() \ No newline at end of file +gt_effects.type .= "UnfoldSim effects" +ef.type .= "Unfold effects" + +gt_effects.time = gt_effects.time ./ 100 .- 1 / 100 +ef.continuous .= 2.5 # needed to be able to easily merge the two dataframes +comb = vcat(gt_effects, ef) +plot_erp(comb; mapping = (; color = :type, col = :condition)) + +# The simulated ground truth marginal effects, and the fitted marginal effects look similar as expected, but the fitted has some additional noise because of finite data (also as expected). \ No newline at end of file From a8d4d083566fb2e2b8fbecc44b9fb211b360ed47 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Thu, 27 Mar 2025 14:38:39 +0100 Subject: [PATCH 104/156] fix size of sequencedesign, added rng to all size/length functions --- src/component.jl | 6 +++--- src/design.jl | 35 ++++++++++++++++++----------------- src/onset.jl | 10 ++++++++-- src/simulation.jl | 5 +++-- 4 files changed, 32 insertions(+), 24 deletions(-) diff --git a/src/component.jl b/src/component.jl index e8f58b6e..30164977 100644 --- a/src/component.jl +++ b/src/component.jl @@ -620,7 +620,7 @@ end Initializes an Array with zeros. Returns either a 2-dimensional for component-length x length(design), or a 3-D for channels x component-length x length(design) """ -function init_epoch_data(components, design) +function init_epoch_data(rng, components, design) max_offset = maxoffset(components) min_offset = minoffset(components) range_offset = (max_offset - min_offset) @@ -631,14 +631,14 @@ function init_epoch_data(components, design) length(design), ) else - epoch_data = zeros(maxlength(components) + range_offset, length(design)) + epoch_data = zeros(maxlength(components) + range_offset, length(rng, design)) end return epoch_data end function simulate_responses(rng, event_component_dict::Dict, s::Simulation) #@debug rng.state - epoch_data = init_epoch_data(event_component_dict, s.design) + epoch_data = init_epoch_data(deepcopy(rng), event_component_dict, s.design) #@debug rng.state evts = generate_events(deepcopy(rng), s.design) #@debug rng.state diff --git a/src/design.jl b/src/design.jl index 4b162cb8..08f72aa5 100644 --- a/src/design.jl +++ b/src/design.jl @@ -178,16 +178,30 @@ end #---- # Design helper functions -"Return the dimensions of the experiment design." -size(design::MultiSubjectDesign) = (design.n_items, design.n_subjects) -size(design::SingleSubjectDesign) = (*(length.(values(design.conditions))...),) +""" + Base.size([rng],design::AbstractDesign) + +Return the dimensions of the experiment design. For some designs (SequenceDesign), rng is required, as the size is only determined by the design, which in turn can be probabilistic. +""" +Base.size(design::MultiSubjectDesign) = (design.n_items, design.n_subjects) +Base.size(design::SingleSubjectDesign) = (*(length.(values(design.conditions))...),) Base.size(design::RepeatDesign{MultiSubjectDesign}) = size(design.design) .* (design.repeat, 1) Base.size(design::RepeatDesign{SingleSubjectDesign}) = size(design.design) .* design.repeat +Base.size(rng::AbstractRNG, design::AbstractDesign) = size(design) # by default we drop the RNG + +Base.size( + rng::AbstractRNG, + design::Union{<:SequenceDesign,<:SubselectDesign,<:RepeatDesign{<:SequenceDesign}}, +) = size(generate_events(rng, design), 1) + + "Length is the product of all dimensions and equals the number of events in the corresponding events dataframe." length(design::AbstractDesign) = *(size(design)...) +length(rng, design::AbstractDesign) = *(size(rng, design)...) + """ apply_event_order_function(fun, rng, events) @@ -481,7 +495,6 @@ In case of `MultiSubjectDesign`, sort by subject. \\ Please note that when using an `event_order_function`(e.g. `shuffle`) in a `RepeatDesign`, the corresponding RNG is shared across repetitions and not deep-copied for each repetition. As a result, the order of events will differ for each repetition. """ - function UnfoldSim.generate_events(rng::AbstractRNG, design::RepeatDesign) df = map(x -> generate_events(rng, design.design), 1:design.repeat) |> x -> vcat(x...) @@ -525,7 +538,7 @@ struct EffectsDesign <: AbstractDesign effects_dict::Dict end EffectsDesign(design::MultiSubjectDesign, effects_dict::Dict) = error("not yet implemented") -UnfoldSim.size(t::EffectsDesign) = size(generate_events(t), 1) +UnfoldSim.size(rng, t::EffectsDesign) = size(generate_events(rng, t), 1) """ expand_grid(design) @@ -573,15 +586,3 @@ function UnfoldSim.generate_events(rng, t::EffectsDesign) return expand_grid(effects_dict) end - -#Base.size(design::SequenceDesign) = -#size(design.design) .* length(replace(design.sequence, "_" => "",r"\{.*\}"=>"")) - -#Base.size(design::) = size(design.design) .* design.repeat - -# --- -# Size for Sequence design -# No way to find out what size it is without actually generating first... -Base.size( - design::Union{<:SequenceDesign,<:SubselectDesign,<:RepeatDesign{<:SequenceDesign}}, -) = size(generate_events(design), 1) diff --git a/src/onset.jl b/src/onset.jl index ae99eee3..4d632a5b 100644 --- a/src/onset.jl +++ b/src/onset.jl @@ -125,12 +125,18 @@ function simulate_interonset_distances end function simulate_interonset_distances(rng, onset::UniformOnset, design::AbstractDesign) return Int.( - round.(rand(deepcopy(rng), onset.offset:(onset.offset+onset.width), size(design))) + round.( + rand( + deepcopy(rng), + onset.offset:(onset.offset+onset.width), + size(deepcopy(rng), design), + ) + ) ) end function simulate_interonset_distances(rng, onset::LogNormalOnset, design::AbstractDesign) - s = size(design) + s = size(deepcopy(rng), design) fun = LogNormal(onset.μ, onset.σ) if !isnothing(onset.truncate_upper) fun = truncated(fun; upper = onset.truncate_upper) diff --git a/src/simulation.jl b/src/simulation.jl index 62717da4..623a6ffb 100755 --- a/src/simulation.jl +++ b/src/simulation.jl @@ -241,8 +241,9 @@ function create_continuous_signal(rng, responses, simulation) (; design, components, onset, noisetype) = simulation - n_subjects = length(size(design)) == 1 ? 1 : size(design)[2] - n_trials = size(design)[1] + n_subjects = + length(size(deepcopy(rng), design)) == 1 ? 1 : size(deepcopy(rng), design)[2] + n_trials = size(deepcopy(rng), design)[1] n_chan = n_channels(components) # we only need to simulate onsets & pull everything together, if we From d6d64a655bce29227e009eefe50a72e5d042d663 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Thu, 27 Mar 2025 14:40:31 +0100 Subject: [PATCH 105/156] fix size of sequencedesign, added rng to all size/length functions, forgot one --- src/component.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/component.jl b/src/component.jl index 30164977..b10d5af9 100644 --- a/src/component.jl +++ b/src/component.jl @@ -628,7 +628,7 @@ function init_epoch_data(rng, components, design) epoch_data = zeros( n_channels(components), maxlength(components) + range_offset, - length(design), + length(deepcopy(rng), design), ) else epoch_data = zeros(maxlength(components) + range_offset, length(rng, design)) From 5416d4dc1f7d273d785ef0c6b94305225da07dc5 Mon Sep 17 00:00:00 2001 From: Judith Schepers Date: Fri, 11 Apr 2025 11:44:15 +0200 Subject: [PATCH 106/156] JuliaFormatter: Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src/design.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/design.jl b/src/design.jl index 08f72aa5..1a53278a 100644 --- a/src/design.jl +++ b/src/design.jl @@ -585,4 +585,3 @@ function UnfoldSim.generate_events(rng, t::EffectsDesign) end return expand_grid(effects_dict) end - From 3947c57709f36cb3100453b28cc4cb3b06074676 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Fri, 11 Apr 2025 11:48:00 +0200 Subject: [PATCH 107/156] workaround UnfoldMakie fix --- docs/literate/HowTo/componentfunction.jl | 2 +- docs/literate/HowTo/getGroundTruth.jl | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/literate/HowTo/componentfunction.jl b/docs/literate/HowTo/componentfunction.jl index 6ebcb65c..71c8b3b6 100644 --- a/docs/literate/HowTo/componentfunction.jl +++ b/docs/literate/HowTo/componentfunction.jl @@ -54,7 +54,7 @@ df.category = repeat(generate_events(design).category, inner = size(erp, 1)) plot_erp!( f[1, 1], df, - mapping = (; group = :group => nonnumeric, color = :duration, col = :category), + mapping = (; group = :group => nonnumeric, col = :category), # color = :duration, fails right nowUnfoldMakie#353 layout = (; legend_position = :left), colorbar = (; label = "Duration"), ) diff --git a/docs/literate/HowTo/getGroundTruth.jl b/docs/literate/HowTo/getGroundTruth.jl index 16d241b4..5d2f0b43 100644 --- a/docs/literate/HowTo/getGroundTruth.jl +++ b/docs/literate/HowTo/getGroundTruth.jl @@ -3,13 +3,20 @@ # Often when testing some algorithm, we want to compare our results to a known ground truth. In the case of marginalized effects via the `Unfold.effects`/ `Effects.jl` interface, we can do this using an `EffectsDesign`. # You can find more on what marginalized effects are here in the [Unfold.jl documentation](https://unfoldtoolbox.github.io/Unfold.jl/dev/generated/HowTo/effects/) -# ## Setup +# ### Setup +# ```@raw html +#
+# Click to expand +# ``` +## Load required packages using UnfoldSim using Unfold using CairoMakie using UnfoldMakie using Random - +# ```@raw html +#
+# ``` # ## Simulation # First let's make up a SingleSubject simulation From 02d38c7c58f110d78550981b895e35158d08fdea Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Fri, 11 Apr 2025 12:03:24 +0200 Subject: [PATCH 108/156] fix length/size after typedef --- src/design.jl | 49 +++++++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/src/design.jl b/src/design.jl index 1a53278a..a4a0f065 100644 --- a/src/design.jl +++ b/src/design.jl @@ -178,30 +178,6 @@ end #---- # Design helper functions -""" - Base.size([rng],design::AbstractDesign) - -Return the dimensions of the experiment design. For some designs (SequenceDesign), rng is required, as the size is only determined by the design, which in turn can be probabilistic. -""" -Base.size(design::MultiSubjectDesign) = (design.n_items, design.n_subjects) -Base.size(design::SingleSubjectDesign) = (*(length.(values(design.conditions))...),) - -Base.size(design::RepeatDesign{MultiSubjectDesign}) = - size(design.design) .* (design.repeat, 1) -Base.size(design::RepeatDesign{SingleSubjectDesign}) = size(design.design) .* design.repeat - -Base.size(rng::AbstractRNG, design::AbstractDesign) = size(design) # by default we drop the RNG - -Base.size( - rng::AbstractRNG, - design::Union{<:SequenceDesign,<:SubselectDesign,<:RepeatDesign{<:SequenceDesign}}, -) = size(generate_events(rng, design), 1) - - -"Length is the product of all dimensions and equals the number of events in the corresponding events dataframe." -length(design::AbstractDesign) = *(size(design)...) -length(rng, design::AbstractDesign) = *(size(rng, design)...) - """ apply_event_order_function(fun, rng, events) @@ -585,3 +561,28 @@ function UnfoldSim.generate_events(rng, t::EffectsDesign) end return expand_grid(effects_dict) end + + +""" + Base.size([rng],design::AbstractDesign) + +Return the dimensions of the experiment design. For some designs (SequenceDesign), rng is required, as the size is only determined by the design, which in turn can be probabilistic. +""" +Base.size(design::MultiSubjectDesign) = (design.n_items, design.n_subjects) +Base.size(design::SingleSubjectDesign) = (*(length.(values(design.conditions))...),) + +Base.size(design::RepeatDesign{MultiSubjectDesign}) = + size(design.design) .* (design.repeat, 1) +Base.size(design::RepeatDesign{SingleSubjectDesign}) = size(design.design) .* design.repeat + +Base.size(rng::AbstractRNG, design::AbstractDesign) = size(design) # by default we drop the RNG + +Base.size( + rng::AbstractRNG, + design::Union{<:SequenceDesign,<:SubselectDesign,<:RepeatDesign{<:SequenceDesign}}, +) = size(generate_events(rng, design), 1) + + +"Length is the product of all dimensions and equals the number of events in the corresponding events dataframe." +length(design::AbstractDesign) = *(size(design)...) +length(rng, design::AbstractDesign) = *(size(rng, design)...) From 7a0f81fb4b6c4bbf2d3688fe3cca98fb20aff63d Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Fri, 11 Apr 2025 12:04:18 +0200 Subject: [PATCH 109/156] missing rng --- src/component.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/component.jl b/src/component.jl index b10d5af9..f0b56d57 100644 --- a/src/component.jl +++ b/src/component.jl @@ -598,7 +598,7 @@ function simulate_responses( components::Vector{<:AbstractComponent}, simulation::Simulation, ) - epoch_data = init_epoch_data(components, simulation.design) + epoch_data = init_epoch_data(deepcopy(rng), components, simulation.design) simulate_responses!(rng, epoch_data, components, simulation) return epoch_data end From b345da48f5bfa6f58990723d8c9bc2fa48cd8d13 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Fri, 11 Apr 2025 12:13:47 +0200 Subject: [PATCH 110/156] fix a missing rng --- src/simulation.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/simulation.jl b/src/simulation.jl index 623a6ffb..d9d79511 100755 --- a/src/simulation.jl +++ b/src/simulation.jl @@ -145,7 +145,8 @@ function simulate(rng::AbstractRNG, simulation::Simulation; return_epoched::Bool # such that the resulting dimensions are dimensions: channels x times x trials x subjects # TODO: This assumes a balanced design, but create_continuous_signal also assumes this, so we should be fine ;) size_responses = size(responses) - signal = reshape(responses, size_responses[1:end-1]..., size(design)...) + signal = + reshape(responses, size_responses[1:end-1]..., size(deepcopy(rng), design)...) else # if there is an onset distribution given the next step is to create a continuous signal signal, latencies = create_continuous_signal(deepcopy(rng), responses, simulation) events.latency = latencies From a8c3801e1f2851697264595ada26882b27faff5a Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Fri, 11 Apr 2025 12:45:47 +0200 Subject: [PATCH 111/156] un-ambiguate size --- src/design.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/design.jl b/src/design.jl index a4a0f065..44b87e50 100644 --- a/src/design.jl +++ b/src/design.jl @@ -585,4 +585,4 @@ Base.size( "Length is the product of all dimensions and equals the number of events in the corresponding events dataframe." length(design::AbstractDesign) = *(size(design)...) -length(rng, design::AbstractDesign) = *(size(rng, design)...) +length(rng::AbstractRNG, design::AbstractDesign) = *(size(rng, design)...) From b43a5a1382487150390c7eaaac3d2fa4f122fffb Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Fri, 11 Apr 2025 12:50:09 +0200 Subject: [PATCH 112/156] export ShiftOnsetByOne --- src/UnfoldSim.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/UnfoldSim.jl b/src/UnfoldSim.jl index 655c385d..13a95257 100644 --- a/src/UnfoldSim.jl +++ b/src/UnfoldSim.jl @@ -65,7 +65,8 @@ export simulate, export pad_array # export Offsets -export UniformOnset, LogNormalOnset, NoOnset, UniformOnsetFormula, LogNormalOnsetFormula +export UniformOnset, + LogNormalOnset, NoOnset, UniformOnsetFormula, LogNormalOnsetFormula, ShiftOnsetByOne # re-export StatsModels export DummyCoding, EffectsCoding From a6f4e3c5f642cad35de666eff28e6bd9c1a75c2d Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Fri, 11 Apr 2025 12:51:25 +0200 Subject: [PATCH 113/156] add a unittest --- test/onset.jl | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/onset.jl b/test/onset.jl index d2620c5a..2ae563c9 100644 --- a/test/onset.jl +++ b/test/onset.jl @@ -102,5 +102,22 @@ + end + + @testset "ShiftOnset" begin + o = UniformOnset(width = 50, offset = 10) + events = generate_events(design) + without = UnfoldSim.simulate_interonset_distances(StableRNG(1), o, design) + with = UnfoldSim.simulate_interonset_distances( + StableRNG(1), + ShiftOnsetByOne(o), + design, + ) + # ShiftOnsetByOne adds the first onset to the interonset-distances, thereby not startin + @test with[1] == 0 + + @test without[1:end-1] == with[2:end] + + end end From 0c78de8a77ad4a27876ce58db220dadfa8f2464a Mon Sep 17 00:00:00 2001 From: jschepers Date: Tue, 15 Apr 2025 15:58:26 +0200 Subject: [PATCH 114/156] Fix method ambiguity --- src/design.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/design.jl b/src/design.jl index 44b87e50..08fdd97b 100644 --- a/src/design.jl +++ b/src/design.jl @@ -514,7 +514,7 @@ struct EffectsDesign <: AbstractDesign effects_dict::Dict end EffectsDesign(design::MultiSubjectDesign, effects_dict::Dict) = error("not yet implemented") -UnfoldSim.size(rng, t::EffectsDesign) = size(generate_events(rng, t), 1) +UnfoldSim.size(rng::AbstractRNG, t::EffectsDesign) = size(generate_events(rng, t), 1) """ expand_grid(design) From 878c019a00022d45736accf3eb04ea04b1938548 Mon Sep 17 00:00:00 2001 From: jschepers Date: Tue, 15 Apr 2025 17:23:04 +0200 Subject: [PATCH 115/156] Fix UnfoldMixedModels name space issue and add RNGs --- docs/literate/tutorials/multisubject.jl | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/literate/tutorials/multisubject.jl b/docs/literate/tutorials/multisubject.jl index 6150c3e8..b91d95ab 100644 --- a/docs/literate/tutorials/multisubject.jl +++ b/docs/literate/tutorials/multisubject.jl @@ -10,10 +10,11 @@ ## Load required packages using UnfoldSim using Unfold -using UnfoldMixedModels +import UnfoldMixedModels using CairoMakie using UnfoldMakie using DataFrames +using StableRNGs # ```@raw html #
#
@@ -64,7 +65,8 @@ signal = MixedModelComponent(; ) # and simulate! -data, evts = simulate(design, signal, NoOnset(), NoNoise(), return_epoched = true); +data, evts = + simulate(StableRNG(1), design, signal, NoOnset(), NoNoise(), return_epoched = true); # We get data with 50 samples (our `basis` from above), with `4` items and 20 subjects. We get items and subjects separately because we chose no-overlap (via `NoOnset`) and `return_epoched = true``. size(data) @@ -95,7 +97,8 @@ f # Let's continue our tutorial and simulate overlapping signals instead. # # We replace the `NoOnset` with an `UniformOnset` with 20 to 70 samples between subsequent events. We further remove the `return_epoched`, because we want to have continuous data for now. -data, evts = simulate(design, signal, UniformOnset(offset = 20, width = 50), NoNoise()); +data, evts = + simulate(StableRNG(1), design, signal, UniformOnset(offset = 20, width = 50), NoNoise()); size(data) # with the first dimension being continuous data, and the latter still the subjects. @@ -109,6 +112,7 @@ series(data', solid_color = :black) # # Analyzing these data with Unfold.jl # We will analyze these data using the `Unfold.jl` toolbox. While preliminary support for deconvolution (overlap correction) for mixed models is available, here we will not make use of it, but rather apply a MixedModel to each timepoint, following the Mass-univariate approach. data, evts = simulate( + StableRNG(1), design, signal, UniformOnset(offset = 20, width = 50), From 6109b137a3821d6b5d0870f05864512dcc99b106 Mon Sep 17 00:00:00 2001 From: jschepers Date: Wed, 11 Jun 2025 17:28:58 +0200 Subject: [PATCH 116/156] Format docstrings --- src/onset.jl | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/src/onset.jl b/src/onset.jl index 1c03e500..430d175a 100644 --- a/src/onset.jl +++ b/src/onset.jl @@ -226,36 +226,34 @@ end """ UniformOnsetFormula <: AbstractOnset -Provides a Uniform Distribution of the inter-event-distances, but with regression formulas. +Provides a Uniform Distribution of the inter-event distances, but with regression formulas. -This is helpful if your overlap/event-distribution should be dependent on some condition, e.g. more overlap in cond='A' than cond='B'. -**Offset** affects the minimal distance. The maximal distance is `offset + width`. +This is helpful if your overlap/event-distribution should be dependent on some condition, e.g. more overlap in cond = 'A' than cond = 'B'. +`Offset` affects the minimal distance. The maximal distance is `offset + width`. # Fields --`offset_formula= `@formula(0~1)`: choose a formula depending on your `design`, default `@formula(0~1)`` --`offset_β=[0] (optional)`: Choose a `Vector` of betas, number needs to fit the formula chosen, default `[0]` --`offset_contrasts` (optional): Choose a contrasts-`Dict`ionary according to the StatsModels specifications, default `Dict()` --`width_formula = `@formula(0~1)`: choose a formula depending on your `Design`, --`width_β=[0] (optional)`: Choose a `Vector` of betas, number needs to fit the formula chosen. --`width_contrasts::Dict = Dict()` (optional) : Choose a contrasts-`Dict`ionary according to the StatsModels specifications +- `offset_formula = @formula(0~1)`: Choose a formula depending on your `design`. +- `offset_β = [0] `(optional): Choose a `Vector` of betas. The number of betas needs to fit the formula chosen. +- `offset_contrasts = Dict()` (optional): Choose a contrasts-`Dict`ionary according to the StatsModels specifications. +- `width_formula = `@formula(0~1)`: Choose a formula depending on your `Design`. +- `width_β = [0] (optional)`: Choose a `Vector` of betas, number needs to fit the formula chosen. +- `width_contrasts::Dict = Dict()` (optional) : Choose a contrasts-`Dict`ionary according to the StatsModels specifications. -# Combined with ShiftOnsetByOne -Sometimes one wants to bias not the inter-onset-distance prior to the current event, but after the current event. +# Combined with [ShiftOnsetByOne](@ref) +Sometimes one wants to bias not the inter-onset distance prior to the current event, but after the current event. This is possible by using `ShiftOnsetByOne(UniformOnset(...))`, effectively shifting the inter-onset-distance vector by one. See `?ShiftOnsetByOne` for a visualization. - # Examples ```julia-repl julia> o = UnfoldSim.UniformOnsetFormula( width_formula = @formula(0 ~ 1 + cond), width_β = [50, 20], ) - ``` -See also [`UniformOnset`](@ref) for a simplified version without linear regression specifications +See also [`UniformOnset`](@ref) for a simplified version without linear regression specifications. """ @with_kw struct UniformOnsetFormula <: AbstractOnset width_formula = @formula(0 ~ 1) @@ -306,15 +304,14 @@ This is possible by using `ShiftOnsetByOne(LogNormalOnset(...))`, effectively sh # Examples ```julia-repl -julia> o = UnfoldSim.LogNormalOnsetFormula( +julia> o = LogNormalOnsetFormula( σ_formula = @formula(0 ~ 1 + cond), σ_β = [0.25, 0.5], μ_β = [2], ) - ``` -See also [`LogNormalOnset`](@ref) for a simplified version without linear regression specifications +See also [`LogNormalOnset`](@ref) for a simplified version without linear regression specifications. """ @@ -357,22 +354,24 @@ end """ ShiftOnsetByOne <:AbstractOnset -This container AbstractOnset shifts the ShiftOnsetByOne.onset::AbstractOnset inter-onset-distance vector by one, adding a `0` to the front and removing the last `inter-onset-distance`. -This is helpful in combination with `LogNormalOnsetFormula` or `UniformOnsetFormula`, to generate biased distances not of the previous, but of the next Event. +This container AbstractOnset shifts the ShiftOnsetByOne.onset::AbstractOnset inter-onset-distance vector by one, adding a `0` to the front and removing the last `inter-onset distance`. + +This is helpful in combination with `LogNormalOnsetFormula` or `UniformOnsetFormula`, to generate biased distances not of the previous, but of the next event. Visualized: |__1__| A |__2__| B |__3__| C -Right now, the interonset-distances are assigned in the order 1,2,3 inbetween the events A,B,C. After ShiftOnsetByOne we would have +Right now, the inter-onset distances are assigned in the order 1,2,3 inbetween the events A,B,C. After ShiftOnsetByOne we would have |__0__| A |__1__| B |__2__| C -with 0 being a new distance of `0`, and the 3 removed (it would describe the distance after C, because there is nothing coming, the signal is not further prolonged) +with 0 being a new distance of `0`, and the 3 removed (it would describe the distance after C, because there is nothing coming, the signal is not further prolonged). """ struct ShiftOnsetByOne <: AbstractOnset onset::AbstractOnset end + UnfoldSim.simulate_interonset_distances(rng, onsets::ShiftOnsetByOne, design) = vcat(0, UnfoldSim.simulate_interonset_distances(rng, onsets.onset, design)[1:end-1]) \ No newline at end of file From fb14397de5b1ae8d0c92f2feccf7071c2d114a80 Mon Sep 17 00:00:00 2001 From: jschepers Date: Wed, 11 Jun 2025 17:41:55 +0200 Subject: [PATCH 117/156] Adapt CI workflow to run for PRs to all branches not just main --- .github/workflows/CI.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 6788ea41..0b7632e8 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -5,8 +5,6 @@ on: - main tags: '*' pull_request: - branches: - - main concurrency: # Skip intermediate builds: always. # Cancel intermediate builds: only if it is a pull request build. From 89ee8e0bd8462ba3b326294b3ccff12776e2b800 Mon Sep 17 00:00:00 2001 From: jschepers Date: Wed, 11 Jun 2025 17:58:19 +0200 Subject: [PATCH 118/156] Fix ShiftOnsetByOne test --- test/onset.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/onset.jl b/test/onset.jl index 2ae563c9..81558f7b 100644 --- a/test/onset.jl +++ b/test/onset.jl @@ -105,8 +105,12 @@ end @testset "ShiftOnset" begin + design = + SingleSubjectDesign(conditions = Dict(:cond => ["A", "B"])) |> + x -> RepeatDesign(x, 100) + o = UniformOnset(width = 50, offset = 10) - events = generate_events(design) + without = UnfoldSim.simulate_interonset_distances(StableRNG(1), o, design) with = UnfoldSim.simulate_interonset_distances( StableRNG(1), From 8f27939339c17810d820e21220a1c7c6153f2a68 Mon Sep 17 00:00:00 2001 From: jschepers Date: Thu, 12 Jun 2025 16:46:36 +0200 Subject: [PATCH 119/156] More docstring formatting + fixing ambiguous links --- src/onset.jl | 44 +++++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/onset.jl b/src/onset.jl index 430d175a..2da1d681 100644 --- a/src/onset.jl +++ b/src/onset.jl @@ -21,7 +21,7 @@ UniformOnset offset: Int64 5 ``` -See also [`LogNormalOnset`](@ref), [`NoOnset`](@ref). +See also [`LogNormalOnset`](@ref UnfoldSim.LogNormalOnset), [`NoOnset`](@ref). """ @with_kw struct UniformOnset <: AbstractOnset width = 50 # how many samples jitter? @@ -51,7 +51,7 @@ LogNormalOnset truncate_upper: Int64 25 ``` -See also [`UniformOnset`](@ref), [`NoOnset`](@ref). +See also [`UniformOnset`](@ref UnfoldSim.UniformOnset), [`NoOnset`](@ref). """ @with_kw struct LogNormalOnset <: AbstractOnset μ::Any # mean @@ -71,7 +71,7 @@ julia> onset_distribution = NoOnset() NoOnset() ``` -See also [`UniformOnset`](@ref), [`LogNormalOnset`](@ref). +See also [`UniformOnset`](@ref UnfoldSim.UniformOnset), [`LogNormalOnset`](@ref UnfoldSim.LogNormalOnset). """ struct NoOnset <: AbstractOnset end @@ -234,10 +234,10 @@ This is helpful if your overlap/event-distribution should be dependent on some c # Fields - `offset_formula = @formula(0~1)`: Choose a formula depending on your `design`. -- `offset_β = [0] `(optional): Choose a `Vector` of betas. The number of betas needs to fit the formula chosen. -- `offset_contrasts = Dict()` (optional): Choose a contrasts-`Dict`ionary according to the StatsModels specifications. +- `offset_β::Vector = [0] `(optional): Choose a `Vector` of betas. The number of betas needs to fit the formula chosen. +- `offset_contrasts::Dict = Dict()` (optional): Choose a contrasts-`Dict`ionary according to the StatsModels specifications. - `width_formula = `@formula(0~1)`: Choose a formula depending on your `Design`. -- `width_β = [0] (optional)`: Choose a `Vector` of betas, number needs to fit the formula chosen. +- `width_β::Vector = [0] (optional)`: Choose a `Vector` of betas, number needs to fit the formula chosen. - `width_contrasts::Dict = Dict()` (optional) : Choose a contrasts-`Dict`ionary according to the StatsModels specifications. # Combined with [ShiftOnsetByOne](@ref) @@ -253,7 +253,7 @@ julia> o = UnfoldSim.UniformOnsetFormula( ) ``` -See also [`UniformOnset`](@ref) for a simplified version without linear regression specifications. +See also [`UniformOnset`](@ref UnfoldSim.UniformOnset) for a simplified version without linear regression specifications. """ @with_kw struct UniformOnsetFormula <: AbstractOnset width_formula = @formula(0 ~ 1) @@ -284,21 +284,21 @@ end LogNormalOnsetFormula <: AbstractOnset -provide a LogNormal Distribution of the inter-event-distances, but with regression formulas. -This is helpful if your overlap/event-distribution should be dependent on some condition, e.g. more overlap in cond='A' than cond='B'. +Provide a Log-normal Distribution of the inter-event distances, but with regression formulas. +This is helpful if your overlap/event-distribution should be dependent on some condition, e.g. more overlap in cond = 'A' than cond = 'B'. # Fields --`μ_formula= `@formula(0~1)`: choose a formula depending on your `design` --`μ_β`: Choose a `Vector` of betas, number needs to fit the formula chosen. No reasonable default is available. --`μ_contrasts` (optional): Choose a contrasts-`Dict`ionary according to the StatsModels specifications. --`σ_formula = `@formula(0~1)`: choose a formula depending on your `Design`, --`σ_β`: Choose a `Vector` of betas, number needs to fit the formula chosen. No reasonable default is available here --`σ_contrasts::Dict = Dict()` (optional) : Choose a contrasts-`Dict`ionary according to the StatsModels specifications +- `μ_formula = @formula(0~1)` (optional): Choose a formula depending on your `design` +- `μ_β::Vector`: Choose a `Vector` of betas, number needs to fit the formula chosen. +- `μ_contrasts::Dict = Dict()` (optional): Choose a contrasts-`Dict`ionary according to the StatsModels specifications. +- `σ_formula = @formula(0~1)` (optional): Choose a formula depending on your `Design`. +- `σ_β::Vector`: Choose a `Vector` of betas, number needs to fit the formula chosen. +- `σ_contrasts::Dict = Dict()` (optional) : Choose a contrasts-`Dict`ionary according to the StatsModels specifications. -# Combined with ShiftOnsetByOne +# Combined with [ShiftOnsetByOne](@ref) -Sometimes one wants to bias not the inter-onset-distance prior to the current event, but after the current event. +Sometimes one wants to bias not the inter-onset distance prior to the current event, but after the current event. This is possible by using `ShiftOnsetByOne(LogNormalOnset(...))`, effectively shifting the inter-onset-distance vector by one. See `?ShiftOnsetByOne` for a visualization. @@ -311,9 +311,7 @@ julia> o = LogNormalOnsetFormula( ) ``` -See also [`LogNormalOnset`](@ref) for a simplified version without linear regression specifications. - - +See also [`LogNormalOnset`](@ref UnfoldSim.LogNormalOnset) for a simplified version without linear regression specifications. """ @with_kw struct LogNormalOnsetFormula <: AbstractOnset μ_formula = @formula(0 ~ 1) @@ -357,14 +355,14 @@ end This container AbstractOnset shifts the ShiftOnsetByOne.onset::AbstractOnset inter-onset-distance vector by one, adding a `0` to the front and removing the last `inter-onset distance`. -This is helpful in combination with `LogNormalOnsetFormula` or `UniformOnsetFormula`, to generate biased distances not of the previous, but of the next event. +This is helpful in combination with [`LogNormalOnsetFormula`](@ref) or [`UniformOnsetFormula`](@ref), to generate biased distances not of the previous, but of the next event. Visualized: -|__1__| A |__2__| B |__3__| C +|\\_\\_1\\_\\_| A |\\_\\_2\\_\\_| B |\\_\\_3\\_\\_| C \n Right now, the inter-onset distances are assigned in the order 1,2,3 inbetween the events A,B,C. After ShiftOnsetByOne we would have -|__0__| A |__1__| B |__2__| C +|\\_\\_0\\_\\_| A |\\_\\_1\\_\\_| B |\\_\\_2\\_\\_| C with 0 being a new distance of `0`, and the 3 removed (it would describe the distance after C, because there is nothing coming, the signal is not further prolonged). From c8fbae8f7998095da53d0e44a9c1f511451baf76 Mon Sep 17 00:00:00 2001 From: Benedikt Ehinger Date: Thu, 12 Jun 2025 20:30:48 +0200 Subject: [PATCH 120/156] Update src/component.jl Co-authored-by: Judith Schepers --- src/component.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/component.jl b/src/component.jl index f0b56d57..5be7f5a7 100644 --- a/src/component.jl +++ b/src/component.jl @@ -53,7 +53,7 @@ All fields can be named. Works best with [`SingleSubjectDesign`](@ref). - `formula::Any`: StatsModels `formula` object, e.g. `@formula 0 ~ 1 + cond` (left-hand side must be 0). - `β::Vector` Vector of betas/coefficients, must fit the formula. - `contrasts::Dict` (optional): Determines which coding scheme to use for which categorical variables. Default is empty which corresponds to dummy coding. -- `offset::Int`: Default is 0. Can be used to shift the basis function in time. +- `offset::Int = 0`: Can be used to shift the basis function in time. For more information see . From 4bf0034502d7dad2a9b9ec46444e2cffe17ffa2b Mon Sep 17 00:00:00 2001 From: Judith Schepers Date: Fri, 4 Jul 2025 18:08:54 +0200 Subject: [PATCH 121/156] Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src/onset.jl | 7 ++----- test/onset.jl | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/onset.jl b/src/onset.jl index 12264b65..dfacf95f 100644 --- a/src/onset.jl +++ b/src/onset.jl @@ -265,7 +265,7 @@ end Same functionality as `simulate_interonset_distances(rng,onsets::AbstractOnset)` except that it shifts the resulting vector by one, adding a `0` to the front and removing the last simuluated distance. """ UnfoldSim.simulate_interonset_distances(rng, onsets::ShiftOnsetByOne, design) = - vcat(0, UnfoldSim.simulate_interonset_distances(rng, onsets.onset, design)[1:end-1]) + vcat(0, UnfoldSim.simulate_interonset_distances(rng, onsets.onset, design)[1:(end-1)]) """ @@ -302,7 +302,7 @@ See also [`UniformOnset`](@ref UnfoldSim.UniformOnset) for a simplified version """ @with_kw struct UniformOnsetFormula <: AbstractOnset width_formula = @formula(0 ~ 1) - width_β::Vector = [0] + width_β::Vector width_contrasts::Dict = Dict() offset_formula = @formula(0 ~ 1) offset_β::Vector = [0] @@ -393,6 +393,3 @@ function simulate_interonset_distances( #@debug reduce(hcat, rand.(deepcopy(rng), funs, 1)) return Int.(round.(offsets .+ reduce(vcat, rand.(deepcopy(rng), funs, 1)))) end - - - diff --git a/test/onset.jl b/test/onset.jl index d27edd8b..7cea46cf 100644 --- a/test/onset.jl +++ b/test/onset.jl @@ -120,7 +120,7 @@ # ShiftOnsetByOne adds a 0 to the front, thereby the first "non-0" "real" simulated inter onset distance is used for the second event @test with[1] == 0 - @test without[1:end-1] == with[2:end] + @test without[1:(end-1)] == with[2:end] end From badd2dba5fd894f05c8b4511181e789403dd1160 Mon Sep 17 00:00:00 2001 From: jschepers Date: Fri, 10 Oct 2025 16:36:55 +0200 Subject: [PATCH 122/156] Combine changed docstring with docstring on main --- src/bases.jl | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/src/bases.jl b/src/bases.jl index fa8770b5..e931c6ea 100644 --- a/src/bases.jl +++ b/src/bases.jl @@ -26,11 +26,36 @@ Generator for Hanning window, negative (!) peak at 400ms, width 400ms, at kwargs n400(; sfreq = 100) = -hanning(0.4, 0.4, sfreq) """ -generate a hanning window - -width: in s -offset: in s, defines hanning peak, must be > round(width/2) -sfreq: sampling rate in Hz + hanning(duration, offset, sfreq) + +Generate a (potentially shifted) hanning window with a certain duration. + +Note: This function extends the `DSP.hanning` function using multiple dispatch. + +# Arguments +- `width`: in s. +- `offset`: in s, defines the location of the hanning peak i.e. shift of the hanning window. Must be > `round(width/2)` (otherwise the left part of the curve would be cut off). +- `sfreq`: Sampling rate in Hz. + +# Returns +- `Vector`: Contains a shifted (i.e. zero-padded) hanning window. + +# Examples +```julia-repl +julia> UnfoldSim.hanning(0.1, 0.3, 100) +35-element Vector{Float64}: + 0.0 + 0.0 + 0.0 + 0.0 + 0.0 + ⋮ + 0.9698463103929542 + 0.75 + 0.4131759111665348 + 0.116977778440511 + 0.0 +``` """ function DSP.hanning(width, offset, sfreq) width = width * sfreq From d6101c36e10fcbcf6f02d02dea9eb43857b976bb Mon Sep 17 00:00:00 2001 From: Benedikt Ehinger Date: Mon, 13 Oct 2025 15:50:35 +0200 Subject: [PATCH 123/156] Apply suggestions from code review Co-authored-by: Judith Schepers --- src/bases.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bases.jl b/src/bases.jl index e931c6ea..d3531310 100644 --- a/src/bases.jl +++ b/src/bases.jl @@ -63,7 +63,7 @@ function DSP.hanning(width, offset, sfreq) signal = hanning(Int(round(width))) pad_by = Int(round(offset - length(signal) / 2)) - pad_by < 0 ? error("offset has to be > round(width/2)") : "" + pad_by < 0 ? error("The offset has to be > round((width+1)/2). Otherwise, the left part of the curve would be cut off. To create a component which starts before the event onset, one can use the `offset` parameter of a component.") : "" return pad_array(signal, -pad_by, 0) end From 885e0f2f286dc07164ff977fec341e4ae95009c4 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Mon, 31 Mar 2025 23:20:42 +0200 Subject: [PATCH 124/156] fix component offset bug, add tests --- src/onset.jl | 2 +- src/simulation.jl | 10 +++++++-- test/component.jl | 55 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/onset.jl b/src/onset.jl index dfacf95f..9f0f6e37 100644 --- a/src/onset.jl +++ b/src/onset.jl @@ -255,7 +255,7 @@ function simulate_onsets(rng, onset::AbstractOnset, simulation::Simulation) end # accumulate them onsets_accum = accumulate(+, onsets, dims = 1, init = 1) - onsets_accum = onsets_accum .- minoffset(simulation.components) + onsets_accum = onsets_accum .- min(minoffset(simulation.components), 0) return onsets_accum end diff --git a/src/simulation.jl b/src/simulation.jl index d9d79511..2a076901 100755 --- a/src/simulation.jl +++ b/src/simulation.jl @@ -256,9 +256,15 @@ function create_continuous_signal(rng, responses, simulation) # combine responses with onsets max_length_component = maxlength(components) - offset_range = maxoffset(simulation.components) - minoffset(simulation.components) + + + offset_range = max(maxoffset(components), 0) - min(minoffset(components), 0) + + # because the maximum(onset) is already shifted by minoffset in onsets.jl / simulate_onsets, we have to undo it here, if not, we'd get signals that are too long + max_length_continuoustime = - Int(ceil(maximum(onsets))) .+ max_length_component .+ offset_range + Int(ceil(maximum(onsets) + minoffset(components))) .+ max_length_component .+ + offset_range signal = zeros(n_chan, max_length_continuoustime, n_subjects) diff --git a/test/component.jl b/test/component.jl index 19944924..a645b0fe 100644 --- a/test/component.jl +++ b/test/component.jl @@ -103,6 +103,61 @@ d, e = simulate(design, smin10000, UniformOnset(50, 0)) @test length(d) > 10_000 @test e.latency[1] > 10_000 + @test d[e.latency[1]-10_000] == 1 + smax10000 = LinearModelComponent(; + basis = [1, 2, 3], + formula = @formula(0 ~ 1), + β = [1], + offset = +10_000, + ) + d, e = simulate(design, smax10000, UniformOnset(50, 0)) + @test length(d) > 10_000 + @test e.latency[1] < 100 + @test d[e.latency[1]+10_000] == 1 + + + # if we go back -10_000 and front +10_000, we should get a signal measuring 20_000 + d, e = simulate(design, [smax10000, smin10000], UniformOnset(50, 0)) + @test length(d) > 20_000 + @test length(d) < 25_000 # earlier tests had the signal at 30_000, a bti too long + @test d[e.latency[1]+10_000] == 1 + @test d[e.latency[1]-10_000] == 1 + + + + smax10 = LinearModelComponent(; + basis = [1, 2, 3], + formula = @formula(0 ~ 1), + β = [1], + offset = +1000, + ) + smax20 = LinearModelComponent(; + basis = [1, 2, 3], + formula = @formula(0 ~ 1), + β = [1], + offset = +2000, + ) + + d, e = simulate(design, [smax10, smax20], UniformOnset(50, 0)) + @test d[e.latency[1]+1000] == 1 + @test d[e.latency[1]+2000] == 1 + + smin10 = LinearModelComponent(; + basis = [1, 2, 3], + formula = @formula(0 ~ 1), + β = [1], + offset = -1000, + ) + smin20 = LinearModelComponent(; + basis = [1, 2, 3], + formula = @formula(0 ~ 1), + β = [1], + offset = -2000, + ) + + d, e = simulate(design, [smin10, smin20], UniformOnset(50, 0)) + @test d[e.latency[1]-1000] == 1 + @test d[e.latency[1]-2000] == 1 end end From 3b65091557753316cd2d96b7e3da61125bc51d76 Mon Sep 17 00:00:00 2001 From: jschepers Date: Wed, 8 Oct 2025 15:36:56 +0200 Subject: [PATCH 125/156] Replace max_length_continuoustime calculation with new version + add more comments to the code --- src/onset.jl | 1 + src/simulation.jl | 23 ++++++++++++++--------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/onset.jl b/src/onset.jl index 9f0f6e37..d7ca2598 100644 --- a/src/onset.jl +++ b/src/onset.jl @@ -255,6 +255,7 @@ function simulate_onsets(rng, onset::AbstractOnset, simulation::Simulation) end # accumulate them onsets_accum = accumulate(+, onsets, dims = 1, init = 1) + # If the minimum component offset is negative, the onsets are shifted towards later in time to avoid that a component starts before the continuous signal starts. onsets_accum = onsets_accum .- min(minoffset(simulation.components), 0) return onsets_accum diff --git a/src/simulation.jl b/src/simulation.jl index 2a076901..fd281525 100755 --- a/src/simulation.jl +++ b/src/simulation.jl @@ -254,21 +254,26 @@ function create_continuous_signal(rng, responses, simulation) # flatten onsets (since subjects are concatenated in the events df) latencies = onsets[:,] - # combine responses with onsets - max_length_component = maxlength(components) - + # calculate the required length of the continuous signal - offset_range = max(maxoffset(components), 0) - min(minoffset(components), 0) + # Reasoning: + # (1) We want the last event onset time to be within the signal -> lowerbound of max_length_continuous is `maximum(onsets)` + # (2) We want to extend the signal for those cases, where the response is longer than the last event onset time, without offset, the upper bound is: maximum(onsets)+max_length_component + # (3) In cases where we have a positive offset, the largest offset needs to be added => maximum(onsets) + max_length_component + max(maxoffset(components), 0) + # (4) In cases where we have a negative offset, `max_length_component` might be reduced, by maximally the largest minoffset => maximum(onsets) + max_length_component + max(maxoffset(components), 0) + maximum(min.(get_offset.(components),0)) - # because the maximum(onset) is already shifted by minoffset in onsets.jl / simulate_onsets, we have to undo it here, if not, we'd get signals that are too long + last_onset = maximum(onsets) + max_length_component = maxlength(components) - max_length_continuoustime = - Int(ceil(maximum(onsets) + minoffset(components))) .+ max_length_component .+ - offset_range + calculated_onset = maximum(onsets) + max_length_component # add the signal (ideally, we'd add the longest signal of the last event - but it's not so easy). (2) + calculated_onset += max(UnfoldSim.maxoffset(components), 0) # if the largest offset is positive, add it (3) + calculated_onset += maximum(min.(UnfoldSim.get_offset.(components), 0)) # add maximum of offsets that is smaller than 0 (4) + max_length_continuoustime = max(last_onset, calculated_onset) # ensure that maximum(onsets) is lowerbound (1) signal = zeros(n_chan, max_length_continuoustime, n_subjects) - @debug size(signal), offset_range + + # combine responses with onsets for e = 1:n_chan for s = 1:n_subjects for i = 1:n_trials From 45ed6020e9f2897efbf4766adcb35d24a864f65d Mon Sep 17 00:00:00 2001 From: jschepers Date: Wed, 8 Oct 2025 18:22:42 +0200 Subject: [PATCH 126/156] Add get_offset methods for vectors and sequences and adapt max_length_continuoustime calculation --- src/component.jl | 2 ++ src/simulation.jl | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/component.jl b/src/component.jl index 5be7f5a7..a4aea34a 100644 --- a/src/component.jl +++ b/src/component.jl @@ -101,6 +101,8 @@ Should the `basis` be shifted? Returns c.offset for most components, if not impl get_offset(c::AbstractComponent)::Int = 0 get_offset(c::LinearModelComponent)::Int = c.offset get_offset(c::MixedModelComponent)::Int = c.offset +get_offset(c::Vector{<:AbstractComponent}) = get_offset.(c) +get_offset(d::Dict{<:Char,<:Vector{<:AbstractComponent}}) = get_offset.(values(d)) maxoffset(c::Vector{<:AbstractComponent}) = maximum(get_offset.(c)) maxoffset(d::Dict{<:Char,<:Vector{<:AbstractComponent}}) = maximum(maxoffset.(values(d))) diff --git a/src/simulation.jl b/src/simulation.jl index fd281525..08d2478d 100755 --- a/src/simulation.jl +++ b/src/simulation.jl @@ -266,8 +266,8 @@ function create_continuous_signal(rng, responses, simulation) max_length_component = maxlength(components) calculated_onset = maximum(onsets) + max_length_component # add the signal (ideally, we'd add the longest signal of the last event - but it's not so easy). (2) - calculated_onset += max(UnfoldSim.maxoffset(components), 0) # if the largest offset is positive, add it (3) - calculated_onset += maximum(min.(UnfoldSim.get_offset.(components), 0)) # add maximum of offsets that is smaller than 0 (4) + calculated_onset += max(maxoffset(components), 0) # if the largest offset is positive, add it (3) + calculated_onset += maximum(min.(vcat(get_offset(components)...), 0)) # add maximum of offsets that is smaller than 0 (4) max_length_continuoustime = max(last_onset, calculated_onset) # ensure that maximum(onsets) is lowerbound (1) From 0229392a0d34c70ae28068e6da7e524809e3dfa4 Mon Sep 17 00:00:00 2001 From: jschepers Date: Thu, 9 Oct 2025 17:18:05 +0200 Subject: [PATCH 127/156] Add test for get_offset for vectors and sequences and for the combination of component offsets and sequences --- test/component.jl | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/test/component.jl b/test/component.jl index a645b0fe..65656d7f 100644 --- a/test/component.jl +++ b/test/component.jl @@ -86,12 +86,13 @@ offset = 5, ) @test UnfoldSim.get_offset(smin10) == -10 + @test UnfoldSim.get_offset([smin10, splus5]) == [-10, 5] @test UnfoldSim.maxoffset([smin10, splus5]) == 5 @test UnfoldSim.minoffset([smin10, splus5]) == -10 @test UnfoldSim.minoffset(Dict('A' => [smin10, splus5])) == -10 @test UnfoldSim.maxoffset(Dict('A' => [smin10, smin10], 'B' => [splus5, splus5])) == 5 - # test that you can have a super large negative offset and dont run into errors (e.g. an event cannot even run in the issue to start before simulation time = 0) + # test that you can have a super large negative offset and don't run into errors (e.g. an event cannot even run in the issue to start before simulation time = 0) smin10000 = LinearModelComponent(; basis = [1, 2, 3], @@ -120,7 +121,7 @@ # if we go back -10_000 and front +10_000, we should get a signal measuring 20_000 d, e = simulate(design, [smax10000, smin10000], UniformOnset(50, 0)) @test length(d) > 20_000 - @test length(d) < 25_000 # earlier tests had the signal at 30_000, a bti too long + @test length(d) < 25_000 # earlier tests had the signal at 30_000, a bit too long @test d[e.latency[1]+10_000] == 1 @test d[e.latency[1]-10_000] == 1 @@ -159,5 +160,38 @@ d, e = simulate(design, [smin10, smin20], UniformOnset(50, 0)) @test d[e.latency[1]-1000] == 1 @test d[e.latency[1]-2000] == 1 + + # Sequences with component offsets + design = + SingleSubjectDesign(conditions = Dict(:condition => ["one", "two"])) |> + d -> RepeatDesign(SequenceDesign(d, "SR_"), 4) + + components = Dict('S' => [smin10, splus5], 'R' => [smin10000]) + + @test UnfoldSim.get_offset(components) == [[-10_000], [-10, 5]] + + o_width = 20 + o_offset = 0 + minoffset_shift = -1 * min(UnfoldSim.minoffset(components), 0) # latencies should be shifted to the right if minoffset is negative + + for seed in range(1, 10) + d, e = simulate( + StableRNG(seed), + design, + components, + UniformOnset(offset = o_offset, width = o_width), + NoNoise(), + ) + sequence_length = length(UnfoldSim.sequencestring(StableRNG(seed), design)) - 1 # without _ + + # Test onset shifts with component offsets and sequences (in particular inter-event-block distances) combined + @test minoffset_shift + 1 <= e.latency[1] <= minoffset_shift + 1 + o_width + @test minoffset_shift + 1 + (sequence_length + 1) * o_offset <= + e.latency[sequence_length+1] <= + minoffset_shift + + 1 + + (sequence_length + 1) * o_width + + 2 * UnfoldSim.maxlength(components) # TODO: This part will fail once we implement a different went to specify the inter-event-block distances. Should be adapted then. + end end end From b89fab7caa3adcc0228b56db544b2f558bfc68ab Mon Sep 17 00:00:00 2001 From: jschepers Date: Thu, 9 Oct 2025 17:55:39 +0200 Subject: [PATCH 128/156] Adapt the get_offset function for sequence dicts to return a dict instead of a vector and adapt tests and max length continuoustime accordingly --- src/component.jl | 2 +- src/simulation.jl | 2 +- test/component.jl | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/component.jl b/src/component.jl index a4aea34a..b301b7f0 100644 --- a/src/component.jl +++ b/src/component.jl @@ -102,7 +102,7 @@ get_offset(c::AbstractComponent)::Int = 0 get_offset(c::LinearModelComponent)::Int = c.offset get_offset(c::MixedModelComponent)::Int = c.offset get_offset(c::Vector{<:AbstractComponent}) = get_offset.(c) -get_offset(d::Dict{<:Char,<:Vector{<:AbstractComponent}}) = get_offset.(values(d)) +get_offset(d::Dict{<:Char,<:Vector{<:AbstractComponent}}) = Dict(k => get_offset(v) for (k,v) in d) maxoffset(c::Vector{<:AbstractComponent}) = maximum(get_offset.(c)) maxoffset(d::Dict{<:Char,<:Vector{<:AbstractComponent}}) = maximum(maxoffset.(values(d))) diff --git a/src/simulation.jl b/src/simulation.jl index 08d2478d..0c728169 100755 --- a/src/simulation.jl +++ b/src/simulation.jl @@ -267,7 +267,7 @@ function create_continuous_signal(rng, responses, simulation) calculated_onset = maximum(onsets) + max_length_component # add the signal (ideally, we'd add the longest signal of the last event - but it's not so easy). (2) calculated_onset += max(maxoffset(components), 0) # if the largest offset is positive, add it (3) - calculated_onset += maximum(min.(vcat(get_offset(components)...), 0)) # add maximum of offsets that is smaller than 0 (4) + calculated_onset += maximum(min.(vcat(values(get_offset(components))...), 0)) # add maximum of offsets that is smaller than 0 (4) max_length_continuoustime = max(last_onset, calculated_onset) # ensure that maximum(onsets) is lowerbound (1) diff --git a/test/component.jl b/test/component.jl index 65656d7f..ecfcdb1c 100644 --- a/test/component.jl +++ b/test/component.jl @@ -168,7 +168,7 @@ components = Dict('S' => [smin10, splus5], 'R' => [smin10000]) - @test UnfoldSim.get_offset(components) == [[-10_000], [-10, 5]] + @test UnfoldSim.get_offset(components) == Dict('R' => [-10_000], 'S' => [-10, 5]) o_width = 20 o_offset = 0 @@ -191,7 +191,7 @@ minoffset_shift + 1 + (sequence_length + 1) * o_width + - 2 * UnfoldSim.maxlength(components) # TODO: This part will fail once we implement a different went to specify the inter-event-block distances. Should be adapted then. + 2 * UnfoldSim.maxlength(components) # TODO: This part will fail once we implement a different way to specify the inter-event-block distances. Should be adapted then. end end end From 86623ef2969a9073cfd5ffcf57b9fbbb9eb955c1 Mon Sep 17 00:00:00 2001 From: Benedikt Ehinger Date: Mon, 13 Oct 2025 15:36:55 +0200 Subject: [PATCH 129/156] Apply suggestions from code review Co-authored-by: Judith Schepers --- test/component.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/component.jl b/test/component.jl index ecfcdb1c..eff14492 100644 --- a/test/component.jl +++ b/test/component.jl @@ -127,37 +127,37 @@ - smax10 = LinearModelComponent(; + smax1000 = LinearModelComponent(; basis = [1, 2, 3], formula = @formula(0 ~ 1), β = [1], offset = +1000, ) - smax20 = LinearModelComponent(; + smax2000 = LinearModelComponent(; basis = [1, 2, 3], formula = @formula(0 ~ 1), β = [1], offset = +2000, ) - d, e = simulate(design, [smax10, smax20], UniformOnset(50, 0)) + d, e = simulate(design, [smax1000, smax2000], UniformOnset(50, 0)) @test d[e.latency[1]+1000] == 1 @test d[e.latency[1]+2000] == 1 - smin10 = LinearModelComponent(; + smin1000 = LinearModelComponent(; basis = [1, 2, 3], formula = @formula(0 ~ 1), β = [1], offset = -1000, ) - smin20 = LinearModelComponent(; + smin2000 = LinearModelComponent(; basis = [1, 2, 3], formula = @formula(0 ~ 1), β = [1], offset = -2000, ) - d, e = simulate(design, [smin10, smin20], UniformOnset(50, 0)) + d, e = simulate(design, [smin1000, smin2000], UniformOnset(50, 0)) @test d[e.latency[1]-1000] == 1 @test d[e.latency[1]-2000] == 1 From cedde169fd695e671cf62fe4d20044f5bbc1e01c Mon Sep 17 00:00:00 2001 From: Benedikt Ehinger Date: Mon, 13 Oct 2025 15:37:40 +0200 Subject: [PATCH 130/156] Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src/component.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/component.jl b/src/component.jl index b301b7f0..f09b5cd4 100644 --- a/src/component.jl +++ b/src/component.jl @@ -102,7 +102,8 @@ get_offset(c::AbstractComponent)::Int = 0 get_offset(c::LinearModelComponent)::Int = c.offset get_offset(c::MixedModelComponent)::Int = c.offset get_offset(c::Vector{<:AbstractComponent}) = get_offset.(c) -get_offset(d::Dict{<:Char,<:Vector{<:AbstractComponent}}) = Dict(k => get_offset(v) for (k,v) in d) +get_offset(d::Dict{<:Char,<:Vector{<:AbstractComponent}}) = + Dict(k => get_offset(v) for (k, v) in d) maxoffset(c::Vector{<:AbstractComponent}) = maximum(get_offset.(c)) maxoffset(d::Dict{<:Char,<:Vector{<:AbstractComponent}}) = maximum(maxoffset.(values(d))) From 05f699f7c588ffd7790a0e8d1cdf6de791fbb1a0 Mon Sep 17 00:00:00 2001 From: Judith Schepers Date: Mon, 13 Oct 2025 16:43:29 +0200 Subject: [PATCH 131/156] Apply suggestions from code review Co-authored-by: Benedikt Ehinger --- src/bases.jl | 2 +- test/bases.jl | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/bases.jl b/src/bases.jl index d3531310..f2242754 100644 --- a/src/bases.jl +++ b/src/bases.jl @@ -61,7 +61,7 @@ function DSP.hanning(width, offset, sfreq) width = width * sfreq offset = offset * sfreq signal = hanning(Int(round(width))) - pad_by = Int(round(offset - length(signal) / 2)) + pad_by = Int(round(offset - (length(signal)+1) / 2)) pad_by < 0 ? error("The offset has to be > round((width+1)/2). Otherwise, the left part of the curve would be cut off. To create a component which starts before the event onset, one can use the `offset` parameter of a component.") : "" return pad_array(signal, -pad_by, 0) diff --git a/test/bases.jl b/test/bases.jl index 6b0ae558..8db17170 100644 --- a/test/bases.jl +++ b/test/bases.jl @@ -2,6 +2,7 @@ using UnfoldSim @testset "hanning" begin @test UnfoldSim.hanning(0.021, 0.04, 1000)[41] == 1.0 # why 41 not 40? beacuse round(0.5) = 0 and round(1.5) = 2 -- and we are living on the edge! @test UnfoldSim.hanning(0.011, 0.04, 1000)[40] == 1.0 + @test isapprox(UnfoldSim.hanning(0.021, 0.04, 256) ,0.0429688) @test UnfoldSim.hanning(0.011, 0.02, 1000)[20] == 1.0 @test_throws Exception UnfoldSim.hanning(0.011, 0.0, 1000) end From f5d21b70844798fd0084aa62890b2644165d8ec6 Mon Sep 17 00:00:00 2001 From: jschepers Date: Mon, 13 Oct 2025 17:01:28 +0200 Subject: [PATCH 132/156] Remove that CI should only run for the main branch --- .github/workflows/CI.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 6788ea41..0b7632e8 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -5,8 +5,6 @@ on: - main tags: '*' pull_request: - branches: - - main concurrency: # Skip intermediate builds: always. # Cancel intermediate builds: only if it is a pull request build. From 34521a5f24da34e209dcb4ebfb35855a80fb2c18 Mon Sep 17 00:00:00 2001 From: jschepers Date: Mon, 13 Oct 2025 17:07:02 +0200 Subject: [PATCH 133/156] Enable CI for all PRs not just the ones on main --- .github/workflows/CI.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 6788ea41..0b7632e8 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -5,8 +5,6 @@ on: - main tags: '*' pull_request: - branches: - - main concurrency: # Skip intermediate builds: always. # Cancel intermediate builds: only if it is a pull request build. From 564be7b6abd8af2415e2f2e4a298c5eeb0bbca2c Mon Sep 17 00:00:00 2001 From: jschepers Date: Mon, 13 Oct 2025 18:10:37 +0200 Subject: [PATCH 134/156] Replace hard-coded value --- docs/literate/HowTo/getGroundTruth.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/literate/HowTo/getGroundTruth.jl b/docs/literate/HowTo/getGroundTruth.jl index 5d2f0b43..ef57a8b8 100644 --- a/docs/literate/HowTo/getGroundTruth.jl +++ b/docs/literate/HowTo/getGroundTruth.jl @@ -80,7 +80,7 @@ gt_data, gt_events = simulate( # Note that the data has to be reshaped into a channel X times X predictor form. (In our one channel example `size(gt_data) = (45,2)`, missing the channel dimension) g = reshape(gt_data, 1, size(gt_data)...) -times = range(1, 45); +times = range(1, size(gt_data, 1)); gt_effects = Unfold.result_to_table([g], [gt_events], [times], ["effects"]) first(gt_effects, 5) From 5faf54b9b1c6237b5a6e6640c8dd4b8695959ddc Mon Sep 17 00:00:00 2001 From: jschepers Date: Tue, 14 Oct 2025 11:33:05 +0200 Subject: [PATCH 135/156] Include test/bases.jl in tests --- test/runtests.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/runtests.jl b/test/runtests.jl index 2cbc1de5..a1a58973 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,6 +2,7 @@ using UnfoldSim include("setup.jl") @testset "UnfoldSim.jl" begin + include("bases.jl") include("component.jl") include("design.jl") include("noise.jl") From b5d2d0004719e8652fe9cd255913c557adf09494 Mon Sep 17 00:00:00 2001 From: jschepers Date: Tue, 14 Oct 2025 16:28:20 +0200 Subject: [PATCH 136/156] Fix tests --- test/bases.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/bases.jl b/test/bases.jl index 8db17170..b84600b4 100644 --- a/test/bases.jl +++ b/test/bases.jl @@ -1,16 +1,16 @@ using UnfoldSim @testset "hanning" begin - @test UnfoldSim.hanning(0.021, 0.04, 1000)[41] == 1.0 # why 41 not 40? beacuse round(0.5) = 0 and round(1.5) = 2 -- and we are living on the edge! + @test UnfoldSim.hanning(0.021, 0.04, 1000)[40] == 1.0 @test UnfoldSim.hanning(0.011, 0.04, 1000)[40] == 1.0 - @test isapprox(UnfoldSim.hanning(0.021, 0.04, 256) ,0.0429688) @test UnfoldSim.hanning(0.011, 0.02, 1000)[20] == 1.0 + @test isapprox(argmax(UnfoldSim.hanning(0.021, 0.04, 256)) / 256, 0.0390625) @test_throws Exception UnfoldSim.hanning(0.011, 0.0, 1000) end @testset "p100,N170,p300,n400" begin sfreq = 1000 @test argmax(p100(; sfreq)) == 0.1 * sfreq - @test argmin(n170(; sfreq)) == 0.17 * sfreq + @test argmin(n170(; sfreq)) == 0.169 * sfreq # Why not 0.17? Because the peak of the function is in between samples (169 and 170) @test argmax(p300(; sfreq)) == 0.3 * sfreq @test argmin(n400(; sfreq)) == 0.4 * sfreq end \ No newline at end of file From 61b7d36db5558b4caffee031f63216781a1cba89 Mon Sep 17 00:00:00 2001 From: Judith Schepers Date: Tue, 14 Oct 2025 16:29:30 +0200 Subject: [PATCH 137/156] Apply suggestions from reviewdog Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src/bases.jl | 5 ++++- test/bases.jl | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/bases.jl b/src/bases.jl index 0c424da7..ff4a2054 100644 --- a/src/bases.jl +++ b/src/bases.jl @@ -165,7 +165,10 @@ function DSP.hanning(width, offset, sfreq) signal = hanning(Int(round(width))) pad_by = Int(round(offset - (length(signal)+1) / 2)) - pad_by < 0 ? error("The offset has to be > round((width+1)/2). Otherwise, the left part of the curve would be cut off. To create a component which starts before the event onset, one can use the `offset` parameter of a component.") : "" + pad_by < 0 ? + error( + "The offset has to be > round((width+1)/2). Otherwise, the left part of the curve would be cut off. To create a component which starts before the event onset, one can use the `offset` parameter of a component.", + ) : "" return pad_array(signal, -pad_by, 0) end diff --git a/test/bases.jl b/test/bases.jl index b84600b4..bed6067b 100644 --- a/test/bases.jl +++ b/test/bases.jl @@ -13,4 +13,4 @@ end @test argmin(n170(; sfreq)) == 0.169 * sfreq # Why not 0.17? Because the peak of the function is in between samples (169 and 170) @test argmax(p300(; sfreq)) == 0.3 * sfreq @test argmin(n400(; sfreq)) == 0.4 * sfreq -end \ No newline at end of file +end From 3227b90ddc895bb97fa32d4affac0f7395ce3253 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Wed, 15 Oct 2025 14:34:54 +0200 Subject: [PATCH 138/156] add simulation call --- test/simulation.jl | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/test/simulation.jl b/test/simulation.jl index 88ea107a..53a62423 100644 --- a/test/simulation.jl +++ b/test/simulation.jl @@ -222,10 +222,15 @@ using Base: AbstractCartesianIndex end MyLinearModelComponent1(b, f, β) = MyLinearModelComponent1(LinearModelComponent(; basis = b, formula = f, β)) - UnfoldSim.simulate_component(rng, c::MyLinearModelComponent1, design) = - simulate_component(rng, c.comp, design) - Simulation( - SingleSubjectDesign(), + UnfoldSim.simulate_component( + rng, + c::MyLinearModelComponent1, + design::UnfoldSim.SubselectDesign, + ) = simulate_component(rng, c.comp, design) + UnfoldSim.length(c::MyLinearModelComponent1) = length(c.comp) + UnfoldSim.size(c::MyLinearModelComponent1) = size(c.comp) + sim = Simulation( + SingleSubjectDesign(conditions = Dict(:event => ['A', 'B'])), Dict( 'A' => [ LinearModelComponent( @@ -239,6 +244,7 @@ using Base: AbstractCartesianIndex NoOnset(), NoNoise(), ) + simulate(UnfoldSim.MersenneTwister(1), sim; return_epoched = true) end end From 0d8d5d1097a20894424269c2dc043016ae30a6b3 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Wed, 15 Oct 2025 14:36:57 +0200 Subject: [PATCH 139/156] added two more tests, just in case --- test/simulation.jl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/simulation.jl b/test/simulation.jl index 53a62423..83032f03 100644 --- a/test/simulation.jl +++ b/test/simulation.jl @@ -236,15 +236,17 @@ using Base: AbstractCartesianIndex LinearModelComponent( basis = p100(), formula = @formula(0 ~ 1), - β = [0], + β = [1], ), ], - 'B' => [MyLinearModelComponent1(p100(), @formula(0 ~ 1), [0])], + 'B' => [MyLinearModelComponent1(p100(), @formula(0 ~ 1), [2])], ), NoOnset(), NoNoise(), ) - simulate(UnfoldSim.MersenneTwister(1), sim; return_epoched = true) + d, e = simulate(UnfoldSim.MersenneTwister(1), sim; return_epoched = true) + @test d[10, 1] < 1 # 1 if the hanning would hit perfectly + @test d[11, 2] > 1.9 # 2 if the hanning would hit perfectly end end From 683b27303184ea8ffddc8234f94356a230202740 Mon Sep 17 00:00:00 2001 From: jschepers Date: Wed, 15 Oct 2025 15:14:29 +0200 Subject: [PATCH 140/156] Add short docstring for limit_basis function --- src/component.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/component.jl b/src/component.jl index f09b5cd4..c86a2544 100644 --- a/src/component.jl +++ b/src/component.jl @@ -250,7 +250,11 @@ function get_basis(rng::AbstractRNG, basis::Tuple{Function,Int}, design) limit_basis(basis_out, maxlength) end +""" + limit_basis(b::AbstractVector{<:AbstractVector}, maxlength) + Cut all basis vectors to `maxlength` and pad them with 0s if they are shorter than `maxlength`. +""" function limit_basis(b::AbstractVector{<:AbstractVector}, maxlength) # first cut off maxlength From a1ea0031e0efd8adda6b031ddaf0af60dcf61df5 Mon Sep 17 00:00:00 2001 From: jschepers Date: Wed, 29 Oct 2025 11:50:23 +0100 Subject: [PATCH 141/156] Fix multi-component sequence test --- test/simulation.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/simulation.jl b/test/simulation.jl index 83032f03..ae850ed4 100644 --- a/test/simulation.jl +++ b/test/simulation.jl @@ -245,8 +245,8 @@ using Base: AbstractCartesianIndex NoNoise(), ) d, e = simulate(UnfoldSim.MersenneTwister(1), sim; return_epoched = true) - @test d[10, 1] < 1 # 1 if the hanning would hit perfectly - @test d[11, 2] > 1.9 # 2 if the hanning would hit perfectly + @test d[10, 1] > 0.9 # 1 if the hanning would hit perfectly (currently the peak is between samples) + @test d[10, 2] > 1.9 # 2 if the hanning would hit perfectly (currently case the peak is between samples) end end From ac7201b286f54e7237b8adf9532d591f4edfd886 Mon Sep 17 00:00:00 2001 From: jschepers Date: Wed, 29 Oct 2025 12:13:55 +0100 Subject: [PATCH 142/156] Fix typo --- test/simulation.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/simulation.jl b/test/simulation.jl index ae850ed4..a05fd36e 100644 --- a/test/simulation.jl +++ b/test/simulation.jl @@ -246,7 +246,7 @@ using Base: AbstractCartesianIndex ) d, e = simulate(UnfoldSim.MersenneTwister(1), sim; return_epoched = true) @test d[10, 1] > 0.9 # 1 if the hanning would hit perfectly (currently the peak is between samples) - @test d[10, 2] > 1.9 # 2 if the hanning would hit perfectly (currently case the peak is between samples) + @test d[10, 2] > 1.9 # 2 if the hanning would hit perfectly (currently the peak is between samples) end end From 82a2e6aabaf7cfa3d067dfc03870699d438467c9 Mon Sep 17 00:00:00 2001 From: jschepers Date: Mon, 3 Nov 2025 14:34:06 +0100 Subject: [PATCH 143/156] Include missing test scripts --- test/runtests.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index a1a58973..8927505b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -5,9 +5,11 @@ include("setup.jl") include("bases.jl") include("component.jl") include("design.jl") + include("headmodel.jl") + include("helper.jl") + include("multichannel.jl") include("noise.jl") include("onset.jl") - include("simulation.jl") - include("helper.jl") include("sequence.jl") + include("simulation.jl") end From 438123333b944c1c713304aaa04af8352fa6be5c Mon Sep 17 00:00:00 2001 From: jschepers Date: Tue, 4 Nov 2025 14:35:35 +0100 Subject: [PATCH 144/156] Format EffectsDesign docstring + update link --- src/design.jl | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/design.jl b/src/design.jl index 08fdd97b..55b05ccf 100644 --- a/src/design.jl +++ b/src/design.jl @@ -501,13 +501,14 @@ end """ EffectsDesign <: AbstractDesign + Design to obtain ground truth simulation. -## Fields -- `design::AbstractDesign` - The design of your (main) simulation. -- `effects_dict::Dict` - Effects.jl style dictionary specifying variable effects. See also [Unfold.jl marginalized effects](https://unfoldtoolbox.github.io/Unfold.jl/stable/generated/HowTo/effects/) +# Fields +- `design::AbstractDesign`: The design of your (main) simulation. +- `effects_dict::Dict`: Effects.jl style dictionary specifying variable effects. See also [Unfold.jl marginalized effects](https://unfoldtoolbox.github.io/UnfoldDocs/Unfold.jl/stable/generated/HowTo/effects/) + +# Examples """ struct EffectsDesign <: AbstractDesign design::AbstractDesign From ea91727bf78c9e2347c743da77b4dbaa431ec024 Mon Sep 17 00:00:00 2001 From: jschepers Date: Tue, 4 Nov 2025 15:15:08 +0100 Subject: [PATCH 145/156] Format generate_events docstring for EffectsDesign, added type for RNG --- src/design.jl | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/design.jl b/src/design.jl index 55b05ccf..e0ceec95 100644 --- a/src/design.jl +++ b/src/design.jl @@ -533,17 +533,19 @@ typical_value(v::Vector{<:Number}) = [mean(v)] typical_value(v) = unique(v) """ - UnfoldSim.generate_events(rng,design::EffectsDesign) + generate_events(rng::AbstractRNG, design::EffectsDesign) -Generates events to simulate marginalized effects using an Effects.jl reference-grid dictionary. Every covariate that is in the `EffectsDesign` but not in the `effects_dict` will be set to a `typical_value` (i.e. the mean) +Generate events to simulate marginal effects using an Effects.jl reference-grid dictionary. Every covariate that is in the `EffectsDesign` but not in the `effects_dict` will be set to a `typical_value` (i.e. the mean). -# Example -```julia -effects_dict = Dict(:conditionA=>[0,1]) -design = SingleSubjectDesign(; conditions = Dict(:conditionA => [0,1,2])) -eff_design = EffectsDesign(design,effects_dict) -generate_events(MersenneTwister(1),eff_design) +# Examples +```julia-repl +julia> effects_dict = Dict(:conditionA => [0, 1]); + +julia> design = SingleSubjectDesign(; conditions = Dict(:conditionA => [0, 1, 2])); + +julia> eff_design = EffectsDesign(design,effects_dict); +julia> generate_events(MersenneTwister(1),eff_design) 2×1 DataFrame Row │ conditionA │ Int64 @@ -552,7 +554,7 @@ generate_events(MersenneTwister(1),eff_design) 2 │ 1 ``` """ -function UnfoldSim.generate_events(rng, t::EffectsDesign) +function generate_events(rng::AbstractRNG, t::EffectsDesign) effects_dict = Dict{Any,Any}(t.effects_dict) #effects_dict = t.effects_dict current_design = generate_events(deepcopy(rng), t.design) From 48968af6d4af78214dc21dd64e5b89592ff62588 Mon Sep 17 00:00:00 2001 From: jschepers Date: Tue, 4 Nov 2025 17:54:58 +0100 Subject: [PATCH 146/156] Remove unnecessary code --- test/design.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/design.jl b/test/design.jl index 919aa9c7..b10879ba 100644 --- a/test/design.jl +++ b/test/design.jl @@ -202,8 +202,8 @@ effects_dict_2 = Dict(:condition => ["car", "face"], :continuous => [2, 3, 4]) # Generate effects design - ef_design_1 = UnfoldSim.EffectsDesign(design, effects_dict_1) - ef_design_2 = UnfoldSim.EffectsDesign(design, effects_dict_2) + ef_design_1 = EffectsDesign(design, effects_dict_1) + ef_design_2 = EffectsDesign(design, effects_dict_2) # Generate events ef_events_1 = generate_events(ef_design_1) @@ -220,7 +220,7 @@ n_items = 8, items_between = Dict(:condition => ["car", "face"], :continuous => [1, 2]), ) - @test_throws ErrorException UnfoldSim.EffectsDesign(design, effects_dict_1) + @test_throws ErrorException EffectsDesign(design, effects_dict_1) end end From 75bed90431d778001bad09162dee03607c85009b Mon Sep 17 00:00:00 2001 From: jschepers Date: Wed, 5 Nov 2025 11:45:06 +0100 Subject: [PATCH 147/156] Minor revisions EffectsDesign how to page --- docs/literate/HowTo/getGroundTruth.jl | 16 ++++++++-------- docs/make.jl | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/literate/HowTo/getGroundTruth.jl b/docs/literate/HowTo/getGroundTruth.jl index ef57a8b8..2fe69bc0 100644 --- a/docs/literate/HowTo/getGroundTruth.jl +++ b/docs/literate/HowTo/getGroundTruth.jl @@ -1,7 +1,7 @@ -# # Simulate ground truth marginalized Effects +# # Simulate ground truth marginal effects -# Often when testing some algorithm, we want to compare our results to a known ground truth. In the case of marginalized effects via the `Unfold.effects`/ `Effects.jl` interface, we can do this using an `EffectsDesign`. -# You can find more on what marginalized effects are here in the [Unfold.jl documentation](https://unfoldtoolbox.github.io/Unfold.jl/dev/generated/HowTo/effects/) +# Often when testing some algorithm, we want to compare our results to a known ground truth. In the case of marginal effects via the `Unfold.effects`/ `Effects.jl` interface, we can do this using an `EffectsDesign`. +# You can find more on what marginalized effects are here in the [Unfold.jl documentation](https://unfoldtoolbox.github.io/Unfold.jl/dev/generated/HowTo/effects/). # ### Setup # ```@raw html @@ -18,10 +18,10 @@ using Random #
# ``` # ## Simulation -# First let's make up a SingleSubject simulation +# First let's set up a SingleSubject simulation # !!! note -# Getting a ground truth for a MultiSubjectDesign is not implemented yet +# An `EffectsDesign` for a `MultiSubjectDesign` is not implemented yet. design = SingleSubjectDesign(; @@ -77,7 +77,7 @@ gt_data, gt_events = simulate( @show gt_events # Additionally, we can get the simulated effects into a tidy dataframe using Unfold's `result_to_table`. -# Note that the data has to be reshaped into a channel X times X predictor form. (In our one channel example `size(gt_data) = (45,2)`, missing the channel dimension) +# Note that the data has to be reshaped into a channel X times X predictor form. (In our one channel example `size(gt_data) = (45,2)`, missing the channel dimension.) g = reshape(gt_data, 1, size(gt_data)...) times = range(1, size(gt_data, 1)); @@ -102,8 +102,8 @@ m = fit( ef = effects(effects_dict, m); # !!! note -# The ground truth is shorter because the ground truth typically returns values between `[0 maxlength(components)]`, whereas in our unfold-model we included a baseline period of 0.1s. -# If you want to actually compare results with the ground truth, you could either us `UnfoldSim.pad_array()` or set the Unfold modelling window to `τ=[0,1]` +# The ground truth signal is shorter because the ground truth typically returns values between `[0 maxlength(components)]`, whereas in our Unfold model we included a baseline period of 0.1s. +# If you want to actually compare results with the ground truth, you could either us `UnfoldSim.pad_array()` or set the Unfold modelling window to `τ=[0,1]`. gt_effects.type .= "UnfoldSim effects" ef.type .= "Unfold effects" diff --git a/docs/make.jl b/docs/make.jl index f5a9c249..a2b69a8c 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -55,7 +55,7 @@ makedocs(; "Define a new component (with variable duration and shift)" => "./generated/HowTo/newComponent.md", "Generate multi channel data" => "./generated/HowTo/multichannel.md", "Use existing experimental designs & onsets in the simulation" => "./generated/HowTo/predefinedData.md", - "Simulated marginal effects" => "./generated/HowTo/getGroundTruth.md", + "Simulate ground truth marginal effects" => "./generated/HowTo/getGroundTruth.md", "Sequence of events (e.g. SCR)" => "./generated/HowTo/sequence.md", ], "Developer documentation" => "developer_docs.md", From 6f704f59d6b4158a6df6de604890d3ee95366ace Mon Sep 17 00:00:00 2001 From: jschepers Date: Wed, 5 Nov 2025 11:46:47 +0100 Subject: [PATCH 148/156] Minor revisions EffectsDesign how to page --- docs/literate/HowTo/getGroundTruth.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/literate/HowTo/getGroundTruth.jl b/docs/literate/HowTo/getGroundTruth.jl index 2fe69bc0..e9618dbb 100644 --- a/docs/literate/HowTo/getGroundTruth.jl +++ b/docs/literate/HowTo/getGroundTruth.jl @@ -54,8 +54,8 @@ data, evts = simulate( PinkNoise(), ); -# ## Simulate marginalized effects directly -# To marginalize effects we first have to specify an effects dictionary and subsequently hand this dict plus the original design to `EffectsDesign()` +# ## Simulate marginal effects directly +# To simulate marginal effects we first have to specify an effects dictionary and subsequently hand this dict plus the original design to `EffectsDesign()` effects_dict = Dict(:condition => ["bike", "face"]) @@ -113,4 +113,4 @@ ef.continuous .= 2.5 # needed to be able to easily merge the two dataframes comb = vcat(gt_effects, ef) plot_erp(comb; mapping = (; color = :type, col = :condition)) -# The simulated ground truth marginal effects, and the fitted marginal effects look similar as expected, but the fitted has some additional noise because of finite data (also as expected). \ No newline at end of file +# The simulated ground truth marginal effects, and the fitted marginal effects look similar as expected, but the fitted has some additional noise because of finite data (also as expected). From 8249bce03388e03f7fa10e5a65d2157021581e88 Mon Sep 17 00:00:00 2001 From: jschepers Date: Wed, 5 Nov 2025 12:29:05 +0100 Subject: [PATCH 149/156] Minor formatting and language changes --- src/component.jl | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/component.jl b/src/component.jl index c86a2544..e20414fc 100644 --- a/src/component.jl +++ b/src/component.jl @@ -6,7 +6,7 @@ A component that adds a hierarchical relation between parameters according to a All fields can be named. Works best with [`MultiSubjectDesign`](@ref). # Fields -- `basis::Any`: an object, if accessed, provides a 'basis function', e.g. `hanning(40)::Vector`, this defines the response at a single event. It will be weighted by the model prediction. It is also possible to provide a function that evaluates to an `Vector` of `Vectors`, with the `design` as input to the function, the outer vector has to have `nrows(design)`, one for each event. The inner vector represents the basis functions which can be of different size (a ragged array). Alternatively, one can also return a Matrix with the second dimension representing `nrows(design)`. In the case of providing a function, one has to specify the `maxlength` as well in a tuple. E.g. `basis=(myfun,40)`, which would automatically cut the output of `myfun` to 40 samples. If your design depends on `rng`, e.g. because of `event_order_function=shuffle` or some special `SequenceDesign`, then you can provide a two-arguments function `(rng,design)->...` +- `basis::Any`: an object, if accessed, provides a 'basis function', e.g. `hanning(40)::Vector`, this defines the response at a single event. It will be weighted by the model prediction. It is also possible to provide a function that evaluates to an `Vector` of `Vectors`, with the `design` as input to the function, the outer vector has to have `nrows(design)`, one for each event. The inner vector represents the basis functions which can be of different size (a ragged array). Alternatively, one can also return a Matrix with the second dimension representing `nrows(design)`. In the case of providing a function, one has to specify the `maxlength` as well in a tuple. E.g. `basis=(myfun,40)`, which would automatically cut the output of `myfun` to 40 samples. If your design depends on `rng`, e.g. because of `event_order_function=shuffle` or some special `SequenceDesign`, then you can provide a two-arguments function `(rng,design)->...`. - `formula::Any`: Formula-object in the style of MixedModels.jl e.g. `@formula 0 ~ 1 + cond + (1|subject)`. The left-hand side is ignored. - `β::Vector` Vector of betas (fixed effects), must fit the formula. - `σs::Dict` Dict of random effect variances, e.g. `Dict(:subject => [0.5, 0.4])` or to specify correlation matrix `Dict(:subject=>[0.5,0.4,I(2,2)],...)`. Technically, this will be passed to the MixedModels.jl `create_re` function, which creates the θ matrices. @@ -49,15 +49,12 @@ A multiple regression component for one subject. All fields can be named. Works best with [`SingleSubjectDesign`](@ref). # Fields -- `basis::Any`: an object, if accessed, provides a 'basis function', e.g. `hanning(40)::Vector`, this defines the response at a single event. It will be weighted by the model prediction. It is also possible to provide a function that evaluates to an `Vector` of `Vectors`, with the `design` as input to the function, the outer vector has to have `nrows(design)`, one for each event. The inner vector represents the basis functions which can be of different size (a ragged array). Alternatively, one can also return a Matrix with the second dimension representing `nrows(design)`. In the case of providing a function, one has to specify the `maxlength` as well in a tuple. E.g. `basis=(myfun,40)`, which would automatically cut the output of `myfun` to 40 samples. If your design depends on `rng`, e.g. because of `event_order_function=shuffle` or some special `SequenceDesign`, then you can provide a two-arguments function `(rng,design)->...` +- `basis::Any`: an object, if accessed, provides a 'basis function', e.g. `hanning(40)::Vector`, this defines the response at a single event. It will be weighted by the model prediction. It is also possible to provide a function that evaluates to an `Vector` of `Vectors`, with the `design` as input to the function, the outer vector has to have `nrows(design)`, one for each event. The inner vector represents the basis functions which can be of different size (a ragged array). Alternatively, one can also return a Matrix with the second dimension representing `nrows(design)`. In the case of providing a function, one has to specify the `maxlength` as well in a tuple. E.g. `basis=(myfun,40)`, which would automatically cut the output of `myfun` to 40 samples. If your design depends on `rng`, e.g. because of `event_order_function=shuffle` or some special `SequenceDesign`, then you can provide a two-arguments function `(rng,design)->...`. - `formula::Any`: StatsModels `formula` object, e.g. `@formula 0 ~ 1 + cond` (left-hand side must be 0). - `β::Vector` Vector of betas/coefficients, must fit the formula. -- `contrasts::Dict` (optional): Determines which coding scheme to use for which categorical variables. Default is empty which corresponds to dummy coding. +- `contrasts::Dict` (optional): Determines which coding scheme to use for which categorical variables. Default is empty which corresponds to dummy coding. For more information see . - `offset::Int = 0`: Can be used to shift the basis function in time. - For more information see . - - # Examples ```julia-repl julia> LinearModelComponent(; @@ -82,7 +79,7 @@ See also [`MixedModelComponent`](@ref), [`MultichannelComponent`](@ref). contrasts::Dict = Dict() offset::Int = 0 function LinearModelComponent(basis, formula, β, contrasts, offset) - @assert isa(basis, Tuple{Function,Int}) ".basis needs to be an `::Array` or a `Tuple(function::Function,maxlength::Int)`" + @assert isa(basis, Tuple{Function,Int}) "`basis` needs to be an `::Array` or a `Tuple(function::Function,maxlength::Int)`" @assert basis[2] > 0 "`maxlength` needs to be longer than 0" new(basis, formula, β, contrasts, offset) end @@ -93,10 +90,11 @@ end # backwards compatability after introducing the `offset` field LinearModelComponent(basis, formula, β, contrasts) = LinearModelComponent(basis, formula, β, contrasts, 0) + """ get_offset(AbstractComponent) -Should the `basis` be shifted? Returns c.offset for most components, if not implemented for a type, returns 0. Can be positive or negative, but has to be Integer +Should the `basis` be shifted? Returns c.offset for most components, if not implemented for a type, returns 0. Can be positive or negative, but has to be an Integer. """ get_offset(c::AbstractComponent)::Int = 0 get_offset(c::LinearModelComponent)::Int = c.offset From 27b5f154380edaa40056f3ef64d34f77cc935c38 Mon Sep 17 00:00:00 2001 From: jschepers Date: Wed, 5 Nov 2025 14:30:43 +0100 Subject: [PATCH 150/156] Format SequenceDesign doc string --- src/design.jl | 45 ++++++++++++++++++++------------------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/src/design.jl b/src/design.jl index e0ceec95..b1a25d6c 100644 --- a/src/design.jl +++ b/src/design.jl @@ -354,22 +354,19 @@ end """ SequenceDesign{T} <: AbstractDesign -Enforce a sequence of events for each entry of a provided `AbstractDesign`. -The sequence string can contain any number of `char`, but the `_` character is used to indicate a break between events without any overlap and has to be at the end of the sequence string. There can only be one `_` character in a sequence string. - - -Important: The exact same variable sequence is used for current rows of a design. Only, if you later nest in a `RepeatDesign` then each `RepeatDesign` repetition will gain a new variable sequence. If you need imbalanced designs, please refer to the `ImbalancedDesign` tutorial +Create a sequence of events for each entry of a provided `AbstractDesign`. +The sequence string can contain any number of `char`, but the `_` character is used to indicate a break between events without any overlap and has to be at the end of the sequence string. There can only be one `_` character in a sequence string. +Important: The exact same variable sequence is used for current rows of a design. Only, if you later nest in a `RepeatDesign` then each `RepeatDesign` repetition will gain a new variable sequence. If you need imbalanced designs, please refer to the [`ImbalancedDesign`](https://unfoldtoolbox.github.io/UnfoldDocs/UnfoldSim.jl/stable/generated/HowTo/newDesign/) tutorial. # Fields -- `design::AbstractDesign`: The design that is generated for every sequence-event +- `design::AbstractDesign`: The design that is generated for every sequence event. - `sequence::String = ""` (optional): A string of characters depicting sequences. A variable sequence is defined using `[]`. For example, `S[ABC]` could result in any one sequence `SA`, `SB`, `SC`. - Experimental: It is also possible to define variable length sequences using `{}`. For example, `A{10,20}` would result in a sequence of 10 to 20 `A`'s. + Experimental: It is also possible to define variable length sequences using `{}`. For example, `A{10,20}` would result in a sequence of 10 to 20 `A`s. # Examples - ```julia design = SingleSubjectDesign(conditions = Dict(:condition => ["one", "two"])) design = SequenceDesign(design, "SCR_") @@ -388,49 +385,47 @@ Would result in a `generate_events(design)` 6 │ two R ``` -## Example for Sequence -> Repeat vs. Repeat -> Sequence +## Combination of SequenceDesign and RepeatDesign ### Sequence -> Repeat ```julia design = SingleSubjectDesign(conditions = Dict(:condition => ["one", "two"])) design = SequenceDesign(design, "[AB]") design = RepeatDesign(design,2) -generate_events(design) ``` - -```repl +```julia-repl +julia> generate_events(design) 4×2 DataFrame Row │ condition event │ String Char ─────┼────────────────── - 1 │ one A - 2 │ two A - 3 │ one B - 4 │ two B + 1 │ one B + 2 │ two B + 3 │ one A + 4 │ two A ``` -Sequence -> Repeat: a sequence design is repeated, then for each repetition a sequence is generated and applied. Events have different values +Sequence -> Repeat: If a sequence design is repeated, then for each repetition a sequence is generated and applied. Events have different values. ### Repeat -> Sequence ```julia design = SingleSubjectDesign(conditions = Dict(:condition => ["one", "two"])) design = RepeatDesign(design,2) design = SequenceDesign(design, "[AB]") -generate_events(design) ``` -```repl +```julia-repl +julia> generate_events(design) 4×2 DataFrame Row │ condition event │ String Char ─────┼────────────────── - 1 │ one A - 2 │ two A - 3 │ one A - 4 │ two A + 1 │ one B + 2 │ two B + 3 │ one B + 4 │ two B ``` -Repeat -> Sequence: the design is first repeated, then for that design one sequence generated and applied. All events are the same - +Repeat -> Sequence: The design is first repeated, then for that design one sequence is generated and applied. All events are the same. See also [`SingleSubjectDesign`](@ref), [`MultiSubjectDesign`](@ref), [`RepeatDesign`](@ref) """ From 62fcc2443f80806b3fb72f81cdf94e5e51e349b1 Mon Sep 17 00:00:00 2001 From: jschepers Date: Thu, 6 Nov 2025 15:10:27 +0100 Subject: [PATCH 151/156] Formatting --- docs/literate/HowTo/sequence.jl | 21 +++++++++++++-------- src/sequence.jl | 5 ++--- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/docs/literate/HowTo/sequence.jl b/docs/literate/HowTo/sequence.jl index 6a6e6dd6..86848609 100644 --- a/docs/literate/HowTo/sequence.jl +++ b/docs/literate/HowTo/sequence.jl @@ -1,6 +1,7 @@ # # Sequence of events (e.g. SCR) -# In this HoWTo we learn to simulate a "SR"-Sequence, a stimulus response, followed by a button press response. +# In this HowTo you will learn to simulate a "SR"-Sequence, a stimulus response, followed by a button press response. + # ### Setup # ```@raw html #
@@ -12,17 +13,18 @@ using CairoMakie using StableRNGs # ```@raw html #
+#
# ``` - -# +# ## Create sequence design # First we generate the minimal design of the experiment by specifying our conditions (a one-condition-two-levels design in our case) design = SingleSubjectDesign(conditions = Dict(:condition => ["one", "two"])) generate_events(design) # Next we use the `SequenceDesign` and nest our initial design in it. "`SR_`" is code for an "`S`" (stimulus) event and an "`R`" (response) event - only single letter events are supported! The "`_`" is a signal for the onset generator to generate a bigger pause - no overlap between adjacent "`SR`" pairs. design = SequenceDesign(design, "SR_") generate_events(StableRNG(1), design) -# The main thing that happened is that the design was repeated for every event (each 'letter') of the sequence, and an `eventtype` column was added. +# The main thing that happened is that the design was repeated for every event (each 'letter') of the sequence, and an `event` column was added. + # !!! hint # More advanced sequences are possible as well, like "SR{1,3}", or "A[BC]". Infinite sequences are **not** possible like "AB*". @@ -30,12 +32,12 @@ generate_events(StableRNG(1), design) design = RepeatDesign(design, 4) generate_events(StableRNG(1), design) -# This results in 16 trials that nicely follow our sequence +# This results in 16 trials that nicely follow our sequence. # !!! hint # There is a difference between `SequenceDesign(RepeatDesign)` and `RepeatDesign(SequenceDesign)` for variable sequences e.g. "A[BC]", where in the former case, one sequence is drawn e.g. "AC" and applied to all repeated rows, in the latter, one sequence for each repeat is drawn. - +# ## Specify components for sequence events # Next we have to specify for both events `S` and `R` what the responses should look like. p1 = LinearModelComponent(; basis = p100(), @@ -63,10 +65,13 @@ resp = LinearModelComponent(; ) nothing ## hide - -# We combine them into a dictionary with a sequence-`Char` as key and simulate +# We combine them into a dictionary with a sequence-`Char` as key components = Dict('S' => [p1, n1, p3], 'R' => [resp]) +# ## Simulate data +# Given the design and the components. we specify onset and noise and simulate data + + data, evts = simulate( StableRNG(1), design, diff --git a/src/sequence.jl b/src/sequence.jl index 50a78f69..ad8a241a 100644 --- a/src/sequence.jl +++ b/src/sequence.jl @@ -41,7 +41,7 @@ sequencestring(rng, d::SequenceDesign) = sequencestring(rng, d.sequence) sequencestring(rng, dS::SequenceDesign) sequencestring(rng, dR::RepeatDesign) -Generates a sequence based on the reverse regex style string in `str`, `dS.sequence` or `dR.design.sequence`. +Generate a sequence based on the reverse regex style string in `str`, `dS.sequence` or `dR.design.sequence`. Directly converting to Automa.Compileis not possible, as we first need to match & evaluate the curly brackets. We simply detect and expand them. @@ -55,14 +55,13 @@ Directly converting to Automa.Compileis not possible, as we first need to match ```julia-repl julia> sequencestring(MersenneTwister(1),"bla{3,4}") "blaaaa" - ``` See also [`rand_re`](@ref) """ function sequencestring(rng, str::String) #match curly brackets and replace them - @assert isnothing(findfirst("*", str)) && isnothing(findfirst("+", str)) "'infinite' sequences currently not supported" + @assert isnothing(findfirst("*", str)) && isnothing(findfirst("+", str)) "'Infinite' sequences are currently not supported." crly = collect(eachmatch(r"(\{[\d]+,[\d]+\})", str)) for c in reverse(crly) m = replace(c.match, "{" => "", "}" => "") From dfc267de099b6eb3d3979df2c852d4f0e92c7237 Mon Sep 17 00:00:00 2001 From: Judith Schepers Date: Thu, 6 Nov 2025 15:36:49 +0100 Subject: [PATCH 152/156] Apply suggestions from reviewdog Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/literate/HowTo/sequence.jl | 7 ++----- docs/literate/reference/designtypes.jl | 1 - src/component.jl | 4 ++-- src/onset.jl | 6 +++--- src/simulation.jl | 4 ++-- 5 files changed, 9 insertions(+), 13 deletions(-) diff --git a/docs/literate/HowTo/sequence.jl b/docs/literate/HowTo/sequence.jl index 86848609..4e86f6ac 100644 --- a/docs/literate/HowTo/sequence.jl +++ b/docs/literate/HowTo/sequence.jl @@ -83,14 +83,11 @@ nothing ## hide # Finally we can plot the results f, ax, h = lines(data) -vlines!(ax, evts.latency[evts.event.=='S'], color = (:darkblue, 0.5)) -vlines!(ax, evts.latency[evts.event.=='R'], color = (:darkred, 0.5)) +vlines!(ax, evts.latency[evts.event .== 'S'], color = (:darkblue, 0.5)) +vlines!(ax, evts.latency[evts.event .== 'R'], color = (:darkred, 0.5)) ax.xlabel = "Time [samples]" ax.ylabel = "EEG [a.u]" xlims!(ax, 0, 500) f # As visible, the `R` response always follows the `S` response. Due to the "`_`" we have large breaks between the individual sequences. - - - diff --git a/docs/literate/reference/designtypes.jl b/docs/literate/reference/designtypes.jl index 8ede2146..8f5330c0 100644 --- a/docs/literate/reference/designtypes.jl +++ b/docs/literate/reference/designtypes.jl @@ -42,7 +42,6 @@ design_single_shuffled = SingleSubjectDesign(; ), event_order_function = shuffle, - ); # ```@raw html #
diff --git a/src/component.jl b/src/component.jl index e20414fc..b4590f32 100644 --- a/src/component.jl +++ b/src/component.jl @@ -697,7 +697,7 @@ function simulate_and_add!( off = get_offset(component) - minoffset(simulation.components) - @views epoch_data[1+off:length(component)+off, :] .+= + @views epoch_data[(1+off):(length(component)+off), :] .+= simulate_component(rng, component, simulation) end function simulate_and_add!( @@ -708,6 +708,6 @@ function simulate_and_add!( ) @debug "3D Array" off = get_offset(component) - minoffset(simulation.components) - @views epoch_data[:, 1+off:length(component)+off, :] .+= + @views epoch_data[:, (1+off):(length(component)+off), :] .+= simulate_component(rng, component, simulation) end diff --git a/src/onset.jl b/src/onset.jl index d7ca2598..e0182857 100644 --- a/src/onset.jl +++ b/src/onset.jl @@ -162,8 +162,8 @@ function simulate_interonset_distances(rng, onset::UniformOnset, design::Abstrac deepcopy(rng), onset.offset:(onset.offset+onset.width), size(deepcopy(rng), design), - ) - ) + ), + ), ) end @@ -245,7 +245,7 @@ function simulate_onsets(rng, onset::AbstractOnset, simulation::Simulation) # add to every stepsize onset the maxlength of the response #@debug onsets[stepsize:stepsize:end] @debug stepsize - onsets[stepsize+1:stepsize:end] .+= 2 .* maxlength(simulation.components) + onsets[(stepsize+1):stepsize:end] .+= 2 .* maxlength(simulation.components) #@debug onsets[stepsize:stepsize:end] end end diff --git a/src/simulation.jl b/src/simulation.jl index 0c728169..6bf46361 100755 --- a/src/simulation.jl +++ b/src/simulation.jl @@ -146,7 +146,7 @@ function simulate(rng::AbstractRNG, simulation::Simulation; return_epoched::Bool # TODO: This assumes a balanced design, but create_continuous_signal also assumes this, so we should be fine ;) size_responses = size(responses) signal = - reshape(responses, size_responses[1:end-1]..., size(deepcopy(rng), design)...) + reshape(responses, size_responses[1:(end-1)]..., size(deepcopy(rng), design)...) else # if there is an onset distribution given the next step is to create a continuous signal signal, latencies = create_continuous_signal(deepcopy(rng), responses, simulation) events.latency = latencies @@ -283,7 +283,7 @@ function create_continuous_signal(rng, responses, simulation) responses, e, s, - one_onset+minoffset(simulation.components):one_onset+max_length_component-1+maxoffset( + (one_onset+minoffset(simulation.components)):(one_onset+max_length_component-1+maxoffset( simulation.components, ), (s - 1) * n_trials + i, From 75c94770e46d371b28e1bfa23eff5820cf8b4c13 Mon Sep 17 00:00:00 2001 From: jschepers Date: Thu, 6 Nov 2025 16:17:57 +0100 Subject: [PATCH 153/156] Fix brackets messed up by Review dog --- src/simulation.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/simulation.jl b/src/simulation.jl index 6bf46361..098d87ae 100755 --- a/src/simulation.jl +++ b/src/simulation.jl @@ -285,7 +285,7 @@ function create_continuous_signal(rng, responses, simulation) s, (one_onset+minoffset(simulation.components)):(one_onset+max_length_component-1+maxoffset( simulation.components, - ), + )), (s - 1) * n_trials + i, ) end From c66bdb5c9a06fee6d9aeb61b8dc1de97c52db492 Mon Sep 17 00:00:00 2001 From: Benedikt Ehinger Date: Thu, 6 Nov 2025 22:28:01 +0100 Subject: [PATCH 154/156] Apply suggestions from code review Co-authored-by: Judith Schepers Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/literate/HowTo/componentfunction.jl | 12 ++++++------ docs/literate/HowTo/newComponent.jl | 2 +- docs/literate/reference/onsettypes.jl | 4 ++-- docs/make.jl | 2 +- src/component.jl | 4 +++- src/onset.jl | 8 +++++--- test/sequence.jl | 1 + 7 files changed, 19 insertions(+), 14 deletions(-) diff --git a/docs/literate/HowTo/componentfunction.jl b/docs/literate/HowTo/componentfunction.jl index 71c8b3b6..be6a1d06 100644 --- a/docs/literate/HowTo/componentfunction.jl +++ b/docs/literate/HowTo/componentfunction.jl @@ -1,5 +1,5 @@ -# # Component Basisfunctions -# HowTo use functions that depend on the `design` and return per-event basis-vectors, instead of the same basis vector for all events. +# # [Define design-dependent component basis functions](@id componentfunction) +# Here you will learn how to specify a component basis that uses a function that depends on the `design` and returns per-event basis vectors, instead of the same basis vector for all events. # ### Setup @@ -30,10 +30,10 @@ design = UnfoldSim.SingleSubjectDesign(; ); -# Instead of defining a "boring" vector basis function e.g. `[0,0,1,2,3,3,2,1,0,0,0]`, let's use function - in our case a hanning window with the size depending on the experimental design's duration. +# Instead of defining a "boring" vector basis function e.g. `[0,0,1,2,3,3,2,1,0,0,0]`, let's use a function - in our case, a Hanning window with the size depending on the experimental design's duration. # !!! important # Two things have to be taken care of: -# 1. in case a rng is required to e.g. generate the design, or your basisfunction depends on it, you have to specify a two-argument basis-function: `(rng,design)->...` +# 1. in case a rng is required to e.g. generate the design, or your basis function depends on it, you have to specify a two-argument basis function: `(rng,design)->...` # 2. a `maxlength` has to be specified via a tuple `(function,maxlength)`` mybasisfun = design -> hanning.(generate_events(design).duration) @@ -45,7 +45,7 @@ signal = LinearModelComponent(; erp = UnfoldSim.simulate_component(MersenneTwister(1), signal, design); -# After simulation, we are ready to plot it. We expect that the simulated responses are scaled by the design's duration. To show it more effectively, we sort by duration. +# After simulation, we are ready to plot it. We expect that the length of the simulated responses is scaled by the design's duration. To show it more effectively, we sort by duration. ##--- f = Figure() df = UnfoldMakie.eeg_array_to_dataframe(erp') @@ -66,4 +66,4 @@ plot_erpimage!( ) f -# The scaling by the two `condition` effect levels and the modified event duration by the `duration` are clearly visible +# The scaling by the two `condition` effect levels and the modified event duration by the `duration` are clearly visible. diff --git a/docs/literate/HowTo/newComponent.jl b/docs/literate/HowTo/newComponent.jl index 3a8e9df0..a1b39cb7 100644 --- a/docs/literate/HowTo/newComponent.jl +++ b/docs/literate/HowTo/newComponent.jl @@ -2,7 +2,7 @@ # We want a new component that changes its duration and shift depending on a column in the event design. This is somewhat already implemented in the HRF + Pupil bases. # !!! hint -# if you are just interested to use duration-dependency in your simulation, check out the component-function tutorial +# If you are just interested to use duration-dependency in your simulation, check out the [component function tutorial](@ref componentfunction). diff --git a/docs/literate/reference/onsettypes.jl b/docs/literate/reference/onsettypes.jl index 3eee6cd8..18d10f81 100644 --- a/docs/literate/reference/onsettypes.jl +++ b/docs/literate/reference/onsettypes.jl @@ -288,7 +288,7 @@ end # ## Design-dependent `X-OnsetFormula` -# For additional control we provide `UniformOnsetFormula` and `LogNormalOnsetFormula` types, that allow to control all parameters by specifying formulas +# For additional control, we provide `UniformOnsetFormula` and `LogNormalOnsetFormula` types, which allow to control all distribution parameters by specifying formulas based on the design o = UnfoldSim.UniformOnsetFormula( width_formula = @formula(0 ~ 1 + cond), width_β = [50, 20], @@ -303,4 +303,4 @@ hist!(ax, onsets[events.cond .== "B"], bins = range(0, 100, step = 1), label = " axislegend(ax) f -# Voila - the inter-onset intervals are `20` samples longer for condition `B`, exactly as specified. \ No newline at end of file +# Voila - the inter-onset intervals are `20` samples longer for condition `B`, exactly as specified. diff --git a/docs/make.jl b/docs/make.jl index a2b69a8c..7feb7d86 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -50,7 +50,7 @@ makedocs(; ], "HowTo" => [ "Define a new (imbalanced) design" => "./generated/HowTo/newDesign.md", - "Component basisfunction (duration-dependent)" => "./generated/HowTo/componentfunction.md", + "Define design-dependent component basis functions" => "./generated/HowTo/componentfunction.md", "Get multiple trials with identical subject/item combinations" => "./generated/HowTo/repeatTrials.md", "Define a new component (with variable duration and shift)" => "./generated/HowTo/newComponent.md", "Generate multi channel data" => "./generated/HowTo/multichannel.md", diff --git a/src/component.jl b/src/component.jl index b4590f32..62934ef8 100644 --- a/src/component.jl +++ b/src/component.jl @@ -255,7 +255,7 @@ end """ function limit_basis(b::AbstractVector{<:AbstractVector}, maxlength) - # first cut off maxlength + # first cut off after maxlength b = limit_basis.(b, maxlength) # now fill up with 0's Δlengths = maxlength .- length.(b) @@ -429,6 +429,8 @@ function simulate_component( # in case the parameters are of interest, we will return those, not them weighted by basis b = return_parameters ? [1.0] : get_basis(deepcopy(rng), c, design) @debug :b, typeof(b), size(b), :m, size(m.y') + # in case get_basis returns a Matrix, it will be trial x time (or the transpose of it, we didnt check when writing this comment), thus we only need to scale each row by the scaling factor from the LMM + # in case get_basis returns a Vector of length time, it needs to be "repeated" to a trial x time matrix and then scaled again. The kronecker product efficiently does that. if isa(b, AbstractMatrix) epoch_data_component = ((m.y' .* b)) else diff --git a/src/onset.jl b/src/onset.jl index e0182857..58986423 100644 --- a/src/onset.jl +++ b/src/onset.jl @@ -263,6 +263,7 @@ end """ simulate_interonset_distances(rng, onsets::ShiftOnsetByOne, design) + Same functionality as `simulate_interonset_distances(rng,onsets::AbstractOnset)` except that it shifts the resulting vector by one, adding a `0` to the front and removing the last simuluated distance. """ UnfoldSim.simulate_interonset_distances(rng, onsets::ShiftOnsetByOne, design) = @@ -272,7 +273,7 @@ UnfoldSim.simulate_interonset_distances(rng, onsets::ShiftOnsetByOne, design) = """ UniformOnsetFormula <: AbstractOnset -Provides a Uniform Distribution of the inter-event distances, but with regression formulas. +Provide a Uniform Distribution for the inter-event distances, but with regression formulas for the distribution's parameters `offset` and `width`. This is helpful if your overlap/event-distribution should be dependent on some condition, e.g. more overlap in cond = 'A' than cond = 'B'. `Offset` affects the minimal distance. The maximal distance is `offset + width`. @@ -283,7 +284,7 @@ This is helpful if your overlap/event-distribution should be dependent on some c - `offset_β::Vector = [0] `(optional): Choose a `Vector` of betas. The number of betas needs to fit the formula chosen. - `offset_contrasts::Dict = Dict()` (optional): Choose a contrasts-`Dict`ionary according to the StatsModels specifications. - `width_formula = `@formula(0~1)`: Choose a formula depending on your `Design`. -- `width_β::Vector = [0] (optional)`: Choose a `Vector` of betas, number needs to fit the formula chosen. +- `width_β::Vector`: Choose a `Vector` of betas, number needs to fit the formula chosen. - `width_contrasts::Dict = Dict()` (optional) : Choose a contrasts-`Dict`ionary according to the StatsModels specifications. # Combined with [ShiftOnsetByOne](@ref) @@ -330,7 +331,8 @@ end LogNormalOnsetFormula <: AbstractOnset -Provide a Log-normal Distribution of the inter-event distances, but with regression formulas. +Provide a Log-normal Distribution of the inter-event distances, but with regression formulas for the distribution's parameters `offset`, `μ` and `σ`. + This is helpful if your overlap/event-distribution should be dependent on some condition, e.g. more overlap in cond = 'A' than cond = 'B'. # Fields diff --git a/test/sequence.jl b/test/sequence.jl index a9b1e149..2f66ff03 100644 --- a/test/sequence.jl +++ b/test/sequence.jl @@ -54,5 +54,6 @@ end ) s, e = simulate(design, c, NoOnset(); return_epoched = true) @test size(s) == (40, 6) + @test s[:, 1] == s[:, 2] # If no component dict is specified, all events have the same component end From e2f96c2d6f9f5b7694024cb950c003cde974cc5f Mon Sep 17 00:00:00 2001 From: Benedikt Ehinger Date: Fri, 7 Nov 2025 16:17:08 +0100 Subject: [PATCH 155/156] Apply suggestions from code review Co-authored-by: Judith Schepers --- src/design.jl | 2 +- src/onset.jl | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/design.jl b/src/design.jl index b1a25d6c..72532d3b 100644 --- a/src/design.jl +++ b/src/design.jl @@ -501,7 +501,7 @@ Design to obtain ground truth simulation. # Fields - `design::AbstractDesign`: The design of your (main) simulation. -- `effects_dict::Dict`: Effects.jl style dictionary specifying variable effects. See also [Unfold.jl marginalized effects](https://unfoldtoolbox.github.io/UnfoldDocs/Unfold.jl/stable/generated/HowTo/effects/) +- `effects_dict::Dict`: Effects.jl style dictionary specifying variable effects. See also [Unfold.jl marginal effects](https://unfoldtoolbox.github.io/UnfoldDocs/Unfold.jl/stable/generated/HowTo/effects/) # Examples """ diff --git a/src/onset.jl b/src/onset.jl index 58986423..e73d5048 100644 --- a/src/onset.jl +++ b/src/onset.jl @@ -295,9 +295,16 @@ This is possible by using `ShiftOnsetByOne(UniformOnset(...))`, effectively shif # Examples ```julia-repl julia> o = UnfoldSim.UniformOnsetFormula( - width_formula = @formula(0 ~ 1 + cond), - width_β = [50, 20], -) + width_formula = @formula(0 ~ 1 + cond), + width_β = [50, 20], + ) +UniformOnsetFormula + width_formula: StatsModels.FormulaTerm{StatsModels.ConstantTerm{Int64}, Tuple{StatsModels.ConstantTerm{Int64}, StatsModels.Term}} + width_β: Array{Int64}((2,)) [50, 20] + width_contrasts: Dict{Any, Any} + offset_formula: StatsModels.FormulaTerm{StatsModels.ConstantTerm{Int64}, StatsModels.ConstantTerm{Int64}} + offset_β: Array{Int64}((1,)) [0] + offset_contrasts: Dict{Any, Any} ``` See also [`UniformOnset`](@ref UnfoldSim.UniformOnset) for a simplified version without linear regression specifications. From 83682d9f0d5b1f8455600469dc969b59e3d0c431 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Fri, 7 Nov 2025 16:17:35 +0100 Subject: [PATCH 156/156] addressed code review issues, docstrings, some renaming --- src/design.jl | 22 ++++++++++++++++++++-- src/onset.jl | 15 +++++++++++++-- src/sequence.jl | 19 ++++++++++--------- test/component.jl | 3 ++- test/onset.jl | 23 +++++++++++++++++++++++ test/sequence.jl | 30 +++++++++++++++++++++++++++--- 6 files changed, 95 insertions(+), 17 deletions(-) diff --git a/src/design.jl b/src/design.jl index b1a25d6c..5d585ed9 100644 --- a/src/design.jl +++ b/src/design.jl @@ -439,12 +439,18 @@ generate_events(rng, design::SequenceDesign{MultiSubjectDesign}) = error("not yet implemented") +""" + generate_events(rng, design::SequenceDesign) + +First generates a sequence-string using `evaluate_sequencestring(rng,design.sequence)`. Then generates events from the nested `design.design` and repeats them according to the length of the sequence string. +Finally, assigns the sequence-string-characters to the `:event` column in `events`. +""" function generate_events(rng, design::SequenceDesign) df = generate_events(deepcopy(rng), design.design) nrows_df = size(df, 1) # @debug design.sequence - currentsequence = sequencestring(rng, design.sequence) + currentsequence = evaluate_sequencestring(rng, design.sequence) # @debug currentsequence currentsequence = replace(currentsequence, "_" => "") df = repeat(df, inner = length(currentsequence)) @@ -497,13 +503,25 @@ end """ EffectsDesign <: AbstractDesign -Design to obtain ground truth simulation. +This design evaluates the nested design at the marginalized effects specified in the `effects_dict`. That is, it calculates all combination of the variables in the `effects_dict` while setting all other variables to a "typical" value (i.e. the mean for numerical variables). # Fields - `design::AbstractDesign`: The design of your (main) simulation. - `effects_dict::Dict`: Effects.jl style dictionary specifying variable effects. See also [Unfold.jl marginalized effects](https://unfoldtoolbox.github.io/UnfoldDocs/Unfold.jl/stable/generated/HowTo/effects/) # Examples +```julia-repl +design = + SingleSubjectDesign(; + conditions = Dict( + :condition => ["bike", "face"], + :continuous => range(0, 5, length = 10), + ), + ) |> x -> RepeatDesign(x, 5); + +effects_dict = Dict(:condition => ["bike", "face"]) +effects_design = EffectsDesign(design, effects_dict) +``` """ struct EffectsDesign <: AbstractDesign design::AbstractDesign diff --git a/src/onset.jl b/src/onset.jl index 58986423..2a9b2d72 100644 --- a/src/onset.jl +++ b/src/onset.jl @@ -177,9 +177,12 @@ function simulate_interonset_distances(rng, onset::LogNormalOnset, design::Abstr end +""" +Returns true if the design, or any nested design contains the target design type +""" contains_design(d::AbstractDesign, target::Type) = false contains_design(d::Union{RepeatDesign,SequenceDesign,SubselectDesign}, target::Type) = - d.design isa target ? true : contains_design(d.design, target) + (d isa target || d.design isa target) ? true : contains_design(d.design, target) """ @@ -235,7 +238,7 @@ function simulate_onsets(rng, onset::AbstractOnset, simulation::Simulation) if contains_design(simulation.design, SequenceDesign) - currentsequence = sequencestring(deepcopy(rng), simulation.design) + currentsequence = evaluate_sequencestring(deepcopy(rng), simulation.design) if !isnothing(findfirst("_", currentsequence)) @assert currentsequence[end] == '_' "the blank-indicator '_' has to be the last sequence element" @@ -335,6 +338,10 @@ Provide a Log-normal Distribution of the inter-event distances, but with regress This is helpful if your overlap/event-distribution should be dependent on some condition, e.g. more overlap in cond = 'A' than cond = 'B'. +μ: The mean of the log-transformed variable (the log-normal random variable's logarithm follows a normal distribution). +σ: The standard deviation of the log-transformed variable. +offset: The minimal distance between events - aka a shift of the LogNormal distribution. + # Fields - `μ_formula = @formula(0~1)` (optional): Choose a formula depending on your `design` @@ -343,6 +350,10 @@ This is helpful if your overlap/event-distribution should be dependent on some c - `σ_formula = @formula(0~1)` (optional): Choose a formula depending on your `Design`. - `σ_β::Vector`: Choose a `Vector` of betas, number needs to fit the formula chosen. - `σ_contrasts::Dict = Dict()` (optional) : Choose a contrasts-`Dict`ionary according to the StatsModels specifications. +- `offset_formula = @formula(0~1)` (optional): Choose a formula depending on your `design` for the offset. +- `offset_β::Vector = [0] ` (optional): Choose a `Vector` of betas. The number of betas needs to fit the formula chosen. +- `offset_contrasts::Dict = Dict()` (optional): Choose a contrasts-`Dict`ionary according to the StatsModels specifications. +- `truncate_upper::nothing` (optional): Upper limit (in samples) at which the distribution is truncated (formula for truncation currently not implemented) # Combined with [ShiftOnsetByOne](@ref) diff --git a/src/sequence.jl b/src/sequence.jl index ad8a241a..602940a2 100644 --- a/src/sequence.jl +++ b/src/sequence.jl @@ -12,9 +12,10 @@ Mimicks a reverse-regex, generating strings from regex instead of matching. Base # Examples ```julia-repl julia> using Automa -julia> machine = Automa.Compile(Automa.RegExp.RE("b+l+a+)) +julia> machine = Automa.compile(Automa.RegExp.RE("b+l+a+")) julia> rand_re(MersenneTwister(2),machine) "bbbbblllaaa" +``` """ function rand_re(rng::AbstractRNG, machine::Automa.Machine) out = IOBuffer() @@ -22,7 +23,7 @@ function rand_re(rng::AbstractRNG, machine::Automa.Machine) while true if node.state ∈ machine.final_states - (rand() ≤ 1 / (length(node.edges) + 1)) && break + (rand(rng) ≤ 1 / (length(node.edges) + 1)) && break end edge, node = rand(rng, node.edges) @@ -33,13 +34,13 @@ function rand_re(rng::AbstractRNG, machine::Automa.Machine) return String(take!(out)) end -sequencestring(rng, d::SequenceDesign) = sequencestring(rng, d.sequence) +evaluate_sequencestring(rng, d::SequenceDesign) = evaluate_sequencestring(rng, d.sequence) """ - sequencestring(rng, str::String) - sequencestring(rng, dS::SequenceDesign) - sequencestring(rng, dR::RepeatDesign) + evaluate_sequencestring(rng, str::String) + evaluate_sequencestring(rng, dS::SequenceDesign) + evaluate_sequencestring(rng, dR::RepeatDesign) Generate a sequence based on the reverse regex style string in `str`, `dS.sequence` or `dR.design.sequence`. @@ -53,13 +54,13 @@ Directly converting to Automa.Compileis not possible, as we first need to match # Examples ```julia-repl -julia> sequencestring(MersenneTwister(1),"bla{3,4}") +julia> evaluate_sequencestring(MersenneTwister(1),"bla{3,4}") "blaaaa" ``` See also [`rand_re`](@ref) """ -function sequencestring(rng, str::String) +function evaluate_sequencestring(rng, str::String) #match curly brackets and replace them @assert isnothing(findfirst("*", str)) && isnothing(findfirst("+", str)) "'Infinite' sequences are currently not supported." crly = collect(eachmatch(r"(\{[\d]+,[\d]+\})", str)) @@ -90,4 +91,4 @@ function sequencestring(rng, str::String) return rand_re(rng, Automa.compile(RE(str))) end -sequencestring(rng, d::RepeatDesign) = sequencestring(rng, d.design) +evaluate_sequencestring(rng, d::RepeatDesign) = evaluate_sequencestring(rng, d.design) diff --git a/test/component.jl b/test/component.jl index eff14492..9c476225 100644 --- a/test/component.jl +++ b/test/component.jl @@ -182,7 +182,8 @@ UniformOnset(offset = o_offset, width = o_width), NoNoise(), ) - sequence_length = length(UnfoldSim.sequencestring(StableRNG(seed), design)) - 1 # without _ + sequence_length = + length(UnfoldSim.evaluate_sequencestring(StableRNG(seed), design)) - 1 # without _ # Test onset shifts with component offsets and sequences (in particular inter-event-block distances) combined @test minoffset_shift + 1 <= e.latency[1] <= minoffset_shift + 1 + o_width diff --git a/test/onset.jl b/test/onset.jl index 7cea46cf..a21f08a3 100644 --- a/test/onset.jl +++ b/test/onset.jl @@ -125,3 +125,26 @@ end end + +@testset "contains design" begin + @test UnfoldSim.contains_design( + RepeatDesign(SequenceDesign(SingleSubjectDesign(), "ABC"), 1), + SequenceDesign, + ) + @test UnfoldSim.contains_design( + RepeatDesign(SequenceDesign(SingleSubjectDesign(), "ABC"), 1), + SingleSubjectDesign, + ) + @test UnfoldSim.contains_design( + RepeatDesign(SequenceDesign(SingleSubjectDesign(), "ABC"), 1), + RepeatDesign, + ) + @test UnfoldSim.contains_design( + SequenceDesign(SingleSubjectDesign(), "ABC"), + SequenceDesign, + ) + @test !UnfoldSim.contains_design( + SequenceDesign(SingleSubjectDesign(), "ABC"), + RepeatDesign, + ) +end \ No newline at end of file diff --git a/test/sequence.jl b/test/sequence.jl index 2f66ff03..b12cadd6 100644 --- a/test/sequence.jl +++ b/test/sequence.jl @@ -1,3 +1,4 @@ +using Automa, Random, Test @testset "Check Sequences" begin @test isa(UnfoldSim.check_sequence("bla_"), String) @test isa(UnfoldSim.check_sequence("bla"), String) @@ -5,9 +6,9 @@ @test_throws AssertionError UnfoldSim.check_sequence("b_la") @test_throws AssertionError UnfoldSim.check_sequence("_bla") - @test length(UnfoldSim.sequencestring(StableRNG(1), "A{10,10}")) == 10 - @test length(UnfoldSim.sequencestring(StableRNG(1), "A{10,10}B")) == 11 - @test length(UnfoldSim.sequencestring(StableRNG(1), "A{10,20}")) >= 10 + @test length(UnfoldSim.evaluate_sequencestring(StableRNG(1), "A{10,10}")) == 10 + @test length(UnfoldSim.evaluate_sequencestring(StableRNG(1), "A{10,10}B")) == 11 + @test length(UnfoldSim.evaluate_sequencestring(StableRNG(1), "A{10,20}")) >= 10 end @testset "Simulate Sequences" begin @@ -57,3 +58,26 @@ end @test s[:, 1] == s[:, 2] # If no component dict is specified, all events have the same component end + +@testset "rand_re" begin + + machine = Automa.compile(Automa.RegExp.RE("b+l+a+")) + @test UnfoldSim.rand_re(MersenneTwister(2), machine) == "bbbbblllaa" + # trivial single-character regex + @test UnfoldSim.rand_re(MersenneTwister(1), Automa.compile(Automa.RegExp.RE("a"))) == + "a" + + # different seeds should (very likely) produce different strings + r1 = UnfoldSim.rand_re(MersenneTwister(1), machine) + r2 = UnfoldSim.rand_re(MersenneTwister(2), machine) + @test r1 != r2 + + # produced string should match the regex pattern + for k = 1:10 + s = UnfoldSim.rand_re(MersenneTwister(k), machine) + @test occursin(r"^b+l+a+$", s) + end + + # integration: evaluate_sequencestring expands curly braces and delegates to rand_re + @test UnfoldSim.evaluate_sequencestring(MersenneTwister(1), "bla{3,4}") == "blaaaa" +end \ No newline at end of file