-
Notifications
You must be signed in to change notification settings - Fork 7
OnsetFormulas, Sequences, and Component Functions - v0.4.0 -> actually v0.5.0 #104
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
base: main
Are you sure you want to change the base?
Changes from all commits
8405a1a
afc69d7
49db0ba
41d7306
8e6de75
cd489c4
6c35f3c
809dc2e
31c69e6
54e9334
7114cd6
b4a8ac8
0c3a11d
de8c2a8
57a7f68
85f037b
2c65a7f
ee9e00a
ac0d3bd
373e535
b073526
89c09a1
1e2694f
9ab04aa
5671e2e
10c77a3
ea25ddc
e0c2310
34ce887
b5a60bf
b81b605
369faff
541cf67
f449083
fe00ec2
f93a3c1
aca3fd2
e868a14
43dd57c
68ce722
2dfd44b
2c34e5e
8848209
9749685
3a3bd3d
43e28e8
f5300c3
b8d85b9
b204234
04a1ed3
e05a9c6
8aa7591
395b5ee
da51a16
9603df9
612786a
272eb02
360b0eb
f082ce4
ba10e1d
f5facfe
7a0fffc
0e7df88
a10d5a9
5b53765
6685d8d
523f90b
5fb95bf
d3fa46e
5574425
1966fbe
fa6f78b
b9cde99
60b8941
01a1e44
33bc2b6
5d38311
25009e4
1a133ae
e14765b
7996783
d5c7b4d
c528f6a
73409f4
08b0738
ddbafdf
1de2186
36b1920
e610041
2cbad0b
f49004d
9be7376
d1defe0
c23c317
6ccd668
c28fbc1
89c3454
157932f
d7a87a2
cfb46b8
0bb23fc
82603d8
96c3fc5
ea7b288
ad13734
755daaa
8513184
ef9556d
c6b719f
adc225d
f0037f7
2b7930c
34be071
5908840
0e2a446
37539f2
2b8f600
c9afd8a
4adb5fd
a8d4d08
d6d64a6
5416d4d
3947c57
991ab38
02d38c7
7a0f81f
b345da4
a8c3801
b43a5a1
a6f4e3c
0c78de8
878c019
ca9af4c
6109b13
fb14397
89ee8e0
8f27939
c8fbae8
33ca351
4bf0034
f291a95
badd2db
d6101c3
885e0f2
3b65091
45ed602
0229392
b89fab7
86623ef
cedde16
05f699f
c697004
f5d21b7
34521a5
564be7b
5faf54b
b5d2d00
61b7d36
5b3cfc4
3227b90
0d8d5d1
683b273
a1ea003
ac7201b
d173c46
82a2e6a
72131a1
4381233
ea91727
48968af
75bed90
6f704f5
8249bce
27b5f15
62fcc24
dfc267d
75c9477
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,69 @@ | ||||||
| # # Component Basisfunctions | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| # HowTo use functions that depend on the `design` and return per-event basis-vectors, instead of the same basis vector for all events. | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
|
|
||||||
| # ### Setup | ||||||
| # ```@raw html | ||||||
| # <details> | ||||||
| # <summary>Click to expand</summary> | ||||||
| # ``` | ||||||
| ## Load required packages | ||||||
| using UnfoldSim | ||||||
| using Unfold | ||||||
| using Random | ||||||
| using DSP | ||||||
| using CairoMakie, UnfoldMakie | ||||||
| # ```@raw html | ||||||
| # </details > | ||||||
| # ``` | ||||||
|
|
||||||
|
|
||||||
| 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)), | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For this example, the magnitude does not really matter, but do I understand it correctly that the duration is ~20s? |
||||||
| ), | ||||||
| ); | ||||||
|
|
||||||
|
|
||||||
| # 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. | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| # !!! 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)->...` | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| # 2. a `maxlength` has to be specified via a tuple `(function,maxlength)`` | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here, it would be nice to mention whether the maxlength is given in samples or s. I guess samples, right? |
||||||
|
|
||||||
| 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); | ||||||
|
|
||||||
| # 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. | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| ##--- | ||||||
| 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 = :group => nonnumeric, col = :category), # color = :duration, fails right nowUnfoldMakie#353 | ||||||
| layout = (; legend_position = :left), | ||||||
| colorbar = (; label = "Duration"), | ||||||
| ) | ||||||
| plot_erpimage!( | ||||||
| f[2, 1], | ||||||
| erp, | ||||||
| sortvalues = generate_events(design).duration, | ||||||
| layout = (; legend_position = :bottom), | ||||||
| ) | ||||||
| f | ||||||
|
Comment on lines
+49
to
+67
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would suggest to hide the code for the figure or rather have it in an expandable/collapsible element, such that it can be opened if needed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And the legend of the figure needs fixing but we can postpone this and collect it in an issue: #99 (comment) (I added it already) |
||||||
|
|
||||||
| # The scaling by the two `condition` effect levels and the modified event duration by the `duration` are clearly visible | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
behinger marked this conversation as resolved.
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| # # 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 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 | ||
| # <details> | ||
| # <summary>Click to expand</summary> | ||
| # ``` | ||
| ## Load required packages | ||
| using UnfoldSim | ||
| using Unfold | ||
| using CairoMakie | ||
| using UnfoldMakie | ||
| using Random | ||
| # ```@raw html | ||
| # </details > | ||
| # ``` | ||
| # ## Simulation | ||
| # First let's set up a SingleSubject simulation | ||
|
|
||
| # !!! note | ||
| # An `EffectsDesign` for a `MultiSubjectDesign` is not implemented yet. | ||
|
|
||
| design = | ||
| SingleSubjectDesign(; | ||
| conditions = Dict( | ||
| :condition => ["bike", "face"], | ||
| :continuous => range(0, 5, length = 10), | ||
| ), | ||
| ) |> x -> RepeatDesign(x, 5); | ||
|
|
||
| # **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(), | ||
| ); | ||
|
|
||
| # ## 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"]) | ||
|
|
||
| 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 marginal effects | ||
|
|
||
| gt_data, gt_events = simulate( | ||
| MersenneTwister(1), | ||
| effects_design, | ||
| components, | ||
| NoOnset(), | ||
| NoNoise(), | ||
| return_epoched = true, | ||
| ); | ||
| @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, size(gt_data, 1)); | ||
| gt_effects = Unfold.result_to_table([g], [gt_events], [times], ["effects"]) | ||
| first(gt_effects, 5) | ||
|
|
||
|
|
||
| # ## Compare with Unfold.jl results | ||
|
|
||
| m = fit( | ||
| UnfoldModel, | ||
| [ | ||
| Any => ( | ||
| @formula(0 ~ 1 + condition + spl(continuous, 4)), | ||
| firbasis(τ = [-0.1, 1], sfreq = 100, name = "basis"), | ||
| ), | ||
| ], | ||
| evts, | ||
| data, | ||
| ); | ||
|
|
||
| ef = effects(effects_dict, m); | ||
|
|
||
| # !!! note | ||
| # 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" | ||
|
|
||
| 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). |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,13 +1,18 @@ | ||||||
| # # 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. | ||||||
| # !!! hint | ||||||
| # if you are just interested to use duration-dependency in your simulation, check out the component-function tutorial | ||||||
behinger marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
| # ### Setup | ||||||
| # ```@raw html | ||||||
| # <details> | ||||||
| # <summary>Click to expand</summary> | ||||||
| # ``` | ||||||
| using UnfoldSim | ||||||
| import UnfoldSim.simulate_component | ||||||
| using Unfold | ||||||
| using Random | ||||||
| using DSP | ||||||
|
|
@@ -39,7 +44,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 | ||||||
|
|
@@ -62,7 +67,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, | ||||||
|
|
||||||
behinger marked this conversation as resolved.
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| # # Sequence of events (e.g. SCR) | ||
|
|
||
| # In this HowTo you will learn to simulate a "SR"-Sequence, a stimulus response, followed by a button press response. | ||
|
|
||
| # ### Setup | ||
| # ```@raw html | ||
| # <details> | ||
| # <summary>Click to expand</summary> | ||
| # ``` | ||
| ## Load required packages | ||
| using UnfoldSim | ||
| using CairoMakie | ||
| using StableRNGs | ||
| # ```@raw html | ||
| # </details > | ||
| # <br /> | ||
| # ``` | ||
|
|
||
| # ## 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 `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*". | ||
|
|
||
| # Finally, let's repeat the current design 4 times | ||
| design = RepeatDesign(design, 4) | ||
| generate_events(StableRNG(1), 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. | ||
|
|
||
| # ## 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(), | ||
| 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 | ||
| 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, | ||
| components, | ||
| UniformOnset(offset = 40, width = 10), | ||
| NoNoise(), | ||
| ) | ||
| 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)) | ||
| 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. |
behinger marked this conversation as resolved.
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -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 | ||||||||
|
|
@@ -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 `X-OnsetFormula` | ||||||||
|
|
||||||||
| # For additional control we provide `UniformOnsetFormula` and `LogNormalOnsetFormula` types, that allow to control all parameters by specifying formulas | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| o = UnfoldSim.UniformOnsetFormula( | ||||||||
| 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. | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [JuliaFormatter] reported by reviewdog 🐶
Suggested change
|
||||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My suggestions:
design? -> Maybe we should distinguish between the general possibility to define functions for the basis functions and the duration example?