CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
_testing.py420 linesDownload Raw Back to utils
1"""Testing functions."""2 3# Authors: The MNE-Python contributors.4# License: BSD-3-Clause5# Copyright the MNE-Python contributors.6 7import inspect8import os9import sys10import tempfile11import traceback12from functools import wraps13from shutil import rmtree14from unittest import SkipTest15 16import numpy as np17from numpy.testing import assert_allclose, assert_array_equal18from scipy import linalg19 20from ._logging import ClosingStringIO, warn21from .check import check_version22from .misc import run_subprocess23from .numerics import object_diff24 25 26def _explain_exception(start=-1, stop=None, prefix="> "):27    """Explain an exception."""28    # start=-1 means "only the most recent caller"29    etype, value, tb = sys.exc_info()30    string = traceback.format_list(traceback.extract_tb(tb)[start:stop])31    string = "".join(string).split("\n") + traceback.format_exception_only(etype, value)32    string = ":\n" + prefix + ("\n" + prefix).join(string)33    return string34 35 36class _TempDir(str):37    """Create and auto-destroy temp dir.38 39    This is designed to be used with testing modules. Instances should be40    defined inside test functions. Instances defined at module level can not41    guarantee proper destruction of the temporary directory.42 43    When used at module level, the current use of the __del__() method for44    cleanup can fail because the rmtree function may be cleaned up before this45    object (an alternative could be using the atexit module instead).46    """47 48    def __new__(self):  # noqa: D10549        new = str.__new__(self, tempfile.mkdtemp(prefix="tmp_mne_tempdir_"))50        return new51 52    def __init__(self):53        self._path = self.__str__()54 55    def __del__(self):  # noqa: D10556        rmtree(self._path, ignore_errors=True)57 58 59def requires_mne(func):60    """Decorate a function as requiring MNE."""61    return requires_mne_mark()(func)62 63 64def requires_mne_mark():65    """Mark pytest tests that require MNE-C."""66    import pytest67 68    return pytest.mark.skipif(not has_mne_c(), reason="Requires MNE-C")69 70 71def requires_openmeeg_mark():72    """Mark pytest tests that require OpenMEEG."""73    import pytest74 75    return pytest.mark.skipif(76        not check_version("openmeeg", "2.5.6"), reason="Requires OpenMEEG >= 2.5.6"77    )78 79 80def requires_freesurfer(arg):81    """Require Freesurfer."""82    import pytest83 84    reason = "Requires Freesurfer"85    if isinstance(arg, str):86        # Calling as  @requires_freesurfer('progname'): return decorator87        # after checking for progname existence88        reason += f" command: {arg}"89        try:90            run_subprocess([arg, "--version"])91        except Exception:92            skip = True93        else:94            skip = False95        return pytest.mark.skipif(skip, reason=reason)96    else:97        # Calling directly as @requires_freesurfer: return decorated function98        # and just check env var existence99        return pytest.mark.skipif(not has_freesurfer(), reason="Requires Freesurfer")(100            arg101        )102 103 104def requires_good_network(func):105    import pytest106 107    return pytest.mark.skipif(108        int(os.environ.get("MNE_SKIP_NETWORK_TESTS", 0)),109        reason="MNE_SKIP_NETWORK_TESTS is set",110    )(func)111 112 113def run_command_if_main():114    """Run a given command if it's __main__."""115    local_vars = inspect.currentframe().f_back.f_locals116    if local_vars.get("__name__", "") == "__main__":117        local_vars["run"]()118 119 120class ArgvSetter:121    """Temporarily set sys.argv."""122 123    def __init__(self, args=(), disable_stdout=True, disable_stderr=True):124        self.argv = list(("python",) + args)125        self.stdout = ClosingStringIO() if disable_stdout else sys.stdout126        self.stderr = ClosingStringIO() if disable_stderr else sys.stderr127 128    def __enter__(self):  # noqa: D105129        self.orig_argv = sys.argv130        sys.argv = self.argv131        self.orig_stdout = sys.stdout132        sys.stdout = self.stdout133        self.orig_stderr = sys.stderr134        sys.stderr = self.stderr135        return self136 137    def __exit__(self, *args):  # noqa: D105138        sys.argv = self.orig_argv139        sys.stdout = self.orig_stdout140        sys.stderr = self.orig_stderr141 142 143def has_mne_c():144    """Check for MNE-C."""145    return "MNE_ROOT" in os.environ146 147 148def has_freesurfer():149    """Check for Freesurfer."""150    return "FREESURFER_HOME" in os.environ151 152 153def buggy_mkl_svd(function):154    """Decorate tests that make calls to SVD and intermittently fail."""155 156    @wraps(function)157    def dec(*args, **kwargs):158        try:159            return function(*args, **kwargs)160        except np.linalg.LinAlgError as exp:161            if "SVD did not converge" in str(exp):162                msg = "Intel MKL SVD convergence error detected, skipping test"163                warn(msg)164                raise SkipTest(msg)165            raise166 167    return dec168 169 170def assert_and_remove_boundary_annot(annotations, n=1):171    """Assert that there are boundary annotations and remove them."""172    __tracebackhide__ = True173 174    from ..io import BaseRaw175 176    if isinstance(annotations, BaseRaw):  # allow either input177        annotations = annotations.annotations178    for key in ("EDGE", "BAD"):179        idx = np.where(annotations.description == f"{key} boundary")[0]180        assert len(idx) == n, (181            f"Got {len(idx)} '{key} boundary' annotations, expected {n}"182        )183        annotations.delete(idx)184 185 186def assert_object_equal(a, b, *, err_msg="Object mismatch", allclose=False):187    """Assert two objects are equal."""188    __tracebackhide__ = True189 190    d = object_diff(a, b, allclose=allclose)191    assert d == "", f"{err_msg}\n{d}"192 193 194def _raw_annot(meas_date, orig_time):195    from .._fiff.meas_info import create_info196    from ..annotations import Annotations, _handle_meas_date197    from ..io import RawArray198 199    info = create_info(ch_names=10, sfreq=10.0)200    raw = RawArray(data=np.empty((10, 10)), info=info, first_samp=10)201    if meas_date is not None:202        meas_date = _handle_meas_date(meas_date)203    with raw.info._unlock(check_after=True):204        raw.info["meas_date"] = meas_date205    annot = Annotations([0.5], [0.2], ["dummy"], orig_time)206    raw.set_annotations(annotations=annot)207    return raw208 209 210def _get_data(x, ch_idx):211    """Get the (n_ch, n_times) data array."""212    from ..evoked import Evoked213    from ..io import BaseRaw214 215    if isinstance(x, BaseRaw):216        return x[ch_idx][0]217    elif isinstance(x, Evoked):218        return x.data[ch_idx]219 220 221def _check_snr(actual, desired, picks, min_tol, med_tol, msg, kind="MEG"):222    """Check the SNR of a set of channels."""223    __tracebackhide__ = True224 225    actual_data = _get_data(actual, picks)226    desired_data = _get_data(desired, picks)227    bench_rms = np.sqrt(np.mean(desired_data * desired_data, axis=1))228    error = actual_data - desired_data229    error_rms = np.sqrt(np.mean(error * error, axis=1))230    np.clip(error_rms, 1e-60, np.inf, out=error_rms)  # avoid division by zero231    snrs = bench_rms / error_rms232    # min tol233    snr = snrs.min()234    bad_count = (snrs < min_tol).sum()235    msg = f" ({msg})" if msg != "" else msg236    assert bad_count == 0, (237        f"SNR (worst {snr:0.2f}) < {min_tol:0.2f} "238        f"for {bad_count}/{len(picks)} channels{msg}"239    )240    # median tol241    snr = np.median(snrs)242    assert snr >= med_tol, f"{kind} SNR median {snr:0.2f} < {med_tol:0.2f}{msg}"243 244 245def assert_meg_snr(246    actual, desired, min_tol, med_tol=500.0, chpi_med_tol=500.0, msg=None247):248    """Assert channel SNR of a certain level.249 250    Mostly useful for operations like Maxwell filtering that modify251    MEG channels while leaving EEG and others intact.252    """253    __tracebackhide__ = True254 255    from .._fiff.pick import pick_types256 257    picks = pick_types(desired.info, meg=True, exclude=[])258    picks_desired = pick_types(desired.info, meg=True, exclude=[])259    assert_array_equal(picks, picks_desired, err_msg="MEG pick mismatch")260    chpis = pick_types(actual.info, meg=False, chpi=True, exclude=[])261    chpis_desired = pick_types(desired.info, meg=False, chpi=True, exclude=[])262    if chpi_med_tol is not None:263        assert_array_equal(chpis, chpis_desired, err_msg="cHPI pick mismatch")264    others = np.setdiff1d(265        np.arange(len(actual.ch_names)), np.concatenate([picks, chpis])266    )267    others_desired = np.setdiff1d(268        np.arange(len(desired.ch_names)), np.concatenate([picks_desired, chpis_desired])269    )270    assert_array_equal(others, others_desired, err_msg="Other pick mismatch")271    if len(others) > 0:  # if non-MEG channels present272        assert_allclose(273            _get_data(actual, others),274            _get_data(desired, others),275            atol=1e-11,276            rtol=1e-5,277            err_msg="non-MEG channel mismatch",278        )279    _check_snr(actual, desired, picks, min_tol, med_tol, msg, kind="MEG")280    if chpi_med_tol is not None and len(chpis) > 0:281        _check_snr(actual, desired, chpis, 0.0, chpi_med_tol, msg, kind="cHPI")282 283 284def assert_snr(actual, desired, tol):285    """Assert actual and desired arrays are within some SNR tolerance."""286    __tracebackhide__ = True287 288    with np.errstate(divide="ignore"):  # allow infinite289        snr = linalg.norm(desired, ord="fro") / linalg.norm(desired - actual, ord="fro")290    assert snr >= tol, f"{snr=} < {tol=}"291 292 293def assert_stcs_equal(stc1, stc2):294    """Check that two STC are equal."""295    __tracebackhide__ = True296 297    assert_allclose(stc1.times, stc2.times, err_msg="Times mismatch")298    assert_allclose(stc1.data, stc2.data, err_msg="Data mismatch")299    assert_array_equal(300        stc1.vertices[0],301        stc2.vertices[0],302        err_msg="Left vertices mismatch",303    )304    assert_array_equal(305        stc1.vertices[1],306        stc2.vertices[1],307        err_msg="Right vertices mismatch",308    )309    assert_allclose(stc1.tmin, stc2.tmin, err_msg="tmin mismatch")310    assert_allclose(stc1.tstep, stc2.tstep, err_msg="tstep mismatch")311 312 313def _dig_sort_key(dig):314    """Sort dig keys."""315    return (dig["kind"], dig["ident"])316 317 318def assert_dig_allclose(info_py, info_bin, limit=None):319    """Assert dig allclose."""320    __tracebackhide__ = True321 322    from .._fiff.constants import FIFF323    from .._fiff.meas_info import Info324    from ..bem import fit_sphere_to_headshape325    from ..channels.montage import DigMontage326 327    # test dig positions328    dig_py, dig_bin = info_py, info_bin329    if isinstance(dig_py, Info):330        assert isinstance(dig_bin, Info), "Both must be Info or DigMontage"331        dig_py, dig_bin = dig_py["dig"], dig_bin["dig"]332    else:333        assert isinstance(dig_bin, DigMontage), "Both must be Info or DigMontage"334        assert isinstance(dig_py, DigMontage), "Both must be Info or DigMontage"335        dig_py, dig_bin = dig_py.dig, dig_bin.dig336        info_py = info_bin = None337    assert isinstance(dig_py, list), "dig_py must be a list"338    assert isinstance(dig_bin, list), "dig_bin must be a list"339    dig_py = sorted(dig_py, key=_dig_sort_key)340    dig_bin = sorted(dig_bin, key=_dig_sort_key)341    assert len(dig_py) == len(dig_bin), "Different number of dig points"342    for ii, (d_py, d_bin) in enumerate(zip(dig_py[:limit], dig_bin[:limit])):343        for key in ("ident", "kind", "coord_frame"):344            assert d_py[key] == d_bin[key], f"{key=} mismatch on point {ii}"345        assert_allclose(346            d_py["r"],347            d_bin["r"],348            rtol=1e-5,349            atol=1e-5,350            err_msg=f"Failure on {ii}:\n{d_py['r']}\n{d_bin['r']}",351        )352    if any(d["kind"] == FIFF.FIFFV_POINT_EXTRA for d in dig_py) and info_py is not None:353        r_bin, o_head_bin, o_dev_bin = fit_sphere_to_headshape(354            info_bin, units="m", verbose="error"355        )356        r_py, o_head_py, o_dev_py = fit_sphere_to_headshape(357            info_py, units="m", verbose="error"358        )359        assert_allclose(r_py, r_bin, atol=1e-6, err_msg="Sphere radius mismatch")360        assert_allclose(361            o_dev_py,362            o_dev_bin,363            rtol=1e-5,364            atol=1e-6,365            err_msg="Sphere device origin mismatch",366        )367        assert_allclose(368            o_head_py,369            o_head_bin,370            rtol=1e-5,371            atol=1e-6,372            err_msg="Sphere origin mismatch",373        )374 375 376def _click_ch_name(fig, ch_index=0, button=1):377    """Click on a channel name in a raw/epochs/ICA browse-style plot."""378    from ..viz.utils import _fake_click379 380    fig.canvas.draw()381    text = fig.mne.ax_main.get_yticklabels()[ch_index]382    bbox = text.get_window_extent()383    x = bbox.intervalx.mean()384    y = bbox.intervaly.mean()385    _fake_click(fig, fig.mne.ax_main, (x, y), xform="pix", button=button)386 387 388def _get_suptitle(fig):389    """Get fig suptitle (shim for matplotlib < 3.8.0)."""390    # TODO: obsolete when minimum MPL version is 3.8391    if check_version("matplotlib", "3.8"):392        return fig.get_suptitle()393    else:394        # unreliable hack; should work in most tests as we rarely use `sup_{x,y}label`395        return fig.texts[0].get_text()396 397 398def assert_trans_allclose(actual, desired, dist_tol=0.0, angle_tol=0.0):399    __tracebackhide__ = True400 401    from ..transforms import Transform, angle_distance_between_rigid402 403    if isinstance(actual, Transform):404        assert isinstance(desired, Transform), "Both must be Transform or ndarray"405        assert actual["from"] == desired["from"], "'from' frame mismatch"406        assert actual["to"] == desired["to"], "'to' frame mismatch"407        actual = actual["trans"]408        desired = desired["trans"]409    assert isinstance(actual, np.ndarray), "actual should be ndarray"410    assert isinstance(desired, np.ndarray), "desired should be ndarray"411    assert actual.shape == (4, 4), "actual.shape should be (4, 4)"412    assert desired.shape == (4, 4), "desired.shape should be (4, 4)"413    angle, dist = angle_distance_between_rigid(414        actual, desired, angle_units="deg", distance_units="m"415    )416    assert dist <= dist_tol, (417        f"{1000 * dist:0.3f} > {1000 * dist_tol:0.3f} mm translation"418    )419    assert angle <= angle_tol, f"{angle:0.3f} > {angle_tol:0.3f}° rotation"420 
Aluode/PerceptionLabPortable · CoolFace