Multiple topoplots in time steps: RuntimeWarnings, decibels, and colour bar issues

Hi everyone,

If I should break this into individual issues, please let me know. The thinking here was to group the issues affecting the desired output.

I’m trying to produce topoplots in 100-ms time steps across the frequencies of interest for my study, looking at relative change from baseline in decibels. Ideally, I’d want them to look something like this (wonky electrode aside):

I’m running into a couple of issues:

  1. RuntimeWarning when applying a baseline from within the plot_topomap function.
  2. Conversion to decibels of the bel "logratio" power.
  3. Producing a valid colour bar.

Baselining and converting to decibels

In the code where I produce the TFR representation, I apply a baseline directly to the tfr object.

  • Does this baselining persist when outputting to a data frame (tfr.to_data_frame()) or when using the plotting functions (e.g. tfr.plot_topomap)? I would assume so, as
tfr.apply_baseline(baseline, mode="logratio")

Based on the codebase, it seems that mode="logratio" returns the data in bels. As I would like decibels, I multiply the baselined power values across the board.

  • Is it acceptable to multiply in this manner to get decibels if it’s not already available in the function?
times = np.arange(-0.4, 1.1, step=0.1).round(decimals=1)
iter_freqs = [
    ("sub_theta", 2, 4),  # 2 Hz is the minimum due to epoch length
    ("theta", 4, 7),
    ("alpha", 8, 12),
    ("low_beta", 13, 20),
    ("high_beta", 21, 25)
]
baseline = (-1.25, -0.75)

tfr = epochs.compute_tfr(
        method="morlet",
        freqs=freqs,
        n_cycles=3,
        use_fft=True,
        return_itc=False,
        average=False,
        n_jobs=config.n_jobs,
        decim=25,  # For 50 ms time steps in the output data frame
    )

# Compute log ratio power and convert to decibels
log_tfr = tfr.copy().apply_baseline(baseline, mode="logratio")
db = log_tfr.__mul__(10)

for band, fmin, fmax in iter_freqs:
    fig, axes = plt.subplots(3, 5, figsize=(12, 8))
    axes = axes.flatten()

    for i, time in enumerate(times):
        im = db.plot_topomap(tmin=time, tmax=time + 0.1, fmin=fmin, fmax=fmax, show=False, axes=axes[i], colorbar=False, vlim=(-3, 3))
        axes[i].set_title(f"{time * 1000:.0f} ms")

        im_cbar = im.axes[-2].images[-1] 
        cbar = fig.colorbar(im_cbar, ax=axes, orientation="vertical", shrink=0.6)
        cbar.set_label("dB change from baseline")

        fig.suptitle(f"Topomap of {band} ({fmin} Hz to {fmax} Hz) frequency band (Participant cndeeg01, {condition} condition)")

        plt.show()

RuntimeWarning

When trying to apply mode="logratio" baseline correction from within the plot_topomap function, there are multiple warnings thrown out:

RuntimeWarning: invalid value encountered in log10
  np.log10(d, out=d)

This doesn’t apply to all plots, it seems, as some of the topoplots are produced, as can be seen in the examples below. In some cases many topoplots are present, in others only a few, and sometimes nothing appears.

  • Is this warning connected to the data already having been baselined, and the reapplication (if that’s indeed what’s happening) of np.log10 is messing things about?



Valid colour bars

Lastly, I attempted to produce valid colour bars which should reflect the range of activity across all subplots, but I’m not sure that I’m getting that. The outputs are varied, as can be seen below.



It’s unclear to me whether the colour bar is only reflecting activity from the last topoplot, as might be indicated from the last image where the range is from 0.00 to 2.00, seemingly missing any of the negative activity. The other two plots could be valid, but I’m not sure how to sanity-check the output here.

Many thanks for any insights into these issues!

MNE 1.9.0
macOS 15.4.1

1 Like

First of all, thank you for writing up your questions in such a nice way! This makes it really easy for others to understand the issues you are facing.

First, I would like to note that the various plotting functions support a dB=True argument to show all values in dB. In contrast, it is not possible to convert the underlying values to dB with predefined methods, but your baselining approach is a viable solution (although you could just do db = log_tfr * 10). However, if you do not need to perform subsequent calculations with data in dB, I would recommend to just use dB=True for the plots. Also, it is likely that none of the TFR methods will work correctly if you mutate the TFR values like that.

Regarding the RuntimeWarning, are you sure that this comes from within the plot_topomap function? In your function call, it should use baseline=None (the default), meaning no baselining should be performed (because you have already applied it manually via tfr.apply_baseline). So yes, applying a baseline modifies the underlying data in-place.

In any case, this warning is likely due to a value less than or equal to zero, since that’s going to result in negative infinity for the log. If you skip the manual dB conversion and just set dB=True for the plots, this warning might disappear. Otherwise, you will probably have to take care of data points that have zero power (hopefully you do not have negative power values). If you are really applying the baseline twice, this explains the behavior and you shouldn’t do it.

Regarding the color bars, they should show the same range across your plots because you are explicitly setting vlim=(-3, 3). However, since you manually converted the underlying power values to dB, this scale might not be appropriate anymore. Again, I think you really should not do the manual dB conversion, which will probably solve this issue as well.

2 Likes

Thank you for your thorough reply!

The reason I did the dB conversion to the underlying values is because the plot_topomap function unfortunately doesn’t have a dB parameter, unlike the plot and plot_topo functions. Perhaps there’s a reason for this (and maybe this could be my first pull request!).

Passing dB=True to plot_topomap does return a TypeError.

TypeError: BaseTFR.plot_topomap() got an unexpected keyword argument 'dB'

To prevent the TFR methods from not working, I would keep track of which object I’m working with, ensuring I apply to TFR methods to the original TFR object. Would this be an outlandish solution to working with decibels for these plots? A check of the raw data with this multiplication looks reasonable to me.

tfr_avg_db = tfr[condition].average() * 10
tfr_avg_db.plot_topomap(...)

I see that the TFR object has a baseline attribute which correctly shows (-1.25, -0.75), which is reassuring. Doing no baselining within the plot_topomap function then indeed produces a full set of images, no RuntimeWarnings to be found.

And yes, sorry, code snippets all over the place! There was a baseline call, plot_topomap(..., baseline=True, mode="logratio"), from within plot_topomap, but not in the snippet I shared. Sorry for the confusion.

As for the colour bars, in the examples I posted, there vlim is not set. Again, I should have been explicit here.

I have been playing around with trying to get a common colour bar for all plots. I was drawing from this StackOverflow post. I now consistently run into the following error, and am stuck trying to diagnose the problem.

AttributeError: 'Figure' object has no attribute 'cmap'

Here’s what I’m trying to work from:

for condition in ["crossed", "nested", "adjoined"]:
    tfr_avg_db = tfr[condition].average() * 10  # Conversion to decibels

    for band, fmin, fmax in iter_freqs:
        fig, axes = plt.subplots(3, 5, figsize=(12, 8))
        axes = axes.flatten()
        fig.suptitle(f"Topomap of {band} ({fmin} Hz to {fmax} Hz) frequency band (Participant cndeeg01, {condition} condition)")

        for i, time in enumerate(times):
            im = tfr_avg_db.plot_topomap(tmin=time, tmax=time + 0.1, fmin=fmin, fmax=fmax, show=False, axes=axes[i], colorbar=False, cmap="RdBu_r")
            axes[i].set_title(f"{time * 1000:.0f} ms")

        cbar = fig.colorbar(im, ax=axes.ravel().tolist())
        cbar.set_label("dB change from baseline")

In the examples from the first post with a common colour bar, the following was done to extract a colour bar, but now I’m sceptical of this approach.

im_cbar = im.axes[-2].images[-1] 
cbar = fig.colorbar(im_cbar, ax=axes, orientation="vertical", shrink=0.6)
cbar.set_label("dB change from baseline")