fNIRS: RuntimeError: None must operate on continuous wave data, but none was found.

MNE version: 1.10.0

Operating system: Windows 11

I have an issue regarding converting my fNIRS raw intensities to optical density. More specifically I receive the error code below:

mne.preprocessing.nirs.optical_density(raw_intensity)
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "<decorator-gen-206>", line 12, in optical_density
  File "c:", line 30, in optical_density
    picks = _validate_nirs_info(raw.info, fnirs="cw_amplitude")
  File "c:", line 258, in _validate_nirs_info
    raise RuntimeError(
        f"{which} must operate on {kind} data, but none was found."
    )
RuntimeError: None must operate on continuous wave data, but none was found.

The above happens when all channels are marked as bad due to my SNR computations. Obviously this is not ideal at all, but beyond that I don’t understand why bad channels would have any effect on the conversion from raw intensities to optical densities and it makes me question if the conversion is different for bad channels normally as well. As you can see from the code snippet below, I interpolate the bad channels later, but first after bad channels related to the SCI have been found. I considered interpolating the bad channels twice, but is unsure on whether this is mathematically rigor.

            raw_intensity = self.define_raw_intensity(filename)
            raw_intensity = self.make_annotations(raw_intensity)

            raw_intensity.annotations.rename(self.annotation_names)
            for _unwanted in self.unwanted:
                    unwanted = np.nonzero(raw_intensity.annotations.description == _unwanted)
                    raw_intensity.annotations.delete(unwanted)

            if self.snr_rejection != "None":
                snr = snr_rejection(raw_intensity, self.snr_rejection)
                
                # Validation
                if self.snr_rejection == "SNR" and self.snr_threshold < 1:
                    raise ValueError("Currently the classic signal to noise ratio is used but the threshold for SNR is below 1 resulting in all channels being marked as bad. Please set the threshold to a value above 1.")
                if self.snr_rejection == "CV" and self.snr_threshold > 1:
                    raise ValueError("Currently the coefficient of variation is used but the threshold for CV is above 1 resulting in all channels being marked as bad. Please set the threshold to a value below 1.")
                
                # Get bad channels based on pair logic
                snr_bad_channels = get_bad_channels_by_pairs(raw_intensity.ch_names, snr, self.snr_threshold, self.snr_rejection)
                raw_intensity.info["bads"] = snr_bad_channels
            else:
                snr_bad_channels = []

            raw_od = mne.preprocessing.nirs.optical_density(raw_intensity)
            raw_od_original = raw_od.copy()

            # Check channel name consistency
            assert raw_intensity.ch_names == raw_od.ch_names, \
                f"Channel names mismatch!\nraw_intensity: {len(raw_intensity.ch_names)} channels\nraw_od: {len(raw_od.ch_names)} channels"
            
            if self.short_channel_correction:
                raw_od = mne_nirs.signal_enhancement.short_channel_regression(raw_od)
            raw_od = mne_nirs.channels.get_long_channels(raw_od)
            
            if self.apply_tddr:
                raw_od = mne.preprocessing.nirs.temporal_derivative_distribution_repair(raw_od)

            sci = mne.preprocessing.nirs.scalp_coupling_index(raw_od)

            sci_bad_channels = list(compress(raw_od.ch_names, sci < self.scalp_coupling_threshold))
            
            # Filter SNR bad channels to only include those that still exist in the long channels dataset
            snr_bad_channels_long_only = [ch for ch in snr_bad_channels if ch in raw_od.ch_names]
            
            # Combine bad channels from all preprocessing
            all_bad_channels = list(set(snr_bad_channels_long_only + sci_bad_channels))         
            raw_od.info["bads"] = all_bad_channels
            
            if self.interpolate_bad_channels:
                raw_od.interpolate_bads()

Buttom line:

  1. Why do bad channels affect the computation from the raw intensities to optical densities
  2. How should I handle this the best way?

@richard I have seen several posts with similar error codes that also haven’t been answered. Is this because there is no workaround yet?

UPDATE:

I received a similar error using the modified beer-lambert law, which I also don’t understand why would depend on bad channels, as all channels need to be converted no matter if they are bad or not. I have checked if the computation from raw intensities to optical density and from optical density to haemoglobin levels depend on bad channels, and it’s evident that the results from the computation are identical, which confirms the theory above that this error code is not appropiate. Furthermore the error code for the conversion below is literally wrong as I have checked the values and they are optical density data.

            if self.short_channel_correction:
                raw_od = mne_nirs.signal_enhancement.short_channel_regression(raw_od)
            raw_od = mne_nirs.channels.get_long_channels(raw_od)
            
            if self.apply_tddr:
                raw_od = mne.preprocessing.nirs.temporal_derivative_distribution_repair(raw_od)

            sci = mne.preprocessing.nirs.scalp_coupling_index(raw_od)

            sci_bad_channels = list(compress(raw_od.ch_names, sci < self.scalp_coupling_threshold))
            
            # Filter SNR bad channels to only include those that still exist in the long channels dataset
            snr_bad_channels_long_only = [ch for ch in snr_bad_channels if ch in raw_od.ch_names]
            
            # Combine bad channels from all preprocessing
            all_bad_channels = list(set(snr_bad_channels_long_only + sci_bad_channels))         
            raw_od.info["bads"] = all_bad_channels
            
            if self.interpolate_bad_channels:
                raw_od.interpolate_bads()

            dpf = compute_differential_pathlength(raw_od)
            raw_haemo = mne.preprocessing.nirs.beer_lambert_law(raw_od, ppf=dpf)
mne.preprocessing.nirs.beer_lambert_law(raw_od, ppf=dpf)
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "c:\Users\NKUE0003\Documents\GitHub\fNIRS_processing_ver.2\.venv\Lib\site-packages\mne\preprocessing\nirs\_beer_lambert_law.py", line 46, in beer_lambert_law
    picks = _validate_nirs_info(raw.info, fnirs="od", which="Beer-lambert")
  File "c:\Users\NKUE0003\Documents\GitHub\fNIRS_processing_ver.2\.venv\Lib\site-packages\mne\preprocessing\nirs\nirs.py", line 258, in _validate_nirs_info
    raise RuntimeError(
        f"{which} must operate on {kind} data, but none was found."
    )
RuntimeError: Beer-lambert must operate on optical density data, but none was found.

Hello, I cannot help with fNIRS questions, sorry :frowning:

@rob-luke if you have the time, could you take a look?

Richard

1 Like