forked from nipraxis/textbook
-
Notifications
You must be signed in to change notification settings - Fork 0
/
non_tr_onsets.Rmd
205 lines (162 loc) · 5.64 KB
/
non_tr_onsets.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
---
jupyter:
jupytext:
text_representation:
extension: .Rmd
format_name: rmarkdown
format_version: '1.2'
jupytext_version: 1.11.5
kernelspec:
display_name: Python 3 (ipykernel)
language: python
name: python3
---
# Using onsets that do not start on a TR
You’ll need the file `new_cond.txt` to type along with this
demonstration.
```{python}
import numpy as np
import matplotlib.pyplot as plt
import nibabel as nib
```
Imagine we are analyzing our `example image`. It has a TR of 2.5, and 173 TRs.
```{python}
TR = 2.5
n_trs = 173
```
The actual condition file for this dataset is `ds114_sub009_t2r1_cond.txt`.
```{python}
# Load the function to fetch the data file we need.
import nipraxis
# Fetch the data file.
cond_fname = nipraxis.fetch_file('ds114_sub009_t2r1_cond.txt')
# Show the file name of the fetched data.
cond_fname
```
You may remember it has a *block design* with blocks of length 12 TRs while
the subject is doing the task.
What if we had a different *event related* condition file like the one at
`new_cond.txt`?
```{python}
# Fetch the data file.
new_cond_fname = nipraxis.fetch_file('new_cond.txt')
# Show the file name of the fetched data.
new_cond_fname
```
```{python}
cond_data = np.loadtxt(new_cond_fname)
cond_data
onsets_seconds = cond_data[:, 0]
durations_seconds = cond_data[:, 1]
amplitudes = cond_data[:, 2]
```
Notice that the onsets of the events can happen in the middle of the volumes
(well after the volumes have started).
```{python}
onsets_in_scans = onsets_seconds / TR
onsets_in_scans
```
Notice also that the events have *amplitudes* between 1 and 3. The events of
amplitude 3 we expect to have an evoked brain response three times higher than
events with amplitude 1.
What to do about the events with onsets that don’t exactly align with the
start of the TRs (volumes)?
One option would be to round the event onsets to the nearest TR. This will
mean that the event model will be different from our expected response by TR
seconds / 2 == 1.25 seconds in this case.
Can we do better than that?
We could make a neural and hemodynamic regressor at a finer time resolution
than the TRs, and later sample this regressor at the TR onset times.
This is what we do next.
```{python}
tr_divs = 100.0 # finer resolution has 100 steps per TR
```
With each TR divided into 100 intervals, one element corresponds to time
intervals of 1/100 of a TR:
```{python}
high_res_times = np.arange(0, n_trs, 1 / tr_divs) * TR
```
We will soon create a new neural prediction time-course where one element
corresponds to 1 / 100 of a TR:
```{python}
high_res_neural = np.zeros(high_res_times.shape)
```
We have the onset indices in terms of TRs, but now we want the onset indices
in terms of the new vector with 100 elements per TR:
```{python}
high_res_onset_indices = onsets_in_scans * tr_divs
high_res_onset_indices
```
In the same way, the durations were in seconds. We divide by the TR to get
duration in terms of scans, then multiply by 100 to get the number in terms of
elements in the new `high_res_neural` time-course.
```{python}
high_res_durations = durations_seconds / TR * tr_divs
high_res_durations
```
Now we fill in the `high_res_neural` time course by setting values between
the start and the end of each event with the matching amplitudes:
```{python}
for hr_onset, hr_duration, amplitude in zip(
high_res_onset_indices, high_res_durations, amplitudes):
hr_onset = int(round(hr_onset)) # index - must be int
hr_duration = int(round(hr_duration)) # makes index - must be int
high_res_neural[hr_onset:hr_onset + hr_duration] = amplitude
plt.plot(high_res_times, high_res_neural)
plt.xlabel('Time (seconds)')
plt.ylabel('High resolution neural prediction')
```
We can use the hemodynamic response function we created earlier:
```{python}
from scipy.stats import gamma
def hrf(times):
""" Return values for HRF at given times """
# Gamma pdf for the peak
peak_values = gamma.pdf(times, 6)
# Gamma pdf for the undershoot
undershoot_values = gamma.pdf(times, 12)
# Combine them
values = peak_values - 0.35 * undershoot_values
# Scale max to 0.6
return values / np.max(values) * 0.6
```
We are going to convolve at this higher time resolution. First we need to
sample the HRF at this finer time resolution, to match the neural prediction:
```{python}
hrf_times = np.arange(0, 24, 1 / tr_divs)
hrf_at_hr = hrf(hrf_times)
```
Next we convolve the sampled HRF with the high resolution neural time course:
```{python}
high_res_hemo = np.convolve(high_res_neural, hrf_at_hr)
# Drop tail from convolution
high_res_hemo = high_res_hemo[:len(high_res_neural)]
plt.plot(high_res_times, high_res_hemo)
plt.xlabel('Time (seconds)')
plt.ylabel('High resolution convolved values')
len(high_res_times)
```
We can see that this is sampled at high resolution on the x axis by looking at
the first 20 TRs-worth of data:
```{python}
top_index = int(20 * tr_divs)
plt.plot(high_res_times[:top_index], high_res_hemo[:top_index], 'x:')
```
We can then subsample this high-resolution time course to get the values
corresponding to the start of each original TR (volume):
```{python}
tr_indices = np.arange(n_trs)
hr_tr_indices = np.round(tr_indices * tr_divs).astype(int)
tr_hemo = high_res_hemo[hr_tr_indices]
tr_times = tr_indices * TR # times of TR onsets in seconds
plt.plot(tr_times, tr_hemo)
plt.xlabel('Time (seconds)')
plt.ylabel('Convolved values at TR onsets')
```
The first 20 TRs-worth of data shows these values are sampled every TR rather
than every 1/100th of a TR:
```{python}
plt.plot(tr_times[:20], tr_hemo[:20], 'x:')
plt.xlabel('Time (seconds)')
plt.ylabel('Convolved values at TR onsets')
```