Skip to content

ENH: Add Forward-based projection reconstruction - #14235

Merged
larsoner merged 9 commits into
mne-tools:mainfrom
BabaSanfour:enh-forward-proj-reconstruction
Aug 29, 2026
Merged

ENH: Add Forward-based projection reconstruction#14235
larsoner merged 9 commits into
mne-tools:mainfrom
BabaSanfour:enh-forward-proj-reconstruction

Conversation

@BabaSanfour

@BabaSanfour BabaSanfour commented Aug 28, 2026

Copy link
Copy Markdown
Member

Reference issue (if any)

Second phase and closes #14208, following #14220.

What does this implement/fix?

Extends reconstruct_proj() with optional forward and integer rank params.

API:

  • forward=None, rank=None: existing geometry-based reconstruction;
  • forward=Forward, rank=int: Forward-based exact-rank reconstruction;
  • rank without forward, or forward without rank: for now error. This is what we need in SSP. Accepting rank / forward alone would require further tests and more branching.

Additional information

Numerical validation

For SSP-SIR (mne-denoise#32, mne-denoise#46), with a projector $P$ and a lead field $L$, the reconstruction is

$$ L(PL)^+_M P $$

which is equivalent in exact arithmetic to $K = LL^\top$ in the sensor-space formulation:

$$ KP(PKP)^+_M P $$

I checked this using TMS-EEG data from three subjects (CON001, CON006, CON009), 64 EEG channels, and the current MNE-Denoise spherical radial 5000-dipole lead field:

  • data rank after average reference was 63;
  • 1, 3, and 5 artifact components gave effective reconstruction ranks 62, 60, and 58;
  • independent $LL^\top$ reconstruction vs direct SSP-SIR differed by at most 1.07e-13 at the effective ranks;
  • MNE field-mapping machinery using $LL^\top$ differed from the direct formulation by at most 1.22e-13 at the effective ranks;
  • reconstructed real-data outputs differed by at most 8.37e-14.
Limitation

I spent some time playing with where this implementation can fail for a user outside of the intended SSP-SIR goal.

$$ K = LL^\top $$

squares the singular spectrum, so Forwards with a very wide singular-value spectrum can lose weak modes in finite precision.

In one strongly ill-conditioned free-orientation EEG Forward used for stress testing:

  • the direct projected lead field had numerical rank 24;
  • the corresponding Gram representation retained only about 8 reliable modes;
  • equal-rank Gram/direct differences became noticeable from approximately M=10 and became large toward the high-rank tail.
AI usage

Ideation and research with GPT-5.6 Sol; implementation assisted by Luna. All tests and code reviewed by me.

@BabaSanfour
BabaSanfour force-pushed the enh-forward-proj-reconstruction branch from a6d7b36 to 4d386a5 Compare August 28, 2026 12:02
@BabaSanfour
BabaSanfour force-pushed the enh-forward-proj-reconstruction branch from 4d386a5 to 874820f Compare August 28, 2026 12:33

@larsoner larsoner left a comment

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.

Looks good to me! Just one idea for supporting more / better rank modes more consistent with other places in the codebase

Comment thread mne/_fiff/proj.py Outdated
Comment thread mne/forward/_field_interpolation.py Outdated
if rank is not None:
inv, _, _ = _reg_pinv(whitened_dots, reg=0, rank=rank)
elif fmd.get("pinv_method", "tsvd") == "tsvd":
inv, fmd["nest"] = _pinv_trunc(whitened_dots, fmd["miss"])

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 should maybe respect rank actually... maybe we don't need pinv_trunc. (I suspect pinv_trunc is the same as _reg_pinv(..., reg=0, rank=<something>) where the <something> is picked from the data / first singular value.

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 one is still open

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

yes I had to leave for dinner, did not have time to look at it. will do now.

@BabaSanfour BabaSanfour Aug 28, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ok I looked into this, in the change I added it was

if rank is not None:
        inv, _, _ = _reg_pinv(whitened_dots, reg=0, rank=rank)

the elif fmd.get("pinv_method", "tsvd") == "tsvd": was the original code changed from if to elif. New changes never use _pinv_trunc.

Now for the existing code, I don't think they do the same code and removing _pinv_trunc might require further changes to the function.

Comment thread mne/_fiff/proj.py Outdated
@larsoner
larsoner enabled auto-merge (squash) August 29, 2026 09:24
@larsoner

Copy link
Copy Markdown
Member

Just pushed a little simplification, marking for merge-when-green, thanks in advance @BabaSanfour !

@larsoner
larsoner merged commit bb034ad into mne-tools:main Aug 29, 2026
30 checks passed
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.

Make projection reconstruction public; allow projector selection; add optional forward/rank control

2 participants