File: _beer_lambert_law.py

package info (click to toggle)
python-mne 1.3.0%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 100,172 kB
  • sloc: python: 166,349; pascal: 3,602; javascript: 1,472; sh: 334; makefile: 236
file content (102 lines) | stat: -rw-r--r-- 3,682 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# Authors: Robert Luke <mail@robertluke.net>
#          Eric Larson <larson.eric.d@gmail.com>
#          Alexandre Gramfort <alexandre.gramfort@inria.fr>
#
# License: BSD-3-Clause

import os.path as op

import numpy as np

from ...io import BaseRaw
from ...io.constants import FIFF
from ...utils import _validate_type, warn
from ..nirs import source_detector_distances, _validate_nirs_info


def beer_lambert_law(raw, ppf=6.):
    r"""Convert NIRS optical density data to haemoglobin concentration.

    Parameters
    ----------
    raw : instance of Raw
        The optical density data.
    ppf : float
        The partial pathlength factor.

    Returns
    -------
    raw : instance of Raw
        The modified raw instance.
    """
    from scipy import linalg
    raw = raw.copy().load_data()
    _validate_type(raw, BaseRaw, 'raw')
    _validate_type(ppf, 'numeric', 'ppf')
    ppf = float(ppf)
    picks = _validate_nirs_info(raw.info, fnirs='od', which='Beer-lambert')
    # This is the one place we *really* need the actual/accurate frequencies
    freqs = np.array(
        [raw.info['chs'][pick]['loc'][9] for pick in picks], float)
    abs_coef = _load_absorption(freqs)
    distances = source_detector_distances(raw.info, picks='all')
    bad = ~np.isfinite(distances[picks])
    bad |= distances[picks] <= 0
    if bad.any():
        warn('Source-detector distances are zero on NaN, some resulting '
             'concentrations will be zero. Consider setting a montage '
             'with raw.set_montage.')
    distances[picks[bad]] = 0.
    if (distances[picks] > 0.1).any():
        warn('Source-detector distances are greater than 10 cm. '
             'Large distances will result in invalid data, and are '
             'likely due to optode locations being stored in a '
             ' unit other than meters.')
    rename = dict()
    for ii, jj in zip(picks[::2], picks[1::2]):
        EL = abs_coef * distances[ii] * ppf
        iEL = linalg.pinv(EL)

        raw._data[[ii, jj]] = iEL @ raw._data[[ii, jj]] * 1e-3

        # Update channel information
        coil_dict = dict(hbo=FIFF.FIFFV_COIL_FNIRS_HBO,
                         hbr=FIFF.FIFFV_COIL_FNIRS_HBR)
        for ki, kind in zip((ii, jj), ('hbo', 'hbr')):
            ch = raw.info['chs'][ki]
            ch.update(coil_type=coil_dict[kind], unit=FIFF.FIFF_UNIT_MOL)
            new_name = f'{ch["ch_name"].split(" ")[0]} {kind}'
            rename[ch['ch_name']] = new_name
    raw.rename_channels(rename)

    # Validate the format of data after transformation is valid
    _validate_nirs_info(raw.info, fnirs='hb')
    return raw


def _load_absorption(freqs):
    """Load molar extinction coefficients."""
    # Data from https://omlc.org/spectra/hemoglobin/summary.html
    # The text was copied to a text file. The text before and
    # after the table was deleted. The the following was run in
    # matlab
    # extinct_coef=importdata('extinction_coef.txt')
    # save('extinction_coef.mat', 'extinct_coef')
    #
    # Returns data as [[HbO2(freq1), Hb(freq1)],
    #                  [HbO2(freq2), Hb(freq2)]]
    from scipy.io import loadmat
    from scipy.interpolate import interp1d

    extinction_fname = op.join(op.dirname(__file__), '..', '..', 'data',
                               'extinction_coef.mat')
    a = loadmat(extinction_fname)['extinct_coef']

    interp_hbo = interp1d(a[:, 0], a[:, 1], kind='linear')
    interp_hb = interp1d(a[:, 0], a[:, 2], kind='linear')

    ext_coef = np.array([[interp_hbo(freqs[0]), interp_hb(freqs[0])],
                         [interp_hbo(freqs[1]), interp_hb(freqs[1])]])
    abs_coef = ext_coef * 0.2303

    return abs_coef