Skip to content

apply diataxis to tutorials/inverse/80_brainstorm_phantom_elekta.py#13584

Open
CarinaFo wants to merge 38 commits intomne-tools:mainfrom
CarinaFo:diataxis
Open

apply diataxis to tutorials/inverse/80_brainstorm_phantom_elekta.py#13584
CarinaFo wants to merge 38 commits intomne-tools:mainfrom
CarinaFo:diataxis

Conversation

@CarinaFo
Copy link
Copy Markdown
Contributor

dependabot bot and others added 11 commits March 11, 2024 03:30
Bumps [davidslusser/actions_python_bandit](https://github.com/davidslusser/actions_python_bandit) from 1.0.0 to 1.0.1.
- [Release notes](https://github.com/davidslusser/actions_python_bandit/releases)
- [Commits](davidslusser/actions_python_bandit@v1.0.0...v1.0.1)

---
updated-dependencies:
- dependency-name: davidslusser/actions_python_bandit
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
…usser/actions_python_bandit-1.0.1

Bump davidslusser/actions_python_bandit from 1.0.0 to 1.0.1
raw.plot(events=events)

# We can see that the simulated dipoles produce sinusoidal bursts at 20 Hz
# (can we really see that in the plot?)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Here I wonder if we can get rid of one of the screenshots of the dipole events and maybe zoom into one event to show the 20Hz burst

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yes that sounds good, you should be able to see it clearly at least in the average, and depending on the dipole amplitude you can sometimes see it in the raw / epoched data

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I plotted fewer channels, I think now it's a bit easier to see

cov = mne.compute_covariance(epochs, tmax=bmax)
mne.viz.plot_evoked_white(epochs["1"].average(), cov)

# Not sure what we see here TBH
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Here we should explain why we plot the whitened data and what the user is expected to see vs. what we actually see in this plot.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Or can we just link to the whitening / covariance tutorials? It is explained there I think

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I would add one sentence on whitening plus link to the whitening tutorial for further referenc (this is in line with diataxis)

# Now we can compare to the actual locations, taking the difference in mm:

# Finally, we compare the estimated to the true dipole locations
# To do this, we calculate the difference by .....
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think we should explain how we compute the difference and why, this seems to be crucial info for a tutorial on comparing estimated vs. true dipoles.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Makes sense to me

ax3.set_ylabel("Amplitude error (nAm)")

# We can see that the error magnitude depends on the position of the estimate dipole
# however, the location error is never greater than 5 mm which is good?
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

again I am not sure if we can interpret the error magnitude (or should). I think it would be very useful for a user to get some guidance on what to do with those results but maybe this goes too far for the current tutorial.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we can, we can say something about the potential localization accuracy of MEG. Like if we can get ~2-5mm error localizing these dipoles we can say MEG can localize point sources with that accuracy (given proper coreg etc.), which helps justify claims of "sub-centimeter" accuracy

Copy link
Copy Markdown
Member

@larsoner larsoner left a comment

Choose a reason for hiding this comment

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

Didn't read through all changes but wanted to give input on questions!

raw.plot(events=events)

# We can see that the simulated dipoles produce sinusoidal bursts at 20 Hz
# (can we really see that in the plot?)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yes that sounds good, you should be able to see it clearly at least in the average, and depending on the dipole amplitude you can sometimes see it in the raw / epoched data

cov = mne.compute_covariance(epochs, tmax=bmax)
mne.viz.plot_evoked_white(epochs["1"].average(), cov)

# Not sure what we see here TBH
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Or can we just link to the whitening / covariance tutorials? It is explained there I think

# Now we can compare to the actual locations, taking the difference in mm:

# Finally, we compare the estimated to the true dipole locations
# To do this, we calculate the difference by .....
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Makes sense to me

ax3.set_ylabel("Amplitude error (nAm)")

# We can see that the error magnitude depends on the position of the estimate dipole
# however, the location error is never greater than 5 mm which is good?
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we can, we can say something about the potential localization accuracy of MEG. Like if we can get ~2-5mm error localizing these dipoles we can say MEG can localize point sources with that accuracy (given proper coreg etc.), which helps justify claims of "sub-centimeter" accuracy

@CarinaFo CarinaFo marked this pull request as ready for review February 17, 2026 00:31
Copy link
Copy Markdown
Member

@drammock drammock left a comment

Choose a reason for hiding this comment

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

The tutorials go .py -> .rst -> .html.
if you build the docs locally, you should be able to view the intermediate rST files, which means the line number in the error message (line 663) will tell you exactly where the problem is. (You might need to tweak the build command, it might delete the generated rST files by default)

https://app.circleci.com/pipelines/github/mne-tools/mne-python/30229/workflows/0e25b414-b050-40d7-8e10-fa5c75b00a1a/jobs/79087?invite=true#step-143-0_171

Comment on lines +91 to +92
# .. _plt_brainstorm_phantom_elekta_eeg_sphere_geometry:
# We averaged over 640 simulated events for the first dipole.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

between these two, this might be where you need a blank line?

Comment on lines +237 to +240
#
# .. footbibliography::
#
#
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

remove the extra empty lines you added here. It's possible that is what causes the cryptic sphinx build error.

@CarinaFo
Copy link
Copy Markdown
Contributor Author

CarinaFo commented Feb 21, 2026

The tutorials go .py -> .rst -> .html. if you build the docs locally, you should be able to view the intermediate rST files, which means the line number in the error message (line 663) will tell you exactly where the problem is. (You might need to tweak the build command, it might delete the generated rST files by default)

https://app.circleci.com/pipelines/github/mne-tools/mne-python/30229/workflows/0e25b414-b050-40d7-8e10-fa5c75b00a1a/jobs/79087?invite=true#step-143-0_171

I ran into issues building docs locally (Windows 11 Pro, python 3.11), currently trying to document where it breaks and what are possible solutions.

Update: local docs are finally working

@CarinaFo CarinaFo changed the title apply diataxis framework to tutorials apply diataxis to tutorials/inverse/80_brainstorm_phantom_elekta.py Mar 21, 2026
@CarinaFo
Copy link
Copy Markdown
Contributor Author

@drammock ready for review

Comment on lines +13 to +14
For comparison, see :footcite:`TadelEtAl2011` and
`the original Brainstorm tutorial with an explanation of phantom recordings
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

maybe worthwhile to add 1 sentence at start of this paragraph, explaining what a phantom is? Something like "Phantoms are devices that fit inside a MEG measurement volume, and can generate electric dipoles with known current amplitudes and locations."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I added a sentence after mentioning phantom recordings the first time

# Next, let's look at the events in the phantom data for one stimulus channel.
events = find_events(raw, "STI201")
raw.plot(events=events)
raw.info["bads"] = ["MEG1933", "MEG2421"] # known bad channels
Copy link
Copy Markdown
Member

@drammock drammock Mar 18, 2026

Choose a reason for hiding this comment

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

seems like this should come earlier (right after read_raw_fif I think); it's known a priori (not inferred from the raw.info output or the find_events call)

# The first peak appears around 3 ms. Because we low-passed at 40 Hz,
# we can also decimate our data to save memory.

# The simulated dipoles produce sinusoidal bursts at 20 Hz.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In this data the phantom was programmed to produce 20 Hz sinusoidal bursts of current.

(i.e., make it clear that this was user-determined, 20 Hz is not intrinsic to the device)


# The simulated dipoles produce sinusoidal bursts at 20 Hz.
#
# Next, we epoch the the data based on the dipoles events (1:32).
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
# Next, we epoch the the data based on the dipoles events (1:32).
# Next, we epoch the the data based on the dipole events (1:32).

# Next, we epoch the the data based on the dipoles events (1:32).
#
# We select 100 ms before and after the event trigger
# and baseline correct the epochs from -100 ms to -0.05 ms before stimulus onset.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
# and baseline correct the epochs from -100 ms to -0.05 ms before stimulus onset.
# and baseline correct the epochs using a baseline period from -100 ms to -50 ms (before stimulus onset).

# but in general "shrunk" is usually better
# We can see that our head model aligns with the phantom head model.
#
# Let's do some dipole fits.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is a critical part of the process, and deserves more emphasis (and hand-holding). Consider making this a titled subsection, or at least spelling out the step a bit more clearly. For example: "Our next step is to use the epoched data to fit dipoles, then compare them to the known dipole locations built into the phantom"

mne.viz.plot_evoked_white(epochs["1"].average(), cov)

# The plot shows the evoked signal divided by the estimated noise standard deviation.
mne.viz.plot_evoked_white(epochs["1"].average(), cov)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

not sure this is strictly necessary for the tutorial's stated goal

Comment on lines 139 to 141
# We have seen in the evoked plot that this is around 3 ms after dipole onset.
data = []
t_peak = 0.036 # true for Elekta phantom
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

text says 3 ms, but t_peak is 36 ms. 3 ms is when the first wave starts right, not the peak location?

Also: might be worth going through the process of computing the peak exactly here, instead of just saying "true for Elekta phantom"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, I include the peak determination

Comment on lines +153 to +156
# Let's visualize the explained variance.
# To do this, we need to make sure that the
# data and the residuals are on the same scale
# (here the "time points" are the 32 dipole peak values that we fit).
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

to me this is a confusing visualization. Surely there's a better way to summarize explained variance?

Copy link
Copy Markdown
Contributor Author

@CarinaFo CarinaFo Mar 29, 2026

Choose a reason for hiding this comment

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

We can plot goodness of fit for each dipole. Currently trying to figure out what values that actually is.

Comment on lines 144 to 146
evoked = epochs[str(ii)][1:-1].average().crop(t_peak, t_peak)
data.append(evoked.data[:, 0])
evoked = mne.EvokedArray(np.array(data).T, evoked.info, tmin=0.0)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this approach of cramming a single (peak) timepoint from each of 32 separate evokeds into a single EvokedArray is very confusing. We should probably do this a more straightforward way, or at least add a lot more explanation of this code block.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fit_dipoles() expects an Evoked Object, we can store every dipole as a separate evoked, that migh be less confusing.

@CarinaFo CarinaFo marked this pull request as draft March 29, 2026 05:08
@CarinaFo CarinaFo marked this pull request as ready for review April 6, 2026 09:05
@CarinaFo
Copy link
Copy Markdown
Contributor Author

CI failure seems unrelated to this PR (PyVista/VTK on Python 3.13).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants