Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sequence #83

Open
wants to merge 40 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
8405a1a
FormulaOnsets
behinger Feb 23, 2024
afc69d7
initial sequence tryout
behinger Feb 25, 2024
49db0ba
fix bug in predef_eeg
behinger Feb 25, 2024
8e6de75
merge
behinger Feb 28, 2024
cd489c4
forgot the end
behinger Feb 28, 2024
6c35f3c
half way through to success for sequence designs or something
behinger Feb 28, 2024
809dc2e
everythinig except sequencelength seems to be working now
behinger Feb 28, 2024
31c69e6
added string sequence tests
behinger Feb 28, 2024
54e9334
small doc update
behinger Mar 1, 2024
7114cd6
added jitter to the '_' trial divisor
behinger Mar 9, 2024
85f037b
Improve documentation especially quickstart, home and power analysis
jschepers Jul 10, 2024
2c65a7f
adapted the order of reference overviews and adapted titles
jschepers Jul 18, 2024
ee9e00a
Updated quickstart page
jschepers Jul 22, 2024
ac0d3bd
minor changes
jschepers Jul 22, 2024
373e535
fixed docstrings for predef_eeg and predef_2x2
jschepers Jul 23, 2024
b073526
added draft of design types reference page
jschepers Jul 23, 2024
89c09a1
Update quickstart.jl
behinger Jul 24, 2024
1e2694f
Add UnfoldSim logo to the documentation
jschepers Jul 24, 2024
9ab04aa
Finished experimental design reference page
jschepers Jul 24, 2024
5671e2e
Replace logo file
jschepers Jul 25, 2024
10c77a3
Update logo file
jschepers Jul 25, 2024
ea25ddc
Delete docs/src/assets/logo.svg
jschepers Jul 25, 2024
e0c2310
Add logo as png file
jschepers Jul 25, 2024
34ce887
Added intro paragraph for Simulate ERP tutorial
jschepers Jul 30, 2024
b5a60bf
Improved docstrings for single- and multi-subject design
jschepers Jul 30, 2024
b81b605
Fixed simulate docstring
jschepers Jul 30, 2024
369faff
Added cross references in docstrings
jschepers Jul 31, 2024
541cf67
Added intro sentences, matched titles and sidebar, reordered pages an…
jschepers Jul 31, 2024
f449083
Update noise.jl
behinger Jul 26, 2024
fe00ec2
Update src/noise.jl
behinger Jul 26, 2024
f93a3c1
Update src/noise.jl
behinger Jul 26, 2024
aca3fd2
Update src/noise.jl
jschepers Jul 31, 2024
e868a14
add empty line for formatting reasons
jschepers Jul 31, 2024
43dd57c
Update docs/literate/reference/noisetypes.jl
jschepers Aug 1, 2024
68ce722
Update docs/literate/reference/overview.jl
jschepers Aug 1, 2024
2dfd44b
Update docs/literate/reference/overview.jl
jschepers Aug 1, 2024
2c34e5e
Update docs/literate/reference/noisetypes.jl
jschepers Aug 1, 2024
8848209
Merge branch 'main' into sequence
behinger Aug 7, 2024
9749685
merge joss doc changes
behinger Aug 7, 2024
3a3bd3d
added docstring
behinger Aug 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
52 changes: 52 additions & 0 deletions docs/literate/HowTo/sequence.jl
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

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

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
lines(data)
lines(data)

22 changes: 22 additions & 0 deletions docs/literate/reference/onsettypes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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.`
Copy link
Contributor

Choose a reason for hiding this comment

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

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
# Voila - the inter-onset intervals are `20` samples longer for condition `B`, exactly as specified.`
# Voila - the inter-onset intervals are `20` samples longer for condition `B`, exactly as specified.`

Copy link
Contributor

Choose a reason for hiding this comment

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

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
# Voila - the inter-onset intervals are `20` samples longer for condition `B`, exactly as specified.`
# Voila - the inter-onset intervals are `20` samples longer for condition `B`, exactly as specified.`

1 change: 1 addition & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
Expand Down
4 changes: 3 additions & 1 deletion src/UnfoldSim.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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)
Expand Down
83 changes: 69 additions & 14 deletions src/component.jl
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,16 @@
"""
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]

Check warning on line 109 in src/component.jl

View check run for this annotation

Codecov / codecov/patch

src/component.jl#L106-L109

Added lines #L106 - L109 were not covered by tests

end
"""
simulate_component(rng,c::MultichannelComponent,design::AbstractDesign)
Return the projection of a component from source to "sensor" space.
Expand All @@ -124,10 +130,13 @@

"""
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)])

Check warning on line 139 in src/component.jl

View check run for this annotation

Codecov / codecov/patch

src/component.jl#L139

Added line #L139 was not covered by tests
"""
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 :)
Expand All @@ -145,22 +154,32 @@
"""
function simulate_component(rng, c::LinearModelComponent, design::AbstractDesign)
events = generate_events(design)
X = generate_designmatrix(c.formula, events, c.contrasts)
y = X * c.β

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)

Check warning on line 176 in src/component.jl

View check run for this annotation

Codecov / codecov/patch

src/component.jl#L176

Added line #L176 was not covered by tests
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.
Expand Down Expand Up @@ -286,19 +305,55 @@
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

Check warning on line 339 in src/component.jl

View check run for this annotation

Codecov / codecov/patch

src/component.jl#L333-L339

Added lines #L333 - L339 were not covered by tests
end
s_key = Simulation(
s.design |> x -> SubselectDesign(x, key),

Check warning on line 342 in src/component.jl

View check run for this annotation

Codecov / codecov/patch

src/component.jl#L341-L342

Added lines #L341 - L342 were not covered by tests
components[key],
s.onset,
s.noisetype,
)
ix = evts.event .== key
if multichannel
simulate_responses!(rng, @view(epoch_data[:, :, ix]), components[key], s_key)

Check warning on line 349 in src/component.jl

View check run for this annotation

Codecov / codecov/patch

src/component.jl#L347-L349

Added lines #L347 - L349 were not covered by tests
else
#@debug sum(ix), size(simulate_responses(rng, components[key], s_key)), key
simulate_responses!(rng, @view(epoch_data[:, ix]), components[key], s_key)

Check warning on line 352 in src/component.jl

View check run for this annotation

Codecov / codecov/patch

src/component.jl#L352

Added line #L352 was not covered by tests
end
end
return epoch_data

Check warning on line 355 in src/component.jl

View check run for this annotation

Codecov / codecov/patch

src/component.jl#L354-L355

Added lines #L354 - L355 were not covered by tests
end

"""
simulate_and_add!(epoch_data::AbstractMatrix, c, simulation, rng)
Expand Down
44 changes: 44 additions & 0 deletions src/design.jl
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,28 @@
repeat::Int = 1
end


@with_kw struct SequenceDesign{T} <: AbstractDesign
design::T
sequence::String = ""
end

UnfoldSim.generate_events(design::SequenceDesign{MultiSubjectDesign}) =

Check warning on line 156 in src/design.jl

View check run for this annotation

Codecov / codecov/patch

src/design.jl#L156

Added line #L156 was not covered by tests
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)

Check warning on line 165 in src/design.jl

View check run for this annotation

Codecov / codecov/patch

src/design.jl#L159-L165

Added lines #L159 - L165 were not covered by tests

return df

Check warning on line 167 in src/design.jl

View check run for this annotation

Codecov / codecov/patch

src/design.jl#L167

Added line #L167 was not covered by tests

end


"""
UnfoldSim.generate_events(design::RepeatDesign{T})

Expand All @@ -160,6 +182,28 @@
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)

Check warning on line 196 in src/design.jl

View check run for this annotation

Codecov / codecov/patch

src/design.jl#L195-L196

Added lines #L195 - L196 were not covered by tests
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) =

Check warning on line 203 in src/design.jl

View check run for this annotation

Codecov / codecov/patch

src/design.jl#L203

Added line #L203 was not covered by tests
size(design.design) .* length(replace(design.sequence, "_" => ""))

Base.size(design::RepeatDesign{SequenceDesign{SingleSubjectDesign}}) =

Check warning on line 206 in src/design.jl

View check run for this annotation

Codecov / codecov/patch

src/design.jl#L206

Added line #L206 was not covered by tests
size(design.design) .* design.repeat

Base.size(design::SubselectDesign) = size(generate_events(design), 1)

Check warning on line 209 in src/design.jl

View check run for this annotation

Codecov / codecov/patch

src/design.jl#L209

Added line #L209 was not covered by tests
Copy link
Contributor

Choose a reason for hiding this comment

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

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
Base.size(design::SubselectDesign) = size(generate_events(design), 1)
Base.size(design::SubselectDesign) = size(generate_events(design), 1)

Loading
Loading