Skip to content
Draft

trf #107

Show file tree
Hide file tree
Changes from all commits
Commits
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
81 changes: 81 additions & 0 deletions docs/literate/tutorials/TRF.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using UnfoldSim
using CairoMakie
using Random

# # Temporal response functions
# So far, we simulated event-related potentials. In this tutorial, we will switch gears a bit, and simulate a continuous response to a continuous feature vector.
#
# One good example is the visual response to a grayscale circle, which continuously changes its luminance (this goes back to VESPA, described in the ground breaking [Lalor 2006 paper](https://doi.org/10.1016/j.neuroimage.2006.05.054). The brain will react to this luminance change continuously. TRFs typically describe the process to recover the brain response (in terms of a filter response). Here we set out to simulate such a dataset first and foremost.
#
# ## Simulation
# we start with the simplest possible design, one condition
design = SingleSubjectDesign(conditions=Dict(:dummy=>["A"]));
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
design = SingleSubjectDesign(conditions=Dict(:dummy=>["A"]));
design = SingleSubjectDesign(conditions = Dict(:dummy => ["A"]));


# next we define the convolution kernel that the feature signal should be convolved with (= the brain response == the TRF)
brain_response = [LinearModelComponent(basis=p100(),formula=@formula(0~1),β=[1]),
LinearModelComponent(basis=n170(),formula=@formula(0~1),β=[1]),
LinearModelComponent(basis=p300(),formula=@formula(0~1),β=[1])];
Comment on lines +15 to +17
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
brain_response = [LinearModelComponent(basis=p100(),formula=@formula(0~1),β=[1]),
LinearModelComponent(basis=n170(),formula=@formula(0~1),β=[1]),
LinearModelComponent(basis=p300(),formula=@formula(0~1),β=[1])];
brain_response = [
LinearModelComponent(basis = p100(), formula = @formula(0 ~ 1), β = [1]),
LinearModelComponent(basis = n170(), formula = @formula(0 ~ 1), β = [1]),
LinearModelComponent(basis = p300(), formula = @formula(0 ~ 1), β = [1]),
];


# !!! hint
# For a fancy way to write the above code, you could use `LinearModelComponent.([p100,n170,p300],@formula(0~1),Ref([1]))`, notice the `.(...)` indicating broadcasting

# Now we can simulate our feature signal, 10_000 samples of random gray scale values
feature = rand(1_000)

# Next we have to nest the response in a `TRFComponent` and add ou
trf_component = TRFComponent(brain_response,feature);
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
trf_component = TRFComponent(brain_response,feature);
trf_component = TRFComponent(brain_response, feature);


# Finally, when simulating, we have only a single "event" (that is, TRF-) onset, the first sample. Therefore, we use `TRFOnset` to indicate this.
dat,evts = simulate(design,trf_component,UnfoldSim.TRFOnset());
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
dat,evts = simulate(design,trf_component,UnfoldSim.TRFOnset());
dat, evts = simulate(design, trf_component, UnfoldSim.TRFOnset());


# Let's plot the feature signal and the TRF response
f,ax,h =lines(dat)
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
f,ax,h =lines(dat)
f, ax, h = lines(dat)

lines!(feature)
f

# ## Multivariate TRFs
# Now TRFs can depend on multiple variables e.g. the luminance and the size of the circle.
feature_luminance = rand(1_000)
feature_size = rand(1_000)

# We could call the `simulate` function twice and simply add the results:
dat_l,_ = simulate(design,TRFComponent(brain_response,feature_luminance),UnfoldSim.TRFOnset());
dat_r,_ = simulate(design,TRFComponent(brain_response,feature_size),UnfoldSim.TRFOnset());
Comment on lines +42 to +43
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
dat_l,_ = simulate(design,TRFComponent(brain_response,feature_luminance),UnfoldSim.TRFOnset());
dat_r,_ = simulate(design,TRFComponent(brain_response,feature_size),UnfoldSim.TRFOnset());
dat_l, _ =
simulate(design, TRFComponent(brain_response, feature_luminance), UnfoldSim.TRFOnset());
dat_r, _ =
simulate(design, TRFComponent(brain_response, feature_size), UnfoldSim.TRFOnset());


# let's plot and compare to the previous plot
f,ax,h = lines(dat_l .+ dat_r)
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
f,ax,h = lines(dat_l .+ dat_r)
f, ax, h = lines(dat_l .+ dat_r)

lines!(dat)
f
# as visible, the blue line (luminance+size) has ~double the amplitude. This is because we simulated two brain responses and simply added them.

# A bit more convenient way is to do the following
dat_combined,_ = simulate(design,[TRFComponent(brain_response,feature_size),TRFComponent(brain_response,feature_luminance)],UnfoldSim.TRFOnset());
f,ax,h = lines(dat_combined)
Comment on lines +52 to +53
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
dat_combined,_ = simulate(design,[TRFComponent(brain_response,feature_size),TRFComponent(brain_response,feature_luminance)],UnfoldSim.TRFOnset());
f,ax,h = lines(dat_combined)
dat_combined, _ = simulate(
design,
[
TRFComponent(brain_response, feature_size),
TRFComponent(brain_response, feature_luminance),
],
UnfoldSim.TRFOnset(),
);
f, ax, h = lines(dat_combined)

lines!(dat_l .+ dat_r)
f
# where you can see that the results are equivalent.

# ## Another cool feature is to modulate the feature vector based on the design
design_mod = SingleSubjectDesign(conditions=Dict(:condition=>["A","B"]));
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
design_mod = SingleSubjectDesign(conditions=Dict(:condition=>["A","B"]));
design_mod = SingleSubjectDesign(conditions = Dict(:condition => ["A", "B"]));


# Let's take only a single component for simplicity. Note how the `formula` has been changed. The β allows to control the amplitude. In this linear model component, the default contrast-function is `Dummy` (also known as `Reference` coding), which means, the second beta indicated a "difference"
brain_response_mod = LinearModelComponent(basis=p100(),formula=@formula(0~1+condition),β=[1,1]);
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
brain_response_mod = LinearModelComponent(basis=p100(),formula=@formula(0~1+condition),β=[1,1]);
brain_response_mod =
LinearModelComponent(basis = p100(), formula = @formula(0 ~ 1 + condition), β = [1, 1]);


# let's simulate another feature signal, but this time, we simulate a Matrix.
feature_mod = rand(1000,2)
feature_mod[:,2] .= 0
feature_mod[500:600,2] .= 1;
Comment on lines +65 to +67
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
feature_mod = rand(1000,2)
feature_mod[:,2] .= 0
feature_mod[500:600,2] .= 1;
feature_mod = rand(1000, 2)
feature_mod[:, 2] .= 0
feature_mod[500:600, 2] .= 1;


# to better understand how our (experimental) feature now looks like, let's visualize it
series(feature_mod')
# As visible, the first column has again a random signal, indicating e.g. luminance changes. The second temporal feature indicates some offset (a colorchange?) between 500 and 600 samples.


dat_mod,_ = simulate(design_mod,TRFComponent([brain_response_mod],feature_mod),UnfoldSim.TRFOnset());
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
dat_mod,_ = simulate(design_mod,TRFComponent([brain_response_mod],feature_mod),UnfoldSim.TRFOnset());
dat_mod, _ = simulate(
design_mod,
TRFComponent([brain_response_mod], feature_mod),
UnfoldSim.TRFOnset(),
);

lines(dat_mod)

# !!! hint
# Excourse: now you rightfully might ask why the jump is to >10 a.u.? The reason is that you are effectively convolving a feature-signal above 1 (feature_mod * (β_0 + β_1) = 1 * (1+1)), and with a kernel with maximum = 1, these 2's add up. The kernel has a rough width of ~5 which results in additional 5*2 => 10 per affected sample

# ## Combination with a event-related design
# Right now there is no easy interface to do this. You have to simulate a TRF signal, and an rERP signal and then add the two signals.
1 change: 1 addition & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ makedocs(;
"Simulate event-related potentials (ERPs)" => "generated/tutorials/simulateERP.md",
"Power analysis" => "generated/tutorials/poweranalysis.md",
"Multi-subject simulation" => "generated/tutorials/multisubject.md",
"Temporal Response Function" => "generated/tutorials/TRF.md",
],
"Reference" => [
"Overview of functionality" => "./generated/reference/overview.md",
Expand Down
6 changes: 4 additions & 2 deletions src/UnfoldSim.jl
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export create_re
export Simulation

# component types
export MixedModelComponent, LinearModelComponent
export MixedModelComponent, LinearModelComponent, TRFComponent

# export designs
export MultiSubjectDesign, SingleSubjectDesign, RepeatDesign, SequenceDesign
Expand All @@ -66,7 +66,9 @@ export simulate,
export pad_array, convert

# export Offsets
export UniformOnset, LogNormalOnset, NoOnset, UniformOnsetFormula, LogNormalOnsetFormula

export UniformOnset,
LogNormalOnset, NoOnset, UniformOnsetFormula, LogNormalOnsetFormula, TRFOnset

# re-export StatsModels
export DummyCoding, EffectsCoding
Expand Down
35 changes: 35 additions & 0 deletions src/component.jl
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,41 @@ function weight_σs(σs::Dict, b_σs::Float64, σ_lmm::Float64)
return named_random_effects
end


#--- TRF Component

"""
TRFComponent(components::Vector{<:AbstractComponents},features::AbstractArray)

This component can be used to convolve a `response` of a component-vector with a feature-array.

Each column of the feature-array will be convolved with one generated response from the AbstractComponent-vector (that is, each row of the respective `AbstractDesign`)

If only a single TRF is needed, a vector can be provided.
"""
struct TRFComponent <: AbstractComponent
components::Any
features::AbstractArray
end
UnfoldSim.length(t::TRFComponent) = size(t.features, 1)


function UnfoldSim.simulate_component(rng, c::TRFComponent, design::AbstractDesign)
kernel = UnfoldSim.simulate_responses(
rng,
c.components,
UnfoldSim.Simulation(design, c, NoOnset(), NoNoise()),
)

@assert size(kernel, 2) == size(c.features, 2) "if the $(typeof(design)) generates multiple columns (sz=$(size(kernel))), the $(typeof(c)) needs to have multiple columns as well (sz=$(size(c.signal)))"
x = reduce(hcat, UnfoldSim.conv.(eachcol(c.features), eachcol(kernel)))[
1:size(c.features, 1),
:,
]
return x

end

#----

"""
Expand Down
29 changes: 24 additions & 5 deletions src/onset.jl
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ In the case that the user directly wants no overlap to be simulated (=> epoched
"""
struct NoOnset <: AbstractOnset end


"""
struct TRFOnset <: AbstractOnset end
In the case that a TRF is simulated (via `TRFComponent`). This onset returns a vector of zero-latencies, indicating that the TRF starts at the beginning of the signal.
"""
struct TRFOnset <: AbstractOnset end



"""
simulate_interonset_distances(rng, onset::UniformOnset, design::AbstractDesign)
simulate_interonset_distances(rng, onset::LogNormalOnset, design::AbstractDesign)
Expand All @@ -59,6 +68,16 @@ function simulate_interonset_distances(rng, onset::LogNormalOnset, design::Abstr
end


"""
simulate_interonset_distances(rng, onset::UniformOnset, design::AbstractDesign)
In the case that a TRF is simulated (via `TRFComponent`). This onset returns a vector of zero-latencies, indicating that the TRF starts at the beginning of the signal.
"""
function simulate_interonset_distances(rng, onset::TRFOnset, design)
sz = size(design)
return Int.(zeros(sz))
end


#function simulate_interonset_distances(rng, onset::AbstractOnset,design::)


Expand Down Expand Up @@ -135,10 +154,10 @@ end
function simulate_interonset_distances(rng, o::UniformOnsetFormula, design::AbstractDesign)
events = generate_events(design)
widths =
UnfoldSim.generate_designmatrix(o.width_formula, events, o.width_contrasts) *
generate_designmatrix(o.width_formula, events, o.width_contrasts) *
o.width_β
Comment on lines 156 to 158
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
widths =
UnfoldSim.generate_designmatrix(o.width_formula, events, o.width_contrasts) *
generate_designmatrix(o.width_formula, events, o.width_contrasts) *
o.width_β
widths = generate_designmatrix(o.width_formula, events, o.width_contrasts) * o.width_β

offsets =
UnfoldSim.generate_designmatrix(o.offset_formula, events, o.offset_contrasts) *
generate_designmatrix(o.offset_formula, events, o.offset_contrasts) *
o.offset_β
Comment on lines +160 to 161
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
generate_designmatrix(o.offset_formula, events, o.offset_contrasts) *
o.offset_β
generate_designmatrix(o.offset_formula, events, o.offset_contrasts) * o.offset_β


return Int.(
Expand Down Expand Up @@ -193,10 +212,10 @@ function simulate_interonset_distances(
events = generate_events(design)


μs = UnfoldSim.generate_designmatrix(o.μ_formula, events, o.μ_contrasts) * o.μ_β
σs = UnfoldSim.generate_designmatrix(o.σ_formula, events, o.σ_contrasts) * o.σ_β
μs = generate_designmatrix(o.μ_formula, events, o.μ_contrasts) * o.μ_β
σs = generate_designmatrix(o.σ_formula, events, o.σ_contrasts) * o.σ_β
offsets =
UnfoldSim.generate_designmatrix(o.offset_formula, events, o.offset_contrasts) *
generate_designmatrix(o.offset_formula, events, o.offset_contrasts) *
o.offset_β
Comment on lines +218 to 219
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
generate_designmatrix(o.offset_formula, events, o.offset_contrasts) *
o.offset_β
generate_designmatrix(o.offset_formula, events, o.offset_contrasts) * o.offset_β



Expand Down
Loading