Head radius discrepancies


I’ve been having some closely-related issues where the head radius is apparently considerably larger than it should be, but I’m unsure why as I assume the radius to be the default/9.5cm. I think part of it may be the way MNE calculates radius when there aren’t enough channels/fewer than in the montage file, but even so some of what’s occurring seems quite odd.

The main situation is:
-I input my data (32 channel, 10-20 scalp EEG data).
-I set the montage to the standard 10-20 montage.
-I use matlab.eng to open EEGLAB, and run cleanRawData on the data (saving it as a .set file before and after as an ‘intermediate’).
-After this has run, I receive a warning along the lines of " RuntimeWarning: Estimated head radius (11.6 cm) is above the 99th percentile for adult head size. Check if the montage_units argument is correct (the default is “mm”, but your channel positions may be in different units).", with the radius possibly varying a little from that.
-If I calculate the difference between channel coordinates (before or after running the EEGLAB fn), I get values on the order of several cm. A typical example: “O1: [ 0.00216017 -0.03188065 -0.04595065]”.
-Also, if I interpolate channels, there is a message along the lines of “Automatic origin fit: head of radius 93.9 mm”, generally 93-98mm. Not problematic in itself, but strange how this disagrees with both the warning message, and the montage value of 9.5cm slightly.

Additional notes:
-The magnitude seems to vary a little with how many channels are removed. E.g, for a particularly bad recording I had four flat channels. With these removed the head radius would be reported as 11.6cm, and with additional three noisy ones removed it would be reported as 11.8cm. It also seems to be partly random, having run the code a few times on the same file.
-I am also working with cEEGrid files, where I created my own approximate montage. For now I have assumed the ears are about 9cm from the head centre, and yet I still receive the same error (in fact here it is worse, it claims the head radius is ~15.5cm). I suppose it makes sense this would be inaccurate, it has limited channels to work with, and they are only around the ears (plus Fpz), but I’m still a little surprised it is so far out.
-I know that the default head radius for set_montage() is meant to be 9.5cm, but for some reason explicitly inputting “head_radius = 0.095” seems to slightly help. E.g, the O1 difference from above becomes “O1: [ 0.00207045 -0.03055664 -0.0440423 ]”, i.e slightly smaller differences between my O1 coordinates and the standard 10-20 values.

As I only seem to receive the error warning when using this EEGLab function, I had thought perhaps it is a conversion issue. However I’ve checked and they both seem to use the exact same file, “standard_1020.elc”.

I’d also thought that perhaps as the standard_1020 montage file contains 90 electrodes and we only have 32 (or fewer, where they are removed), maybe MNE is at some point trying to calculate the radius and getting confused. However I couldn’t find any reports of this here or elsewhere. Also this still doesn’t really explain the discrepancy between the warning value vs. the “automatic origin fit” value, or why inputting “head_radius = 0.095” makes a difference.

  • MNE version: 1.6.1
  • operating system: Windows 11
    -(EEGLab 2023.1 if it is of any relevance)

I’ve attached the code below (for terseness I’ve not included the interpolation code, but essentially I’d add placeholder zeroes for the removed channels, set them as ‘bads’ and use interpolate_bads).

Any help would be hugely appreciated.

standard_1020 = mne.channels.make_standard_montage("standard_1020")#,head_size=0.095)

# Get channel names from montage and EEG data
montage_channel_names = set(standard_1020.ch_names)
eeg_channel_names = set(eeg_fullyFiltered.ch_names)

# Get channels present in both montage and EEG data
common_channel_names = montage_channel_names.intersection(eeg_channel_names)

# Print differences in electrode positions
print("Differences in electrode positions (montage - EEG data):")
for ch_name in common_channel_names:
    montage_pos = standard_1020.get_positions()['ch_pos'].get(ch_name)
    eeg_pos = eeg_fullyFiltered.get_montage().get_positions()['ch_pos'].get(ch_name)
    if montage_pos is not None and eeg_pos is not None:
        diff = montage_pos - eeg_pos
        if not np.allclose(diff, 0):  # Check if all elements are close to zero
            print(f"{ch_name}: {diff}")
        print(f"Positions not available for channel: {ch_name}")

#cleanRawData, with only flats removed:
filePath = basePath + "/Intermediate Files/"
fileNamePartialCleanVersion = "P" + participantNumber + "_" + eeg_type + "_" + task + "_PartialCleanVers.set"
fullFilePath = filePath + fileNamePartialCleanVersion
eeg_fullyFiltered.export(fullFilePath, overwrite=True)
eng = matlab.engine.start_matlab()
eng.CRD_IntermedFn_KeepsChans(fullFilePath, nargout=0)