From 74399cee4cbe1ba9e31150c5b648af1382935f23 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Mon, 24 Jan 2022 10:08:15 +0100 Subject: [PATCH 001/166] CHG: Removed deprecated IO code - removed loading code for deprecated binary format On branch nwb Changes to be committed: deleted: syncopy/io/load_raw_binary.py --- syncopy/io/load_raw_binary.py | 206 ---------------------------------- 1 file changed, 206 deletions(-) delete mode 100644 syncopy/io/load_raw_binary.py diff --git a/syncopy/io/load_raw_binary.py b/syncopy/io/load_raw_binary.py deleted file mode 100644 index 6f594fa5c..000000000 --- a/syncopy/io/load_raw_binary.py +++ /dev/null @@ -1,206 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Read binary files from disk -# - -# Builtin/3rd party package imports -import os -import sys -import numpy as np - -# Local imports -from syncopy.shared.parsers import io_parser, data_parser -from syncopy.shared.errors import SPYIOError, SPYTypeError, SPYValueError -from syncopy.datatype import AnalogData -from syncopy.datatype.base_data import VirtualData -import syncopy.datatype as spd - -__all__ = [] - -########################################################################################## -def load_binary_esi(filename, - channel="channel", - unit="unit", - trialdefinition=None, - out=None): - """ - Docstring - """ - - # Convert input to list (if it is not already) - parsing is performed - # by ``read_binary_esi_header`` below - if not isinstance(filename, (list, np.ndarray)): - filename = [filename] - - # Read headers of provided file(s) to see what we're dealing with here - headers = [] - tsample = [] - filename = [os.path.abspath(fname) for fname in filename] - for fname in filename: - hdr = read_binary_esi_header(fname) - hdr["file"] = fname - headers.append(hdr) - tsample.append(hdr["tSample"]) - - # Make sure we're not mixing file-types - exts = [os.path.splitext(fname)[1] for fname in filename] - if not set(exts).issubset([".lfp", ".mua"]) and np.unique(exts).size > 1: - lgl = "files of identical type" - act = "{}-files".format("".join(ext + ", " for ext in exts)[:-2]) - raise SPYValueError(legal=lgl, actual=act, varname="filename") - - # In case of spike or event data, we only support reading single files - if exts[0] in [".spk", ".dpd", ".evt"] and len(exts) > 1: - lgl = "single .spk/.dpd/.evt file" - act = "{} .spk/.dpd/.evt files".format(str(len(exts))) - raise SPYValueError(legal=lgl, varname="filename", actual=act) - - # FIXME: does this make sense for every type of data? - # Abort, if files have differing sampling times - if not np.array_equal(tsample, [tsample[0]]*len(tsample)): - raise SPYValueError(legal="identical sampling interval per file") - - # Depending on file-extension, we either deal with LFP/MUA or Spike/Event data - if exts[0] == ".spk": - dclass = "SpikeData" - elif exts[0] in [".lfp", ".mua"]: - dclass = "AnalogData" - elif exts[0] in [".dpd", ".evt"]: - dclass = "EventData" - else: - raise NotImplementedError("Cannot handle {}-files atm".format(exts[0])) - - # Make sure `out` does not contain unpleasant surprises (if provided) - new_out = True - if out is not None: - try: - data_parser(out, varname="out", writable=True, dataclass=dclass) - except Exception as exc: - raise exc - new_out = False - else: - out = getattr(spd, dclass)() # dynamically spawn new data object - new_out = True - - # Deal with MUA/LFP data - if dclass == "AnalogData": - - # Open each file as memmap - dsets = [] - for fk, fname in enumerate(filename): - dsets.append(np.memmap(fname, offset=int(headers[fk]["length"]), - mode="r", dtype=headers[fk]["dtype"], - shape=(headers[fk]["M"], headers[fk]["N"]), - order="F")) - - # Instantiate VirtualData class w/ constructed memmaps (error checking is done in there) - data = VirtualData(dsets) - - # First things first: attach data to output object - out.data = data - - # If necessary, construct list of channel labels (parsing is done by setter) - if isinstance(channel, str): - channel = [channel + str(i + 1) for i in range(data.N)] - - # Set remaining attributes - out.channel = np.array(channel) - - # Handle spike patterns - elif dclass == "SpikeData": - - # Open provided data-file as memmap and attach it to `out` - out.data = np.memmap(filename[0], offset=int(headers[0]["length"]), - mode="r", dtype=headers[0]["dtype"], - shape=(headers[0]["M"], headers[0]["N"]), order="F") - - # If necessary, construct lists for channel and unit labels - if isinstance(channel, str): - nchan = np.unique(out.data[:, out.dimord.index("channel")]).size - channel = [channel + str(i + 1) for i in range(nchan)] - if isinstance(unit, str): - nunit = np.unique(out.data[:, out.dimord.index("unit")]).size - unit = [unit + str(i + 1) for i in range(nunit)] - - # Set meta-data - out.channel = channel - out.unit = unit - - # Handle event data - elif dclass == "EventData": - - # Open provided data-file as memmap and attach it to `out` - out.data = np.memmap(filename[0], offset=int(headers[0]["length"]), - mode="r", dtype=headers[0]["dtype"], - shape=(headers[0]["M"], headers[0]["N"]), order="F") - - # Attach file-header and detected samplerate - out._hdr = headers - out.samplerate = float(1/headers[0]["tSample"]*1e9) - - # Now we can abuse ``definetrial`` to set trial-related props - if dclass != "EventData" or (dclass == "EventData" and trialdefinition is not None): - out.definetrial(trialdefinition) - - # Write `cfg` entries - out.cfg = {"method" : sys._getframe().f_code.co_name, - "hdr" : headers} - - # Write log entry - log = "loaded data:\n" +\ - "\tfile(s) = {fls:s}" - out.log = log.format(fls="\n\t\t ".join(fl for fl in filename)) - - # Happy breakdown - return out if new_out else None - -########################################################################################## -def read_binary_esi_header(filename): - """ - Docstring - """ - - # SyNCoPy raw binary dtype-codes - dtype = { - 1 : 'int8', - 2 : 'uint8', - 3 : 'int16', - 4 : 'uint16', - 5 : 'int32', - 6 : 'uint32', - 7 : 'int64', - 8 : 'uint64', - 9 : 'float32', - 10 : 'float64' - } - - # First and foremost, make sure input arguments make sense - try: - io_parser(filename, varname="filename", isfile=True, exists=True, - ext=[".lfp", ".mua", ".evt", ".dpd", - ".apd", ".eye", ".pup", ".spk"]) - except Exception as exc: - raise exc - - # Try to access file on disk, abort if this goes wrong - try: - fid = open(filename, "r") - except: - raise SPYIOError(filename) - - # Extract file header - hdr = {} - hdr["version"] = int(np.fromfile(fid,dtype='uint8',count=1)[0]) - hdr["length"] = int(np.fromfile(fid,dtype='uint16',count=1)[0]) - hdr["dtype"] = dtype[np.fromfile(fid,dtype='uint8',count=1)[0]] - # if os.path.splitext(filename)[1] in [".lfp", ".mua"]: - # hdr["N"] = int(np.fromfile(fid,dtype='uint64',count=1)[0]) - # hdr["M"] = int(np.fromfile(fid,dtype='uint64',count=1)[0]) - # else: - hdr["M"] = int(np.fromfile(fid,dtype='uint64',count=1)[0]) - hdr["N"] = int(np.fromfile(fid,dtype='uint64',count=1)[0]) - hdr["tSample"] = int(np.fromfile(fid,dtype='uint64',count=1)[0]) - fid.close() - - return hdr - From 9a67631caea6c186d19608df8d3f6320e851a635 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Mon, 24 Jan 2022 20:10:12 +0100 Subject: [PATCH 002/166] FIX: Make Syncopy matplotlib 3.5 compatible - update syncopy dependencies and modify changelog On branch nwb Changes to be committed: modified: CHANGELOG.md modified: syncopy.yml --- CHANGELOG.md | 11 +++++++++++ syncopy.yml | 21 +++++++++++---------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1368682ab..204eaa15f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] +### NEW +### CHANGED +- Made plotting routines matplotlib 3.5 compatible + +### REMOVED +### DEPRECATED +- Removed loading code for ESI binary format that is no longer supported + +### FIXED + ## [v0.2] - 2022-01-18 Major Release diff --git a/syncopy.yml b/syncopy.yml index f6d05ea95..deffd450a 100644 --- a/syncopy.yml +++ b/syncopy.yml @@ -4,26 +4,27 @@ channels: - conda-forge dependencies: # SyNCoPy runtime requirements - - python >= 3.8, < 3.9 - - pip - - numpy >= 1.10, < 2.0 - - scipy >= 1.5 - h5py >= 2.9, < 3 - matplotlib >= 3.3, < 3.5 - - tqdm >= 4.31 - natsort + - numpy >= 1.10, < 2.0 + - pip + - python >= 3.8, < 3.9 + - scipy >= 1.5 + - tqdm >= 4.31 # Optional packages required for running the test-suite and building the HTML docs - esi-acme - - python-graphviz + - ipdb - memory_profiler - numpydoc - - sphinx_bootstrap_theme - - pytest-cov - pylint - - ipdb - - tox + - pynwb + - python-graphviz + - pytest-cov - ruamel.yaml >=0.16, < 0.17 - setuptools_scm + - sphinx_bootstrap_theme + - tox - pip: # Optional: only necessary when building the HTML documentation - sphinx_automodapi From 7edeb92205f950d1114ef30da6dda377871af73c Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Mon, 24 Jan 2022 20:11:25 +0100 Subject: [PATCH 003/166] WIP: First NWB import code draft - skeleton code; one general purpose read function intended to be a management routine for all 3rd party data formats - skeleton code for the actual NWB importer On branch nwb Changes to be committed: modified: syncopy/__init__.py modified: syncopy/io/__init__.py new file: syncopy/io/_read_nwb.py modified: syncopy/io/load_spy_container.py new file: syncopy/io/read_external.py modified: syncopy/plotting/spy_plotting.py modified: syncopy/tests/local_spy.py --- syncopy/__init__.py | 7 ++ syncopy/io/__init__.py | 13 ++-- syncopy/io/_read_nwb.py | 43 ++++++++++++ syncopy/io/load_spy_container.py | 108 +++++++++++++++---------------- syncopy/io/read_external.py | 13 ++++ syncopy/plotting/spy_plotting.py | 2 +- syncopy/tests/local_spy.py | 56 ++-------------- 7 files changed, 129 insertions(+), 113 deletions(-) create mode 100644 syncopy/io/_read_nwb.py create mode 100644 syncopy/io/read_external.py diff --git a/syncopy/__init__.py b/syncopy/__init__.py index a4ac6bd63..8ccb448f4 100644 --- a/syncopy/__init__.py +++ b/syncopy/__init__.py @@ -59,6 +59,13 @@ except ImportError: __plt__ = False +# See if NWB is available +try: + import pynwb + __nwb__ = True +except ImportError: + __nwb__ = False + # Set package-wide temp directory csHome = "/cs/home/{}".format(getpass.getuser()) if os.environ.get("SPYTMPDIR"): diff --git a/syncopy/io/__init__.py b/syncopy/io/__init__.py index 496ab8a42..07a94aa64 100644 --- a/syncopy/io/__init__.py +++ b/syncopy/io/__init__.py @@ -1,18 +1,21 @@ # -*- coding: utf-8 -*- -# +# # Populate namespace with io routines -# +# # Import __all__ routines from local modules -from . import utils, load_raw_binary, load_spy_container, save_spy_container +from . import (utils, load_spy_container, save_spy_container, + read_external, _read_nwb) from .utils import * -from .load_raw_binary import * from .load_spy_container import * from .save_spy_container import * +from .read_external import * +from ._read_nwb import * # Populate local __all__ namespace __all__ = [] __all__.extend(utils.__all__) -__all__.extend(load_raw_binary.__all__) __all__.extend(load_spy_container.__all__) __all__.extend(save_spy_container.__all__) +__all__.extend(read_external.__all__) +__all__.extend(_read_nwb.__all__) diff --git a/syncopy/io/_read_nwb.py b/syncopy/io/_read_nwb.py new file mode 100644 index 000000000..ae20bf391 --- /dev/null +++ b/syncopy/io/_read_nwb.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# +# Load data from NWB file +# + +# Builtin/3rd party package imports +from genericpath import exists +import numpy as np + +# Local imports +from syncopy import __nwb__ +from syncopy.shared.errors import SPYError +from syncopy.shared.parsers import io_parser + +# Conditional imports +if __nwb__: + from pynwb import NWBHDF5IO + +# Global consistent error message if NWB is missing +nwbErrMsg = "\nSyncopy WARNING: Could not import 'pynwb'. \n" +\ + "{} requires a working pyNWB installation. \n" +\ + "Please consider installing 'pynwb', e.g., via conda: \n" +\ + "\tconda install -c conda-forge pynwb\n" +\ + "or using pip:\n" +\ + "\tpip install pynwb" + +__all__ = ["read_nwb"] + + +def read_nwb(filename): + """ + Coming soon... + """ + + # Abort if NWB is not installed + if not __nwb__: + raise SPYError(nwbErrMsg.format("read_nwb")) + + nwbFilePath, nwbName = io_parser(filename, varname="filename", isfile=True, exists=True) + + nwbio = NWBHDF5IO(nwbFilePath, "r", load_namespaces=True) + nwbfile = nwbio.read() + diff --git a/syncopy/io/load_spy_container.py b/syncopy/io/load_spy_container.py index 7597ec86f..a14ae5226 100644 --- a/syncopy/io/load_spy_container.py +++ b/syncopy/io/load_spy_container.py @@ -6,17 +6,15 @@ # Builtin/3rd party package imports import os import json -import inspect import h5py import sys import numpy as np -from collections import OrderedDict from glob import glob # Local imports from syncopy.shared.filetypes import FILE_EXT from syncopy.shared.parsers import io_parser, data_parser, filename_parser, array_parser -from syncopy.shared.errors import (SPYTypeError, SPYValueError, SPYIOError, +from syncopy.shared.errors import (SPYTypeError, SPYValueError, SPYIOError, SPYError, SPYWarning) from syncopy.io.utils import hash_file, startInfoDict @@ -28,108 +26,108 @@ def load(filename, tag=None, dataclass=None, checksum=False, mode="r+", out=None): """ Load Syncopy data object(s) from disk - + Either loads single files within or outside of '.spy'-containers or loads - multiple objects from a single '.spy'-container. Loading from containers can - be further controlled by imposing restrictions on object class(es) (via - `dataclass`) and file-name tag(s) (via `tag`). - + multiple objects from a single '.spy'-container. Loading from containers can + be further controlled by imposing restrictions on object class(es) (via + `dataclass`) and file-name tag(s) (via `tag`). + Parameters ---------- filename : str Either path to Syncopy container folder (\*.spy, if omitted, the extension '.spy' will be appended) or name of data or metadata file. If `filename` - points to a container and no further specifications are provided, the + points to a container and no further specifications are provided, the entire contents of the container is loaded. Otherwise, specific objects - may be selected using the `dataclass` or `tag` keywords (see below). + may be selected using the `dataclass` or `tag` keywords (see below). tag : None or str or list If `filename` points to a container, `tag` may be used to filter objects - by filename-`tag`. Multiple tags can be provided using a list, e.g., + by filename-`tag`. Multiple tags can be provided using a list, e.g., ``tag = ['experiment1', 'experiment2']``. Can be combined with `dataclass` - (see below). Invalid if `filename` points to a single file. + (see below). Invalid if `filename` points to a single file. dataclass : None or str or list - If provided, only objects of provided dataclass are loaded from disk. - Available options are '.analog', '.spectral', .spike' and '.event' + If provided, only objects of provided dataclass are loaded from disk. + Available options are '.analog', '.spectral', .spike' and '.event' (as listed in ``spy.FILE_EXT["data"]``). Multiple class specifications can be provided using a list, e.g., ``dataclass = ['.analog', '.spike']``. Can be combined with `tag` (see above) and is also valid if `filename` points to a single file (e.g., to ensure loaded object is of a specific - type). + type). checksum : bool If `True`, checksum-matching is performed on loaded object(s) to ensure - data-integrity (impairs performance particularly when loading large files). + data-integrity (impairs performance particularly when loading large files). mode : str Data access mode of loaded objects (can be 'r' for read-only, 'r+' or 'w' - for read/write access). + for read/write access). out : Syncopy data object - Empty object to be filled with data loaded from disk. Has to match the + Empty object to be filled with data loaded from disk. Has to match the type of the on-disk file (e.g., ``filename = 'mydata.analog'`` requires - `out` to be a :class:`syncopy.AnalogData` object). Can only be used + `out` to be a :class:`syncopy.AnalogData` object). Can only be used when loading single objects from disk (`out` is ignored when multiple - files are loaded from a container). - + files are loaded from a container). + Returns ------- Nothing : None If a single file is loaded and `out` was provided, `out` is filled with - data loaded from disk, i.e., :func:`syncopy.load` does **not** create a + data loaded from disk, i.e., :func:`syncopy.load` does **not** create a new object obj : Syncopy data object - If a single file is loaded and `out` was `None`, :func:`syncopy.load` - returns a new object. + If a single file is loaded and `out` was `None`, :func:`syncopy.load` + returns a new object. objdict : dict If multiple files are loaded, :func:`syncopy.load` creates a new object for each file and places them in a dictionary whose keys are the base-names - (sans path) of the corresponding files. - + (sans path) of the corresponding files. + Notes ----- All of Syncopy's classes offer (limited) support for data loading upon object creation. Just as the class method ``.save`` can be used as a shortcut for - :func:`syncopy.save`, Syncopy objects can be created from Syncopy data-files - upon creation, e.g., - + :func:`syncopy.save`, Syncopy objects can be created from Syncopy data-files + upon creation, e.g., + >>> adata = spy.AnalogData('/path/to/session1.analog') - - creates a new :class:`syncopy.AnalogData` object and immediately fills it - with data loaded from the file "/path/to/session1.analog". - - Since only one object can be created at a time, this loading shortcut only + + creates a new :class:`syncopy.AnalogData` object and immediately fills it + with data loaded from the file "/path/to/session1.analog". + + Since only one object can be created at a time, this loading shortcut only supports single file specifications (i.e., ``spy.AnalogData("container.spy")`` - is invalid). + is invalid). Examples - -------- - Load all objects found in the spy-container "sessionName" (the extension ".spy" + -------- + Load all objects found in the spy-container "sessionName" (the extension ".spy" may or may not be provided) - + >>> objectDict = spy.load("sessionName") >>> # --> returns a dict with base-filenames as keys - + Load all :class:`syncopy.AnalogData` and :class:`syncopy.SpectralData` objects from the spy-container "sessionName" - + >>> objectDict = spy.load("sessionName.spy", dataclass=['analog', 'spectral']) - + Load a specific :class:`syncopy.AnalogData` object from the above spy-container - + >>> obj = spy.load("sessionName.spy/sessionName_someTag.analog") - + This is equivalent to - + >>> obj = spy.AnalogData("sessionName.spy/sessionName_someTag.analog") - - If the "sessionName" spy-container only contains one object with the tag + + If the "sessionName" spy-container only contains one object with the tag "someTag", the above call is equivalent to - + >>> obj = spy.load("sessionName.spy", tag="someTag") - + If there are multiple objects of different types using the same tag "someTag", - the above call can be further narrowed down to only load the requested + the above call can be further narrowed down to only load the requested :class:`syncopy.AnalogData` object - + >>> obj = spy.load("sessionName.spy", tag="someTag", dataclass="analog") - + See also -------- syncopy.save : save syncopy object on disk @@ -145,7 +143,7 @@ def load(filename, tag=None, dataclass=None, checksum=False, mode="r+", out=None fileInfo = filename_parser(filename) except Exception as exc: raise exc - + if tag is not None: if isinstance(tag, str): tags = [tag] @@ -189,7 +187,7 @@ def load(filename, tag=None, dataclass=None, checksum=False, mode="r+", out=None # If `filename` points to a spy container, `glob` what's inside, otherwise just load if fileInfo["filename"] is None: - + if dataclass is None: extensions = FILE_EXT["data"] container = os.path.join(fileInfo["folder"], fileInfo["container"]) @@ -259,7 +257,7 @@ def _load(filename, checksum, mode, out): .format(field=key, cls=dataclass.__name__, file=jsonFile)) - + # If `_hdr` is an empty list, set it to `None` to not confuse meta-functions hdr = jsonDict.get("_hdr") if isinstance(hdr, (list, np.ndarray)): @@ -296,7 +294,7 @@ def _load(filename, checksum, mode, out): out.mode = mode for datasetProperty in out._hdfFileDatasetProperties: setattr(out, datasetProperty, h5py.File(hdfFile, mode="r")[datasetProperty]) - + # Abuse ``definetrial`` to set trial-related props trialdef = h5py.File(hdfFile, mode="r")["trialdefinition"][()] diff --git a/syncopy/io/read_external.py b/syncopy/io/read_external.py new file mode 100644 index 000000000..f3ffe5988 --- /dev/null +++ b/syncopy/io/read_external.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# +# Import data from 3rd party formats +# + +__all__ = ["read"] + + +def read(filename): + """ + Coming soon.... + """ + diff --git a/syncopy/plotting/spy_plotting.py b/syncopy/plotting/spy_plotting.py index 3af9f3760..2e8286778 100644 --- a/syncopy/plotting/spy_plotting.py +++ b/syncopy/plotting/spy_plotting.py @@ -64,7 +64,7 @@ # Global consistent error message if matplotlib is missing pltErrMsg = "\nSyncopy WARNING: Could not import 'matplotlib'. \n" +\ - "{}} requires a working matplotlib installation. \n" +\ + "{} requires a working matplotlib installation. \n" +\ "Please consider installing 'matplotlib', e.g., via conda: \n" +\ "\tconda install matplotlib\n" +\ "or using pip:\n" +\ diff --git a/syncopy/tests/local_spy.py b/syncopy/tests/local_spy.py index 39b4c9a9d..2d1526772 100644 --- a/syncopy/tests/local_spy.py +++ b/syncopy/tests/local_spy.py @@ -20,62 +20,14 @@ from syncopy.tests.misc import generate_artificial_data from syncopy.tests import synth_data - -def call_con(data, method, **kwargs): - - res = spy.connectivity(data=data, - method=method, - **kwargs) - return res - - -def call_freq(data, method, **kwargs): - res = spy.freqanalysis(data=data, method=method, **kwargs) - - return res +from pynwb import NWBHDF5IO # Prepare code to be executed using, e.g., iPython's `%run` magic command if __name__ == "__main__": - nSamples = 2500 - nChannels = 4 - nTrials = 10 - fs = 200 - - foilim = [5, 80] - foi = np.arange(5, 80, 1) - # this still gives type(tsel) = slice :) - sdict1 = {'channels' : ['channel01', 'channel03'], 'toilim' : [-.221, 1.12]} - - # AR(2) Network test data - AdjMat = synth_data.mk_RandomAdjMat(nChannels) - trls = [100 * synth_data.AR2_network(AdjMat) for _ in range(nTrials)] - tdat1 = spy.AnalogData(trls, samplerate=fs) - - # phase difusion test data - f1, f2 = 10, 40 - trls = [] - for _ in range(nTrials): - - p1 = synth_data.phase_evo(f1, eps=.01, nChannels=nChannels, nSamples=nSamples) - p2 = synth_data.phase_evo(f2, eps=0.001, nChannels=nChannels, nSamples=nSamples) - trls.append( - 1 * np.cos(p1) + 1 * np.cos(p2) + 0.6 * np.random.randn( - nSamples, nChannels)) - - tdat2 = spy.AnalogData(trls, samplerate=1000) - - - # Test stuff within here... - data1 = generate_artificial_data(nTrials=5, nChannels=16, equidistant=False, inmemory=False) - data2 = generate_artificial_data(nTrials=5, nChannels=16, equidistant=True, inmemory=False) - - - - # client = spy.esi_cluster_setup(interactive=False) - # data1 + data2 + nwbFilePath = "/home/fuertingers/Documents/job/SyNCoPy/Data/test.nwb" - # sys.exit() - # spec = spy.freqanalysis(artdata, method="mtmfft", taper="dpss", output="pow") + nwbio = NWBHDF5IO(nwbFilePath, "r", load_namespaces=True) + nwbfile = nwbio.read() From daf66d9af5c32543d35f94dea15f4fa040b13432 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 28 Jan 2022 11:12:14 +0100 Subject: [PATCH 004/166] NEW : Start a doc readme On branch doc-improvements Changes to be committed: new file: doc/README.md --- doc/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 doc/README.md diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 000000000..bbd066780 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,10 @@ + +# (Online-) Documentation + +## Build Requirements + +Install (debian based packages): +- `sphinx-common` +- `python3-sphinx-bootstrap-theme` + +then run `make html` from this folder \ No newline at end of file From b6497be82f02cc8f6dc27fc6b021546511a5978a Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 28 Jan 2022 12:06:38 +0100 Subject: [PATCH 005/166] CHG: Improve doc --- doc/source/README.rst | 9 +++--- doc/source/quickstart.rst | 62 ++++++++++++++++++++++++++++----------- 2 files changed, 50 insertions(+), 21 deletions(-) diff --git a/doc/source/README.rst b/doc/source/README.rst index f2d02606e..d09c6791a 100644 --- a/doc/source/README.rst +++ b/doc/source/README.rst @@ -27,11 +27,12 @@ Getting Started Our :doc:`Quickstart Guide ` covers installation and basic usage. More in-depth information relevant to Syncopy users can be found in our :doc:`User Guide `. Want to contribute or just curious how the sausage -is made? Take a look at our :doc:`Developer Guide `. Once again -in order of brevity: +is made? Take a look at our :doc:`Developer Guide `. -* :doc:`Quickstart Guide ` -* :doc:`User Guide ` + +Guides and Tutorials +-------------------- +* :doc:`General User Guide ` * :doc:`Developer Guide ` Navigation diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index bafd386d8..3ba19bab3 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -4,40 +4,38 @@ Getting started with Syncopy Installing Syncopy ------------------ -Syncopy can be installed using `Pip `_: +Syncopy can be installed using `conda `_: + +.. code-block:: bash + + conda install esi-syncopy + +Alternatively it is also available on `Pip `_: .. code-block:: bash pip install esi-syncopy -Syncopy will soon be hosted on `conda-forge `_ as well. If you're working on the ESI cluster installing Syncopy is only necessary if you create your own Conda environment. -Setting Up Your Python Environment ----------------------------------- +Installing parallel processing engine ACME +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -On the ESI cluster, ``/opt/conda/envs/syncopy`` provides a -pre-configured and tested Conda environment with the most recent Syncopy -version. This environment can be easily started using the `ESI JupyterHub -`_ +To harness the parallel processing capabilities of Syncopy +it is necessary to install `ACME `_. -Syncopy makes heavy use of temporary files, which may become large (> 100 GB). -The storage location can be set using the `environmental variable -`_ :envvar:`SPYTMPDIR`, which -by default points to your home directory: +Again either via conda .. code-block:: bash - SPYTMPDIR=~/.spy + conda install esi-acme -The performance of Syncopy strongly depends on the read and write speed in -this folder. On the `ESI JupyterHub `_, the -variable is set to use the high performance storage: +or pip .. code-block:: bash - SPYTMPDIR=/cs/home/$USER/.spy + pip install esi-acme Importing Syncopy @@ -72,3 +70,33 @@ This will allocate a parallel worker for each trial defined in `data`. If your c is running on the ESI cluster, Syncopy will automatically use the existing SLURM scheduler, in a single-machine setup, any available local multi-processing resources will be utilized. More details can be found in the :doc:`Data Analysis Guide ` + + +Setting Up Your Python Environment +---------------------------------- + +On the ESI cluster, ``/opt/conda/envs/syncopy`` provides a +pre-configured and tested Conda environment with the most recent Syncopy +version. This environment can be easily started using the `ESI JupyterHub +`_ + +Syncopy makes heavy use of temporary files, which may become large (> 100 GB). +The storage location can be set using the `environmental variable +`_ :envvar:`SPYTMPDIR`, which +by default points to your home directory: + +.. code-block:: bash + + SPYTMPDIR=~/.spy + +The performance of Syncopy strongly depends on the read and write speed in +this folder. On the `ESI JupyterHub `_, the +variable is set to use the high performance storage: + +.. code-block:: bash + + SPYTMPDIR=/cs/home/$USER/.spy + + + + From bf1099a4d5854130eeeeeb2485ae1ea203188ee1 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Fri, 28 Jan 2022 14:04:40 +0100 Subject: [PATCH 006/166] FIX: Improve scalar checks - replace `isinstance(x, numbers.Number)` checks with `np.number` so that Boolean values are correctly weeded out (`isinstance(True, numbers.Number)` returns `True`, `np.number` does not) On branch dev Changes to be committed: modified: CHANGELOG.md modified: syncopy/datatype/base_data.py modified: syncopy/datatype/methods/arithmetic.py modified: syncopy/datatype/methods/definetrial.py modified: syncopy/shared/input_validators.py modified: syncopy/shared/parsers.py modified: syncopy/specest/compRoutines.py modified: syncopy/specest/freqanalysis.py --- CHANGELOG.md | 15 +++++++++++++++ syncopy/datatype/base_data.py | 2 +- syncopy/datatype/methods/arithmetic.py | 3 +-- syncopy/datatype/methods/definetrial.py | 3 +-- syncopy/shared/input_validators.py | 9 +++------ syncopy/shared/parsers.py | 3 +-- syncopy/specest/compRoutines.py | 4 +--- syncopy/specest/freqanalysis.py | 5 ++--- 8 files changed, 25 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1368682ab..67aded280 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] +### NEW +### CHANGED +- Repaired top-level imports: renamed `connectivity` to `connectivityanalysis` + and the "connectivity" module is now called "nwanalysis" +- include `conda clean` in CD pipeline to avoid disk fillup by unused conda + packages/cache + +### REMOVED +- Do not parse scalars using `numbers.Number`, use `numpy.number` instead to + catch Boolean values + +### DEPRECATED +### FIXED + ## [v0.2] - 2022-01-18 Major Release diff --git a/syncopy/datatype/base_data.py b/syncopy/datatype/base_data.py index 826074044..088e2026d 100644 --- a/syncopy/datatype/base_data.py +++ b/syncopy/datatype/base_data.py @@ -1176,7 +1176,7 @@ def __iter__(self): return self._iterobj def __getitem__(self, idx): - if isinstance(idx, numbers.Number): + if isinstance(idx, np.number): try: scalar_parser(idx, varname="idx", ntype="int_like", lims=[0, self._iterlen - 1]) diff --git a/syncopy/datatype/methods/arithmetic.py b/syncopy/datatype/methods/arithmetic.py index f05178094..de8dd64e0 100644 --- a/syncopy/datatype/methods/arithmetic.py +++ b/syncopy/datatype/methods/arithmetic.py @@ -4,7 +4,6 @@ # # Builtin/3rd party package imports -import numbers import numpy as np import h5py @@ -148,7 +147,7 @@ def _parse_input(obj1, obj2, operator): # Depending on the what is thrown at `baseObj` perform more or less extensive parsing # First up: operand is a scalar - if isinstance(operand, numbers.Number): + if isinstance(operand, np.number): # Don't allow `np.inf` manipulations and catch zero-divisions if np.isinf(operand): diff --git a/syncopy/datatype/methods/definetrial.py b/syncopy/datatype/methods/definetrial.py index 6e0108263..257d2bd2e 100644 --- a/syncopy/datatype/methods/definetrial.py +++ b/syncopy/datatype/methods/definetrial.py @@ -4,7 +4,6 @@ # # Builtin/3rd party package imports -import numbers import sys import numpy as np @@ -196,7 +195,7 @@ def definetrial(obj, trialdefinition=None, pre=None, post=None, start=None, "stop": {"var": stop, "hasnan": None, "ntype": "int_like", "fillvalue": np.nan}} for vname, opts in vdict.items(): if opts["var"] is not None: - if isinstance(opts["var"], numbers.Number): + if isinstance(opts["var"], np.number): try: scalar_parser(opts["var"], varname=vname, ntype=opts["ntype"], lims=[-np.inf, np.inf]) diff --git a/syncopy/shared/input_validators.py b/syncopy/shared/input_validators.py index 7ad29695a..2d63f8d1f 100644 --- a/syncopy/shared/input_validators.py +++ b/syncopy/shared/input_validators.py @@ -7,7 +7,6 @@ # Builtin/3rd party package imports import numpy as np -from numbers import Number from syncopy.shared.errors import SPYValueError, SPYWarning, SPYInfo from syncopy.shared.parsers import scalar_parser, array_parser @@ -21,12 +20,10 @@ def validate_padding(pad_to_length, lenTrials): """ # supported padding options not_valid = False - if not isinstance(pad_to_length, (Number, str, type(None))): + if not isinstance(pad_to_length, (np.number, str, type(None))): not_valid = True elif isinstance(pad_to_length, str) and pad_to_length not in availablePaddingOpt: not_valid = True - if isinstance(pad_to_length, bool): # bool is an int subclass, check for it separately... - not_valid = True if not_valid: lgl = "`None`, 'nextpow2' or an integer like number" actual = f"{pad_to_length}" @@ -34,13 +31,13 @@ def validate_padding(pad_to_length, lenTrials): # here we check for equal lengths trials in case of no user specified absolute padding length # we do a rough 'maxlen' padding, nextpow2 will be overruled in this case - if lenTrials.min() != lenTrials.max() and not isinstance(pad_to_length, Number): + if lenTrials.min() != lenTrials.max() and not isinstance(pad_to_length, np.number): abs_pad = int(lenTrials.max()) msg = f"Unequal trial lengths present, automatic padding to {abs_pad} samples" SPYWarning(msg) # zero padding of ALL trials the same way - if isinstance(pad_to_length, Number): + if isinstance(pad_to_length, np.number): scalar_parser(pad_to_length, varname='pad_to_length', diff --git a/syncopy/shared/parsers.py b/syncopy/shared/parsers.py index 2cb9398da..6ef5ffdce 100644 --- a/syncopy/shared/parsers.py +++ b/syncopy/shared/parsers.py @@ -6,7 +6,6 @@ # Builtin/3rd party package imports import os import numpy as np -import numbers # Local imports from syncopy.shared.filetypes import FILE_EXT @@ -192,7 +191,7 @@ def scalar_parser(var, varname="", ntype=None, lims=None): """ # Make sure `var` is a scalar-like number - if not isinstance(var, numbers.Number): + if not isinstance(var, np.number): raise SPYTypeError(var, varname=varname, expected="scalar") # If required, parse type ("int_like" is a bit of a special case here...) diff --git a/syncopy/specest/compRoutines.py b/syncopy/specest/compRoutines.py index 779377e94..d05166d84 100644 --- a/syncopy/specest/compRoutines.py +++ b/syncopy/specest/compRoutines.py @@ -21,7 +21,6 @@ # Builtin/3rd party package imports from inspect import signature import numpy as np -from numbers import Number from scipy import signal # backend method imports @@ -842,8 +841,7 @@ def _make_trialdef(cfg, trialdefinition, samplerate): # If `toi` was a percentage, some cumsum/winSize algebra is required # Note: if `toi` was "all", simply use provided `trialdefinition` and `samplerate` - - elif isinstance(toi, Number): + elif isinstance(toi, np.number): mKw = cfg['method_kwargs'] winSize = mKw["nperseg"] - mKw["noverlap"] trialdefinitionLens = np.ceil(np.diff(trialdefinition[:, :2]) / winSize) diff --git a/syncopy/specest/freqanalysis.py b/syncopy/specest/freqanalysis.py index c42fed45c..93b02774c 100644 --- a/syncopy/specest/freqanalysis.py +++ b/syncopy/specest/freqanalysis.py @@ -4,7 +4,6 @@ # # Builtin/3rd party package imports -from numbers import Number import numpy as np # Syncopy imports @@ -383,7 +382,7 @@ def freqanalysis(data, method='mtmfft', output='fourier', if method in ["wavelet", "superlet"]: valid = True - if isinstance(toi, Number): + if isinstance(toi, np.number): valid = False elif isinstance(toi, str): @@ -535,7 +534,7 @@ def freqanalysis(data, method='mtmfft', output='fourier', equidistant = True overlap = np.inf - elif isinstance(toi, Number): + elif isinstance(toi, np.number): try: scalar_parser(toi, varname="toi", lims=[0, 1]) except Exception as exc: From e5e3360256f4a10ff7fb0d805ac90dc1e10e1ccb Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Fri, 28 Jan 2022 14:07:44 +0100 Subject: [PATCH 007/166] FIX: DevOps hygiene - included `conda clean` step in GitLab CI setup to avoid disk fillup by unused conda packages/caches On branch dev Changes to be committed: modified: .gitlab-ci.yml --- .gitlab-ci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a196e53aa..1ddcedfcb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -19,6 +19,7 @@ intellinux: script: - rm -rf ~/.spy - source $HOME/miniconda/etc/profile.d/conda.sh + - conda clean --all -y - conda env update -f syncopy.yml --prune - conda activate syncopy - tox -p 0 @@ -37,6 +38,7 @@ powerlinux: script: - rm -rf ~/.spy - source /opt/conda/etc/profile.d/conda.sh + - conda clean --all -y - conda env update -f syncopy.yml --prune - conda activate syncopy - tox -p 0 @@ -53,6 +55,7 @@ intelwin: PYTEST_ADDOPTS: "--color=yes --tb=short --verbose" GIT_FETCH_EXTRA_FLAGS: --tags script: + - conda clean --all -y - conda env update -f syncopy.yml --prune - conda.bat activate syncopy - tox @@ -72,6 +75,7 @@ m1macos: - ulimit -n 25000 - rm -rf ~/.spy - source /opt/conda/etc/profile.d/conda.sh + - conda clean --all -y - conda env update -f syncopy.yml --prune - conda activate syncopy - tox -p 0 @@ -89,6 +93,7 @@ slurmtest: GIT_FETCH_EXTRA_FLAGS: --tags script: - source /opt/conda/etc/profile.d/conda.sh + - conda clean --all -y - conda env update -f syncopy.yml --prune - conda activate syncopy - export PYTHONPATH=$CI_PROJECT_DIR @@ -106,6 +111,7 @@ pypitest: script: - source $HOME/miniconda/etc/profile.d/conda.sh - conda update --yes conda + - conda clean --all -y - conda env update -f syncopy.yml --prune - conda activate syncopy - conda install --yes twine keyring rfc3986 @@ -136,6 +142,7 @@ pypideploy: script: - source $HOME/miniconda/etc/profile.d/conda.sh - conda update --yes conda + - conda clean --all -y - conda env update -f syncopy.yml --prune - conda activate syncopy - conda install --yes twine keyring rfc3986 From efecb2221aafca5370281d61071f05b1957b5a34 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 28 Jan 2022 16:34:25 +0100 Subject: [PATCH 008/166] NEW: Not so quick Quickstart - moved installation instructions to setup.rst - started a new quickstart - increased standard paragraph fontsize to 1.1em Changes to be committed: modified: doc/source/README.rst modified: doc/source/_static/esi-style.css modified: doc/source/conf.py new file: doc/source/quickstart/quickstart.rst new file: doc/source/quickstart/synth_data_plot.png new file: doc/source/scripts/qs_synth_data1.py renamed: doc/source/quickstart.rst -> doc/source/setup.rst modified: doc/source/sitemap.rst --- doc/source/README.rst | 17 +++-- doc/source/_static/esi-style.css | 8 ++- doc/source/conf.py | 6 +- doc/source/quickstart/quickstart.rst | 84 ++++++++++++++++++++++ doc/source/quickstart/synth_data_plot.png | Bin 0 -> 62566 bytes doc/source/scripts/qs_synth_data1.py | 27 +++++++ doc/source/{quickstart.rst => setup.rst} | 0 doc/source/sitemap.rst | 2 +- 8 files changed, 132 insertions(+), 12 deletions(-) create mode 100644 doc/source/quickstart/quickstart.rst create mode 100644 doc/source/quickstart/synth_data_plot.png create mode 100644 doc/source/scripts/qs_synth_data1.py rename doc/source/{quickstart.rst => setup.rst} (100%) diff --git a/doc/source/README.rst b/doc/source/README.rst index d09c6791a..d4a02236e 100644 --- a/doc/source/README.rst +++ b/doc/source/README.rst @@ -24,16 +24,18 @@ We strive to achieve the following goals: Getting Started --------------- -Our :doc:`Quickstart Guide ` covers installation and basic usage. -More in-depth information relevant to Syncopy users can be found in our -:doc:`User Guide `. Want to contribute or just curious how the sausage +Our :doc:`Basic Setup Guide ` covers installation and basic usage. + +After this you might find useful examples in our :doc:`Quickstart Guide `. + +Want to contribute or just curious how the sausage is made? Take a look at our :doc:`Developer Guide `. -Guides and Tutorials --------------------- +In depth Guides and Tutorials +----------------------------- * :doc:`General User Guide ` -* :doc:`Developer Guide ` + Navigation ---------- @@ -51,6 +53,7 @@ For general inquiries please contact syncopy (at) esi-frankfurt.de. .. toctree:: :hidden: - quickstart + quickstart/quickstart.rst + setup user/users.rst developer/developers.rst diff --git a/doc/source/_static/esi-style.css b/doc/source/_static/esi-style.css index 03254e089..d63825c8f 100644 --- a/doc/source/_static/esi-style.css +++ b/doc/source/_static/esi-style.css @@ -8,4 +8,10 @@ a.reference.external{ p.rubric { font-size: 16px; -} \ No newline at end of file +} + +p { + font-size: 1.1em +} + + diff --git a/doc/source/conf.py b/doc/source/conf.py index f5b8acba9..d865aff09 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -21,8 +21,8 @@ # -- Project information ----------------------------------------------------- project = 'Syncopy' -copyright = '2020, Joscha Schmiedt and Stefan Fuertinger' -author = 'Joscha Schmiedt and Stefan Fuertinger' +copyright = '2020, Joscha Schmied and Stefan Fuertinger' +author = 'Joscha Schmiedt, Stefan Fuertinger and Gregor Mönke' # The short X.Y version version = syncopy.__version__ @@ -195,7 +195,7 @@ def setup(app): # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'Syncopy.tex', 'Syncopy Documentation', - 'Gregor Moenke, Joscha Schmiedt and Stefan Fuertinger', 'manual'), + 'Gregor Mönke, Joscha Schmiedt and Stefan Fuertinger', 'manual'), ] diff --git a/doc/source/quickstart/quickstart.rst b/doc/source/quickstart/quickstart.rst new file mode 100644 index 000000000..31746c2cb --- /dev/null +++ b/doc/source/quickstart/quickstart.rst @@ -0,0 +1,84 @@ +Quickstart with Syncopy +============================ + +.. currentmodule:: syncopy + +Here we want to quickly explore some standard analyses for analog data (e.g. MUA or LFP measurements), and how to do these in Syncopy. Explorative coding is best done interactively by using e.g. `Jupyter `_ or `IPython `_. Note that for plotting also `matplotlib `_ has to be installed. The following topics are covered here: + +- :ref:`synth_data` + +.. _synth_data: + +Synthetic Data +-------------- + +For testing and demonstrational purposes it is always good to work with synthetic data. In Syncopy we can easily create a synthetic dataset using basic `NumPy `_ functionality and Syncopy's :class:`~syncopy.AnalogData`. To start simple we create two harmonics and add some white noise to it: + +.. literalinclude:: /scripts/qs_synth_data1.py + + +Here we first defined the number of trials and then the number of samples and channels per trial. With a sampling rate of 500Hz and 1000 samples this gives us a trial length of two seconds. After creating the two harmonics we sampled Gaussian white noise for each trial, and added the 30Hz harmonic on the 1st channel and the 42Hz harmonic on the 2nd channel. With this the 3rd channel is left with only the noise. Every trial got collected into a Python ``list``, which at the last line was used to initialize our :class:`~syncopy.AnalogData` object. Note that synthetic data always is created with a default trigger offset of -1 seconds. + +We can get some basic information about any Syncopy data set by just typing its name in an interpreter: + +.. code-block:: python + + data + +which then gives a nicely formatted output: + +.. code-block:: bash + + Syncopy AnalogData object with fields + + cfg : dictionary with keys '' + channel : [3] element + container : None + data : 50 trials of length 1000.0 defined on [50000 x 3] float64 Dataset of size 1.14 MB + dimord : time by channel + filename : /home/whir/.spy/spy_910e_572582c9.analog + mode : r+ + sampleinfo : [50 x 2] element + samplerate : 500.0 + tag : None + time : 50 element list + trialinfo : [50 x 0] element + trials : 50 element iterable + + Use `.log` to see object history + +So we see that we indeed got 50 trials and 3 channels. To quickly plot some raw data we can use the :meth:`~syncopy.AnalogData.show` method to make various selections: + +.. code-block:: python + + import matplotlib.pyplot as ppl + + # the selection + chan1and2 = data.show(trials=[3], channels=['channel1', 'channel2']) + + # the plotting + ppl.plot(data.time[0], chan1and2, label=['channel1', 'channel2']) + # zoom into first 250ms + ppl.xlim((-1, -.75)) + # add a xlabel + ppl.xlabel("time (s)") + # add the legend + ppl.legend() + +Here we are looking at the first 250ms of the 4th trial of channels 1 and 2. After inputting the above code you should see a plot akin to this one here: + +.. image:: synth_data_plot.png + :height: 400px + :align: left + +| + +We see two noisy oscillatory time-series, with channel 2 being faster as channel 1 (42Hz vs. 30Hz) as expected. + +Time-Frequency Analysis +----------------------- + + + + + diff --git a/doc/source/quickstart/synth_data_plot.png b/doc/source/quickstart/synth_data_plot.png new file mode 100644 index 0000000000000000000000000000000000000000..2770baf7578212d15c276ead47c1379043fba251 GIT binary patch literal 62566 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

ulJn6>6U{{x4>C^#9WFX7HX4pG4S}OX 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 G2mcql>glNf literal 0 HcmV?d00001 diff --git a/doc/source/scripts/qs_synth_data1.py b/doc/source/scripts/qs_synth_data1.py new file mode 100644 index 000000000..15dce2159 --- /dev/null +++ b/doc/source/scripts/qs_synth_data1.py @@ -0,0 +1,27 @@ +import numpy as np +import syncopy as spy + +nTrials = 50 +nSamples = 1000 +nChannels = 3 +samplerate = 500 # in Hz +# the sampling times vector +tvec = np.arange(nSamples) * 1 / samplerate +f1, f2 = 30, 42 # the harmonic frequencies in Hz + +# define the two harmonics +harm1 = np.cos(2 * np.pi * f1 * tvec) +harm2 = np.cos(2 * np.pi * f2 * tvec) + +trials = [] +for _ in range(nTrials): + # the white noise + trl = 0.5 * np.random.randn(nSamples, nChannels) + # add 1st harmonic to 1st channel + trl[:, 0] += harm1 + # add 2nd harmonic to 2nd channel + trl[:, 1] += harm2 + + trials.append(trl) + +data = spy.AnalogData(trials, samplerate=samplerate) diff --git a/doc/source/quickstart.rst b/doc/source/setup.rst similarity index 100% rename from doc/source/quickstart.rst rename to doc/source/setup.rst diff --git a/doc/source/sitemap.rst b/doc/source/sitemap.rst index 49bfe6807..387ab8051 100644 --- a/doc/source/sitemap.rst +++ b/doc/source/sitemap.rst @@ -82,7 +82,7 @@ Sitemap .. toctree:: :maxdepth: 2 - quickstart + quickstart/quickstart.rst user/users.rst developer/developers.rst From c40001b45d6d22d3aadba67b88af0d16a1b5c5db Mon Sep 17 00:00:00 2001 From: tensionhead Date: Wed, 2 Feb 2022 16:15:34 +0100 Subject: [PATCH 009/166] WIP: Synthetic Data User Guide - general recipe how to get numpy arrays into Syncopy - list available built-in generators - example synthetic data from scratch Changes to be committed: modified: doc/source/README.rst modified: doc/source/quickstart/quickstart.rst new file: doc/source/user/synth_data.rst renamed: doc/source/quickstart/synth_data_plot.png -> doc/source/user/synth_data_plot.png modified: doc/source/user/users.rst modified: syncopy/__init__.py modified: syncopy/specest/freqanalysis.py modified: syncopy/tests/backend/test_conn.py modified: syncopy/tests/local_spy.py modified: syncopy/tests/synth_data.py modified: syncopy/tests/test_connectivity.py --- doc/source/README.rst | 4 +- doc/source/quickstart/quickstart.rst | 68 ---------- doc/source/user/synth_data.rst | 118 ++++++++++++++++++ .../{quickstart => user}/synth_data_plot.png | Bin doc/source/user/users.rst | 4 +- syncopy/__init__.py | 1 + syncopy/specest/freqanalysis.py | 6 +- syncopy/tests/backend/test_conn.py | 4 +- syncopy/tests/local_spy.py | 4 +- syncopy/tests/synth_data.py | 6 +- syncopy/tests/test_connectivity.py | 8 +- 11 files changed, 138 insertions(+), 85 deletions(-) create mode 100644 doc/source/user/synth_data.rst rename doc/source/{quickstart => user}/synth_data_plot.png (100%) diff --git a/doc/source/README.rst b/doc/source/README.rst index d4a02236e..5205c0141 100644 --- a/doc/source/README.rst +++ b/doc/source/README.rst @@ -36,7 +36,6 @@ In depth Guides and Tutorials ----------------------------- * :doc:`General User Guide ` - Navigation ---------- * :doc:`Sitemap ` @@ -55,5 +54,6 @@ For general inquiries please contact syncopy (at) esi-frankfurt.de. quickstart/quickstart.rst setup - user/users.rst + user/users.rst + user/user_api.rst developer/developers.rst diff --git a/doc/source/quickstart/quickstart.rst b/doc/source/quickstart/quickstart.rst index 31746c2cb..e2e238e64 100644 --- a/doc/source/quickstart/quickstart.rst +++ b/doc/source/quickstart/quickstart.rst @@ -5,75 +5,7 @@ Quickstart with Syncopy Here we want to quickly explore some standard analyses for analog data (e.g. MUA or LFP measurements), and how to do these in Syncopy. Explorative coding is best done interactively by using e.g. `Jupyter `_ or `IPython `_. Note that for plotting also `matplotlib `_ has to be installed. The following topics are covered here: -- :ref:`synth_data` -.. _synth_data: - -Synthetic Data --------------- - -For testing and demonstrational purposes it is always good to work with synthetic data. In Syncopy we can easily create a synthetic dataset using basic `NumPy `_ functionality and Syncopy's :class:`~syncopy.AnalogData`. To start simple we create two harmonics and add some white noise to it: - -.. literalinclude:: /scripts/qs_synth_data1.py - - -Here we first defined the number of trials and then the number of samples and channels per trial. With a sampling rate of 500Hz and 1000 samples this gives us a trial length of two seconds. After creating the two harmonics we sampled Gaussian white noise for each trial, and added the 30Hz harmonic on the 1st channel and the 42Hz harmonic on the 2nd channel. With this the 3rd channel is left with only the noise. Every trial got collected into a Python ``list``, which at the last line was used to initialize our :class:`~syncopy.AnalogData` object. Note that synthetic data always is created with a default trigger offset of -1 seconds. - -We can get some basic information about any Syncopy data set by just typing its name in an interpreter: - -.. code-block:: python - - data - -which then gives a nicely formatted output: - -.. code-block:: bash - - Syncopy AnalogData object with fields - - cfg : dictionary with keys '' - channel : [3] element - container : None - data : 50 trials of length 1000.0 defined on [50000 x 3] float64 Dataset of size 1.14 MB - dimord : time by channel - filename : /home/whir/.spy/spy_910e_572582c9.analog - mode : r+ - sampleinfo : [50 x 2] element - samplerate : 500.0 - tag : None - time : 50 element list - trialinfo : [50 x 0] element - trials : 50 element iterable - - Use `.log` to see object history - -So we see that we indeed got 50 trials and 3 channels. To quickly plot some raw data we can use the :meth:`~syncopy.AnalogData.show` method to make various selections: - -.. code-block:: python - - import matplotlib.pyplot as ppl - - # the selection - chan1and2 = data.show(trials=[3], channels=['channel1', 'channel2']) - - # the plotting - ppl.plot(data.time[0], chan1and2, label=['channel1', 'channel2']) - # zoom into first 250ms - ppl.xlim((-1, -.75)) - # add a xlabel - ppl.xlabel("time (s)") - # add the legend - ppl.legend() - -Here we are looking at the first 250ms of the 4th trial of channels 1 and 2. After inputting the above code you should see a plot akin to this one here: - -.. image:: synth_data_plot.png - :height: 400px - :align: left - -| - -We see two noisy oscillatory time-series, with channel 2 being faster as channel 1 (42Hz vs. 30Hz) as expected. Time-Frequency Analysis ----------------------- diff --git a/doc/source/user/synth_data.rst b/doc/source/user/synth_data.rst new file mode 100644 index 000000000..26a99c5e6 --- /dev/null +++ b/doc/source/user/synth_data.rst @@ -0,0 +1,118 @@ +.. _synth_data: + +Synthetic Data +============== + +For testing and demonstrational purposes it is always good to work with synthetic data. Syncopy brings its own suite of synthetic data generators, but it is also possible to devise your own synthetic data and conveniently load it into Syncopy. + +.. _gen_synth_recipe: + +General Recipe +-------------- + +To create a synthetic data set follow the following steps: + +- write a function which returns a single trial with desired shape ``(nSamples, nChannels)``, such that each trial is a 2d-:class:`~numpy.ndarray` +- collect all the trials into a Python ``list``, for example with a list comprehension or simply a for loop +- Instantiate an :class:`~syncopy.AnalogData` object by passing this list holding the trials and set the samplerate + +In (pseudo-)Python code: + +.. code-block:: python + + def generate_trial(nSamples, nChannels): + + trial = .. something fancy .. + + # These should evaluate to True + isinstance(trial, np.ndarray) + trial.shape == (nSamples, nChannels) + + return trial + + # here we use a list comprehension + trial_list = [generate_trial(nSamples, nChannels) for _ in range(nTrials)] + + my_fancy_data = spy.AnalogData(trial_list, samplerate=my_samplerate) + +.. note:: + The same recipe can be used to generally instantiate Syncopy data objects from NumPy arrays. + + +Built-in Generators +------------------- + +These generators return single-trial NumPy arrays, so to import them into Syncopy use the :ref:`gen_synth_recipe` described above. + +.. automodapi:: syncopy.tests.synth_data + :no-heading: + + +Synthetic Data from Scratch +--------------------------- + +We can easily create custom synthetic datasets using basic `NumPy `_ functionality and Syncopy's :class:`~syncopy.AnalogData`. +Let's create two harmonics and add some white noise to it: + +.. literalinclude:: /scripts/qs_synth_data1.py + +Here we first defined the number of trials and then the number of samples and channels per trial. With a sampling rate of 500Hz and 1000 samples this gives us a trial length of two seconds. After creating the two harmonics we sampled Gaussian white noise for each trial, and added the 30Hz harmonic on the 1st channel and the 42Hz harmonic on the 2nd channel. With this the 3rd channel is left with only the noise. Every trial got collected into a Python ``list``, which at the last line was used to initialize our :class:`~syncopy.AnalogData` object. Note that synthetic data always is created with a default trigger offset of -1 seconds. + +We can get some basic information about any Syncopy data set by just typing its name in an interpreter: + +.. code-block:: python + + data + +which then gives a nicely formatted output: + +.. code-block:: bash + + Syncopy AnalogData object with fields + + cfg : dictionary with keys '' + channel : [3] element + container : None + data : 50 trials of length 1000.0 defined on [50000 x 3] float64 Dataset of size 1.14 MB + dimord : time by channel + filename : /home/whir/.spy/spy_910e_572582c9.analog + mode : r+ + sampleinfo : [50 x 2] element + samplerate : 500.0 + tag : None + time : 50 element list + trialinfo : [50 x 0] element + trials : 50 element iterable + + Use `.log` to see object history + +So we see that we indeed got 50 trials and 3 channels. + +To quickly plot the created raw data we can use the :meth:`~syncopy.AnalogData.show` method to make various selections: + +.. code-block:: python + + import matplotlib.pyplot as ppl + + # the selection + chan1and2 = data.show(trials=[0], channels=['channel1', 'channel2']) + + # the plotting + ppl.plot(data.time[0], chan1and2, label=['channel1', 'channel2']) + # zoom into first 250ms + ppl.xlim((-1, -.75)) + # add a xlabel + ppl.xlabel("time (s)") + # add the legend + ppl.legend() + +Here we are looking at the first 250ms of the 1st trial of channels 1 and 2. After inputting the above code you should see a plot akin to this one here: + +.. image:: synth_data_plot.png + :height: 400px + +| + +We see two noisy oscillatory time-series, with channel 2 oscillating faster than channel 1 (42Hz vs. 30Hz). + + diff --git a/doc/source/quickstart/synth_data_plot.png b/doc/source/user/synth_data_plot.png similarity index 100% rename from doc/source/quickstart/synth_data_plot.png rename to doc/source/user/synth_data_plot.png diff --git a/doc/source/user/users.rst b/doc/source/user/users.rst index 4ef519e68..acbf06561 100644 --- a/doc/source/user/users.rst +++ b/doc/source/user/users.rst @@ -3,7 +3,7 @@ Syncopy User Guide ****************** This section of the Syncopy documentation contains information aimed at users -who primarily want to apply existing analysis functions to their data. This +who want to apply Syncopy analysis functions to their data. This usually entails writing analysis scripts operating on a given list of data files. @@ -12,5 +12,7 @@ files. fieldtrip data_handling + synth_data processing user_api + diff --git a/syncopy/__init__.py b/syncopy/__init__.py index 64812c512..5062c27f0 100644 --- a/syncopy/__init__.py +++ b/syncopy/__init__.py @@ -95,6 +95,7 @@ from .nwanalysis import * from .statistics import * from .plotting import * +from .tests import synth_data # Register session __session__ = datatype.base_data.SessionLogger() diff --git a/syncopy/specest/freqanalysis.py b/syncopy/specest/freqanalysis.py index c42fed45c..f9c65d624 100644 --- a/syncopy/specest/freqanalysis.py +++ b/syncopy/specest/freqanalysis.py @@ -87,7 +87,7 @@ def freqanalysis(data, method='mtmfft', output='fourier', window short-time Fourier transform using either a single Hanning taper or multiple DPSS tapers. - * **taper** : one of :data:`~syncopy.specest.const_def.availableTapers` + * **taper** : one of :data:`~syncopy.shared.const_def.availableTapers` * **tapsmofrq** : spectral smoothing box for slepian tapers (in Hz) * **nTaper** : number of orthogonal tapers for slepian tapers * **keeptapers** : return individual tapers or average @@ -166,7 +166,7 @@ def freqanalysis(data, method='mtmfft', output='fourier', default. taper : str Only valid if `method` is `'mtmfft'` or `'mtmconvol'`. Windowing function, - one of :data:`~syncopy.specest.const_def.availableTapers` (see below). + one of :data:`~syncopy.shared.const_def.availableTapers` (see below). tapsmofrq : float Only valid if `method` is `'mtmfft'` or `'mtmconvol'` and `taper` is `'dpss'`. The amount of spectral smoothing through multi-tapering (Hz). @@ -258,7 +258,7 @@ def freqanalysis(data, method='mtmfft', output='fourier', .. autodata:: syncopy.specest.const_def.availableOutputs - .. autodata:: syncopy.specest.const_def.availableTapers + .. autodata:: syncopy.shared.const_def.availableTapers .. autodata:: syncopy.specest.const_def.availableWavelets diff --git a/syncopy/tests/backend/test_conn.py b/syncopy/tests/backend/test_conn.py index 3ee220691..1056168f9 100644 --- a/syncopy/tests/backend/test_conn.py +++ b/syncopy/tests/backend/test_conn.py @@ -181,9 +181,9 @@ def test_wilson(): data = np.zeros((nSamples, nChannels)) # more phase diffusion in the 60Hz band - p1 = synth_data.phase_evo(f1, eps=.3, fs=fs, + p1 = synth_data.phase_diffusion(f1, eps=.3, fs=fs, nSamples=nSamples, nChannels=nChannels) - p2 = synth_data.phase_evo(f2, eps=1, fs=fs, + p2 = synth_data.phase_diffusion(f2, eps=1, fs=fs, nSamples=nSamples, nChannels=nChannels) data = np.cos(p1) + 2 * np.sin(p2) + .5 * np.random.randn(nSamples, nChannels) diff --git a/syncopy/tests/local_spy.py b/syncopy/tests/local_spy.py index 3ed47ec53..8854d413f 100644 --- a/syncopy/tests/local_spy.py +++ b/syncopy/tests/local_spy.py @@ -45,8 +45,8 @@ trls = [] for _ in range(nTrials): - p1 = synth_data.phase_evo(f1, eps=.01, nChannels=nChannels, nSamples=nSamples) - p2 = synth_data.phase_evo(f2, eps=0.001, nChannels=nChannels, nSamples=nSamples) + p1 = synth_data.phase_diffusion(f1, eps=.01, nChannels=nChannels, nSamples=nSamples) + p2 = synth_data.phase_diffusion(f2, eps=0.001, nChannels=nChannels, nSamples=nSamples) trls.append( 1 * np.cos(p1) + 1 * np.cos(p2) + 0.6 * np.random.randn( nSamples, nChannels)) diff --git a/syncopy/tests/synth_data.py b/syncopy/tests/synth_data.py index d06bb09a5..b53650a7c 100644 --- a/syncopy/tests/synth_data.py +++ b/syncopy/tests/synth_data.py @@ -7,7 +7,7 @@ # noisy phase evolution <-> phase diffusion -def phase_evo(freq, eps=.1, fs=1000, nChannels=2, nSamples=1000): +def phase_diffusion(freq, eps=.1, fs=1000, nChannels=2, nSamples=1000): """ Linear (harmonic) phase evolution + a Brownian noise term @@ -79,7 +79,7 @@ def AR2_network(AdjMat=None, nSamples=2500, alphas=[0.55, -0.8]): Returns ------- - sol : np.ndarray + sol : numpy.ndarray The `nSamples` x `nChannel` solution of the network dynamics """ @@ -131,7 +131,7 @@ def mk_RandomAdjMat(nChannels=3, conn_thresh=0.25, max_coupling=0.25): Returns ------- - AdjMat : np.ndarray or None + AdjMat : numpy.ndarray `nChannels` x `nChannels` adjacency matrix where """ diff --git a/syncopy/tests/test_connectivity.py b/syncopy/tests/test_connectivity.py index 56d5a46fe..063d26ee6 100644 --- a/syncopy/tests/test_connectivity.py +++ b/syncopy/tests/test_connectivity.py @@ -162,9 +162,9 @@ class TestCoherence: trls = [] for _ in range(nTrials): # a lot of phase diffusion (1% per step) in the 20Hz band - p1 = synth_data.phase_evo(f1, eps=.01, nChannels=nChannels, nSamples=nSamples) + p1 = synth_data.phase_diffusion(f1, eps=.01, nChannels=nChannels, nSamples=nSamples) # little diffusion in the 40Hz band - p2 = synth_data.phase_evo(f2, eps=0.001, nChannels=nChannels, nSamples=nSamples) + p2 = synth_data.phase_diffusion(f2, eps=0.001, nChannels=nChannels, nSamples=nSamples) # superposition signals = np.cos(p1) + np.cos(p2) # noise stabilizes the result(!!) @@ -271,9 +271,9 @@ class TestCorrelation: for _ in range(nTrials): # no phase diffusion - p1 = synth_data.phase_evo(f1, eps=0, nChannels=nChannels, nSamples=nSamples) + p1 = synth_data.phase_diffusion(f1, eps=0, nChannels=nChannels, nSamples=nSamples) # same frequency but more diffusion - p2 = synth_data.phase_evo(f1, eps=0.1, nChannels=1, nSamples=nSamples) + p2 = synth_data.phase_diffusion(f1, eps=0.1, nChannels=1, nSamples=nSamples) # set 2nd channel to higher phase diffusion p1[:, 1] = p2[:, 0] # add a pi/2 phase shift for the even channels From 709be2a15c534fddbaa715a1d22badf4333c2314 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Thu, 3 Feb 2022 15:02:17 +0100 Subject: [PATCH 010/166] WIP : Plotting hacks - disabled erroneous titles - set avg_channels=False as default Changes to be committed: modified: syncopy/plotting/_plot_analog.py modified: syncopy/plotting/spy_plotting.py --- syncopy/plotting/_plot_analog.py | 4 ++-- syncopy/plotting/spy_plotting.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/syncopy/plotting/_plot_analog.py b/syncopy/plotting/_plot_analog.py index 6a53a46c3..1e88f7a12 100644 --- a/syncopy/plotting/_plot_analog.py +++ b/syncopy/plotting/_plot_analog.py @@ -132,13 +132,13 @@ def singlepanelplot(self, trials="all", channels="all", toilim=None, avg_channel if fig.objCount == 0: if title is None: title = chanTitle - ax.set_title(title, size=pltConfig["singleTitleSize"]) + # ax.set_title(title, size=pltConfig["singleTitleSize"]) else: handles, labels = ax.get_legend_handles_labels() ax.legend(handles, labels) if title is None: title = overlayTitle.format(len(handles)) - ax.set_title(title, size=pltConfig["singleTitleSize"]) + # ax.set_title(title, size=pltConfig["singleTitleSize"]) # Average across trials else: diff --git a/syncopy/plotting/spy_plotting.py b/syncopy/plotting/spy_plotting.py index 3af9f3760..78120bd48 100644 --- a/syncopy/plotting/spy_plotting.py +++ b/syncopy/plotting/spy_plotting.py @@ -76,7 +76,7 @@ @unwrap_cfg def singlepanelplot(*data, trials="all", channels="all", tapers="all", - toilim=None, foilim=None, avg_channels=True, avg_tapers=True, + toilim=None, foilim=None, avg_channels=False, avg_tapers=True, interp="spline36", cmap="plasma", vmin=None, vmax=None, title=None, grid=None, overlay=True, fig=None, **kwargs): """ From cb57fde9d2670768a25a49cfffb68b8d19627c97 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Thu, 3 Feb 2022 17:11:19 +0100 Subject: [PATCH 011/166] CHG: Set default output of freqanalysis to 'pow' - this is imho what is most often needed Changes to be committed: modified: syncopy/specest/freqanalysis.py --- syncopy/specest/freqanalysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncopy/specest/freqanalysis.py b/syncopy/specest/freqanalysis.py index f9c65d624..b3e546be2 100644 --- a/syncopy/specest/freqanalysis.py +++ b/syncopy/specest/freqanalysis.py @@ -47,7 +47,7 @@ @unwrap_cfg @unwrap_select @detect_parallel_client -def freqanalysis(data, method='mtmfft', output='fourier', +def freqanalysis(data, method='mtmfft', output='pow', keeptrials=True, foi=None, foilim=None, pad_to_length=None, polyremoval=None, taper="hann", tapsmofrq=None, nTaper=None, keeptapers=False, From 92f22213e649060f8941fb613859061d92e7a28c Mon Sep 17 00:00:00 2001 From: tensionhead Date: Thu, 3 Feb 2022 17:33:37 +0100 Subject: [PATCH 012/166] WIP: Quickstart Doc - so far covered object inspection and mtmfft Changes to be committed: new file: doc/source/quickstart/mtmfft_spec.png modified: doc/source/quickstart/quickstart.rst new file: doc/source/scripts/qs_damped_harm.py deleted: doc/source/scripts/qs_synth_data1.py new file: doc/source/scripts/synth_data1.py modified: doc/source/setup.rst modified: doc/source/user/synth_data.rst new file: doc/source/user/synth_data_spec.png modified: syncopy/__init__.py modified: syncopy/plotting/_plot_analog.py modified: syncopy/plotting/_plot_spectral.py modified: syncopy/plotting/spy_plotting.py --- doc/source/quickstart/mtmfft_spec.png | Bin 0 -> 17610 bytes doc/source/quickstart/quickstart.rst | 113 +++++++++++++++++++++++++- doc/source/scripts/qs_damped_harm.py | 26 ++++++ doc/source/scripts/qs_synth_data1.py | 27 ------ doc/source/scripts/synth_data1.py | 36 ++++++++ doc/source/setup.rst | 1 + doc/source/user/synth_data.rst | 73 ++++------------- doc/source/user/synth_data_spec.png | Bin 0 -> 58667 bytes syncopy/__init__.py | 1 - syncopy/plotting/_plot_analog.py | 14 ++-- syncopy/plotting/_plot_spectral.py | 8 +- syncopy/plotting/spy_plotting.py | 4 +- 12 files changed, 205 insertions(+), 98 deletions(-) create mode 100644 doc/source/quickstart/mtmfft_spec.png create mode 100644 doc/source/scripts/qs_damped_harm.py delete mode 100644 doc/source/scripts/qs_synth_data1.py create mode 100644 doc/source/scripts/synth_data1.py create mode 100644 doc/source/user/synth_data_spec.png diff --git a/doc/source/quickstart/mtmfft_spec.png b/doc/source/quickstart/mtmfft_spec.png new file mode 100644 index 0000000000000000000000000000000000000000..c9c121d59dd60ab8c4789db20a9a4f7775d04838 GIT binary patch literal 17610 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 F{}&+*EYSb} literal 0 HcmV?d00001 diff --git a/doc/source/quickstart/quickstart.rst b/doc/source/quickstart/quickstart.rst index e2e238e64..b760c085a 100644 --- a/doc/source/quickstart/quickstart.rst +++ b/doc/source/quickstart/quickstart.rst @@ -5,12 +5,123 @@ Quickstart with Syncopy Here we want to quickly explore some standard analyses for analog data (e.g. MUA or LFP measurements), and how to do these in Syncopy. Explorative coding is best done interactively by using e.g. `Jupyter `_ or `IPython `_. Note that for plotting also `matplotlib `_ has to be installed. The following topics are covered here: - +.. contents:: Topics covered + :local: + +Preparations +------------ + +To start with a clean slate, we will construct a synthetic damped harmonic with additive white noise. + +.. hint:: + Further details about artifical data generatation can be found at the :ref:`synth_data` section. + +.. literalinclude:: /scripts/qs_damped_harm.py + + +Data Object Inspection +---------------------- + +We can get some basic information about any Syncopy data set by just typing its name in an interpreter: + +.. code-block:: python + + synth_data + +which then gives a nicely formatted output: + +.. code-block:: bash + + Syncopy AnalogData object with fields + + cfg : dictionary with keys '' + channel : [2] element + container : None + data : 50 trials of length 1000.0 defined on [50000 x 3] float64 Dataset of size 1.14 MB + dimord : time by channel + filename : /xxx/xxx/.spy/spy_910e_572582c9.analog + mode : r+ + sampleinfo : [50 x 2] element + samplerate : 500.0 + tag : None + time : 50 element list + trialinfo : [50 x 0] element + trials : 50 element iterable + + Use `.log` to see object history + +So we see that we indeed got 50 trials with 2 channels and 1000 samples each. Note that Syncopy per default **stores and writes all data on disc**, as this allows for seamless processing of larger than RAM datasets. The exact location and filename of a dataset in question is listed at the ``filename`` field. The standard location is the ``.spy`` directory created automatically in the users home directory. To change this and for more details please see :ref:`setup_env`. + +.. hint:: + You can access each of the shown dataset fields separately using standard Python attribute access, e.g. ``synth_data.filename`` or ``synth_data.samplerate``. + + Time-Frequency Analysis ----------------------- +Syncopy groups analysis functionality into *meta-functions*, which in turn have various parameters selecting and controlling specific methods. In the case of spectral analysis the function to use is :func:`~syncopy.freqanalysis`. + +Here we quickly want to showcase two important methods for (time-)frequency analysis: (multi-tapered) FFT and Wavelet analysis. + +Multitapered Fourier Analysis +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +`Multitaper methods `_ allow for frequency smoothing of Fourier spectra. Syncopy implements the standard `Slepian/DPSS tapers `_ and provides a convenient parameter, the *taper smoothing frequency* ``tapsmofrq`` to control the amount of spectral smoothing in Hz. To perform a multi-tapered Fourier analysis with 3Hz spectral smoothing, we simply do: + +.. code-block:: + + spectra = spy.freqanalsysis(synth_data, method='mtmfft', foilim=[0, 50], taper='dpss', tapsmofrq=3) + +The parameter ``foilim`` controls the *frequencies of interest limits*, so in this case we are interested in the range 0-50Hz. Starting the computation interactively will show additional information:: + + Syncopy INFO: Using 5 taper(s) for multi-tapering + +informing us, that for this dataset a spectral smoothing of 3Hz required 5 Slepian tapers. + +Excurs: Logging +""""""""""""""" + +An important feature of Syncopy fostering reproducibility is a ``log`` which gets attached to and propagated between datasets. Typing:: + + spectra.log + +Gives the following (similar) output:: + + |=== user@machine: Thu Feb 3 17:05:59 2022 ===| + + __init__: created AnalogData object + + |=== user@machine: Thu Feb 3 17:12:11 2022 ===| + + __init__: created SpectralData object + + |=== user@machine: Thu Feb 3 17:12:11 2022 ===| + + definetrial: updated trial-definition with [50 x 3] element array + + |=== user@machine: Thu Feb 3 17:12:11 2022 ===| + + write_log: computed mtmfft_cF with settings + method = mtmfft + output = pow + keeptapers = False + keeptrials = True + polyremoval = None + pad_to_length = None + foi = [ 0. 0.5 1. 1.5 2. 2.5, ..., 47.5 48. 48.5 49. 49.5 50. ] + taper = dpss + nTaper = 5 + tapsmofrq = 3 + + +We see that from the creation of the original :class:`~syncopy.AnalogData` all steps needed to compute our new :class:`~syncopy.SpectralData` got recorded. +To quickly have something for the eye we can plot the power spectrum using the generic :func:`syncopy.singlepanelplot`:: + spectra.singlepanelplot() +.. image:: mtmfft_spec.png + :height: 250px +The originally very sharp harmonic peak around 30Hz got widened to about 3Hz, for all other frequencies we have the expected flat white noise floor. diff --git a/doc/source/scripts/qs_damped_harm.py b/doc/source/scripts/qs_damped_harm.py new file mode 100644 index 000000000..0b207fa35 --- /dev/null +++ b/doc/source/scripts/qs_damped_harm.py @@ -0,0 +1,26 @@ +import numpy as np +import syncopy as spy + + +nTrials = 50 +nSamples = 1000 +nChannels = 2 +samplerate = 500 # in Hz + +# the sampling times vector needed for construction +tvec = np.arange(nSamples) * 1 / samplerate +# the 30Hz harmonic +harm = np.cos(2 * np.pi * 30 * tvec) +# the damped amplitudes +dampening = np.linspace(1, 0.1, nSamples) +signal = dampening * harm + +trials = [] +for _ in range(nTrials): + + white_noise = np.random.randn(nSamples, nChannels) + trial = np.tile(signal, (2, 1)).T + white_noise + trials.append(trial) + +# instantiate Syncopy data object +synth_data = spy.AnalogData(trials, samplerate=samplerate) diff --git a/doc/source/scripts/qs_synth_data1.py b/doc/source/scripts/qs_synth_data1.py deleted file mode 100644 index 15dce2159..000000000 --- a/doc/source/scripts/qs_synth_data1.py +++ /dev/null @@ -1,27 +0,0 @@ -import numpy as np -import syncopy as spy - -nTrials = 50 -nSamples = 1000 -nChannels = 3 -samplerate = 500 # in Hz -# the sampling times vector -tvec = np.arange(nSamples) * 1 / samplerate -f1, f2 = 30, 42 # the harmonic frequencies in Hz - -# define the two harmonics -harm1 = np.cos(2 * np.pi * f1 * tvec) -harm2 = np.cos(2 * np.pi * f2 * tvec) - -trials = [] -for _ in range(nTrials): - # the white noise - trl = 0.5 * np.random.randn(nSamples, nChannels) - # add 1st harmonic to 1st channel - trl[:, 0] += harm1 - # add 2nd harmonic to 2nd channel - trl[:, 1] += harm2 - - trials.append(trl) - -data = spy.AnalogData(trials, samplerate=samplerate) diff --git a/doc/source/scripts/synth_data1.py b/doc/source/scripts/synth_data1.py new file mode 100644 index 000000000..4209f4e64 --- /dev/null +++ b/doc/source/scripts/synth_data1.py @@ -0,0 +1,36 @@ +import numpy as np +import syncopy as spy + + +def generate_noisy_harmonics(nSamples, nChannels, samplerate): + + f1, f2 = 20, 50 # the harmonic frequencies in Hz + + # the sampling times vector + tvec = np.arange(nSamples) * 1 / samplerate + + # define the two harmonics + harm1 = np.cos(2 * np.pi * f1 * tvec) + harm2 = np.cos(2 * np.pi * f2 * tvec) + + # add some white noise + trial = 0.5 * np.random.randn(nSamples, nChannels) + # add 1st harmonic to 1st channel + trial[:, 0] += harm1 + # add 2nd harmonic to 2nd channel + trial[:, 1] += 0.5 * harm2 + + return trial + + +nTrials = 50 +nSamples = 1000 +nChannels = 3 +samplerate = 500 # in Hz + +trials = [] +for _ in range(nTrials): + trial = generate_noisy_harmonics(nSamples, nChannels, samplerate) + trials.append(trial) + +synth_data = spy.AnalogData(trials, samplerate=samplerate) diff --git a/doc/source/setup.rst b/doc/source/setup.rst index 3ba19bab3..7a3573258 100644 --- a/doc/source/setup.rst +++ b/doc/source/setup.rst @@ -71,6 +71,7 @@ is running on the ESI cluster, Syncopy will automatically use the existing SLURM scheduler, in a single-machine setup, any available local multi-processing resources will be utilized. More details can be found in the :doc:`Data Analysis Guide ` +.. _setup_env: Setting Up Your Python Environment ---------------------------------- diff --git a/doc/source/user/synth_data.rst b/doc/source/user/synth_data.rst index 26a99c5e6..516a724db 100644 --- a/doc/source/user/synth_data.rst +++ b/doc/source/user/synth_data.rst @@ -3,7 +3,10 @@ Synthetic Data ============== -For testing and demonstrational purposes it is always good to work with synthetic data. Syncopy brings its own suite of synthetic data generators, but it is also possible to devise your own synthetic data and conveniently load it into Syncopy. +For testing and demonstrational purposes it is always good to work with synthetic data. Syncopy brings its own suite of synthetic data generators, but it is also possible to devise your own synthetic data and conveniently analyze it with Syncopy. + +.. contents:: + :local: .. _gen_synth_recipe: @@ -44,8 +47,9 @@ Built-in Generators These generators return single-trial NumPy arrays, so to import them into Syncopy use the :ref:`gen_synth_recipe` described above. -.. automodapi:: syncopy.tests.synth_data - :no-heading: +.. currentmodule:: syncopy.tests.synth_data +.. autofunction:: phase_diffusion +.. autofunction:: AR2_network Synthetic Data from Scratch @@ -54,65 +58,22 @@ Synthetic Data from Scratch We can easily create custom synthetic datasets using basic `NumPy `_ functionality and Syncopy's :class:`~syncopy.AnalogData`. Let's create two harmonics and add some white noise to it: -.. literalinclude:: /scripts/qs_synth_data1.py +.. literalinclude:: /scripts/synth_data1.py -Here we first defined the number of trials and then the number of samples and channels per trial. With a sampling rate of 500Hz and 1000 samples this gives us a trial length of two seconds. After creating the two harmonics we sampled Gaussian white noise for each trial, and added the 30Hz harmonic on the 1st channel and the 42Hz harmonic on the 2nd channel. With this the 3rd channel is left with only the noise. Every trial got collected into a Python ``list``, which at the last line was used to initialize our :class:`~syncopy.AnalogData` object. Note that synthetic data always is created with a default trigger offset of -1 seconds. +Here we first defined the number of trials and then the number of samples and channels per trial. With a sampling rate of 500Hz and 1000 samples this gives us a trial length of two seconds. The function ``generate_noisy_harmonics`` adds white noise to all channels, a 20Hz harmonic on the 1st channel and a 50Hz harmonic on the 2nd channel. Every trial got collected into a Python ``list``, which at the last line was used to initialize our :class:`~syncopy.AnalogData` object. Note that data instantiated that way always has a default trigger offset of -1 seconds. -We can get some basic information about any Syncopy data set by just typing its name in an interpreter: +Now we can directly run a simple FFT analysis and plot the power spectra of all 3 channels: .. code-block:: python - - data - -which then gives a nicely formatted output: - -.. code-block:: bash - - Syncopy AnalogData object with fields - - cfg : dictionary with keys '' - channel : [3] element - container : None - data : 50 trials of length 1000.0 defined on [50000 x 3] float64 Dataset of size 1.14 MB - dimord : time by channel - filename : /home/whir/.spy/spy_910e_572582c9.analog - mode : r+ - sampleinfo : [50 x 2] element - samplerate : 500.0 - tag : None - time : 50 element list - trialinfo : [50 x 0] element - trials : 50 element iterable - - Use `.log` to see object history - -So we see that we indeed got 50 trials and 3 channels. - -To quickly plot the created raw data we can use the :meth:`~syncopy.AnalogData.show` method to make various selections: -.. code-block:: python - - import matplotlib.pyplot as ppl - - # the selection - chan1and2 = data.show(trials=[0], channels=['channel1', 'channel2']) - - # the plotting - ppl.plot(data.time[0], chan1and2, label=['channel1', 'channel2']) - # zoom into first 250ms - ppl.xlim((-1, -.75)) - # add a xlabel - ppl.xlabel("time (s)") - # add the legend - ppl.legend() - -Here we are looking at the first 250ms of the 1st trial of channels 1 and 2. After inputting the above code you should see a plot akin to this one here: + spectrum = spy.freqanalysis(synth_data, foilim=[0,80], keeptrials=False) + spectrum.singlepanelplot() + -.. image:: synth_data_plot.png - :height: 400px +.. image:: synth_data_spec.png + :height: 300px | - -We see two noisy oscillatory time-series, with channel 2 oscillating faster than channel 1 (42Hz vs. 30Hz). - +As constructed, we have two harmonic peaks at the respective frequencies (20Hz and 50Hz) and the white noise floor on all channels. + diff --git a/doc/source/user/synth_data_spec.png b/doc/source/user/synth_data_spec.png new file mode 100644 index 0000000000000000000000000000000000000000..453193abff44be0e007b4562e56c2f04076bc478 GIT binary patch literal 58667 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 1 else "", nTrials) - ax.set_title(title, size=pltConfig["singleTitleSize"]) + # ax.set_title(title, size=pltConfig["singleTitleSize"]) else: handles, labels = ax.get_legend_handles_labels() ax.legend(handles[ : : (nChan + 1)], labels[ : : (nChan + 1)]) if title is None: title = overlayTitle.format(len(handles)) - ax.set_title(title, size=pltConfig["singleTitleSize"]) + # ax.set_title(title, size=pltConfig["singleTitleSize"]) # Increment overlay-counter and draw figure fig.objCount += 1 diff --git a/syncopy/plotting/_plot_spectral.py b/syncopy/plotting/_plot_spectral.py index 23765db83..aa0cd477f 100644 --- a/syncopy/plotting/_plot_spectral.py +++ b/syncopy/plotting/_plot_spectral.py @@ -25,7 +25,7 @@ def singlepanelplot(self, trials="all", channels="all", tapers="all", - toilim=None, foilim=None, avg_channels=True, avg_tapers=True, + toilim=None, foilim=None, avg_channels=False, avg_tapers=True, interp="spline36", cmap="plasma", vmin=None, vmax=None, title=None, grid=None, fig=None, **kwargs): """ @@ -127,13 +127,13 @@ def singlepanelplot(self, trials="all", channels="all", tapers="all", if fig.objCount == 0: if title is None: title = panelTitle - ax.set_title(title, size=pltConfig["singleTitleSize"]) + # ax.set_title(title, size=pltConfig["singleTitleSize"]) else: handles, labels = ax.get_legend_handles_labels() ax.legend(handles, labels) if title is None: title = overlayTitle.format(len(handles)) - ax.set_title(title, size=pltConfig["singleTitleSize"]) + # ax.set_title(title, size=pltConfig["singleTitleSize"]) else: @@ -164,7 +164,7 @@ def singlepanelplot(self, trials="all", channels="all", tapers="all", cbar = _setup_colorbar(fig, ax, cax, label=dataLbl.replace(" (dB)", "")) if title is None: title = panelTitle - ax.set_title(title, size=pltConfig["singleTitleSize"]) + # ax.set_title(title, size=pltConfig["singleTitleSize"]) # Increment overlay-counter and draw figure fig.objCount += 1 diff --git a/syncopy/plotting/spy_plotting.py b/syncopy/plotting/spy_plotting.py index 78120bd48..85f63a25f 100644 --- a/syncopy/plotting/spy_plotting.py +++ b/syncopy/plotting/spy_plotting.py @@ -52,8 +52,8 @@ # Global style settings for single-/multi-plots pltConfig = {"singleTitleSize": 12, - "singleLabelSize": 10, - "singleTickSize": 8, + "singleLabelSize": 14, + "singleTickSize": 12, "singleLegendSize": 10, "singleFigSize": (6.4, 4.8), "multiTitleSize": 10, From 7d6761cc5408dd420e2fd06b0dd657c71c5ba3b8 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Fri, 4 Feb 2022 17:39:46 +0100 Subject: [PATCH 013/166] WIP: First reading code ready - first stub for reading the continuous portion of an NWB file; TTL pulses need to be stored as `EventData` - code not functional yet On branch nwb Changes to be committed: modified: syncopy/io/_read_nwb.py modified: syncopy/tests/local_spy.py --- syncopy/io/_read_nwb.py | 109 +++++++++++++++++++++++++++++++++++-- syncopy/tests/local_spy.py | 3 +- 2 files changed, 105 insertions(+), 7 deletions(-) diff --git a/syncopy/io/_read_nwb.py b/syncopy/io/_read_nwb.py index ae20bf391..a9d5406bc 100644 --- a/syncopy/io/_read_nwb.py +++ b/syncopy/io/_read_nwb.py @@ -4,17 +4,19 @@ # # Builtin/3rd party package imports -from genericpath import exists +import h5py +import subprocess import numpy as np # Local imports from syncopy import __nwb__ -from syncopy.shared.errors import SPYError +from syncopy.datatype.continuous_data import AnalogData +from syncopy.shared.errors import SPYError, SPYValueError, SPYWarning from syncopy.shared.parsers import io_parser # Conditional imports if __nwb__: - from pynwb import NWBHDF5IO + import pynwb # Global consistent error message if NWB is missing nwbErrMsg = "\nSyncopy WARNING: Could not import 'pynwb'. \n" +\ @@ -27,17 +29,112 @@ __all__ = ["read_nwb"] -def read_nwb(filename): +def read_nwb(filename, memuse=3000): """ Coming soon... + + memuse : scalar + Approximate in-memory cache size (in MB) for writing data to disk """ # Abort if NWB is not installed if not __nwb__: raise SPYError(nwbErrMsg.format("read_nwb")) - nwbFilePath, nwbName = io_parser(filename, varname="filename", isfile=True, exists=True) + # Check if file exists + nwbFullName, nwbBaseName = io_parser(filename, varname="filename", isfile=True, exists=True) + + # First, perform some basal validation w/NWB + subprocess.run(["python", "-m", "pynwb.validate", nwbFullName], check=True) - nwbio = NWBHDF5IO(nwbFilePath, "r", load_namespaces=True) + nwbio = pynwb.NWBHDF5IO(nwbFullName, "r", load_namespaces=True) nwbfile = nwbio.read() + # Electrodes: nwbfile.acquisition['ElectricalSeries_1'].electrodes[:] + + # Trials: if "epochs" in nwbfile.fields.keys() + + nSamples = 0 + nChannels = 0 + chanNames = [] + tStarts = [] + sRates = [] + dTypes = [] + + hasTrials = "epochs" in nwbfile.fields.keys() + + + for acqName, acqValue in nwbfile.acquisition.items(): + + if isinstance(acqValue, pynwb.ecephys.ElectricalSeries): + + channels = acqValue.electrodes[:].location + if channels.unique().size == 1: + SPYWarning("No channel names found for {}".format(acqName)) + else: + chanNames += channels.to_list() + + dTypes.append(acqValue.data.dtype) + if acqValue.channel_conversion is not None: + dTypes.append(acqValue.channel_conversion.dtype) + + tStarts.append(acqValue.starting_time) + sRates.append(acqValue.rate) + nChannels += acqValue.data.shape[1] + nSamples = max(nSamples, acqValue.data.shape[0]) + + elif str(acqValue.__class__) == "TTLs": + pass + + else: + lgl = "supported NWB data class" + raise SPYValueError(lgl, varname=acqName, actual=str(acqValue.__class__)) + + + if hasTrials: + if all(tStarts) is None or all(sRates) is None: + lgl = "acquisition timings defined by `starting_time` and `rate`" + act = "`starting_time` or `rate` not set" + raise SPYValueError(lgl, varname="starting_time/rate", actual=act) + if np.unique(tStarts).size > 1 or np.unique(sRates).size > 1: + lgl = "acquisitions with unique `starting_time` and `rate`" + act = "`starting_time` or `rate` different across acquisitions" + raise SPYValueError(lgl, varname="starting_time/rate", actual=act) + epochs = nwbfile.epochs[:] + trl = np.zeros((epochs.shape[0], 3), dtype=np.intp) + trl[:, :2] = epochs * sRates[0] + else: + trl = np.array([[0, nSamples, 0]]) + + angData = AnalogData() + angShape = [None, None] + angShape[angData._defaultDimord.index("time")] = nSamples + angShape[angData._defaultDimord.index("channel")] = nChannels + + h5ang = h5py.File(angData.filename, mode="w") + angDset = h5ang.create_dataset("data", dtype=np.result_type(*dTypes), shape=angShape) + + # Compute actually available memory (divide by 2 since we're working with an add'l tmp array) + memuse *= 1024**2 / 2 + + for acqName, acqValue in nwbfile.acquisition.items(): + + if isinstance(acqValue, pynwb.ecephys.ElectricalSeries): + + # Given memory cap, compute how many data blocks can be grabbed per swipe + nSamp = int(memuse / (np.prod(angDset.shape[1:]) * angDset.dtype.itemsize)) + rem = int(angDset.shape[0] % nSamp) + nBlocks = [nSamp] * int(angDset.shape[0] // nSamp) + [rem] * int(rem > 0) + + # If channel-specific gains are set, load them now + if acqValue.channel_conversion is not None: + gains = acqValue.channel_conversion.data[()] + + # Write data block-wise to `angDset` (use `del` to wipe blocks from memory) + for m, M in enumerate(nBlocks): + tmp = acqValue.data[m * nSamp: m * nSamp + M, :] + if acqValue.channel_conversion is not None: + tmp *= gains + angDset[m * nSamp: m * nSamp + M, :] = tmp + del tmp + diff --git a/syncopy/tests/local_spy.py b/syncopy/tests/local_spy.py index 2d1526772..f37249ea9 100644 --- a/syncopy/tests/local_spy.py +++ b/syncopy/tests/local_spy.py @@ -26,7 +26,8 @@ # Prepare code to be executed using, e.g., iPython's `%run` magic command if __name__ == "__main__": - nwbFilePath = "/home/fuertingers/Documents/job/SyNCoPy/Data/test.nwb" + nwbFilePath = "/home/fuertingers/Documents/job/SyNCoPy/Data/tt.nwb" + # nwbFilePath = "/home/fuertingers/Documents/job/SyNCoPy/Data/test.nwb" nwbio = NWBHDF5IO(nwbFilePath, "r", load_namespaces=True) nwbfile = nwbio.read() From e9e9b28d755f2bd89f3dcdac8a77d0e4dd65a6e2 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 4 Feb 2022 18:39:08 +0100 Subject: [PATCH 014/166] WIP: Quistart Guide - explored some bootstraps themes, 'simplex' is the best so far --- doc/source/README.rst | 5 +- doc/source/conf.py | 4 +- doc/source/quickstart/quickstart.rst | 75 ++++++++-------------------- doc/source/scripts/qs_damped_harm.py | 1 + doc/source/setup.rst | 12 ++--- doc/source/user/logging.rst | 39 +++++++++++++++ 6 files changed, 72 insertions(+), 64 deletions(-) create mode 100644 doc/source/user/logging.rst diff --git a/doc/source/README.rst b/doc/source/README.rst index 5205c0141..493b799da 100644 --- a/doc/source/README.rst +++ b/doc/source/README.rst @@ -24,9 +24,8 @@ We strive to achieve the following goals: Getting Started --------------- -Our :doc:`Basic Setup Guide ` covers installation and basic usage. - -After this you might find useful examples in our :doc:`Quickstart Guide `. +- Prerequisites: :doc:`Install Syncopy ` +- Jumping right in: :doc:`Quickstart Guide ` Want to contribute or just curious how the sausage is made? Take a look at our :doc:`Developer Guide `. diff --git a/doc/source/conf.py b/doc/source/conf.py index d865aff09..04decf0b7 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -129,7 +129,9 @@ def setup(app): # Global TOC depth for "site" navbar tab. (Default: 1) # Switching to -1 shows all levels. 'globaltoc_depth': 2, - 'bootswatch_theme': "lumen", + # Currently, the supported themes are: + # - Bootstrap 3: https://bootswatch.com/3 + 'bootswatch_theme': "simplex", 'navbar_links': [ ("GitHub", "https://www.github.com/esi-neuroscience/syncopy", True), ], diff --git a/doc/source/quickstart/quickstart.rst b/doc/source/quickstart/quickstart.rst index b760c085a..f2ba193ee 100644 --- a/doc/source/quickstart/quickstart.rst +++ b/doc/source/quickstart/quickstart.rst @@ -1,35 +1,38 @@ -Quickstart with Syncopy -============================ +**************************** +A Quick Tour with Syncopy +**************************** .. currentmodule:: syncopy -Here we want to quickly explore some standard analyses for analog data (e.g. MUA or LFP measurements), and how to do these in Syncopy. Explorative coding is best done interactively by using e.g. `Jupyter `_ or `IPython `_. Note that for plotting also `matplotlib `_ has to be installed. The following topics are covered here: +Here we want to quickly explore some standard analyses for analog data (e.g. MUA or LFP measurements), and how to do these in Syncopy. Explorative coding is best done interactively by using e.g. `Jupyter `_ or `IPython `_. Note that for plotting also `matplotlib `_ has to be installed. .. contents:: Topics covered :local: +.. note:: + Installation of Syncopy itself is covered in :doc:`here `. + Preparations ------------- +============ -To start with a clean slate, we will construct a synthetic damped harmonic with additive white noise. - -.. hint:: - Further details about artifical data generatation can be found at the :ref:`synth_data` section. +To start with a clean slate, let's construct a synthetic signal consisting of a damped harmonic and additive white noise: .. literalinclude:: /scripts/qs_damped_harm.py +.. hint:: + Further details about artifical data generatation can be found at the :ref:`synth_data` section. Data Object Inspection ----------------------- +====================== -We can get some basic information about any Syncopy data set by just typing its name in an interpreter: +We can get some basic information about any Syncopy dataset by just typing its name in an interpreter: .. code-block:: python synth_data -which then gives a nicely formatted output: +which gives nicely formatted output: .. code-block:: bash @@ -54,18 +57,18 @@ which then gives a nicely formatted output: So we see that we indeed got 50 trials with 2 channels and 1000 samples each. Note that Syncopy per default **stores and writes all data on disc**, as this allows for seamless processing of larger than RAM datasets. The exact location and filename of a dataset in question is listed at the ``filename`` field. The standard location is the ``.spy`` directory created automatically in the users home directory. To change this and for more details please see :ref:`setup_env`. .. hint:: - You can access each of the shown dataset fields separately using standard Python attribute access, e.g. ``synth_data.filename`` or ``synth_data.samplerate``. + You can access each of the shown meta-information fields separately using standard Python attribute access, e.g. ``synth_data.filename`` or ``synth_data.samplerate``. - + Time-Frequency Analysis ------------------------ +======================= Syncopy groups analysis functionality into *meta-functions*, which in turn have various parameters selecting and controlling specific methods. In the case of spectral analysis the function to use is :func:`~syncopy.freqanalysis`. Here we quickly want to showcase two important methods for (time-)frequency analysis: (multi-tapered) FFT and Wavelet analysis. Multitapered Fourier Analysis -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +------------------------------ `Multitaper methods `_ allow for frequency smoothing of Fourier spectra. Syncopy implements the standard `Slepian/DPSS tapers `_ and provides a convenient parameter, the *taper smoothing frequency* ``tapsmofrq`` to control the amount of spectral smoothing in Hz. To perform a multi-tapered Fourier analysis with 3Hz spectral smoothing, we simply do: @@ -79,44 +82,9 @@ The parameter ``foilim`` controls the *frequencies of interest limits*, so in t informing us, that for this dataset a spectral smoothing of 3Hz required 5 Slepian tapers. -Excurs: Logging -""""""""""""""" - -An important feature of Syncopy fostering reproducibility is a ``log`` which gets attached to and propagated between datasets. Typing:: - - spectra.log - -Gives the following (similar) output:: - - |=== user@machine: Thu Feb 3 17:05:59 2022 ===| - - __init__: created AnalogData object - - |=== user@machine: Thu Feb 3 17:12:11 2022 ===| - - __init__: created SpectralData object - - |=== user@machine: Thu Feb 3 17:12:11 2022 ===| - - definetrial: updated trial-definition with [50 x 3] element array - - |=== user@machine: Thu Feb 3 17:12:11 2022 ===| - - write_log: computed mtmfft_cF with settings - method = mtmfft - output = pow - keeptapers = False - keeptrials = True - polyremoval = None - pad_to_length = None - foi = [ 0. 0.5 1. 1.5 2. 2.5, ..., 47.5 48. 48.5 49. 49.5 50. ] - taper = dpss - nTaper = 5 - tapsmofrq = 3 - - -We see that from the creation of the original :class:`~syncopy.AnalogData` all steps needed to compute our new :class:`~syncopy.SpectralData` got recorded. - +.. hint:: + Try typing ``spectra.log`` into your intepreter and have a look at :doc:`Trace Your Steps: History ` so learn more about Syncopy's logging features + To quickly have something for the eye we can plot the power spectrum using the generic :func:`syncopy.singlepanelplot`:: spectra.singlepanelplot() @@ -125,3 +93,4 @@ To quickly have something for the eye we can plot the power spectrum using the g :height: 250px The originally very sharp harmonic peak around 30Hz got widened to about 3Hz, for all other frequencies we have the expected flat white noise floor. + diff --git a/doc/source/scripts/qs_damped_harm.py b/doc/source/scripts/qs_damped_harm.py index 0b207fa35..3743c7c2d 100644 --- a/doc/source/scripts/qs_damped_harm.py +++ b/doc/source/scripts/qs_damped_harm.py @@ -15,6 +15,7 @@ dampening = np.linspace(1, 0.1, nSamples) signal = dampening * harm +# collect trials trials = [] for _ in range(nTrials): diff --git a/doc/source/setup.rst b/doc/source/setup.rst index 7a3573258..e9520d0f4 100644 --- a/doc/source/setup.rst +++ b/doc/source/setup.rst @@ -1,8 +1,5 @@ -Getting started with Syncopy -============================ - -Installing Syncopy ------------------- +Install Syncopy +=============== Syncopy can be installed using `conda `_: @@ -20,7 +17,7 @@ If you're working on the ESI cluster installing Syncopy is only necessary if you create your own Conda environment. Installing parallel processing engine ACME -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +-------------------------------------------- To harness the parallel processing capabilities of Syncopy it is necessary to install `ACME `_. @@ -54,7 +51,8 @@ accessed with the ``spy.`` prefix, e.g. spy.load("~/testdata.spy") - +.. _start_parallel: + Starting Up Parallel Workers ---------------------------- diff --git a/doc/source/user/logging.rst b/doc/source/user/logging.rst new file mode 100644 index 000000000..970bebe33 --- /dev/null +++ b/doc/source/user/logging.rst @@ -0,0 +1,39 @@ +.. _logging: + +Trace Your Steps: Logging +========================== + +An important feature of Syncopy fostering reproducibility is a ``log`` which gets attached to and propagated between datasets. Typing:: + + spectra.log + +Gives a output like this:: + + |=== user@machine: Thu Feb 3 17:05:59 2022 ===| + + __init__: created AnalogData object + + |=== user@machine: Thu Feb 3 17:12:11 2022 ===| + + __init__: created SpectralData object + + |=== user@machine: Thu Feb 3 17:12:11 2022 ===| + + definetrial: updated trial-definition with [50 x 3] element array + + |=== user@machine: Thu Feb 3 17:12:11 2022 ===| + + write_log: computed mtmfft_cF with settings + method = mtmfft + output = pow + keeptapers = False + keeptrials = True + polyremoval = None + pad_to_length = None + foi = [ 0. 0.5 1. 1.5 2. 2.5, ..., 47.5 48. 48.5 49. 49.5 50. ] + taper = dpss + nTaper = 5 + tapsmofrq = 3 + + +We see that from the creation of the original :class:`~syncopy.AnalogData` all steps needed to compute our new :class:`~syncopy.SpectralData` got recorded. From 1429dcc2fa6c040a44f1e20402c859b6be4b2388 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Mon, 7 Feb 2022 14:30:15 +0100 Subject: [PATCH 015/166] WIP: Time-series read-in done, events in progress - converting NWB's `ElectricalSeries` to `spy.AnalogData` works, creating `EventData` from TTL pulses is in progress On branch nwb Changes to be committed: modified: syncopy/io/_read_nwb.py modified: syncopy/tests/local_spy.py --- syncopy/io/_read_nwb.py | 119 ++++++++++++++++++++++++++++--------- syncopy/tests/local_spy.py | 2 + 2 files changed, 94 insertions(+), 27 deletions(-) diff --git a/syncopy/io/_read_nwb.py b/syncopy/io/_read_nwb.py index a9d5406bc..aa2034e51 100644 --- a/syncopy/io/_read_nwb.py +++ b/syncopy/io/_read_nwb.py @@ -4,6 +4,7 @@ # # Builtin/3rd party package imports +import os import h5py import subprocess import numpy as np @@ -12,7 +13,7 @@ from syncopy import __nwb__ from syncopy.datatype.continuous_data import AnalogData from syncopy.shared.errors import SPYError, SPYValueError, SPYWarning -from syncopy.shared.parsers import io_parser +from syncopy.shared.parsers import io_parser, scalar_parser # Conditional imports if __nwb__: @@ -42,30 +43,48 @@ def read_nwb(filename, memuse=3000): raise SPYError(nwbErrMsg.format("read_nwb")) # Check if file exists - nwbFullName, nwbBaseName = io_parser(filename, varname="filename", isfile=True, exists=True) + nwbPath, nwbBaseName = io_parser(filename, varname="filename", isfile=True, exists=True) + nwbFullName = os.path.join(nwbPath, nwbBaseName) + + # # Ensure `memuse` makes sense` + # try: + # scalar_parser(memuse, varname="memuse", lims=[0, np.inf]) + # except Exception as exc: + # raise exc # First, perform some basal validation w/NWB - subprocess.run(["python", "-m", "pynwb.validate", nwbFullName], check=True) + try: + subprocess.run(["python", "-m", "pynwb.validate", nwbFullName], check=True) + except subprocess.CalledProcessError as exc: + err = "NWB file validation failed. Original error message: {}" + raise SPYError(err.format(str(exc))) + # Load NWB meta data from disk nwbio = pynwb.NWBHDF5IO(nwbFullName, "r", load_namespaces=True) nwbfile = nwbio.read() # Electrodes: nwbfile.acquisition['ElectricalSeries_1'].electrodes[:] - # Trials: if "epochs" in nwbfile.fields.keys() + # Allocate lists for storing temporary NWB info: IMPORTANT use lists to preserve + # order of data chunks/channels nSamples = 0 nChannels = 0 chanNames = [] tStarts = [] sRates = [] dTypes = [] + angSeries = [] + ttlVals = [] + ttlChans = [] + # If the file contains `epochs`, use it to infer trial information hasTrials = "epochs" in nwbfile.fields.keys() - + # Access all (supported) `acquisition` fields in the file for acqName, acqValue in nwbfile.acquisition.items(): + # Actual extracellular analog time-series data if isinstance(acqValue, pynwb.ecephys.ElectricalSeries): channels = acqValue.electrodes[:].location @@ -82,15 +101,27 @@ def read_nwb(filename, memuse=3000): sRates.append(acqValue.rate) nChannels += acqValue.data.shape[1] nSamples = max(nSamples, acqValue.data.shape[0]) + angSeries.append(acqValue) - elif str(acqValue.__class__) == "TTLs": - pass + # TTL event pulse data + elif "abc.TTLs" in str(acqValue.__class__): + + if acqValue.name == "TTL_PulseValues": + ttlVals.append(acqValue) + elif acqValue.name == "TTL_ChannelStates": + ttlChans.append(acqValue) + else: + lgl = "TTL data exported via `esi-oephys2nwb`" + act = "unformatted TTL data '{}'" + raise SPYValueError(lgl, varname=acqName, actual=act.format(acqValue.description)) + # Unsupported else: lgl = "supported NWB data class" raise SPYValueError(lgl, varname=acqName, actual=str(acqValue.__class__)) - + # If the NWB data is split up in "trials" (i.e., epochs), ensure things don't + # get too wild (uniform sampling rates and timing offsets) if hasTrials: if all(tStarts) is None or all(sRates) is None: lgl = "acquisition timings defined by `starting_time` and `rate`" @@ -102,39 +133,73 @@ def read_nwb(filename, memuse=3000): raise SPYValueError(lgl, varname="starting_time/rate", actual=act) epochs = nwbfile.epochs[:] trl = np.zeros((epochs.shape[0], 3), dtype=np.intp) - trl[:, :2] = epochs * sRates[0] + trl[:, :2] = (epochs - tStarts[0]) * sRates[0] else: trl = np.array([[0, nSamples, 0]]) - angData = AnalogData() + # If TTL data was found, ensure we have exactly one set of values and associated + # channel markers + if max(len(ttlVals), len(ttlChans)) > min(len(ttlVals), len(ttlChans)): + lgl = "TTL pulse values and channel markers" + act = "pulses: {}, channels: {}".format(str(ttlVals), str(ttlChans)) + raise SPYValueError(lgl, varname=ttlVals[0].name, actual=act) + if len(ttlVals) > 1: + lgl = "one set of TTL pulses" + act = "{} TTL data sets".format(len(ttlVals)) + raise SPYValueError(lgl, varname=ttlVals[0].name, actual=act) + + if len(ttlVals) > 0 and hasTrials: + import ipdb; ipdb.set_trace() + + # Allocate `AnalogData` object and use generated HDF5 file-name to manually + # allocate a target dataset for reading the NWB data + angData = AnalogData(dimord=AnalogData._defaultDimord) angShape = [None, None] angShape[angData._defaultDimord.index("time")] = nSamples angShape[angData._defaultDimord.index("channel")] = nChannels - h5ang = h5py.File(angData.filename, mode="w") angDset = h5ang.create_dataset("data", dtype=np.result_type(*dTypes), shape=angShape) # Compute actually available memory (divide by 2 since we're working with an add'l tmp array) memuse *= 1024**2 / 2 + chanCounter = 0 - for acqName, acqValue in nwbfile.acquisition.items(): + # Process analog time series data and save stuff block by block (if necessary) + for acqValue in angSeries: - if isinstance(acqValue, pynwb.ecephys.ElectricalSeries): + # Given memory cap, compute how many data blocks can be grabbed per swipe + nSamp = int(memuse / (np.prod(angDset.shape[1:]) * angDset.dtype.itemsize)) + rem = int(angDset.shape[0] % nSamp) + nBlocks = [nSamp] * int(angDset.shape[0] // nSamp) + [rem] * int(rem > 0) - # Given memory cap, compute how many data blocks can be grabbed per swipe - nSamp = int(memuse / (np.prod(angDset.shape[1:]) * angDset.dtype.itemsize)) - rem = int(angDset.shape[0] % nSamp) - nBlocks = [nSamp] * int(angDset.shape[0] // nSamp) + [rem] * int(rem > 0) + # If channel-specific gains are set, load them now + if acqValue.channel_conversion is not None: + gains = acqValue.channel_conversion[()] - # If channel-specific gains are set, load them now + # Write data block-wise to `angDset` (use `del` to wipe blocks from memory) + # Use 'unsafe' casting to allow `tmp` array conversion int -> float + endChan = chanCounter + acqValue.data.shape[1] + for m, M in enumerate(nBlocks): + tmp = acqValue.data[m * nSamp: m * nSamp + M, :] if acqValue.channel_conversion is not None: - gains = acqValue.channel_conversion.data[()] - - # Write data block-wise to `angDset` (use `del` to wipe blocks from memory) - for m, M in enumerate(nBlocks): - tmp = acqValue.data[m * nSamp: m * nSamp + M, :] - if acqValue.channel_conversion is not None: - tmp *= gains - angDset[m * nSamp: m * nSamp + M, :] = tmp - del tmp + np.multiply(tmp, gains, out=tmp, casting="unsafe") + angDset[m * nSamp: m * nSamp + M, chanCounter : endChan] = tmp + del tmp + + # Update channel counter for next `acqValue`` + chanCounter += acqValue.data.shape[1] + + # Finalize angData + angData.data = angDset + angData.channel = chanNames + angData.samplerate = sRates[0] + angData.trialdefinition = trl + + # # Write log-entry + # msg = "Read files v. {ver:s} ".format(ver=jsonDict["_version"]) + # msg += "{hdf:s}\n\t" + (len(msg) + len(thisMethod) + 2) * " " + "{json:s}" + # out.log = msg.format(hdf=hdfFile, json=jsonFile) + + + import ipdb; ipdb.set_trace() diff --git a/syncopy/tests/local_spy.py b/syncopy/tests/local_spy.py index f37249ea9..c231c29c0 100644 --- a/syncopy/tests/local_spy.py +++ b/syncopy/tests/local_spy.py @@ -29,6 +29,8 @@ nwbFilePath = "/home/fuertingers/Documents/job/SyNCoPy/Data/tt.nwb" # nwbFilePath = "/home/fuertingers/Documents/job/SyNCoPy/Data/test.nwb" + spy.read_nwb(nwbFilePath) + nwbio = NWBHDF5IO(nwbFilePath, "r", load_namespaces=True) nwbfile = nwbio.read() From f39ffdfb57701f528ed759b2dc9ca5f2c02f9635 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Mon, 7 Feb 2022 14:40:50 +0100 Subject: [PATCH 016/166] FIX: Use np.number correctly - checking for `np.number` requires type comparisons, use `np.issubdtype` instead of `isinstance` On branch dev Changes to be committed: modified: syncopy/datatype/base_data.py modified: syncopy/datatype/methods/arithmetic.py modified: syncopy/datatype/methods/definetrial.py modified: syncopy/shared/input_validators.py modified: syncopy/shared/parsers.py modified: syncopy/specest/compRoutines.py modified: syncopy/specest/freqanalysis.py --- syncopy/datatype/base_data.py | 2 +- syncopy/datatype/methods/arithmetic.py | 2 +- syncopy/datatype/methods/definetrial.py | 2 +- syncopy/shared/input_validators.py | 9 ++++++--- syncopy/shared/parsers.py | 2 +- syncopy/specest/compRoutines.py | 2 +- syncopy/specest/freqanalysis.py | 4 ++-- 7 files changed, 13 insertions(+), 10 deletions(-) diff --git a/syncopy/datatype/base_data.py b/syncopy/datatype/base_data.py index 088e2026d..f1f5701bf 100644 --- a/syncopy/datatype/base_data.py +++ b/syncopy/datatype/base_data.py @@ -1176,7 +1176,7 @@ def __iter__(self): return self._iterobj def __getitem__(self, idx): - if isinstance(idx, np.number): + if np.issubdtype(type(idx), np.number): try: scalar_parser(idx, varname="idx", ntype="int_like", lims=[0, self._iterlen - 1]) diff --git a/syncopy/datatype/methods/arithmetic.py b/syncopy/datatype/methods/arithmetic.py index de8dd64e0..84810c9d1 100644 --- a/syncopy/datatype/methods/arithmetic.py +++ b/syncopy/datatype/methods/arithmetic.py @@ -147,7 +147,7 @@ def _parse_input(obj1, obj2, operator): # Depending on the what is thrown at `baseObj` perform more or less extensive parsing # First up: operand is a scalar - if isinstance(operand, np.number): + if np.issubdtype(type(operand), np.number): # Don't allow `np.inf` manipulations and catch zero-divisions if np.isinf(operand): diff --git a/syncopy/datatype/methods/definetrial.py b/syncopy/datatype/methods/definetrial.py index 257d2bd2e..fa5489790 100644 --- a/syncopy/datatype/methods/definetrial.py +++ b/syncopy/datatype/methods/definetrial.py @@ -195,7 +195,7 @@ def definetrial(obj, trialdefinition=None, pre=None, post=None, start=None, "stop": {"var": stop, "hasnan": None, "ntype": "int_like", "fillvalue": np.nan}} for vname, opts in vdict.items(): if opts["var"] is not None: - if isinstance(opts["var"], np.number): + if np.issubdtype(type(opts["var"]), np.number): try: scalar_parser(opts["var"], varname=vname, ntype=opts["ntype"], lims=[-np.inf, np.inf]) diff --git a/syncopy/shared/input_validators.py b/syncopy/shared/input_validators.py index 2d63f8d1f..bc185ddd1 100644 --- a/syncopy/shared/input_validators.py +++ b/syncopy/shared/input_validators.py @@ -7,6 +7,7 @@ # Builtin/3rd party package imports import numpy as np +import numbers from syncopy.shared.errors import SPYValueError, SPYWarning, SPYInfo from syncopy.shared.parsers import scalar_parser, array_parser @@ -20,10 +21,12 @@ def validate_padding(pad_to_length, lenTrials): """ # supported padding options not_valid = False - if not isinstance(pad_to_length, (np.number, str, type(None))): + if not isinstance(pad_to_length, (numbers.Number, str, type(None))): not_valid = True elif isinstance(pad_to_length, str) and pad_to_length not in availablePaddingOpt: not_valid = True + if isinstance(pad_to_length, bool): # bool is an int subclass, check for it separately... + not_valid = True if not_valid: lgl = "`None`, 'nextpow2' or an integer like number" actual = f"{pad_to_length}" @@ -31,13 +34,13 @@ def validate_padding(pad_to_length, lenTrials): # here we check for equal lengths trials in case of no user specified absolute padding length # we do a rough 'maxlen' padding, nextpow2 will be overruled in this case - if lenTrials.min() != lenTrials.max() and not isinstance(pad_to_length, np.number): + if lenTrials.min() != lenTrials.max() and not isinstance(pad_to_length, numbers.Number): abs_pad = int(lenTrials.max()) msg = f"Unequal trial lengths present, automatic padding to {abs_pad} samples" SPYWarning(msg) # zero padding of ALL trials the same way - if isinstance(pad_to_length, np.number): + if isinstance(pad_to_length, numbers.Number): scalar_parser(pad_to_length, varname='pad_to_length', diff --git a/syncopy/shared/parsers.py b/syncopy/shared/parsers.py index 6ef5ffdce..73f642b8f 100644 --- a/syncopy/shared/parsers.py +++ b/syncopy/shared/parsers.py @@ -191,7 +191,7 @@ def scalar_parser(var, varname="", ntype=None, lims=None): """ # Make sure `var` is a scalar-like number - if not isinstance(var, np.number): + if not np.issubdtype(type(var), np.number): raise SPYTypeError(var, varname=varname, expected="scalar") # If required, parse type ("int_like" is a bit of a special case here...) diff --git a/syncopy/specest/compRoutines.py b/syncopy/specest/compRoutines.py index d05166d84..3271155e4 100644 --- a/syncopy/specest/compRoutines.py +++ b/syncopy/specest/compRoutines.py @@ -841,7 +841,7 @@ def _make_trialdef(cfg, trialdefinition, samplerate): # If `toi` was a percentage, some cumsum/winSize algebra is required # Note: if `toi` was "all", simply use provided `trialdefinition` and `samplerate` - elif isinstance(toi, np.number): + elif np.issubdtype(type(toi), np.number): mKw = cfg['method_kwargs'] winSize = mKw["nperseg"] - mKw["noverlap"] trialdefinitionLens = np.ceil(np.diff(trialdefinition[:, :2]) / winSize) diff --git a/syncopy/specest/freqanalysis.py b/syncopy/specest/freqanalysis.py index 93b02774c..875e9e444 100644 --- a/syncopy/specest/freqanalysis.py +++ b/syncopy/specest/freqanalysis.py @@ -382,7 +382,7 @@ def freqanalysis(data, method='mtmfft', output='fourier', if method in ["wavelet", "superlet"]: valid = True - if isinstance(toi, np.number): + if np.issubdtype(type(toi), np.number): valid = False elif isinstance(toi, str): @@ -534,7 +534,7 @@ def freqanalysis(data, method='mtmfft', output='fourier', equidistant = True overlap = np.inf - elif isinstance(toi, np.number): + elif np.issubdtype(type(toi), np.number): try: scalar_parser(toi, varname="toi", lims=[0, 1]) except Exception as exc: From 1733073ecae16c139db503660b8f3c134fcd4e8a Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Mon, 7 Feb 2022 15:25:39 +0100 Subject: [PATCH 017/166] WIP: Wrapped up EventData construction - use TTL pulse data to construct `EventData` object On branch nwb Changes to be committed: modified: syncopy/io/_read_nwb.py modified: syncopy/tests/local_spy.py --- syncopy/io/_read_nwb.py | 34 ++++++++++++++++++++++++---------- syncopy/tests/local_spy.py | 2 +- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/syncopy/io/_read_nwb.py b/syncopy/io/_read_nwb.py index aa2034e51..7641d98ed 100644 --- a/syncopy/io/_read_nwb.py +++ b/syncopy/io/_read_nwb.py @@ -12,6 +12,7 @@ # Local imports from syncopy import __nwb__ from syncopy.datatype.continuous_data import AnalogData +from syncopy.datatype.discrete_data import EventData from syncopy.shared.errors import SPYError, SPYValueError, SPYWarning from syncopy.shared.parsers import io_parser, scalar_parser @@ -46,11 +47,11 @@ def read_nwb(filename, memuse=3000): nwbPath, nwbBaseName = io_parser(filename, varname="filename", isfile=True, exists=True) nwbFullName = os.path.join(nwbPath, nwbBaseName) - # # Ensure `memuse` makes sense` - # try: - # scalar_parser(memuse, varname="memuse", lims=[0, np.inf]) - # except Exception as exc: - # raise exc + # Ensure `memuse` makes sense` + try: + scalar_parser(memuse, varname="memuse", lims=[0, np.inf]) + except Exception as exc: + raise exc # First, perform some basal validation w/NWB try: @@ -63,9 +64,6 @@ def read_nwb(filename, memuse=3000): nwbio = pynwb.NWBHDF5IO(nwbFullName, "r", load_namespaces=True) nwbfile = nwbio.read() - # Electrodes: nwbfile.acquisition['ElectricalSeries_1'].electrodes[:] - - # Allocate lists for storing temporary NWB info: IMPORTANT use lists to preserve # order of data chunks/channels nSamples = 0 @@ -77,6 +75,7 @@ def read_nwb(filename, memuse=3000): angSeries = [] ttlVals = [] ttlChans = [] + ttlDtypes = [] # If the file contains `epochs`, use it to infer trial information hasTrials = "epochs" in nwbfile.fields.keys() @@ -115,6 +114,9 @@ def read_nwb(filename, memuse=3000): act = "unformatted TTL data '{}'" raise SPYValueError(lgl, varname=acqName, actual=act.format(acqValue.description)) + ttlDtypes.append(acqValue.data.dtype) + ttlDtypes.append(acqValue.timestamps.dtype) + # Unsupported else: lgl = "supported NWB data class" @@ -148,8 +150,19 @@ def read_nwb(filename, memuse=3000): act = "{} TTL data sets".format(len(ttlVals)) raise SPYValueError(lgl, varname=ttlVals[0].name, actual=act) - if len(ttlVals) > 0 and hasTrials: - import ipdb; ipdb.set_trace() + # Use provided TTL data to initialize `EventData` object + if len(ttlVals) > 0: + evtData = EventData(dimord=EventData._defaultDimord) + h5evt = h5py.File(evtData.filename, mode="w") + evtDset = h5evt.create_dataset("data", dtype=np.result_type(*ttlDtypes), + shape=(ttlVals[0].data.size, 3)) + evtDset[:, 0] = ((ttlChans[0].timestamps[()] - tStarts[0]) / ttlChans[0].timestamps__resolution).astype(np.intp) + evtDset[:, 1] = ttlVals[0].data[()] + evtDset[:, 2] = ttlChans[0].data[()] + evtData.data = evtDset + evtData.samplerate = 1 / ttlChans[0].timestamps__resolution + if hasTrials: + evtData.trialdefinition = trl # Allocate `AnalogData` object and use generated HDF5 file-name to manually # allocate a target dataset for reading the NWB data @@ -165,6 +178,7 @@ def read_nwb(filename, memuse=3000): chanCounter = 0 # Process analog time series data and save stuff block by block (if necessary) + # FIXME: >>>>>>>>>>>>>>>> Use tqdm here for acqValue in angSeries: # Given memory cap, compute how many data blocks can be grabbed per swipe diff --git a/syncopy/tests/local_spy.py b/syncopy/tests/local_spy.py index c231c29c0..30c26229c 100644 --- a/syncopy/tests/local_spy.py +++ b/syncopy/tests/local_spy.py @@ -26,7 +26,7 @@ # Prepare code to be executed using, e.g., iPython's `%run` magic command if __name__ == "__main__": - nwbFilePath = "/home/fuertingers/Documents/job/SyNCoPy/Data/tt.nwb" + nwbFilePath = "/home/fuertingers/Documents/job/SyNCoPy/Data/tt2.nwb" # nwbFilePath = "/home/fuertingers/Documents/job/SyNCoPy/Data/test.nwb" spy.read_nwb(nwbFilePath) From b28f92ecf89024eb6aa81a5421202a1c338af354 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 7 Feb 2022 16:55:21 +0100 Subject: [PATCH 018/166] WIP: Quickstart Guide - added Wavelet analysis Changes to be committed: renamed: doc/source/scripts/qs_damped_harm.py -> doc/source/quickstart/damped_harm.py modified: doc/source/quickstart/mtmfft_spec.png modified: doc/source/quickstart/quickstart.rst new file: doc/source/quickstart/wavelet_spec.png modified: doc/source/user/logging.rst modified: doc/source/user/synth_data.rst modified: syncopy/plotting/spy_plotting.py --- .../damped_harm.py} | 8 ++-- doc/source/quickstart/mtmfft_spec.png | Bin 17610 -> 33650 bytes doc/source/quickstart/quickstart.rst | 37 +++++++++++++++--- doc/source/quickstart/wavelet_spec.png | Bin 0 -> 93344 bytes doc/source/user/logging.rst | 10 ++--- doc/source/user/synth_data.rst | 2 +- syncopy/plotting/spy_plotting.py | 10 ++--- 7 files changed, 47 insertions(+), 20 deletions(-) rename doc/source/{scripts/qs_damped_harm.py => quickstart/damped_harm.py} (69%) create mode 100644 doc/source/quickstart/wavelet_spec.png diff --git a/doc/source/scripts/qs_damped_harm.py b/doc/source/quickstart/damped_harm.py similarity index 69% rename from doc/source/scripts/qs_damped_harm.py rename to doc/source/quickstart/damped_harm.py index 3743c7c2d..44e94ed70 100644 --- a/doc/source/scripts/qs_damped_harm.py +++ b/doc/source/quickstart/damped_harm.py @@ -19,9 +19,11 @@ trials = [] for _ in range(nTrials): - white_noise = np.random.randn(nSamples, nChannels) - trial = np.tile(signal, (2, 1)).T + white_noise + # start with the noise + trial = np.random.randn(nSamples, nChannels) + # now add the damped harmonic on the 1st channel + trial[:, 0] += signal trials.append(trial) # instantiate Syncopy data object -synth_data = spy.AnalogData(trials, samplerate=samplerate) +data = spy.AnalogData(trials, samplerate=samplerate) diff --git a/doc/source/quickstart/mtmfft_spec.png b/doc/source/quickstart/mtmfft_spec.png index c9c121d59dd60ab8c4789db20a9a4f7775d04838..1bb767fbfc547b3be793216d4ae9afbcfdc602cf 100644 GIT binary patch literal 33650 zcmc$`WmHyQ)IIvpASoc7A|OgicZwk0-Q8W%-6EiLi*!nNcXv0^jdb0OzxRLd9q+gM z=`wT(4?O3bz4uyk%{kY?P+93us4oa!Kp+rQaWN5j2n5;(0)b*jga@y1?VUJ)e>fdK zsyZsz7(2S?*&9Km^c-z1Z5%Dl^vRr!>>bQ(teNRq=$UEBOdTC<9k>`6tp4vGptrF% zVHivk837kTvK3PU7em*3{tK1IpKAtz_^pVG2r9a!94@)2em9xEK0Rh;{>!G@L9ZJg zDu*D&tVEuiwj|={j$0kyNEmu&%^)|o(CsK|~ zI?2(*^J+TZI{w{}%aKaPnKTfC;K7QEi~D?m#Ky*Uc(6S$4G%RbDJg-^v!41ZA{7$E zzqRQgIVFV_D&zUhL2t1C82m;O`FE>`Xo&3phfmlTpD)n)YoDcNe^y_l%h=d$7w40C5^iZ+O*ZzVl#f*MSSSe%-jto@jmp$m(7t=uZpj9tTQR3vW3AOPQ<|5TQCpj^xTyJonVE)x!DQ~YQIUGpJ6MzV z0T^pD#xAEd@$-4r&X}ad(G_pw2ywb&Fps$#TplRPHTl?sObQm}Zlw`QkE+kx1~yc^ zrwsdw0-)oYhyL@HwzM0jnzHFJtY#lL6ZnC zUdpj(&0N@~i!lDm$&uI4*zbe*C6&*t@HZ3{MWmz|GT7^?#L&oR$ zy-JFT0gnM7u)dpG&xnT+&VA}!q>=Nmar)q*^|Gh1;n3qshpoVz3&t@>zp{U5=%Q%& zV-ncQ7s$vRo&@A}Z3sTi2}8z~Y#J>!OTUefjf{-GfB%khZCJK&$im9%_JAoAGFDXA z@lpPux5A0)XSbBZ`t{f+`2?nAky8gcma01V=1h#A0%~gbb#--Xe35$iu^;ix%*-Zd zX6~96^~)jO5)+XG#Kn=^+}ys$#oabO2B8vOuc3S6U1Plx3Sl&}o?)VfrT@f8-`ATO z-*vhiO;4b%_2zC)62zRWs;U@xE4bVm-w!=PB`95RpJa9$l9Mm{Go@FscFlir@7r!# zN31UPrm(TGueGGLTfBI$79FCtkENufB-8mkThFnDLd2#PkMz4;4FryvnWWDbYcPJ! zffSpqG{d5#q_mu`q*=^mrv171ezriV@GTu3@rlYYina|~^3btS2J}$cqF{Op<)4HE zyqMV7CNCuNPSfedl30#*g4X3l*1(}d+`r4hW5s!SAw@;BEK(HQ+zW-5%_SvY{rsTi z6%_jV`_Jl&z_}kBSc31qe_uFaVy~@D^6}$GFB|^RKRz=?;3))eNkc4uzJuXVOiWA! zM^dD0k?zAlP2IC?WywYec5iZW($#vS1owc!6s!n2aNyU(d}b^W?ksV`7*%A#+dc?d zP?N;=_aXeF2?*JeC6OXwM%s%PFY=0tvMVdU4-QJcd;h-4{5U);>@7&6i5#gSt%f-8 zi00} zz*z$i;Fpv-m!l)zDZQ>v%sNU86j6Wz8#O;oWCUv9N}q)BRzO=YGFx(FOblkJ$miqi z@Gkw+`=X4DRtt)cBO^N?a{vnD=jENe7`<)q^p0=fKPMz0U`!qYJH}=;-?=rEObKZm zEz+p@nw9lh=7rnc<)OAz#*za67ik2w7O$omoQwK;t`lZ4`KHL1?nyk3gR4H|J0)R1_o15QKQ+cx&&>ic#^fLk<|g3-Q3)4{`uoZW&h*H4^;E^AosfICgz%=qQMGF!UzWkQVMzM zSlHNqg|m^RszEGbVzhGA5R#k?f8wX2sVONfjRtZ&8{nCy=3o-51$<*_1MI+3&acMC z6!76=ydM@GUam@OXky|%JfvfS;r;pZ=TVkO5Mo8jg}FvUrdhjf{S{yJGKscm12B9q zN8&Z|IS*Ie3l*TfR&0(c&U3dlZnV7Kjpei=BnU+q1FU-k^ELpZT|%KDJR5Qaag>NL zBNGyVMS=uk2)UZ=j*5q&4)^#s)MQLTy?CkpU*#Dm8br~(8rF9y6(kpN6@&b{fI|bx zcN#KVKA71_DtrO*c5!F{P|Zk=I9uELA)<*a`ad&1e((_r`0;P~Jbs)Avvf4W0^^-$z|DZU$YzT7lfA36MSj2B#h4+bp!hYTO z27)bwxHh(!xdrJFg*(O*FHfSLcpHF-{@()fNPL1a2BDlKA|`keMVT%)x3=UJ6|s*d zPG~?1wzs$2ri+UGw*cV_2+C2!@+Jq2Gs0}c8|1g5)X77l07UZh^9_xRBKtGs{#((| zAw8x)5v2ZD=L=NWnEBy+)$>&~G$icom;ktg00_LR4C=%_!UBZhC1=utQ#U0XJNviA zrZy4c_j$pXuUCtd--8UizxROH+uOgTr>A0P$4wpznzKYZMna1*{I~Z3R4_Ye5K0(V z^7rrFy?*~5S!$wpcsM63tKC6OON$r|4la#yGA$lNT6Ah^WR4klMmc~EPAve)uwT8p zX?hTdhtZDz0cy?e%8$fEaXY*6nUOeptrr4DMt{B?y-)e?`5At(sN z%E}4>1?l{MLmVv*a>m{X>q2)e3C}16b_dp1C70#|Ub8mynZ_lYsO7)}KGo z$;tawhP+9=^tvEBA!#>_AdM)ZL3nt1@qx%mi=zQC>~9`-pk3m0JV^Kc{Wlts8lO@c}-+?cR z)GCZy7ZQhz!4_|uO~`9&OM?#qb`@+~TySV0sxMPCj-}o`*x9_Hp{0Gp$~v{La_r0; zEji);!-Tj4&~d;~GRw=O!DVgJzXEF6?_E=MdYdL4#%F|fTmH`3`DACo>Yl#E2C4?C=wEaQgfROo^_az=WMJr#|I-C2^r^}X z()uCh5UHheAB}7PS7qOx;R}{-$-yU9xDpvp5l8kJ{(}Dlu?9QgA5gHYb|Rpf{Ld4d z9CS;e$)S?vq3DJK2tY*s55hj!WzNGL@F~asKd+50@*;3h!8-UX#ptuj9A;v?uKb6C zf({Mg0KbxO`YCnu0Xk~_pI4>z5%-@2rXc-1U=70m)==pP?(?>8AN3cSY{+Qrzh{*? z(QNg%FvMp@an=SbF~W@g5!m^(na2SEyGZL=)qod*7LuF@Y1N}4NFv8n&dARU= zJDvM=To}90BvVsUmUF-1K#}qEe8{j?P*j8!07(cqgeol#V3p8cv_bu#;c@WU5Kf1Z zj01H!;G=-{A;|3Uad|e&S;m7qz*Tqv7Y8?iUH{&A#Lmy3J~%iixMJK9gu1!C?f#e+ z8!HS1uE4*i{^-yk{r#oBpYp4t^yg@b@`3evuTo;z7enb=`<~5u5%1d_KK*~fhxX}G zQBLmVEf$a`WEMw9P4K?|8;WjLNU9!HqZ_3Z7 z!ONuzzz`fG3L^2b4z(lDk}l&WV|z!NOV*iyJ?2u61`Nl6qYCZ;wXI=~W>rqNQ( zHrlITX`e%pTo-B;XfrVhiJt`pVc)-FgGdG?@Xt2?(=*|H@2{+>88K!4=ke~CR8f@S z`(k8a@&9NPYDp8y0F1T?bC+n097^3p1X>YS*n?L(a&m2+S4-Pw8|%Y=U*X`K9ol#| zj9efiBMa9TPOo5=OqpN&eQ?ltrsZ{@c)L{vzI;|TCHs|*J8wDzs9{V$n0J1(sGOsO z0jCN_3n?loF$T-i<{uau`Z(j1e&Gr-;k==c?b{-hHefg)YO!%}1B$+M z0cZg<5XNBtK`L3UG&hYnJ#+ujT}be`2NgZhx&ezi7hoGAZ_U2=Br)Klz=MVN_4Rpr zSy!nivRcH=jDW2dmyqb#b%b9(dHE|pKNRc^NYA@_Z$xsDH~ALM_A4udH@8-ynja{* zXtz&d$3=~$8Bas^vD)oD$X|bayAgbX?Fr175X55VA24DeNGm;pLJ?b>=;R`%u$8Ei zW@a>i$Z7LWutWxvdd*e{&svo^xB-$2D`RmrZov`ngQbsJ=_rN@kOzAdVhd;;pSRa> z)79<08%H-95_tf}2(c?=05c?FjKk<3Kw+0b%7BWKbpa>yT11`O5rv@hV=yyJp4*3L zp^#BTRY)yJErF#U7(yW!W{0EVAye)3uVXPRGL+_95nsyxvyr;Xa2noQ@w46`D+lg{XgE{-4|tN2k_so zp)WekXnE3ueLOp}Pmc#1gBYpY5FU`=;|xpq!3AE{85;Q|m*B&^3aInO#yKMcfOGZ( zxc75&eYUqucbDOaq)(3^vd`K5K@b%(F+M(9Yxi?xfX!x^Xi)h1L3|`;%&LN0;71K7 zLn8f@6X9Qr^un?pw^%Q6HJ=1BKxAcQ1w=*R|ARuHfq>7Tz=+NefRRN?oXS;CvJ9{a zsT^Jn8s~L@9|2@*dmF}f#q+A%XaI*rN_fU;(3qK!-Fj1F*3w|Jh6TIZkY0X9BRO9?pQa&*ybnksci#z2-7g2F|nAxTA8L&z`io^LN^ zF>OIIQc_Ybg7h~{>IP!5x33Qb0;9z=ZRtJ$OuZ`geBeufx(i6}v!pL7`fj8TPAhsE zO+~~K0_B35K?}&oJE&qPN1N-nG&GsOUO^!B7(MpT)lymc3rMx=QQfwOH0DcChj{3m zd0(ry`ltsT?LY@B^cY4&WGIFptI4wY*1`z-goV8@x3B=L5CUpDV8wu^eqU+@?-MO1 zUbBHd+*%G9H=Ysy8WrJyxh{nBSF4qile3Kv>_v#4X`~O6t@Y8lDM*p8=;wWiOz-(!d#fC4U+W_<3g} zg#6>w2H&_x!=kgVF`( z2h7Pu#|bGSJ`awBU`+4>u*Jmm`aeVR>Ka1JlS~}KFkaXCalJU|Cba1UPLY0>7mGG+P6pH@Lz-QRm3>YZ4*&+o z0Fa(-CQxa|oQ~1IDHSS%tc}wp(MDtvi?pIAS)7lUfZK>gshra~;M-cABPPB-y-`l{ zuAx}WeG6SwB^(7rzpWRNDq!2NIlHEo(-1(?bh@d@()8{k` z!I`!F8cIFslSl3A){3Djog!XEfi^;e?y)*0{iz z0USIyIQaKOh*8Z`(nB-|aHL`fSocu0eIYrQ5wSryYHNdYDb@ zzmM$Grr-w(`lkkS)jh_{Vx(sx22CRp!EDE2Sb#t!7b4sFxk}=?3UzjVKITMRF&ELL z-v(e_O|1_ouV>U1;g_D|zS&1E)?}kj`VrrK-J~s4kDX zh2ac~-^C;+TLoL!)#Ny|PL`ufFj7(xtK&knp{Jiy5CicR(-ufcKtv24ZZ8fN>zEUQ zv4l{@8-nY5hr_6V5j%M?5fd?(qPt042XSlK~Qmz9fIb7I!9*Uo&z;{0$9HN$*LeQczL8UXMh)?uC88&&i_tn#_J9n2A!oA#}}EhHkap zbKI%bq(J_r&aD|u2Gy_9OO-=2&htz9Qph$9%T{8F4%NiO`7gTh1 z`-2pUH$6DGeDM+{SLG$m11;58na!9uHUXEhWl!v$)`eyS5+AbW-14yEkM26^TsmA{ zy0*CeP9%+I8=GajX9zBKPVGco8aiA~T%;Ok6@pcgU1)vwkoV z#F6mu>eTeKK3lTCgS^*H{cr_0mnsKEn302i0yAaU3g6TqfTinoDRJ6_qXxNB41cHV zqFA?mXVZH}4!GKM0Kdl=3*geA7*hZcQO0Lr2WD5&N1262Z%lMlYU& zu3e^CKa6}lpU=Codd>+`lZEMvJL%#RNyr`pY6|F`^bQPcgzMbDW@x_FXGG^LXmY6y z-?)&r{+(xryafw$<@GjkF81g3kirmX_K|EcWB*(#`KblSyrJaY>C(s_pLy@r!6-i_ z0{WY@+nYQ7BZs>pajKYgmgU_s?E0ajbrl)5^Q~rSX*Lcs>-a&bGJt-eW03u0Y0(+I ziJwv_MJ{MJUa#GeYPlhm5;p^Zd!XT{b6=A8{nVBn{NqOzE1d^CtwgSjoXe zkH-#9w$B^yYu?yT-Qzu;>Ax>M{y`>-vQk2!HJEPRZn|^2=6%Mw!vU9=uIwF^3PGOk z@Lds}fGS-eL?H+_{^$jEVfz9xkB!l`StYa97KF(A2JY*%Ez!LsEsvWm5th|e8XAWe z!QYx-K)RO!>)^A5L>;J~W~QdPp!br=b^i^>b&#abQV@V9uwkR)kC@H(n zEpGqr3iEBX#cC08?5-q-0}PKV*T?0_NtR8r6HufjrCDuv zX;+-Ql5$SVEjG-|9poCT0-x%b=iKpS5FuI!{8cROQQWq>t}{<_`)jk_dSmd%3yJHi z3$V9tpSBRxLPc9VR%^gnOQ&7up&J!;@-q;Ou6)(io;8@Yt9aE0&8A+A!RU) zXg+Dt1>Grgzku|o1}Jql%h7|!>1JD3Bh_WQvBvrc2?yh2aMCJNK!M2QrWF2b;*Sm48x~2>d8Z z5i(a{0*r|8lKnK4RnXZC;zhHbDo`Syi`01b;)z^FO{&yA;p2cI(An9Ea_8E!Pkg{L z*_EsE=I=tnl!T&lZvMLti|>TS7G=a@Qk1vtqw8F>gUuPD0F{SB(D}%Zj zmP=!T=6=2d+al<$O9^iyOBZ8AJR-VF(@b;BV2jB*;c{-h1;YK98QNmc_3q*oC2%Ky zuK-;1bcxY;f>yBhtrhn?mSJv$yU1E*2yr#8m(WZmP# z3%w*!2sl0p#&ItGUod}h^9O4v1+Tk1zRUr^XW}T}2SXbxy70M|>#cm=EfvQ+v~wQd zDyZZ;S8Vn^vUVbBCFy4)1LpC_q&?s-p;%DHS_daK}zq zT9R5_?JKXLC9wwVhScLk@R?XqMDer(nyy-HiElAov^MsM6m?6_^K1rY6!1Ys#m$?B z|J5;m@mV7QfaU_4V@v}FPOP1+O|Q5ee6yDZW-40h3eS|}ry00v#n(Ycn+~@3cD;|R zi(VKwOQpZvqfFYk_c8Z|0PPeW6irIIC%KulN)W#W?#}5 zc)o)r>qXZo6MU-DJ~qlCk6uY1Jp63nchE($ROh4r9x0LBRA7H&ThP`wO-xMA8Kw29 ze#gE&M)FAq(*d{3zPhh#SonbE)KB|tj zSHrxlG%GoLn<2P}{X;6<3wNmyG5Re@ zrqgqo+%sS#?bsKLe=B_|_x-eDy&V)-?%^%2E{MUMyu%y>$8)b zlM@8`JAn1o0zO~2n=?AeR}0e!%rL-*?j$cjiyutX%&YyR-UKNwYW!R?e{kA%`4w7+co(x?zp#pUG=6+KEBGD(CsgN zSn<9r)fFUHT!A@$P<`OPem)DUNm8c9Tfu}BWKM>B$ob&Fs2v;RX?E0Zm#f9IKGCFr zyLJw90R4-{Hz`t^_3o~69mFphF-Vj z*veY?JvGPD|Inilws|nkGA5Et4`Y&`HT~hh1Sn#6KmHofDFC(jx{JZ9?dg8x3HBTt zI1z@RPe+h=q;k*@)!rSK~zc0Q;#^o{N{?9$(JpQDd-`F{CT(7uS zP35qJzswZYrwnp6r@5WBtV-UhB)DFbw(16$kp>*<1Q;r-}<&81CEdHM-Y^zd}ZMj7e}eqeZv0S{ubjsGz#_a<@4g zWg;M>@*CK})JL6Ubw{vXK$}ej9O3Bw#;i+Y_;2Unuva>Jiac<97^Ahq^@aklcg@`j~O^Lzi4$8J>pv7fm zQ|Yif`1Wgx!^yN)gG$GxK*8=K%Knm~mws3@Auf8c|J6YsRY7qo(8gI((aDL;qp%mR zo<~09rp1LxCP zaLo14wnE)n%wu@pH z!;%Qj*6B`8II_?mNzq-FWq#C&!FfZ1w+{7%^b*c_d zJnH&F!T}5<~z@-aF;Qed+k5ESL_#R6%0X0=!JjwQ?CT|;j_`owZ`vld}v-) z|0%4eT>ACFAFISKyg88VkMU(QV=_%s{6~`_zXBTp6BvR(8zy)xLq5=s~@oXTxK$r;q&g zH34QU1DOY_T+6-WH`ewynd83$U2mE znP3RKpM;b(As_&**Ch@9>oP8UqQK2-hx5t4s(L?CwptH)}&QjCBVLv_Rl5@?7k4lgjD+#DTryn1( zD`-P{+%2qZfkq9O<2)VaK0X0uCpwtqORvRMF?2+ug*E@=z3XW~#X8r++`S{k~WnVmgI4as;inSF~kCM5-F$iF7cLR;orYUBQ zXT2I$i)EwSAv-eiDZZ=&8*^S|u}KQPbcUt8huP&)SCUV75wk~r_bSc{p3aE2r6MR2 z3rJ;;P9n+J@Ex9BR?fhn9pjlC{nGO8whD!+2&+435Fv!lXmcG`$PS!Pr%ZB;s2bfZRu9*~{51f&^Wv>86M zA%xXHBGY+<8D6-7r(ez2zv$uJi4n}v>}Fn_c3$+F>oBn=0RiXsg&)2@%|L5swT4HZ z!&v5P*4B3zm2WQe;pf0x2hH|0_an|{Ye88V=Xp%a#H7OQ+!(yjzTOoMN(j@RBsivQ zO8o0%wqsh6c;Q@E3uK$aF@$g86*Q}$E> z3wEv>28=hNwYA4@Sd8={Abxg9CJpnafeC3(8Qb+sst90sKb~ZVzTGkZF;+sJafHNd2dt8gA9lvR;~w4cZ;cl=nY^wUBs+xtYZRmQke>RR zvx)morbVMFhVv17@HXTslE~_0(O!uHphL)Kl=+)f3uh+2IRX8mz1+G#k!+7Qp9yUa zDiqx3ck@XJUx4~-s?2gsv}ZV_WCt~;o4L=$U6e|j_A0WCvGR^^W(lue-vj|G2K7xR zfmp~`NYt3A*^GD!PYC+arNZ8Y{Rz2gu@bjD(9L_L!grx-4?p4aw|Q2r5V#Xr$6Sz8 zfS7zwuat^Wl0bOKs$W#|_GdS*%bU}qO!{{gTy6ygsX&~V%yUqH>hzlJbBC$oKr}pR zQ7*A1|7hQ~JyzM@S)XSHHjatfc%qGvuEp8AZMGnHaVN{{!<)FSYrYaEWkZtK`C8>& z9lraW;mr^dJdR}e`p`qFru*p$JK^P~^*ismgI2E~L&*<%qZwP-9TxdIurjSr@iV15 zG9N#}fx$&E-42FD#O&=q;Dj5AoV#_kg2x@Im^$RCE>0c!^X4R(I+=5BbrZB!+~O*L z;x|6GDAr^d>a5LX`mL9Jh{xkpKA%^963lodf$^h=UP1$Uh97Es+0Uh=(A~H$pJG!+)7!17`G_yqSl=_8 z504u=A^P~#bS{f)LZAhR+uEy>mC849Sv^U>TBY-(mk2w@lHH?MGI1^wsXAmzP#nmZ zdkQ#J$t$`lNzYql3|UN89A@Qod$PqPt#9|%h}NJjPe3W4Xcx$mB9JioRN?4cX;S~$ zNQ&C#7$DEMUAdrwb4qZ*V8YI(9cApZ&%8aF{PM z?NL7PK47rqSAG3F^QSBRryF#QYD>@)iUIaYuP_p61bienLwN3W9S-Qs1%Q!uz-*W+ zKJk}LfieL^hK{!BpJbOea7GHBIp)-r(z}d!TT)5xO3e#x7hZ+ag4K4hWrT56x8)Wy0nf7TUNg;5>(zOLI=L?~@yU6MfyE%|t)9+8}rq(XiG; zTJT3(;p*$4KYDv+OJRh?f*u%Qy+9IcVtEVj@<4x#_89%pg5j%?oM`1!t0@M363)j; zq-D;@zRk+a3&H)NwMn<@AvXFXWBtd;wI->N1l{%cbn#!mgmb5kvZ{o0he{@v^D9i1 zzkf+{VHGr?aAa-If05^gs9t2wR{RI|+i7TXL$$$#5^^S4*zqu=xq0U61f0ZOA6r)s z-u3Dq;gvvAtY3P4VcjvTe<63Egz%I%rK-3r)KR))L{;S?IqFszPb;A9X=rwu(E7r! z5Eg-64wixWpTmdL#cD^x5ec8APsAeldeRvEpN(^qU9V1Ntr3a)PfMUQTA$`dyn?38 z!7r@@(P>n>Lbre^R=&u%k0+|Xvl2MlLI&XW`8J8~eZN%PPog3n@E)!HxuP(Yl!weV zIC?!8G6UNG1-FINTj=889QXc$RaEK(8*X7q80nb3{daK+ph+t2sJ5gc_4{aHRES#w z1A5kV9f){7Af=@sYsBXd?gtc-UpdwO@w^D^RvAdE{hMI|`6)#(Ml}lpEQN+D{m`Q8pneXr_=!b&~yoFhDTgfko+ckeIrx4scG=#~}?C9_|d-2xk$hMpb_9rDNqzl32l zEg1guiBw$gce+sv+{Dk?wHQ3$>F%GCFa4S1lsoR^3%O|r-mx>uUPOo#B}hA&ks8}Z zcihVDm8mI|TpdN;J3piTq1E5$O~ern2~gLG@#IX%n70gX`6qU=ehH&1Kz68M5?BZa z!vPQh0lN3Dineicd(Ym0Me~wuvI&mW8}Zt0inS0u&aSTw=`GGkSofECrYxINFs>41 zW3wIRHa3Vyy4rIzYhZpp+D_W~!#mjG3HiN_Uvj~kaPDeHXcG*+u$z&F-Ux>KKbT9BVJm;IE5 z3x!ggDN4g_EAVb|jJwq(>iaNV!L6tfyGCn)X1Y>3`E=a1Prk9ic!-wZm|74RDUHAA z89)Yu=*T|QV*w-jCXusEMERCXqrH-p@DWXyA1H5AQ>W@-PB7?PSV}@`=pq`~ z&%=pFr-d%svBGzSZ%OEM8i&B$kXfBe^E=N>yT67;LaH?&T+z9O9o#`BXS&of%Fedi zf9y0_`GYn&A}5ZNp?lJ0FC;^osmEaha)}2g_J+(Dzw&4P4$2KmAU@{EB@mFQ_i8Ov zm*GxGF-IJj4F0P~A&r}2bry}3>{XLQ?QzMf+N^egcwl9B^|o%$2JUYoV1z@bZTX>+ zVa(rtSg)tfuT?FXn%c=VSNJb}Ecey9ELtepQXje7>OJa7g+e*ItLJQM_1kSrEWvyW zM1bD09YW7O!Zqe1!8PS-K@z?EdV>Zmeiqpu%oeDOwhX(u6ujkGlkI1}X+OO9d-F0d zat<5o8w_OM^JT<&E3C8T!0X1w@;gu^1B-6v$;5=>fL?y6j5HDPDHX1C8THN!b)hA@ z9sy;luNZ?_@Zsj~00rN1bXl5n+06x;vNmQ{qX8T_cgaB&NynDt*LC)E8OnaKbgrM_ zxt~rieWlr2rn|2mmwJ~}I-~JSR)L&Zw{6>Yz^t{DW?4XYK9jw2JIHudD& z4>6X0F(SdURK!OHT2!7AFXidwCy=SMbAQV;cYY1-;#cq_VCF~NeGhG~eMI8Zwut@Q z^6ROoF*qLgtd8B?83)SC^HTQixS94M&#r6)95K zbhT(7CVK;Z$ZzJKG?6wNzpf5E4!HPBSzWMpYFnSfFejR}?_lipTc`3B2!^6fZw?#o z&>|NZ)YKm6dC^n&uCuH!Uoz0XO} zGE3i9-4v1x+3c?)VD_qgA1rP6pBBd(7TQu=jaMOMg>b`7C46EdjaiSR@OMF9ZYeLw z(-?oQ6goNX5tX|9*4J~r<<04|=oDRRK*Lxq@J8{V2AyZmo$6Pu{V%Ep`GlU2sS>CG zBb(}qZPgY0BKr7Yn>5R1)j*fOPqjktl00Zhjw}~3dYht4`H(C-;%c_X|DSj19 zk2AI(NNLvJHa#eU7P{E*eO+S2y9#BciO`jk%%(dSPoJcv%sk$W?B=en@iguPh4?)D zwi5auFAt0~W1>g?{5{mDJ(s0!ajy@|eaBtvnfSy&0>-1hPI2C}#19W9ZH&I;?AR7|>&JbbLb&=8hxsl2nK62LsrtZE1X7{oyFYC$QPm{MSV}lc=t**i zKIky<65Hsn=zvRm#{VtEY=*yKWg#kH^BM{j0%lzHuYS89H`R=cut>#17~3hk39bjj zMB+w{vwmqEGsjMvj@I^J4)AN6Zy5SVS;}}jKVYNbbVzZ-&p4DKfaahM|6GK`%+0t| zpSG`M{aow9UFuE`>8Egf^o$R1f$q@202yG&i*-ah>}X*$br$(eEF; zZF@M+F(n%TgiWQ{WdzqkIm#sEF!fn(PKwpvS`cVHBDkK4g7%u~C1mh+CuF$L1ie*P zT!uBqnyu_yb_H^>yN8CBW^ZfZOsmAIb${P7BuQvu*wwCDO+k@xS9;lGxH92b!9{Y;log`PvH{p`HLgr;| z({4yaWE4Tz<@wtzrirL)#uNPD+;C`R5VF7bX~I%gQi9tn5rIwO?s^J;>~|}mo}MN? z^!U<6#=X%{KN&3FLo)?$c*Bq2Ne||p?)O5C9f_OWK4IoJng60njCAB`b^9wWsbG!o z)@@KmwSvOkQas<5a7(kwuT=6|Eiv71&nC}ksSVcW=(Z*N)+=#u=u5w#W+hvt4^Qj( z(2~R{MKlkt^)k{A9p#Oiul#TWUi{an$W7OP;L+9>uFx=n{5bFC+=j{DQpA<9 zwAr3MP(5m-P2-N+J>cA0y>a0N!wJPZt?|~MI}K56FGt35noW(o(6WLOeqD7vkmLJB zlKu4MT=tpxl@oV;K(T8E&C5#Gog0UKRpo*66bkY;1-0WEs z{g>pJ5SYgs3-fMZ@+58$W73f15uHkf*`q9IAORYI%grDiI1+^fNix!`zuz#2ff~VN z@)^524t?ym(~D{s()C~j`mK`p>$Q$skbz0+nu6uhF`DfGGwBwT3O=GmZw+)4f>a)* zxi%08`BJQ(NR@s;Gb=OiLAO#$#KD4N;n?Lmc;LUt!mDwVWe@#ezDXbUC%R_7&NI=$ zmYY_Fr`WG0{3wu@-#^h}@~en<(xsp!7wg3-;J7&Qm?$0((<0*@L>Bf1(RM);2}vu# zA6G~xTgb_B7E!n#KhaHGvS%G`I(+l`dCMqW)tacHcS-ie2<_M7aixERcy}4wt+Wj5 z?|oB9QoU$y9_0p2P3t8?J9B9v4i#1@{`k`085rA9N8*AOq~Ir*8UCNnBe!Ls5)~gi z%DQ(8Du2N~Oy%;t`i=6YxE@!N7jTsHc1eUx({*HZW$8A{!#=%>z zufUQ5wpdpb#)UJYcxpuPTeSfOH18u4nZaJ`Iw>Bb z&_$i;-s3d0t%XAQknm{^D~B^#H{_nZ6px)S4!$yu+JP1b(QW(}hRAf$58uFlNRaZe zJjL7=lla%g9MO+70#sza9#(g+F7C0b^QScD4>kp%rOWFk**^y|bCw35NH4Bx9mUde zm5qh)AlBTY;*N?@NNm83k2^(ZAi#!1>ll*Xnz-9sKWuu;U0KYY(p6>ZA(m)iNJxGc zz2WvZ4TadApx4UB+f!|LCD^~WeDZrr)eL?_mC=egkIqk-O&a;NL5O05ISyxJwmsl{%LK&P2Aw3}^8 z;i8Jy*{kta^rduaJ36I91ttA8R$vmk(1UZrT+Ji}@6?XBiN}@+6Nkq2{I8L-DGf|c z*KxrcY5w$&PBw)MBC-6ok0zie2c;VV2Cf$F4$VJHe1}hEn0^Ns$1ODbu_VZg=QzgO0k0W$TMa$ZviU_kB z>$IvjuT{O^Ko^vHtM#%p@0PeG$MfX2XEk3!ar*BymhC~guRDb8=#*0&N|w8S<*_j- z+WjVdHRtoIgO)N(f?Ku~Ry-E^)K_4%Ar*{z3_mx^XRDAT#gI;8?)!@Oo(!^!b-p0W zG4G6#aY_rrSQdM!=fjpEIgb3%(b0Ls!|fY9mqpDx)w^wv>6%*|-qJkVKR!3+;o|fH zKlAp3+s=ya=Yaeg)I;ac3Nl`Vq1#X3W<&iF25vd^OD?ir_U}b<^R+sy!8zy*F33df z{6*OB<)qHFq^JMCN;}K2sM@#P51=4QcOxQT0@58Kpby=Gq>_Wu9Wy9Wl7iAH-Q6ux z(k0zJ#83lLGy9(BzxR8*$Nsc~FCrYwTI*hSUe|e@zqHwuPD#xisXogV1#j;sh%Q4>mooY?-YJM9>0si!g1I`R#SYbej!D)+ zKiYeCAm03R6HXhw3@l`+F)yD5eLmUKb zr!w65Cd&Soj`1QU1B=HM#3IxH5=)x}V^u|j9;gVhzK*{3($fudhb zj!Qrhl=U3q5ytEBLb(rb#2Q=Wq#*Un9`e;IEh|5?tQs9aGb+qP8xBd3bEP+p&9B-#u0KF#=@;)nv7wQ>vWvG#T$YU(}u}goNV|~Lov9KR{86zmG%V#TwtlvyPj>-d#%h+ zl&3zOvZ?1caEYog^`g>Hm|(hiH;Hdn&@GclS`c>JWKYy0*=70)e>haMeHdt0&WV=5 z<_HYIeedUkceR;!te7pdZ5yRRL4R8QV$hS`h?iHf%x=V zcY<x?*0?I9gXUS81k8QlPCzUi=nCqvKy##9L}cIi67E- za4c#CEJ#(5v7`_?oIJD6M;WsW8y#$LrK5rq?=Ym?&JP8J#~8Btx(C<<&>q2+kDS;s zU)8t+uPpV$p)`ihH39as`IMj2=cGSG6Gbi$oGe-Dg9^4v`HpiIkHf{Id;dApV3M|@ z_gVT7oPbiHP3aR##<8>h)_lQ{kyz&x0}jvN$fRBAC6b0@c7*QYCjiC zm1;UCNh?I>TINUrIsLZ6197P;I9u>Q8Ec{?JpXV?XBXq3p2_j?jkygjDWoSYg7^{>Eg&mB521c6vp}-swz*OyZ&~tx^ zsTc;|lxofOT*|_GQ#iQp+<>>+fzjQRx-MD1Zd8=PZBx(pW?Z;r8NYhwaHelUY(vvy zP*_2Wgms^x8Vr^K3_VbC(EQ!}2|K+lmy_lyET($qhGJ_? zMHSFe7}+WMwyK26X4V^=>Itc}E|U^3(*4WtXG3aIxrjGiX9y*cLj&uDMfi1ZLL9yh zW^y6l`NsnN>4kLdiME`nE#&gum*dojb7MbQqc`T1+{WK6NF$+v2yfGwAk%-`d;K2D z-uzm-7i7!oPtDVILV6)>*DpeSBge91$96oZ{j27%k)hb*nZoQq6RSnPGuT>Xj_XvS z?fCIiOKV9X0uCYHVfx7OHhcVGqf`p%`AWj=%*if(_b(wpp7h`<3CxR7#yUF&!HYfR zccmLc%{DWBl(P4t_cirTw>_njUXvR9*=&Q6L`X-Bq?cb5c#tH!sDIdl8Kuk>=o+r* zD6tq#dSYI3C9&9SkndP154#_)Gl~WC{bclRjG;qbLx4K%h($$&H}!Qo}$?(s53aB8T~$@BX>QxEa`zWH>n$UR+PX!Irm# z8$~Sb8gen*c+!n%8ETo{@gniK?kFJhSr2{GC%nV{i1Ibcb?QvXb=Iai$0AE#y9-JO zIx2`D=q=jUV#l{x&jpirgm^qMGs7CO!qdhqm}QI_Yv*~Bu(FKM)Y&q=Rw+wkcZ-ES zq^w%+{@9NR{QU?uch})aG~PvSQMit9eouL@1##IDsE*O|p7}^(wurL}wv#|dDY(l~ z>PHV3ZA__uZ6NGL@Dz!8Dalg_pFR`9&ot0Y^gy+pv5K?Uc^LST{Zi3E<*V^%cFucA zv+jp`FMTI{e(vOfye#0*Os{&a^Crr)3ipc=!8~D#>&I54Mh_D*7k`)%qMF$vFNx$% zq3;AclX}d%hfmU2JMJJ|9WWg#Xxm#F&1#sVw|Dw16)R0IAfs zrla&arbMs#H1>)}>VRf#T3hUL#r~`rGtPl{v&P%9%#%nR(2v>7^)A?>E z!z(J3?4CWn#{(2Rm-$z>wemmVVR5JTE6H<}R5%H1_6YXIlaEqlceX}7vx=jBm0Ux6 zZ)y)LESTgApFzSB@Wu(+zZ6GZHbCRA3|J&2aE$Af1_Y{`b8&yxtR9>icwA6S{Idb3 z77ab*wR&?6UPfnuu~Pd|D*io2S>WaI{{5I^y_fIE;SE16kU=#Y9y}vfR^qGh@|2K% z9rHX+F8tPsj`;jY_A#iK%i>`8YaaPE4y|VStcpdmSY9C%Ona`;AE%1GRqOgL*kSml zc6#5isQFfg);=!GxW3WIBz)ixG6ijwPl1-N#qkZr+5GqZruxc>e`dDh>MnlJ4GPs; z&!gw}bFR}EuMUY7ULV~NIm>y(vu!r{m3wAaEUw-i!*Y>3^HoBWh>_+igjZk}Rgk(B zjm&GyX_a#HpD4rIQdRU%{HJUSM*XC>i%%wsdRXKc-(IiOF+rxO$t|Kd_;xPOA1? zj8gjml!>4sY9W8GJoNSCL~TWWZRO^ihk)tA_+9gS%gv7J$WQHgoEo2@=jFn3RQ=aq zygs@oOG={$OeA&Tu6|spn)b^6MVP*1^V_y~Td90p3GqUMJwT5x)tW_d17wwe^yhJ; zz3rK&r%`20#~?>lSgXoS{Sxl#L-uE@K5|nsi4N379wc@?j2kd&u^qGS=K1R9lscD* z?;?LjWBg}N3;MgWrL{MY|5)KwR?TSQz0zlIWi^>Q_RA(AQy27WAW_+C#%s$Y$ZEtV7dkDpPQ!D@F1>DzVkWOy5+Z&R;N;Oxh&n7Lc^cGsK8d@ z`>$qJ<&713$(%r!GrVN}>0i;`Qtqc*md?D`D6RUHc0vu+KTCP;kc#|rc#2ld3Fqe% z9LPe?gC&>~PJ}|^y#Ons$aT}jAL*UUf<48%u^)E|C*q4zGgCkqeILH*_5FkQrc|}8 zxyZ5fd}A*nsP=@6dfTN99Yd0KW-^(@*FG2uhdd?FvE1)hin^Zl zQ?WjC@|KQ^!I2Wgh^z1X^5w41!3IjxhyG;%|2{1QV>Z0 zdFIJN_ZDA|z+=V6RPY>A0$Ds zjKe4_;jkN+sjnf)YG3Y5_ZCN0$TX8HH{-dXOARQ(7}n+tO&AJ>crXn%3{Pet)RS#V ztU+OF?LS$Gkm$?-=%kHYwfj!!$1O1?pD4!Uppe|~u1S@wuEwRgSu$a2$!mqv*jk6q zxuvrgN@{|d`ePv_J0H`^cA&w*|ZD8*Eyqjg-xP?LzSr3?iGl2kfb;s1(4n+v2*@)7l<;AeMkod9oe@Fa7=|4S_u|G23w{~esXF(!f}{P?V?24J5;6lh?}D#p!jcL;d1gYZpp^hpCAYYQBzeOm5UU zy&IEiMjv6HrE7bid&l&owhC-0aGVXe>zR(B_Pb){Sp~-SwFi>aB!#aoT|_5p#oc2| zazpeAKaJYB7i2Gfy&+{2n}NJxR~)cs^3!JnX|5<8aJZ)R=yLsiK!Eg;d1k@)1nH-L z7bQmr6n2e^(}RU0Vp2exw!T6wzxV=|2797YXrcMj5K6a>uhv`pIB_OCjX!?r zG(U?79iE=Nid-S$R1|0O&GJ4MerYnwWtjiZGrOtV&Mm_jS4zI|)#}a7sD{ra4_Q~L zCD|Vs>Hn3aj%vO1tzq6;i$0<0`p~MsN7jO#Mq70S0S}$_KUIppcU&f690R)3ZSNvU zF;YpSd1y@=n#DMibN6Zv-z?Xg`zdX5^p9Qa7$GwQ;+~d>Mk>*Tz&GygPmw=&hrsTS zRspY{KCf8`l9i+r_^_z2Z6j1?@;r5Q(PyM;U#v~9(cP{TM={ikNrli@PZAWIesxxL zqV0_Sj!g)MIKG0un&0%uyT#|49D*58GVAi7#Q0=WTOog(vyHl@9HlTDit&E|$=*9T zNf&lf&nF-e?n4mO`dhkE(o4$gWzRU;*5T@|nFOIh?yOIBtEp&Mf59^a{7-r51fo|3 zS#i>TL&S{4YIHG5ZIbg2ub#L+31#PKmuU|VL3^W7RWcq}XE_zOp|zZt(Y1o_IP|xW}&S>B! z^fI^Rg!9Kapw@1-drS6OwJH58`pXpQk+e%1aX#&@Kxe9wg%K(~&j~@|tn+*Pt{G ztEA)?n(S#Y!fT8a6EQ}vff5;rp@ig@Rn(vq>#xm3L$nn{@dsG@wr>3jKGZIbBG&_q9nWDi1m<$W9u4J+2Vcv5PyQ`Tq9+ za0!riO77Z`TF6Btrd**DERh2dE&b;d#TxWiYAXb01+xX7FMS`M9TJxh?7zKi zR|OIDG54(L2Ym0ELi#opj(?q=l7G1U5R?gp9V!f27oCs&7~Y!wlxIiQXHRa`J2Y;0 zgxAV?@nK#P;{Q-L*3TO;Ml_>HxmES69ZpE^IDa(9Q`lzrMt0G>!&_R@nayjJ}$$OhjG7({19z81x21my54cTu@Uf zK>NofJu~_XlrCX~sficc%N{>QdRRXs<%Krm%Iy~xNbx@pt8o9N(KS*M>*zJa%v7t4 zy2ZF}w{!u;Ot;E>Ny<{VLtK-e*N21Bf$HN3-f?R`dEoY^fONIPDy3@LmHW2rb=sST z>7IP@6I=et1Ig}kj!YE&|oQQ>54;*76PzUhL}T}tfD@(Q_8WaH+Nc%D z))&d1Iu;O~^ZoOQANjQf?=4c0+tmJlyl$vUWqukeMBfZYb~G;Tc{fYSL%*;7hUXn- zlD|Kfe^xc)x=`-6;()+PH!vU}EgGY8wZ;CBj_6=W@4~JJ&tY2(4zK@SN%;6x%W8Pf zZJH=-JV@7s0CBNT?vN2B_Bs4+c%wG5&2e*VHq=U9%lWsg zR#GSksRci4FAB-H1K|pHpxNuNul{PMtJJ%1e<+(tuid;>v3P0NHR+lHwru*zhJNu>^XpCqnO7k+-X( zpR{l&PYM1bXEqsjc;!0y!QZ`=fVMO;Sgh_ND3#_DpML?0ovwDvIaA@K{y>&_4mO7% zsD8dBNC--W@+78&$IgNCH-Vbi-@mSeFB0zAcUn@lut(g>P1U2X`WAX=EQC_k$>kp} z!4wB&^!=&5sa%%zR6}XdpHO?J=A)%{?RJerj={b!(t*}H!WE=A#}B?F20fgAzhoE_ z%H2I2NAC{A-9yg^fX%gdx855PQNSQmd#IPMY`iCp4^JAzN3oFa5~X`H zyi}CtFH&6j#ecA?>EXrp8dFq}A*|DpqPP_6e)mkUZdt!BNBKjQm;p`lG2MMIhi=1;yn~WdK8(27H`+2Q--1<-8=#1*FHf{Z4X>DyIeqw<2zK#kF_}0y6lF51{P^02 z=OQ*y2GzwiF7vs>vieD~B|R*(f){(-EXqw0sM#*MROhP?9V``llENI$!T$x`JcB^V z*OU*)Pc!a;2nKUSMQ%nkgg0KbHQkNnykPyMNrv&a?w~rNx#5)XI~Mq5t6@+JzNm%| z&|$Yoqr%=p{EAT(?LRI~2U+Y{eXZ$yeqs(ej@J4zK>iO(Gt5=^%0XJy@fE(>U2<+7 zWCj1TPO@aeM`qDCWkr+`Pn~EW>n^IwW@>u0|8`O)g0@3P{^H2jA+8jXl^gv5Fqwddm%T{r$VH%5`hquW2a^Q$$> z6-zPs3LI?mz1;9{=eKspKX~w~KG#p=@&hij3;$%^6C0)Q#OLo0z7ngok^>!kB~yfd zXH-p*a7d1@ zI0SX@y1Gwx?}-{eolUcK)0|ve`8E)Os=|BY9vXhwiw{y>D}Q!HTF>f13g(TmeaAw! z_ZfsKJbJUO?yXX3f@Y1CrhHygMP-7#~eDBYf1{%>;DLhY8fQ#-~FA*27poA5gAo>H{sG%gz+7yQ8dW)0O zmvG*FCpvgGvhO;enRm7N`3TW6_A-9znw~a9vNwG>D9ryR4q}BC-f|)!ID-(;pG=b zj?%@4wfSOg?JXwEPc(dQcvoR4GCOV_l+d*+Gz=K=w~a357oQWbsH5Qdv26nv%tcw# z1uY#Wa$6D3k9L+a(L5A&aqoG6<|Ehc)*A<{bpP#PE42A0=;)*`;xud4- zR(AGx*iw3@VUXRvhX38Cue;NktBe-+6Qn2BJo7&{qaTrh@hRlagy6a}<@Vykk6Uh( z*0mXykfc-DBcHmW7b>Y9xr!7G!ez`pCS{;JitM>UV z6iI!G`S>}(p+om&{I(4aKeVMyrCG(|gkj;(x4xv%V6Y=e$G)9%=;f*Xw}!IGZ`?Ol zV+&{}8y7W--|GlWdX3HJG-1hb_eM2`1J~N zhU3svjbBUObu*wFWO9$jf@ai(VMbBep4m;;eA5A!k(1mfjIB4g71;bUs7&S zmg@DjRmxTA+YJpZr?`?yr^v0g^h!sREC)>*t<-~HtEd&4GTS z;Cl`;@E7!UqS9bbmf@4?70KdQ+Hm_<$T)NJS&=@=P-tQC$CHz?WD#aaEdeI)sz zE_NV-?On9Z33$j4f4H<`k^O}&Jofn-tmB7ca?Voa+XdB%$ZSKCL|Ym6JfO|)%OmVt z-Ww{sSivJLFuf;cVZJHw?mGkVq{)C&dR=lEauN}G&Rm_Ga>%SYmFuCxQef*AUP4s$ z%)qxjn7$OZrDpY!gZ&)wpb&Ms$)(7gE5m+O0SlZH^EBbb8dfwCOivIhjAQV(;{7pF zgP&!wz7%QUI;&0Gb2BxYAP~fP><*ky^h~Otn2pOLnnD8@(>?`4y`FKD)RMl=Gwnh$UmP+{ zP#n|_b5jO_ zTituE1FAU@JD6-Opo<4ZC`EMwe~v2y_%q`LM|yzeP`z~`#-NRM5!vmb|DdrTmU~=+w3;MaAd>oAu@}<^>YY9$~ z{(12&q7o{xxRU0HRsmj)`}u;YMq8Or@g8)2zJu1+2QI=5t~-DrmBMw z+KeEX*KbRNRrmo9G&Mcfva26-qgsj@^HH}_vZi^_1ucyqT_vj|!N%_-%+bFOB7_t- z!A!=m{ezNFNu{hi1}W4;<&%xvi5z-&lNK4V6H*!fut;wh6>Ty32Qg`q5}1JCN1Ato zH?NBTGY6C;1LydESrO?!e&7~PAkN&dZf(+-tC~|#Aqo71KY12pcK<4zs7p)P(OGt% z7y6#SgonEOkdPq<#|sCr-ihf>S#-!SBNY{OMd$`^=pGqeb zX6}QlYKJ0e+TCm$VVZ%ckKr%g&apwBU$mNrrs2k7v~UiS()_w}+9{ov_k1vWDuK@@ zu|-kh%?dMMQ~9NlUI1F%CON7e{L;By`Z4%w{?y#BjM|vuf3VvNt$qt0qOVmF0IF`R zQYof&CTSm>q1Ch@BKWaz9HK;p83>~+*kNvJ`^O0O`PwglQiR#YO25X)f+wc7L7}vZ zJ4=dh7(6W%3F<@NuH(s_*mprE5?2pehcNx~xSZPWmgZLQ$ssd4<2)uHw~&A#u0u&y zzd<21Cwwl}_z$|c6kv<4v+0g;Ll(BqpW3zXUN5d_nlLF&eUu3{txLCs3|R6L6B8Rt zI}(ILXNV;*NeHWb2=Dk_JU%~ZUA{Mg z!_n1Jnwot%rH*Ypec^c6a;=o$(TvPans#BzNwO5J=LXIgcr=ZFL|>RQINJa-b|G~` z!(DzL$*rmq0G^h<#+J3SHc;r%g=>4`)*CWmc@%_^onFSzu~(s0(C;ZTcf_Q4y^9}2 zbxdI2kY;GaSrs}YQny+m0#FnieZpLzJiJQ4m&QNl@KtrPjknr$bGE`$#fYlN1$AOh zFyKgs$CIZW63Yy)VW;8Ci$R&xB-f05(#41$-mhK_H$fp|=6J!u>Ht#(SWo}mXzxNu znfu8T^MOP`W0bG|I?rD`YHjpEkTlbgZv-?)qzC;}+jo8p*X#*hoOyafmpjB`>9fIT z3ctlt+(ak;kExSa{P~25C+CzM{O6n#MYX5MvaCFB;FKN*H;bZgYmVY&zasXhM8N;d!g32Vv|tSJfgjt`r^M<>QggnTIbg32v?;YkMekqtb*=ndEu}S0n(qW1XG1xbnCaf<(b$4E zglP!El{SLNbQdy^Zbb4ug=n2*39VdbFu^UnOLy_182%}bD*lwMRqxd{3B*=4Kbj1f z#ePM6f?3YI7XsFAuguKYfaPkc34mRts@w&T8edNT|J|40SA9(GR{)Qwaff-9+T(a& z%5{kgg$4u!QytlG{2~iiLd@RYXd@(W&VA7vVNC8t$ZtAIY#OXv zobVj%9!G4@cx@ZLrvLcW+uiiAXD@hGmw z$#vW4ByC0Z;mjVHO{!`C89IWe6YsN+84*0dNe?zZ+NF9M+{NWh;P+bL!Lw&t zIP5;F+c4|78jnIQ0QkhS+dcn&M%wQDZA%PjH_jyU&q;KRZ@W9fBt#C<5rkWRrXkE$ z_c94x>?uo5=gABvK@vI&#;)Ax{MvmFOQL#kFF`^)fbPdJ=%Oy7E=9Q$_+)HdRiY-^ zg08uPXX-lqej!anck{5(`}PIf{>?+K_ET=dqt=d=oo_c#7Q8A3ZNaxV0We4I8&xj( z#QdGI(=fasEzmUWe+%21)Gcu4C2*GJ4wALtU=!Jz=6!rM}WR zi$C`%wMGHg=BpCG*aW^D6X@cg7Xg+)ZG;m$wlgB0nY6$IsC}2G?XY`7n?fDO^vY!d zpL6uSdEP@#5P4LSF75hdbd^FrkNRtlJv%>(122f=C(=>Fc2;IBoB{>B0YaPxnm9d6PoEA5<6%s==T48n+zAVne_WT9*KFoZgKk|pG0>^`R zah+Ir0Oal?dP`p>D2t}=N#d!(20mFWVP6JJTtr-l^PpeJ;Si&sJM^6G(|`Whu;OD3 zG50P>Jioq>->v0mn|Fp`&#N_Tq;B;dclaux!%avANv!j9r(3^;1~nh+1&=6b(mp5h zZ*0}8BD7SEx{zj*(4_6Cd%QG{^eg0diu1!6jrDs*a(YbzQa1|C#7;9oWSJ=vV*zx5 zhx2>PATS*tR$+MmbDXG2*e(sPUEl*ccTpxZ?S0Gku!&_UcDCmV*r#ew!TE-UKmQ9H z2UTWyyJWAb9MV)Ub9<&Va|HPMRFv<6Bxb=!W{HFY6HO48QLa;y5AJ#Q#oyB;$k|>V z9gXNGl?$cjlcL|}C%>2VA71CLfSmurd%Oac(*FzFdduR9Y}sVsU*PPE^jBD@95BT1 z`q!2J|N5Jr`%!lt&i>dzp|_6{{zbG_Idl_e#E-Dyn39c8OyKbPUYzizg@8-2DI@_7 z2B80Z@*)DX*Slci_G4F!xN(g`)iF6f{tIwvM7>V(!gfQ|kLbcljzr|8Ji`=%y1EoB z`{S2G*gw6(ZM(}$r)^^aFg8uY3Ln?j%srFuB}*t|`b(&ORb$B=_6{$^u->A_jdH5)wjqmLCl3S*1E(d&a=L=J>@2d%X;wWC2Mk?{GjW z0YHRbQNZILOB$eK{|}G$zklNk!7IwkyHz<&fdDJ)z&beSrlu&k${Aq!9ifJ+E0wgC@m zu)?AbcpZP=6qAJ~yO8q&wArZehncl=Z%@zq)U-6#E`ACf9UU)#n`;4BwfVQ@C*UHL zJ!OF1QQ8VT76B6=i zX=%NUSiW?FR)+8;4S31P`A-hr?}{MG&d!#?lkx2TS(=d{JJj2I=*3h}P~g%=`5rVO zJurc6lKKWf0$E7|mP3o{j$fF-P88gfV;TnT8Fo(&(1+5xGji1>R5kJF?&=m&Ck<>= zQ?2O!hk1k--TlM~VDG?UE(ss%@4qo#5d>N#v#Dv$mL3X-m%t1ea^&?z0Qjny0Wzww zbWFFk%dLsk`+{KX9d{IR0UQ1S%R76$!LnbwpYQ=F2H?&$!8Twqef|Ae6&0h%y*0<% zX<+9BP&nXG{F+kly^@y?;NnHTV#(|QZA=6FLi0*XbAeBYN|F#xhIcZ^qwt;iKUN?G z4{Z+sPvHGsUg7U@MrwJ~34a1yK7cB^@C}n;$#npSN$|Ev#9l{rKR5wQDhF6~CcqW| zKa3&TM<%$}BE1w2QN5wi6L51)fSv^iZN$L5`r23xbO*rt#ImDeKWS-cT@nj``z*%7 zLQ}H**&;QzEU-rF(tZG>k?tfr{H9pO3LAx{PQsvR}k<$_k>YXQ9-c3AF3z= zgT+cH>9Q$yPU&o4+4KPmS8-!)*sUmVTSNX0zL#!8B`oADV3>hh1E?Ei05;wEQ+z!A z!HA{u0H9OM&CUN7`EH+|-jCG!JN;e=oI@qx?nuL3NkZ`02hOcXKyWGt*K%@pmi*WN zE;o5u4Gy!Dn~t|mSMUGVga80G*eUP-htT~D|0GqC!5VBMr4;oD+3sPPrNLzdd3iEp z0&G4=_opudZSv2b&%qoa=TroS!&d+p0i(rgzliJ}UoQn<HozL z2BZjJeqK5o3;3Gfzsm#s1QxIS{dGwJR8M;`~a6!RaZ~_FtQLj&67U2U82K;TY z@L}M|1yG#gyT?ph9)R`;x9nF%7m+{E2*=j zgoKBTvY;ig94m5iEVKn22QRMX-V1r7-&Y#|z!#R91_5L+Z~?%9OqKcdwp?9LuN%M( z0QA%nPy)aWl==bQt>w$7s&n|xO{o|bccG%PvSG^cdW8xUoigI000BP*_85rY;3F-j7If8E9@y_Q{W|2 z02v+@$@mO3;=AjSi5bLrttdTLuPv_-r#kex{iV`V;^Pg^^ zYuklX(f}uMQ^5c1-^E}i1+NT5x3-)Cfb;mh_Ez(aU7iC(^U1U~*N#j#<8RN)z8|Qj zw0{x8j{(;ez)jgu$Ib+%9~lO~YA6d9MDXtLEVbvA=D}Fz5Cn~>v6m6Vat}bbwc_*u z$<@XtfAb7GtqPfeR#4a1-{z0mNEI~%a5~^mie*XDZxOpRwLkiobUUfFV+9rk9-dsV z3|1y5CnSJvV8mojOgkdMf@%9Rb|s#El@b%PN-dL{osA+q1#DxhqBJnl0R|YfX#JaS zU=V>r@Ah=_1pvg30fl!0$^lj{Kp?$4ZCUAAQ3@V9d%K|| z=<+@QPkozf#<7J$aa&~X5C8uCd#cQ=BT6WtZXFEB3!*^mvH^IxJ&K`ZUsYoL0?6)t zqNb%K^%kuzE91QzBNO-FOZvxQa{v~eS1ABx_!2JR%4U0P0K_#61{-$YprN55fyR}Q-Ma!j z%3EyQ4q);ItPPUF2!}E-4S@B{y2y|Zr z0*wb38;)4qG-!c;?m0@TJ3h5Bada`TH%7=CINDm;I9i$;GCCXEJDA&8^K$ZW^0G6U zIXc=p2yt;){r3r+Huk1mUsI9&a1%UR84U*ng2({%7ww~1jyVE>6p%$eP;pJ(oN;r# zdf>%*L^&-UdRg2|&n!gZOHzXj(JMwEU%#h_6qrhLO> zf*{>maXV!G#It*)OQPxU>~0Es4^J@s`KV8}T!)5)KOZ=-#hI9xGSJ%*GVr5{J{2Ma zPPl}R!AL?vqU2|QV1{4c=EnC$osAWU_8N703paukeoapGe|2%}_d2v!tnVH#{+uZP zSX30KkkjMj0Jmi&`dk)1FrZ{PUVaZhos*y6nrKG!7%6qi< zzW1K!=nybE`Mt2Pu%D)ZQFh}j*itz;IjtqXXTffveNyKIzq)uI)|V9R?Sgm#{~yM_ zz{8D1I;uv?Qe$K)CDW(2P@9wH1l_cAcSRp?CB2GIu5QELg>8g&m+JNt)4Y~Yp0h94 zx+xp3h6J0n`crmpZ@V1XI_k5ZCfG^dx;vuwzMuw|3oFQ0l=gwLg2*!^&2X{VO7gUG zel+77KRubc`B5{eNj!IdWhd*`SZS3M(U>Vssj=~myQ6CFAEo`hc_US3@x+X5<%8_W zVd23}#7pV<&HE9}uG=gplU1tsEJR4qd==u1?~O&U^1(D@V&|tE4U-IQUDg^=VyEK->NX#rly)HH!s7n= zqH*KE^qJ*SM||4O`X#V^vaov?y{vgMFr%fUQn9_6_gtg1xzQWjXRp0IxRca%fz8Fh zsZ#e9#T5<_%^xrQSJ8j1ulo;}VIpLrF>{9Qt!$ini)nLGYHDgS;-Y(sG9)G?8B}T( zA0;4>auZu3dzU_4UUFS8mmha(pUfAHRLE(!*_sa67#o>CYd)opB#PigkS@2BF@#Kw zj9?*3rZ(Zjoz!b9NbArf9nE}r>Mcl12p>K^zQ>n+E{m{`3@DQ;GVgS*kyMD;wr8}> zWm!CnMMHEl?G(^P1yh(zxS)-Vj%GABi|G_%D5r_99GQAF>Z-HHg&(l*C&U|4a+7e} zxPIl%^bsY_&Zn(JyYt?Sa{I5PX0!rcd-psyhVJjo7hWy1f1Ht}(BoNBz~-C5`Y`$f z^~>bBt^OnFo{wsRao{z%`8z5AD+-q$B(z!TE{zV zWs#GbqiD8wAC)9UOju$gaQzVo+xnT33CnF);b%@?BO?iaPF35GaIi3S(`7w+mePU1 z{ct<904A|B`jW41G3Ct6%rj3N4-XHaPrc8JwsAa(Psr+BwgTH163Q(E$L>r`O_jQA z&Hg#~$Fdx%{DN8aSjk%~ozXDFo!s!GG65KBOFp2#AF=37i14@(k8|%8vuwF2&IkV= zSu0AN#~~T6s$Ihyd*3!{zoL+nnv6v=lq~!4KtG!#|FGXogP<8%Eq3%LG{$oTv$ee z?ii7b>T$AysT6 zNJrxy?`x}`pI*HWF}wA*hoRvm<=oYlUA-o@{H{|yF;|>(r|;+|9%9iWOQamC+PI0q z!M{7z`6Bav4;C=Q$A|~p2cb~(2Omv@sTYAbV(Idz0yhDrpK!*alMAbUlcDjtk41B1f& zXH2ERLiN6^c|G?oC$LifGae&KHeU+^N^T|Ve=bGMVm==R)e{y!TWYF*=dhrYX;KAY ztf96^i{zubSO*Js8fjIxT#K$^POb;a*ephPBYh8$L;p;hvJoOo^?sAG&UYj+lhr@R zhJT?KHL2^+F2f=De@`P9->dW0Kx6JE_~)f9@ZZ;MAa1W*W7@nJO*pA6^_8c1@|_5K z0k~1_K`CdHqmxss86P@=S=N7OEqQnEZW2LZVIeW*wNueojJVs!r?$1)X6EL8@c+M_ zX4ck$BNnP_dg90rB3ZX{OejmrdT`l%0T=esO7inBBlN4M&=B6u41_@#obfM44=~Zu zTpMT2qqGXuT!rZYmYe5@ipMTLApSNw+PN)_>`@Rx;<})?od8C3#J^5}hAbEND32S;1CEcf;%el4Ww68BEXk!P;X!X=x zRwwJAW)Pr>c+G4+dVuHu+74fd%%D`AM|Wsw2qWf8z!TO0eeU4CzCMj8$_Ocv2&tjD zp!Nl~v&+H3_R_iCs(IRI1-H3S4xC2o0v>IXqTp=vC9GZ(USA^rjH%7QUXz|xrY03> zX`KDdsp?>gDpo3#s(DnB4WA5?RhGq09l?oS}C+H82+=7Ad92 z@xf-fG+~_;c)qA*aFENpd}tw5Dui~%+jFs}{1ws6_sS?VIe#hl;$OML0oV7ZAr%aw4c>nKujH)&@e%3Gz7 zg_vZ3Dcpkp@}c2p+OW>jvQRk_67y>tF8i0c8hop&?t(phE=$%8mBjOoX1+#+-?p;* zAewpAQ3zN>MP;SI?vLIHYeEtdovhN2chO2T^)Ai7YzzvK@zSU-Z(rapP-AoV@F?{- zu>LvM%uw&VnYb$gybCr?;MfM(D|q9>cCnLoOMYP06b9P)obz(GeUTp`3c}HTZfylk zSV|cgU4NKPoa*`e3tONVf}08-IE|p|HX|XOP{6NW*22F(U!O1ow$0(s-a{4EsZ=DXnVgf_ee zM1!l96?OdpAEIfy?Xv6sgeeJOP^B;j4Cu8onu^J4Ug0CXwmY!ZyDbp-LG5N%RvlGV zQ`@h&Jg?FH0bnS#n!2mbPN80?Hat4II5l_Xd{aMe;XbSDE2wsAeJOaf5LjI8Q+7YM7C2| z>_q^~XTk!Vgv3}DT#*u@V9)`d@l9Nrv+41vxA6YB(VDR~?4|v!xv0@mHAiP>1rIMR zt*e?^TIEje5Nc!$n9+7f3dP1-@TxS_HIPwCWhN$v1(%Iu(c)slQ)743<>W3u*9e@j zOx-{F@xw$WI<|1=3W*X$gx)l^Le7x6eN&!&BpUOevYOZ=fZR@_GC{+M9o*W+h{S3i zY}kPm_f2*UA+Q2@UW$kum(_0%-!@@pCSyMQ@UCEXhCT|Xva*soYRyqdc=s#s$Kv9C z*ONinYe;0Qp44?a6$^E6_Gk__iJ1zY@BNxBH|-CP5<6PW3>9cCen2SKyUhb(xoKO?)VG8jc?xBN@oh~V)+98~ zrMiD5sj_hR2fm{4PC%kO0^*o})Kq%#dDLugdDkw2o5#&_YmS_&ZcG_jkEnxk zjIo~N45dHn?|2_lk+IIM5B}=(_@Mt#+r_0iF-PGtCi*IneP-F+S+5WOI%wdr09fEp zew+$Jz>=-9P=W8;(s7ES4ub(Ol%cgx?L=bu2#9lFnS)qEf!+hMtp5C|vik?2d`14N z32%e$R>R!Tz<}S`+1Z~1iZCn=4i2CSyY8vMX1AVpE|^bX6(mQYa3g(v#tOv>qD1zo z+}+*Z53Mow@cg^ii#?awEK<+%f-`VsqX%c|<9{EY5@Qxw%gts#ogP7tSNz=QYG` zAV|%Tln z*1`*~jYI+c+TcSq^m*QX;g_?sx{i*H?CXLMl%OEj$)??f8Vs}OSuW!9 zdtS5xU{GQ+*Hlmy6fDDG^hU5-F6@bN!KQV(>Je%38y0?%n@Z{3j}V@#=|Q&J^A9Yi zstByBue!{69r#D>94~_B!Zcu5Cnh%fSAy0 zVmVQEW2q9}B7-%fx|&injR-}GlM$PzOu^$j)%9~(7>_vZ{3LLPb(7^O`@~=l~qu24VUs7*v ztgk_5_^k+DvCC$leVxM?~LepbUG=je%q zbfun>`|Eii09WPv;FbiMoaE~h?(F)9~fjQM~o`qPl-3h4UA8tciK(?kg}PI=fZ zuvvdBz_mS{nRtlLU#MCZndol!gic^R{(z>6q3`7-(zFv{^U>`R+>a^ixKap*TpaO{ zlS?uIc1GS)euQSBn(*K6F&3%*!NFy$o~6h9l;q-!{Dj=XUpV!dDsyQNI-HdF*ciX+ zw?vZ#?H=|%1;nCYl`BzMF}oSL{+pB&LjoQpwm9R^FY=lh&rh7O(WYp??5k_%`x;J3$LFv@a~|NAWX^6-N+em6|)Bt|+!x zS7XRLw1jFepr_IJ#foN~S6r#77{s;W>pfPm!K`o_kH zD3C0jSDxzFQsy`$xzeyYKg;{u3`<+|x%J12bSzRsVG3pAj00{u2xFIM=OTCzq`9&q zvW$RMKn)-`l=ASnKkK?gz^^EcM1~QE+#pfxzNdqo{is+4f!d=iut#~`eS)nM1oHs_ z;m_&XD{Rk-knrxQUi%uqFdrggrQ0Yao^ew8b8-%XUz-sR9?m|}u`X2~e1W!qaL~SW z(7v*3(4Q(oMNR!cxGyO=n3aK5wHjrDqU!jG!NZwr)^n$)r$E6nSPflWsrdPmmlTn) zTv9Ag89qDI5<9M++`ugdv%9T|F^}3>C@-Dr@#}$-5(zuS<)fpcZ{qSQA zk_>2r;Us9X;u=Yo2c9|A#x^i%(ego6Wbf!d4xtBS}EBw6cmMC9T?%#`Ev$Xmd5 zXErs_r=+AnQqA@I8|m%EFs?keZgX@$1o%!y*}(?3NZ8JhP=e3e{Mg`Nw72g6WFjLS zKi_~PR$I&+vNhlCvc=@8%ESAc+6@{vkAx!rOU z(ovXd1z5_lCt42KGlqPain6HoX4l^5E1|63Kq-wyQZSKbCvUdtHg&8>AsF(`7KF7CObA;4dksI$ZSTcmln0#67G3gV^TJw8ReR+9Yrza=gU zy27P0gQYUpwrbC=kt}Bp$l#A0O-G(%KwR;L0!&b=+FIV_?QL@L{E@ov`{=(`SAD@Z zLW=nHx*E19ZUFdSp#1bvrlSX2Q`L$wRiq?3h1<`Cxd6i`>wS$i`rKUj2JExeQ;Jmn zmQ@m!jBQA|xEzq{jm^%c5}JpNjE)A5j*f=gdB;cHoNn|Gpe4j%mVFfTsb{MEKN6W5 z<_siCRS@*t)5D~ZaYE~g;Qx=^ zt9?9LyUH~S6Z@+6+#4bIO}P7|ePhq0LITeV*bkNPp1TNS4)UyE*$Zr$BRSaE9A?7* zs5vgkgu~R{$Wuh0fKfr*2#^9sPJ)lU^G$+iq3QV7)8pZ=jPT>9ZE4K1@W_91p^r(G zSc1N~WZxlC5~bRxx9$y5>sBG!D1LTS|3wfQ-Nl7@!7rwamQTkzX79#9f zxZh}AxP3a$sVXa=nV)Wo{rzIzUpr$he0I1b=>F%UW)9BlORnAHt>TOb$Rm`#2%6fI zGBq`I?;?_oexj!*wiM0P9rY-hxn}0qpCt(i35P!PCT&65z*hYh2=xvmSTY()Py5fV z=vmZKq@C|g@0$Q4+gUi}nom2Mzw7yZa^#-X*C4G7B&l(EMStepJ`VWnXRgZB7@r{%dhPQ%^J^F}_{t0bsQWIzJ`E{~ z5j-F!CJZWfgKAiJ*+4@+BF>ghoQd3cQ^DtCr|Vj%AuJDLrLHdI9DX|APxU@pvrGXe zQpQdjl9^FKmDQ1FL(I`@#2^#_s=s4P%B!)r~h{$=~tm0jZvKBye} zdrn|0C_QZjp2Ba;foR&Fd;vHyL~jF0NoLuf*idvZ`dfB^DrA{TK;V_-M$zB1_i(>^ zJVY-*h5y8^`W}qaA25R7jpvMLo|%}ba=A;N2oC1jGROIlx5y-8VKTjazPFUxwe-Ns zYO|utNr;Y+RuJ*Bm3yY4h{;YOSR-<=yZgs~S^^sTRAeXdG=; z`*~fb(zHKSBhBN_fOL*X_Rt!N?fnLB9e6bI7A{FFqm_5%>4Ol|zhFjndJHmQYnK!| zcTKJe&c751k-mPXfH!ufh9B9Z_#a{K;J6ucoIsd3M!X8*9!Y=M*(QS`1J; zwH4CsD)XWr5kC|uxm!+sLxw+H9BF$a2&qJMCAH*@?C1ZodYX&Nlgn@`P{r4$JRdId z)XEqa!|kcInkPdU5Yhkq$dCG&;Ns`Y(!VRwKO5@@4@dhj{8eF;B?V77zvBvNxS~UM z$S245yo!sXfMOIRb0G7fUO4*aMO|GTkielX7oijj{koYA>~X!KrqzCfEeF&h7ZI~kfT$b1gBlK z{hWP^!Vg;+jr&dPojY$oJ3S4KOF23I9B&L+G5F{fyFA$1*xFKjcE~yQf5gaP=C?_d z6vBU)QI7^-uM<&+m zuS?fT2+-4n$1DL4uVKD?0M>{ugVpkmDnd#ons3Qrz>I9bjPKM-khag=BP6&;&6OuI zG4v!KaeHPk8Sind7rrkq05bydfEa=J57)k$=qql$c5wb$26iFIWiqw6E zsHNgl+Fzcd6T>MU3;Xr!7h6*iWIpH?ivmgiyK-0AhNIty#{xhyUhiUqim1b3`tz+( zE{}C4|F0nSYluG9H7-)qCSN;M!^Spx1*3Ro6(QJMcyf`YUfXkD-8(9%jtMp^jr}!} zC|rP0i+lTCrVC~5wxpkpp*{gQbgGRUl`|kwjJS}r=8eRvP-oZVj|paFYm0%xi<=;J z$w!%>+agd0P?l)LZ27Oa6X!#omqHG2X5xx-Jrsb4-nQ)T?}t-2L-^}luagpZg(O!d zw`h5ZN?d$`AOLDpmOcIVMZ+L}9Yu$aiEj4dMbG$%#KhjxV2UuL=XoD4eEasK6O%Y+ zD5R*U=+C5o5FyPTL(&=R%-i+=tblb@fz z781PVC8@JdZQbWS4K&v=WOe)<++Th$ku_2Ne0oy5owcT5jd_$PF!#g|K>Dn45Z&HAap;Br54H zEZUNx$R5`WhEi)#R7QX%45|EFHr_^YK0Uy#_zO0hyvC2_zO1soZ>2^D)gRb$97j>t?NWAGuDsJyz=9wv@x| z(YNEfDY_@8VO`@dA6CeKZsfbyp~6YW%c%vQ1L0rZCpQm|DWk5DE_ufzKNOvciG~}& zV4B4mue?1vNkQ1>LV`5)a7M~=Cv$51&|zyKs_B4+C0p=Lz^cXCbMNrM|6w)Ys=0Yc z+UG=l$j(m&?JNrlThh*dI8S?>CTa`B#3dk=JHLFx>7WvUd987_A_op%l%zl-bhv+9;*&$)e{ekM8p_mW;pd%TFv z`yyworoFY48fzV=zDcGY9$m2;twSy}UR^;0_Dh;4eL+R7*hZ3llbDG1rKgAM2Xmb* zW|-OCJXFtO)14)6ffOR$TlGfbhP>Oo{ikupCqGvi>pT<&A@dT+QF2z=i+&CTlB z^-BaHj(*i;PfL9=z0elKn#f}f1wae`#3AKb$NZGsP%6Uy#-^A{O8NEzM zc=X;}jzdj#t=xY4?`vafF}E%BSwRkip6*A(#d*ln_3nm~EZ2J)aXS`1xW#{hKq`YT z-5>RkmbObpxc2?hG-txL7<~;|wIxs9&^IhWdv7v~1=hP%J*BPccb0sNVxHpRM@lLw z7qER4yEI|1miXz^=a|>?<18k~X5t+WeYQ@OJa;^b(|ycnHxKlp=9(SCvd>ffY%%+r zoUvz{h6L|&2kr&h^V!^4c-4-P5PsvEeY>XTeqV2mgnI7Z{=eJD20#C<=!Q#QIjk(q z=N7*6_J}6Rz}4^))N3J>OA?^5m2SU1Jy1d_P>|mVMJgF~s8jiw35URTDbmT;sW}^2 zBl&sW?*j87F88Y$;2Y70Pa5&k#!E>K<|+s~$&C{$Z$hCuzP6ULguUl*pm?@R z?`)e6KSFWgO%&g}gNwCauJX+J>dhQdna@0sSmO28_)>;%XG_~Gd^cE+_G+8lA~91b zCcVXUfY_m}NL5j8UEVGo18q>w7Fp`r{g(&r4BN+7)>+&^)vjHhil^=Nv#q9>Z zHVe-&guFjM;s*6c7)qy*G*?-TG`w8vStO?S`6covg(@&TgY0??1N)uQygYn?g5uWe zF8uR-fvV5;ww?~bX-#YQ{hu96eAH12wR(|XY9+6_{=$kMOi)QvB_P^RXFdNTuA~rS zoXjhi<&E(c!KO72BI&-U1p%8G#7g5>dS7f35iJ)zz3=J$#^4*GhfWpnoU0M!thYGZ z=GDnwW-5g%dT}Ec1&VYc`e&P{UG$nwM$e^IF?NuoX`qzu!>d^@DDGwZ!hhQ2O zU)cpyc778RjGTXE*T9haX>#3u_??>mZZ~;NXYA3WN6gv*UJZZ*p}-*d)`p z7TaiH!ERgnAs>i!9$sF!g~XlvL)Xd`NnmgsNb}RGmV%+tyEh~6a>xgKo4H$_LoS%< zJGVUdBROnO0)6@U*H1|rr~9J_7m3c+h~eoLCs8=oEwyiClixB4@(PFW4q`6iQRnCH zsjFsn<*oA+6y5HE-=6gJ+}sA$nI)gvyo*gUcA>nJ`n#-4_Uvyy^+)Q8+v?Nq<=-xK zk{?NCkorAyHHY~m;v5$>VKe(v0~H;y8!W~nVOs?AO8rSP-8^;lby#s%$4vGPF&1#$ z-4z!+ic{NQf&_|+TXV!53FsS~F!0L@(pkX;kOpnfJX2Ww>+)2!6;NusRBLp}FKN_> zC$2@V^+5wO;lkT9;twIiLEo;9j?G`We|H`X(l_Grs#KgY_4)BDqr>biT$pMdiyw{? zZ@XN&%fjbh-JxYj_HC-bJ&HDr)jOG^BqVyjQQKZJT|(hAQ-`4%M#bfBXhu8denzEx ziIC}9k4$&&d{V4&z;(NVyxZ;Z>|MMdyd;EY*L6XFlFW-<-HobXyJeGMn ze)5&%>RF10X_fELY-=@T+Qiu87b{~(Ba<0A2K5xkWhUOo5wMExt`^2YFN%Csi6!(xkO%nMx5ia0jC1^(vLbcAH>eS5AqS1VLg| z!ncMjiijk6b)CYlf!)l*9j&b;>1Lsz_N^#Gkc5n?9pEoQLQi5B?;QDJ>OEDzEA8W#D(JlQq(9m>kz1|Rw=(1kv*@vCY}T_+?`cNgyJGCF471&)kQE}Lc^6Sfg0nwH0$y*~hubv`0uojXu$}J(}h$_ylS` zACFV~ldWWFzSK`JS;DqMLJ0z6i$_lUtZeVj=6b%VlQrH9T-dx%0FvI)Y7outNLQO| zHhC>~wlPtznxVmsVk3h5aJ+ZJwm)LyiHB^|2-vvKL7S4R zyJEYFM&R6C8>FQ9olr+1GRiD(v9Ti7cAL#{!Q$oMj2%qj#+;o`ivEJv1YhcHUJIqX4Osf_5;-s{evYt)4U@9^Qy#muY%&f0o92L4=bf++3$3~my)>4}~ z+08%}TTnRXJd6<(5%wEuk69ISK`TD_`x4)`XT(sh;pq;uLZGto@>wfU_gBz7uySxd z)R0v=^4X(2_c1ReywM@aT7T#?)JcP)M&QN-@WlUK-__gF@hz#l1Z-}8vT}s?ETcQ{ zxpI@?vjA5E{3e$^BUdJD4F#{Z$KB$}adx{>7^0;{OAT!Iz|*7Xn$OaGe}@HJ>q-3< zIs!I3>GnrJ+c?&*?0BB6XsvwP|62S6rKdhUBygS2-ph0D2)KSn7QozV%B1}Bzq z6tI8RA*gXVuDhYtn#={JUtFC03B?^PxmD(EL@SMEm$bSv?10Bz5Fqd+;s6SRhIH2X z0t>U78`~jtS4|7|IX=1aMW)d;`a*!2P@J2x(?nB0zW3LrIIR^Zo*^FfQ8bg~+>@7d zPL<;g_E@t*y#7sB+KF+VhPl~*YxD6wk(zcHs*|kVu?KFqxO+Yv2I1@6(Xax3g!ICi zNQ53HjuI@+aaD2q#2Z-Z=xbT`YCC zaVF$SFT1`Q`i=h78EJ@jGT-X}k;;P;9>L(<1y&$?HO4V$aaTtdTeSq9JDp5}jh}CT z_zaxsr^cxZf!}5l|vH*}iH$%^SLj_gg$B%DtQ->J|C$<1+ zfSwhOLXi~Z83kgO!2Py(%5SQ^`Uwm+jgIb9)SM6wM7tzSH7-^ux#%uB_1G>t)if@A z1t@xGlY*Cd;ED^tGTzqwAK%#_yb9uRDiD5FT?o{zxwP6SpD{|d%G~f$8_dNDGS?zUj&GNwo zZYL-@=z9^L^$un-;_#&aO z0#shqq00mnm6!d#pNMAr&IqAB3F+7Kc|jBjf+*B=XB+ErCn?Ih(rTPAM4fxE3yMas zzl*&?6OvOmd@~1uxEfBbuAy;vh2cA-BId&sn*+-Sna_SMP3^2rt=k{roQ$`y(x1g6 z`eb7c>o(P<#iS_>0V$2`a77w(wwJ1BrP<#gmwsphRj@TQW188+LfkLDvv}XV$74?!PwP5-o;&4`2E|Od-c?x4pSrV<$nd{;XFxUGk2w=&Q2mjN~HTzdc4T50|zY zu|eT`ws1gGn%P(L{s7{bG}>BU8(L2y*k?vB(`l2QWs82g9dplm=K0t4ARp`&_84Y7 zNX>%!2(-t~JR5sWP(|_!3IA7D1!7F-zo^mXtf;6EhyQzu@(Bol7{4!H1e`~YD<9!` z{mRbiSpQ;&v zV`KALX|uJf)P55@^bzfUza||nB(3P56QK2Z6w95XJz_C#aDL>!m9l%dP~5kuVnzz_ z#CYQRw(M@_pM;9%$`=St6p3_l-5xu;4lWZF)_}Gx^m~V& zysSp&o84UNRj;)1NX0p4`J$u-W)yC1H2br1v32Hk^Ar>e8X2onLw2LoOQN?zqpYGK z`7O)ND<6XSlyZZL86Oc*@1CPSTOVps9zcMwDQNaeyV<06Sn;$6oi(Le-mEUn&uSzrcMDm5+Jthc-_JnNHnnzdeKqSXYVWFe-H1gO+H{Rtf9UvO8)yyTx z3jw`Mt2esr{d*Qm*ym6fwoyr089tz5+h_$|MAD1hrqJl(>_u6}gVZ*k25$;kP(a|U zzNTT<)71<=x1PY><-dHDOGf20P>u`fE>J%@0c4IKfUy_GfQnP`w5?TdK47*5uD z&zw{myK}qq7M_3&-+a%3c36~ly=M8GDZcORZID;5t|mM~`~(%<(Arnmuba*}5~-k? zg-#GPx@0bFam{XBilA-jIRbrK%S*7R71K2o9MHhj_rfCnlo^9bvktrMP1urk(0~h5 zKy`m0NwXjY1P!Aq8>O?a*HixXYK<+;Zk895rBiV1w*J2A8G{w?vV3&v&BxEb{E;8| zciGwCM(X)*9V+^?wm7zM3@AKNzOd-tY3bbH#x>U zKIQ|Na~~)Qgfg*H$aTJe(g!FgK*dIkk3BFkac^ViLdQm=rU{)p6E=Kr0`V#9t3a2X zM{az>t(3r|h#R5%z|{1nHs`ZWBPdO@;5-?qCzqWbH4hyM32 zoevyRjKm2$J92CRh!Rj8Lj?p?XsChwx%1DNdI40aK}ttwCSwW&A4o@j(6Lu&7Vhd= z+_(yTSD?hn7WuX3&O4bNAgV-Wil6GJ~=xTsFUrdtWG-tXd3`m=yIxySC> zsB4@yCK;l&TkoayATN&P)A0zWa+~(v&8gtkZWVB_P4_E?wYokn~Hhfe&!3Y zPBe-sLLHn(!k;!`=3KpgUoBSjzDiBq%0H&fyL9r>(g^X@FC%04`-_*YEmygtMuvyC zkG|j0O*`rQ=lrDTD=H#E>!ysTw#bGyR1eEVTcm?{@z_$n-s$b?pq{4o|fEa!~U=|yMQiB*__j5#b@fsn%S9TXJEEvGuo@>7ZaTc|>kmEMZ za$JYumqUz^iHTcRBRc;a!0Z{gx~e~4o^WM#>@840kMnABl_zS5_hqRV11b?X<}mBgK=rT$|wTHQ!u z*=YUpi60Gj5yerXP#y!(4k1B<=;R$J@QJQYfM6FiG;2gkykGYB3@B|QcTBW|Tnp`U z3JXykKA`${Xo`dzNoA72a?OxyYiZp#9iQJDh@9H&OHdMGfLoDZUZXoh=pX!=S$lM3 zS?1+Mg?h_ZO|if6B7Wl(t9tKTXi`2?*>t{$)dB%jVIjv-)LZDqwd!saz1C>r{xg z=?zM?BK8EvYE-HYD$^l7r~)HU|Cwn+9pt0QnGt&w->8f?ym++AS;Qeh13IrqdVgSJ zWSO46u0kpWTnK~&K(Cdouo9kM0#*j<1P~yn*$IEmYA$n)u!qi3luG5Go4vEg1$|-f z3&-+7GB~24HKd%JoY05eqwjso2mRl^eXB@oo*cXA-h&=!sLcZq{onLLW3B#flqv7s zI_R%?OHQ(k19E_FHZW$P6zkFAz`#Il0lO~_g0zCrb#vsgZ0fz%hZzra;oF)cbS)+x z+ye;@w9uMN_r40YFJ>MiU}Q=Rfx!Y7^ZhE4^tRD=Z5^f)*cghQP(A?# zdKD2$5)N7@<1c4{UM`{chqi#xg!zjXFT0iozH5Wb?PFo#D-Z;4iOC`V`azW#iS=-8 zafBy)bGGp+=-d)hqJn>*Cg7tuN~H-KF)?+>q#R@|whwWYQlWz|<3=vX+5wXg$}uz` zKOVY5*r3ZrbMjcjUBly{AEXWZ-p1a;oFU_YSm?%qBPI=s@7V3! z65efif|D_`v_vuzi^DP;@k963WV5WByp9-r$%&ns$T#Q=sdIo9gQ!Pf{;b z&B5@8BH3`!Z~7B2Q-ew-qhWYr0xy3=XwxopAmQvB#Px=yDA5oClop_71L7bQ4B`iT z0)q7N@+;r~JobP3O<4BfGPmm6K9Eah{o&9gcP{dXzW}VCZavl~s@LQtsy+9;-I}@x z=#U~KBg4}|P&NN9?O1W4XX?qDru4X&0A4)w^?w9mZon=@)3}z7Si_}^Iv-mzb&Fig zq*SJz?%P5+j3)rczA86*7Hpq4^jY4OAd`f?2sap-|o!a@!HjhvZ+e@tGz`o30*}P&h=iSUv99(gn&Tu zH-5cuh?#8CtrC19B8j-=@ZzSXCO6gAqE5?|q~gC7Jr3|~?rYoN7(6h%P__Zo!HIPDDBOlpP6z#GQd~-|VoBMwR#hd5E3fF5A?rKNC zz6;K9?bz{(X0}7K$q+_;6GBfQC>oM_Q4DoInOSx%SCD1B8+#drs}s5!3JW*$I!XGT zL3bCkY~q2M(|>#?28f*C&;PGqhJk7X{(t`_jj!iKxvS5g?5_su!}p3HWF-}lMGy7; F{}&+*EYSb} diff --git a/doc/source/quickstart/quickstart.rst b/doc/source/quickstart/quickstart.rst index f2ba193ee..ccd14ebd8 100644 --- a/doc/source/quickstart/quickstart.rst +++ b/doc/source/quickstart/quickstart.rst @@ -18,7 +18,9 @@ Preparations To start with a clean slate, let's construct a synthetic signal consisting of a damped harmonic and additive white noise: -.. literalinclude:: /scripts/qs_damped_harm.py +.. literalinclude:: /quickstart/damped_harm.py + +With this we have white noise on both channels, and only channel 1 additionally got the damped harmonic signal. .. hint:: Further details about artifical data generatation can be found at the :ref:`synth_data` section. @@ -30,7 +32,7 @@ We can get some basic information about any Syncopy dataset by just typing its n .. code-block:: python - synth_data + data which gives nicely formatted output: @@ -57,7 +59,7 @@ which gives nicely formatted output: So we see that we indeed got 50 trials with 2 channels and 1000 samples each. Note that Syncopy per default **stores and writes all data on disc**, as this allows for seamless processing of larger than RAM datasets. The exact location and filename of a dataset in question is listed at the ``filename`` field. The standard location is the ``.spy`` directory created automatically in the users home directory. To change this and for more details please see :ref:`setup_env`. .. hint:: - You can access each of the shown meta-information fields separately using standard Python attribute access, e.g. ``synth_data.filename`` or ``synth_data.samplerate``. + You can access each of the shown meta-information fields separately using standard Python attribute access, e.g. ``data.filename`` or ``data.samplerate``. Time-Frequency Analysis @@ -74,7 +76,7 @@ Multitapered Fourier Analysis .. code-block:: - spectra = spy.freqanalsysis(synth_data, method='mtmfft', foilim=[0, 50], taper='dpss', tapsmofrq=3) + spectra = spy.freqanalsysis(data, method='mtmfft', foilim=[0, 50], taper='dpss', tapsmofrq=3) The parameter ``foilim`` controls the *frequencies of interest limits*, so in this case we are interested in the range 0-50Hz. Starting the computation interactively will show additional information:: @@ -83,7 +85,7 @@ The parameter ``foilim`` controls the *frequencies of interest limits*, so in t informing us, that for this dataset a spectral smoothing of 3Hz required 5 Slepian tapers. .. hint:: - Try typing ``spectra.log`` into your intepreter and have a look at :doc:`Trace Your Steps: History ` so learn more about Syncopy's logging features + Try typing ``spectra.log`` into your intepreter and have a look at :doc:`Trace Your Steps: Data Logs ` to learn more about Syncopy's logging features To quickly have something for the eye we can plot the power spectrum using the generic :func:`syncopy.singlepanelplot`:: @@ -92,5 +94,28 @@ To quickly have something for the eye we can plot the power spectrum using the g .. image:: mtmfft_spec.png :height: 250px -The originally very sharp harmonic peak around 30Hz got widened to about 3Hz, for all other frequencies we have the expected flat white noise floor. +The originally very sharp harmonic peak around 30Hz for channel 1 got widened to about 3Hz, channel 2 just has the flat white noise floor. + +The related short time Fourier transform can be computed via ``method='mtmconvol'``, see :func:`~syncopy.freqanalysis` for more details and examples. + + +Wavelet Analysis +---------------- + +`Wavelet Analysis `_, especially with `Morlet Wavelets `_, is a well established method for time-frequency analysis. For each frequency of interest (``foi``), a Wavelet function gets convolved with the signal yielding a time dependent cross-correlation. By (densely) scanning a range of frequencies, a continuous time-frequency representation of the original signal can be generated. + +In Syncopy we can compute the Wavelet transform by calling :func:`~syncopy.freqanalysis` with the ``method='wavelet'` argument:: + + # define frequencies to scan + fois = np.arange(10, 50, step=2) # 2Hz stepping + wav_spectra = spy.freqanalysis(data, method='wavelet', foi=fois) + +To quickly inspect the results for each channel we can use:: + + wav_spectra.multipanelplot() + + +.. image:: wavelet_spec.png + :height: 250px +Again, we see a strong 30Hz signal in the 1st channel, and channel 2 is devoid of any rhythms. However now we also get information along the time axis, the dampening of the harmonic in channel 1 is clearly visible. diff --git a/doc/source/quickstart/wavelet_spec.png b/doc/source/quickstart/wavelet_spec.png new file mode 100644 index 0000000000000000000000000000000000000000..103beb2bc19730efcdcc54b9154ada5f4543bed4 GIT binary patch literal 93344 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�kJqBj!+qRB`m&>x@&0hGm@e# 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 JN)*ie{{w>Qm#zQ+ literal 0 HcmV?d00001 diff --git a/doc/source/user/logging.rst b/doc/source/user/logging.rst index 970bebe33..a098a2a9c 100644 --- a/doc/source/user/logging.rst +++ b/doc/source/user/logging.rst @@ -1,11 +1,11 @@ .. _logging: -Trace Your Steps: Logging -========================== +Trace Your Steps: Data Logs +=========================== -An important feature of Syncopy fostering reproducibility is a ``log`` which gets attached to and propagated between datasets. Typing:: +An important feature of Syncopy fostering reproducibility is a ``log`` which gets attached to and propagated between all datasets. Suppose we have some :class:`~syncopy.SpectralData` and we want to know how we did arrive at these results, typing:: - spectra.log + spectral_data.log Gives a output like this:: @@ -36,4 +36,4 @@ Gives a output like this:: tapsmofrq = 3 -We see that from the creation of the original :class:`~syncopy.AnalogData` all steps needed to compute our new :class:`~syncopy.SpectralData` got recorded. +We see that from the creation of the original :class:`~syncopy.AnalogData` all steps needed to compute the new :class:`~syncopy.SpectralData` got recorded. In this example the specta were computed via the multitapered FFT, with a spectral smoothing box (``tapsmofrq``) of 3Hz which required 5 Slepian tapers. The frequencies of interest (``foi``) range from 0Hz to 50Hz with 0.5Hz stepping and ``keeptrials`` was set to ``True``, meaning that this dataset contains the results for all trials separately. diff --git a/doc/source/user/synth_data.rst b/doc/source/user/synth_data.rst index 516a724db..c53a050f0 100644 --- a/doc/source/user/synth_data.rst +++ b/doc/source/user/synth_data.rst @@ -52,7 +52,7 @@ These generators return single-trial NumPy arrays, so to import them into Syncop .. autofunction:: AR2_network -Synthetic Data from Scratch +Example: Noisy Harmonics --------------------------- We can easily create custom synthetic datasets using basic `NumPy `_ functionality and Syncopy's :class:`~syncopy.AnalogData`. diff --git a/syncopy/plotting/spy_plotting.py b/syncopy/plotting/spy_plotting.py index 85f63a25f..e700e2c87 100644 --- a/syncopy/plotting/spy_plotting.py +++ b/syncopy/plotting/spy_plotting.py @@ -54,12 +54,12 @@ pltConfig = {"singleTitleSize": 12, "singleLabelSize": 14, "singleTickSize": 12, - "singleLegendSize": 10, + "singleLegendSize": 12, "singleFigSize": (6.4, 4.8), - "multiTitleSize": 10, - "multiLabelSize": 8, - "multiTickSize": 6, - "multiLegendSize": 8, + "multiTitleSize": 14, + "multiLabelSize": 14, + "multiTickSize": 10, + "multiLegendSize": 12, "multiFigSize": (10, 6.8)} # Global consistent error message if matplotlib is missing From b14e3aad1cd6e78f63d570d04de9e15d7700e3da Mon Sep 17 00:00:00 2001 From: tensionhead Date: Tue, 8 Feb 2022 11:51:22 +0100 Subject: [PATCH 019/166] NEW: Quickstart for time-freq analysis Changes to be committed: modified: doc/source/quickstart/quickstart.rst modified: doc/source/setup.rst modified: doc/source/user/users.rst --- doc/source/quickstart/quickstart.rst | 28 +++++++++++++++++++++------- doc/source/setup.rst | 2 ++ doc/source/user/users.rst | 3 ++- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/doc/source/quickstart/quickstart.rst b/doc/source/quickstart/quickstart.rst index ccd14ebd8..8f7514e71 100644 --- a/doc/source/quickstart/quickstart.rst +++ b/doc/source/quickstart/quickstart.rst @@ -76,7 +76,7 @@ Multitapered Fourier Analysis .. code-block:: - spectra = spy.freqanalsysis(data, method='mtmfft', foilim=[0, 50], taper='dpss', tapsmofrq=3) + fft_spectra = spy.freqanalsysis(data, method='mtmfft', foilim=[0, 50], taper='dpss', tapsmofrq=3) The parameter ``foilim`` controls the *frequencies of interest limits*, so in this case we are interested in the range 0-50Hz. Starting the computation interactively will show additional information:: @@ -85,11 +85,11 @@ The parameter ``foilim`` controls the *frequencies of interest limits*, so in t informing us, that for this dataset a spectral smoothing of 3Hz required 5 Slepian tapers. .. hint:: - Try typing ``spectra.log`` into your intepreter and have a look at :doc:`Trace Your Steps: Data Logs ` to learn more about Syncopy's logging features + Try typing ``fft_spectra.log`` into your intepreter and have a look at :doc:`Trace Your Steps: Data Logs ` to learn more about Syncopy's logging features To quickly have something for the eye we can plot the power spectrum using the generic :func:`syncopy.singlepanelplot`:: - spectra.singlepanelplot() + fft_spectra.singlepanelplot() .. image:: mtmfft_spec.png :height: 250px @@ -104,18 +104,32 @@ Wavelet Analysis `Wavelet Analysis `_, especially with `Morlet Wavelets `_, is a well established method for time-frequency analysis. For each frequency of interest (``foi``), a Wavelet function gets convolved with the signal yielding a time dependent cross-correlation. By (densely) scanning a range of frequencies, a continuous time-frequency representation of the original signal can be generated. -In Syncopy we can compute the Wavelet transform by calling :func:`~syncopy.freqanalysis` with the ``method='wavelet'` argument:: +In Syncopy we can compute the Wavelet transform by calling :func:`~syncopy.freqanalysis` with the ``method='wavelet'`` argument:: # define frequencies to scan fois = np.arange(10, 50, step=2) # 2Hz stepping - wav_spectra = spy.freqanalysis(data, method='wavelet', foi=fois) + wav_spectra = spy.freqanalysis(data, + method='wavelet', + foi=fois, + parallel=True, + keeptrials=False) +Here we used two additional parameters supported by every Syncopy analysis method: + +- ``parallel=True`` invokes Syncopy's parallel processing engine +- ``keeptrials=False`` triggers trial averaging + +.. hint:: + + If parallel processing is unavailable, have a look at :ref:`install_acme` + To quickly inspect the results for each channel we can use:: wav_spectra.multipanelplot() - .. image:: wavelet_spec.png :height: 250px -Again, we see a strong 30Hz signal in the 1st channel, and channel 2 is devoid of any rhythms. However now we also get information along the time axis, the dampening of the harmonic in channel 1 is clearly visible. +Again, we see a strong 30Hz signal in the 1st channel, and channel 2 is devoid of any rhythms. However now we also get information along the time axis, the dampening of the harmonic in channel 1 is clearly visible for later time points. + +An improved method, the superlet transform, providing super-resolution time-frequency representations can be computed via ``method='superlet'``, see :func:`~syncopy.freqanalysis` for more details and examples. diff --git a/doc/source/setup.rst b/doc/source/setup.rst index e9520d0f4..32f1631e4 100644 --- a/doc/source/setup.rst +++ b/doc/source/setup.rst @@ -16,6 +16,8 @@ Alternatively it is also available on `Pip `_: If you're working on the ESI cluster installing Syncopy is only necessary if you create your own Conda environment. +.. _install_acme: + Installing parallel processing engine ACME -------------------------------------------- diff --git a/doc/source/user/users.rst b/doc/source/user/users.rst index acbf06561..d9b70e699 100644 --- a/doc/source/user/users.rst +++ b/doc/source/user/users.rst @@ -12,7 +12,8 @@ files. fieldtrip data_handling - synth_data + synth_data + logging processing user_api From 6c97d3988458a51f6cefdd09f415c24c42e40940 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Tue, 8 Feb 2022 14:01:07 +0100 Subject: [PATCH 020/166] FIX: Minor edits/corrections - fixed some typos or misnamings On branch doc-improvements Changes to be committed: modified: doc/source/conf.py modified: doc/source/quickstart/quickstart.rst modified: doc/source/setup.rst modified: doc/source/user/logging.rst --- doc/source/conf.py | 2 +- doc/source/quickstart/quickstart.rst | 22 +++++++++++----------- doc/source/setup.rst | 16 ++++++---------- doc/source/user/logging.rst | 2 +- 4 files changed, 19 insertions(+), 23 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 04decf0b7..93d2e0816 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -21,7 +21,7 @@ # -- Project information ----------------------------------------------------- project = 'Syncopy' -copyright = '2020, Joscha Schmied and Stefan Fuertinger' +copyright = '2020, Joscha Schmiedt and Stefan Fuertinger' author = 'Joscha Schmiedt, Stefan Fuertinger and Gregor Mönke' # The short X.Y version diff --git a/doc/source/quickstart/quickstart.rst b/doc/source/quickstart/quickstart.rst index 8f7514e71..e4b60d888 100644 --- a/doc/source/quickstart/quickstart.rst +++ b/doc/source/quickstart/quickstart.rst @@ -10,9 +10,9 @@ Here we want to quickly explore some standard analyses for analog data (e.g. MUA :local: .. note:: - Installation of Syncopy itself is covered in :doc:`here `. + Installation of Syncopy itself is covered in :doc:`here `. + - Preparations ============ @@ -23,7 +23,7 @@ To start with a clean slate, let's construct a synthetic signal consisting of a With this we have white noise on both channels, and only channel 1 additionally got the damped harmonic signal. .. hint:: - Further details about artifical data generatation can be found at the :ref:`synth_data` section. + Further details about artificial data generation can be found at the :ref:`synth_data` section. Data Object Inspection ====================== @@ -31,7 +31,7 @@ Data Object Inspection We can get some basic information about any Syncopy dataset by just typing its name in an interpreter: .. code-block:: python - + data which gives nicely formatted output: @@ -56,12 +56,12 @@ which gives nicely formatted output: Use `.log` to see object history -So we see that we indeed got 50 trials with 2 channels and 1000 samples each. Note that Syncopy per default **stores and writes all data on disc**, as this allows for seamless processing of larger than RAM datasets. The exact location and filename of a dataset in question is listed at the ``filename`` field. The standard location is the ``.spy`` directory created automatically in the users home directory. To change this and for more details please see :ref:`setup_env`. +So we see that we indeed got 50 trials with 2 channels and 1000 samples each. Note that Syncopy per default **stores and writes all data on disk**, as this allows for seamless processing of larger than RAM datasets. The exact location and filename of a dataset in question is listed at the ``filename`` field. The standard location is the ``.spy`` directory created automatically in the user's home directory. To change this and for more details please see :ref:`setup_env`. .. hint:: You can access each of the shown meta-information fields separately using standard Python attribute access, e.g. ``data.filename`` or ``data.samplerate``. - + Time-Frequency Analysis ======================= @@ -85,8 +85,8 @@ The parameter ``foilim`` controls the *frequencies of interest limits*, so in t informing us, that for this dataset a spectral smoothing of 3Hz required 5 Slepian tapers. .. hint:: - Try typing ``fft_spectra.log`` into your intepreter and have a look at :doc:`Trace Your Steps: Data Logs ` to learn more about Syncopy's logging features - + Try typing ``fft_spectra.log`` into your interpreter and have a look at :doc:`Trace Your Steps: Data Logs ` to learn more about Syncopy's logging features + To quickly have something for the eye we can plot the power spectrum using the generic :func:`syncopy.singlepanelplot`:: fft_spectra.singlepanelplot() @@ -107,7 +107,7 @@ Wavelet Analysis In Syncopy we can compute the Wavelet transform by calling :func:`~syncopy.freqanalysis` with the ``method='wavelet'`` argument:: # define frequencies to scan - fois = np.arange(10, 50, step=2) # 2Hz stepping + fois = np.arange(10, 50, step=2) # 2Hz stepping wav_spectra = spy.freqanalysis(data, method='wavelet', foi=fois, @@ -122,11 +122,11 @@ Here we used two additional parameters supported by every Syncopy analysis metho .. hint:: If parallel processing is unavailable, have a look at :ref:`install_acme` - + To quickly inspect the results for each channel we can use:: wav_spectra.multipanelplot() - + .. image:: wavelet_spec.png :height: 250px diff --git a/doc/source/setup.rst b/doc/source/setup.rst index 32f1631e4..e86faf2b7 100644 --- a/doc/source/setup.rst +++ b/doc/source/setup.rst @@ -1,11 +1,11 @@ -Install Syncopy +Install Syncopy =============== Syncopy can be installed using `conda `_: .. code-block:: bash - conda install esi-syncopy + conda install -c conda-forge esi-syncopy Alternatively it is also available on `Pip `_: @@ -28,7 +28,7 @@ Again either via conda .. code-block:: bash - conda install esi-acme + conda install -c conda-forge esi-acme or pip @@ -54,7 +54,7 @@ accessed with the ``spy.`` prefix, e.g. spy.load("~/testdata.spy") .. _start_parallel: - + Starting Up Parallel Workers ---------------------------- @@ -91,13 +91,9 @@ by default points to your home directory: SPYTMPDIR=~/.spy The performance of Syncopy strongly depends on the read and write speed in -this folder. On the `ESI JupyterHub `_, the -variable is set to use the high performance storage: +this folder. On the ESI cluster, the variable is set to use the high performance +storage: .. code-block:: bash SPYTMPDIR=/cs/home/$USER/.spy - - - - diff --git a/doc/source/user/logging.rst b/doc/source/user/logging.rst index a098a2a9c..cbe301468 100644 --- a/doc/source/user/logging.rst +++ b/doc/source/user/logging.rst @@ -36,4 +36,4 @@ Gives a output like this:: tapsmofrq = 3 -We see that from the creation of the original :class:`~syncopy.AnalogData` all steps needed to compute the new :class:`~syncopy.SpectralData` got recorded. In this example the specta were computed via the multitapered FFT, with a spectral smoothing box (``tapsmofrq``) of 3Hz which required 5 Slepian tapers. The frequencies of interest (``foi``) range from 0Hz to 50Hz with 0.5Hz stepping and ``keeptrials`` was set to ``True``, meaning that this dataset contains the results for all trials separately. +We see that from the creation of the original :class:`~syncopy.AnalogData` all steps needed to compute the new :class:`~syncopy.SpectralData` got recorded. In this example the spectra were computed via the multitapered FFT, with a spectral smoothing box (``tapsmofrq``) of 3Hz which required 5 Slepian tapers. The frequencies of interest (``foi``) range from 0Hz to 50Hz with 0.5Hz stepping and ``keeptrials`` was set to ``True``, meaning that this dataset contains the results for all trials separately. From 8fddeace55f47ccf8b318c5cabdc6459bf04f928 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Tue, 8 Feb 2022 14:45:24 +0100 Subject: [PATCH 021/166] FIX: Minor text fixes - we still need to discuss object vs. data monikers Changes to be committed: modified: doc/source/quickstart/quickstart.rst modified: doc/source/user/synth_data.rst --- doc/source/quickstart/quickstart.rst | 8 ++++---- doc/source/user/synth_data.rst | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/source/quickstart/quickstart.rst b/doc/source/quickstart/quickstart.rst index e4b60d888..bccd6bdb5 100644 --- a/doc/source/quickstart/quickstart.rst +++ b/doc/source/quickstart/quickstart.rst @@ -1,6 +1,6 @@ -**************************** -A Quick Tour with Syncopy -**************************** +************************ +Quickstart with Syncopy +************************ .. currentmodule:: syncopy @@ -94,7 +94,7 @@ To quickly have something for the eye we can plot the power spectrum using the g .. image:: mtmfft_spec.png :height: 250px -The originally very sharp harmonic peak around 30Hz for channel 1 got widened to about 3Hz, channel 2 just has the flat white noise floor. +The originally very sharp harmonic peak around 30Hz for channel 1 got widened to about 3Hz, channel 2 just contains the flat white noise floor. The related short time Fourier transform can be computed via ``method='mtmconvol'``, see :func:`~syncopy.freqanalysis` for more details and examples. diff --git a/doc/source/user/synth_data.rst b/doc/source/user/synth_data.rst index c53a050f0..09e5d24ab 100644 --- a/doc/source/user/synth_data.rst +++ b/doc/source/user/synth_data.rst @@ -13,7 +13,7 @@ For testing and demonstrational purposes it is always good to work with syntheti General Recipe -------------- -To create a synthetic data set follow the following steps: +To create a synthetic data set follow these steps: - write a function which returns a single trial with desired shape ``(nSamples, nChannels)``, such that each trial is a 2d-:class:`~numpy.ndarray` - collect all the trials into a Python ``list``, for example with a list comprehension or simply a for loop From 63f4192e7ed0fd842480558815d3d55f0e370e1b Mon Sep 17 00:00:00 2001 From: tensionhead Date: Wed, 9 Feb 2022 17:29:52 +0100 Subject: [PATCH 022/166] NEW: FT Signal Importer - Prototype - used scipy.io.loadmat to parse the FT data structures into numpy arrays - AnalogData initialization from lists - could successfully read one example dataset On branch ft-importer Changes to be committed: new file: syncopy/io/load_ft.py --- syncopy/io/load_ft.py | 169 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 syncopy/io/load_ft.py diff --git a/syncopy/io/load_ft.py b/syncopy/io/load_ft.py new file mode 100644 index 000000000..12ba2ad0c --- /dev/null +++ b/syncopy/io/load_ft.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +# +# Load data Field Trip .mat files +# + +# Builtin/3rd party package imports +import numpy as np +from scipy import io as sci_io +import re + +# Local imports +from syncopy.shared.errors import (SPYTypeError, SPYValueError, SPYIOError, SPYInfo, + SPYError, SPYWarning) + +from syncopy import AnalogData + + +def load_ft_signals(filename, select_structures=None, **lm_kwargs): + + ''' + Imports time-series data from Field Trip + into potentially multiple `~syncopy.AnalogData` objects, + one for each structure. + + Intended for Matlab versions < 7.3, since 7.3 Matlab + also uses hdf5. Here another local helper should + be implemented + + The aim is to parse each FT data structure, which + have the following fields (Syncopy analogon on the right): + + FT Syncopy + + label - channel + trial - trial + time - time + fsample - samplerate + cfg - ? + + The FT `cfg` contains a lot of meta data which at the + moment we don't import into Syncopy. + + This is still experimental code, use with caution!! + ''' + + raw_dict = sci_io.loadmat(filename, + mat_dtype=True, + simplify_cells=True, + **lm_kwargs) + + bytes_ = raw_dict['__header__'] + header = bytes_.decode() + version = _get_Matlab_version(header) + + if version >= 7.3: + raise NotImplementedError("Only Matlab < 7.3 is supported") + + struct_keys = [key for key in raw_dict.keys() if '__' not in key] + + if len(struct_keys) == 0: + SPYValueError(legal="At least one structure", + varname=filename, + actual="No structure found" + ) + + msg = f"Found {len(struct_keys)} structure(s): {struct_keys}" + SPYInfo(msg) + + out_dict = {} + + # load all structures + if select_structures is None: + for key in struct_keys: + + structure = raw_dict[key] + data = _read_mat_structure(structure) + out_dict[key] = data + + # load only a subset + else: + for key in select_structures: + if key not in struct_keys: + msg = f"Could not find structure `{key}` in {filename}" + SPYWarning(msg) + continue + + structure = raw_dict[key] + + data = _read_mat_structure(structure) + out_dict[key] = data + + return out_dict + + +def _read_mat_structure(structure): + + ''' + Local helper to parse a single FT structure + and return an `~syncopy.AnalogData` object + + Intended for Matlab Version < 7.3 + + This is the translation from FT to Syncopy: + + FT Syncopy + + label - channel + trial - trial + time - time + fsample - samplerate + cfg - ? + + Each trial in FT has nChannels x nSamples ordering, + Syncopy has nSamples x nChannels + ''' + + # nTrials = structure["trial"].shape[0] + trials = [] + + # 1st trial as reference + nChannels, nSamples = structure["trial"][0].shape + + # check equal trial lengths + for trl in structure["trial"]: + + if trl.shape[-1] != nSamples: + lgl = 'Trials of equal lengths' + actual = 'Trials of unequal lengths' + raise SPYValueError(lgl, varname="load .mat", actual=actual) + + # channel x sample ordering in FT + trials.append(trl.T.astype(np.float32)) + + # initialize AnalogData + data = AnalogData(trials, samplerate=structure['fsample']) + + # get the channel ids + channels = structure["label"] + # set the channel ids + data.channel =list(channels.astype(str)) + + # update trialdefinition + times_array = np.vstack(structure["time"]) + # nTrials x nSamples + offsets = times_array[:, 0] + + # does not do anything :/ + data.trialdefinition[:, 2] = offsets + + return data + + +def _get_Matlab_version(header): + + # matches for example 'MATLAB 5.01' + # with the version as only capture group + + pattern = re.compile("^MATLAB\s(\d*\.\d*)") + + match = pattern.match(header) + + if not match: + lgl = 'Recognized .mat file' + actual = 'not recognized .mat file' + raise SPYValueError(lgl, 'load .mat', actual) + + version = float(match.group(1)) + + return version From 65997c3229784fe361a90116e3ee3a59eac7f903 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Wed, 9 Feb 2022 17:57:12 +0100 Subject: [PATCH 023/166] NEW: Wrapped up NWB import code - included `tqdm` support for showing progress bars - write object logs On branch nwb Changes to be committed: modified: syncopy/io/_read_nwb.py modified: syncopy/tests/local_spy.py --- syncopy/io/_read_nwb.py | 37 +++++++++++++++++++++++++------------ syncopy/tests/local_spy.py | 2 +- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/syncopy/io/_read_nwb.py b/syncopy/io/_read_nwb.py index 7641d98ed..1c4f2260c 100644 --- a/syncopy/io/_read_nwb.py +++ b/syncopy/io/_read_nwb.py @@ -8,12 +8,13 @@ import h5py import subprocess import numpy as np +from tqdm import tqdm # Local imports from syncopy import __nwb__ from syncopy.datatype.continuous_data import AnalogData from syncopy.datatype.discrete_data import EventData -from syncopy.shared.errors import SPYError, SPYValueError, SPYWarning +from syncopy.shared.errors import SPYError, SPYValueError, SPYWarning, SPYInfo from syncopy.shared.parsers import io_parser, scalar_parser # Conditional imports @@ -136,8 +137,14 @@ def read_nwb(filename, memuse=3000): epochs = nwbfile.epochs[:] trl = np.zeros((epochs.shape[0], 3), dtype=np.intp) trl[:, :2] = (epochs - tStarts[0]) * sRates[0] + msg = "Found {} trials".format(trl.shape[0]) else: trl = np.array([[0, nSamples, 0]]) + msg = "No trial information found. Proceeding with single all-encompassing trial" + + # Print status update to inform user + SPYInfo(msg) + SPYInfo("Creating AnalogData object...") # If TTL data was found, ensure we have exactly one set of values and associated # channel markers @@ -151,7 +158,10 @@ def read_nwb(filename, memuse=3000): raise SPYValueError(lgl, varname=ttlVals[0].name, actual=act) # Use provided TTL data to initialize `EventData` object + evtData = None if len(ttlVals) > 0: + msg = "Creating separate EventData object for embedded TTL pulse data..." + SPYInfo(msg) evtData = EventData(dimord=EventData._defaultDimord) h5evt = h5py.File(evtData.filename, mode="w") evtDset = h5evt.create_dataset("data", dtype=np.result_type(*ttlDtypes), @@ -160,7 +170,7 @@ def read_nwb(filename, memuse=3000): evtDset[:, 1] = ttlVals[0].data[()] evtDset[:, 2] = ttlChans[0].data[()] evtData.data = evtDset - evtData.samplerate = 1 / ttlChans[0].timestamps__resolution + evtData.samplerate = float(1 / ttlChans[0].timestamps__resolution) if hasTrials: evtData.trialdefinition = trl @@ -174,12 +184,16 @@ def read_nwb(filename, memuse=3000): angDset = h5ang.create_dataset("data", dtype=np.result_type(*dTypes), shape=angShape) # Compute actually available memory (divide by 2 since we're working with an add'l tmp array) + pbarDesc = "Reading data in blocks of {} GB".format(round(memuse / 1000, 2)) memuse *= 1024**2 / 2 chanCounter = 0 # Process analog time series data and save stuff block by block (if necessary) - # FIXME: >>>>>>>>>>>>>>>> Use tqdm here - for acqValue in angSeries: + pbar = tqdm(angSeries, position=0) + for acqValue in pbar: + + # Show dataset name in progress bar label + pbar.set_description("Loading {} from disk".format(acqValue.name)) # Given memory cap, compute how many data blocks can be grabbed per swipe nSamp = int(memuse / (np.prod(angDset.shape[1:]) * angDset.dtype.itemsize)) @@ -193,7 +207,7 @@ def read_nwb(filename, memuse=3000): # Write data block-wise to `angDset` (use `del` to wipe blocks from memory) # Use 'unsafe' casting to allow `tmp` array conversion int -> float endChan = chanCounter + acqValue.data.shape[1] - for m, M in enumerate(nBlocks): + for m, M in enumerate(tqdm(nBlocks, desc=pbarDesc, position=1, leave=False)): tmp = acqValue.data[m * nSamp: m * nSamp + M, :] if acqValue.channel_conversion is not None: np.multiply(tmp, gains, out=tmp, casting="unsafe") @@ -209,11 +223,10 @@ def read_nwb(filename, memuse=3000): angData.samplerate = sRates[0] angData.trialdefinition = trl - # # Write log-entry - # msg = "Read files v. {ver:s} ".format(ver=jsonDict["_version"]) - # msg += "{hdf:s}\n\t" + (len(msg) + len(thisMethod) + 2) * " " + "{json:s}" - # out.log = msg.format(hdf=hdfFile, json=jsonFile) - - - import ipdb; ipdb.set_trace() + # Write logs + msg = "Read data from NWB file {}".format(nwbFullName) + angData.log = msg + if evtData is not None: + evtData.log = msg + return angData, evtData diff --git a/syncopy/tests/local_spy.py b/syncopy/tests/local_spy.py index 30c26229c..822c52c61 100644 --- a/syncopy/tests/local_spy.py +++ b/syncopy/tests/local_spy.py @@ -29,7 +29,7 @@ nwbFilePath = "/home/fuertingers/Documents/job/SyNCoPy/Data/tt2.nwb" # nwbFilePath = "/home/fuertingers/Documents/job/SyNCoPy/Data/test.nwb" - spy.read_nwb(nwbFilePath) + a, b = spy.read_nwb(nwbFilePath) nwbio = NWBHDF5IO(nwbFilePath, "r", load_namespaces=True) nwbfile = nwbio.read() From fa05b43c1550c1431ff3fca1481b6f636aa7fdc3 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Thu, 10 Feb 2022 08:33:47 +0100 Subject: [PATCH 024/166] FIX: Trialdefinition for FT importer - trialdefinition constrcted from the auto-generated sampleinfo and the time field of the FT structure Changes to be committed: modified: syncopy/io/load_ft.py --- syncopy/io/load_ft.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/syncopy/io/load_ft.py b/syncopy/io/load_ft.py index 12ba2ad0c..25d669b8a 100644 --- a/syncopy/io/load_ft.py +++ b/syncopy/io/load_ft.py @@ -5,7 +5,7 @@ # Builtin/3rd party package imports import numpy as np -from scipy import io as sci_io +from scipy import io as sio import re # Local imports @@ -43,10 +43,10 @@ def load_ft_signals(filename, select_structures=None, **lm_kwargs): This is still experimental code, use with caution!! ''' - raw_dict = sci_io.loadmat(filename, - mat_dtype=True, - simplify_cells=True, - **lm_kwargs) + raw_dict = sio.loadmat(filename, + mat_dtype=True, + simplify_cells=True, + **lm_kwargs) bytes_ = raw_dict['__header__'] header = bytes_.decode() @@ -141,11 +141,12 @@ def _read_mat_structure(structure): # update trialdefinition times_array = np.vstack(structure["time"]) + # nTrials x nSamples - offsets = times_array[:, 0] + offsets = times_array[:, 0] * data.samplerate - # does not do anything :/ - data.trialdefinition[:, 2] = offsets + trl_def = np.hstack([data.sampleinfo, offsets[:, None]]) + data.trialdefinition = trl_def return data From ab117d933f2b3138ec4fefc342211c54190d456d Mon Sep 17 00:00:00 2001 From: tensionhead Date: Thu, 10 Feb 2022 09:39:28 +0100 Subject: [PATCH 025/166] FIX: ft_load imports of io module Changes to be committed: modified: syncopy/io/__init__.py modified: syncopy/io/load_ft.py --- syncopy/io/__init__.py | 12 +++++++++++- syncopy/io/load_ft.py | 5 ++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/syncopy/io/__init__.py b/syncopy/io/__init__.py index 496ab8a42..62706d517 100644 --- a/syncopy/io/__init__.py +++ b/syncopy/io/__init__.py @@ -4,11 +4,19 @@ # # Import __all__ routines from local modules -from . import utils, load_raw_binary, load_spy_container, save_spy_container +from . import ( + utils, + load_raw_binary, + load_spy_container, + save_spy_container, + load_ft +) + from .utils import * from .load_raw_binary import * from .load_spy_container import * from .save_spy_container import * +from .load_ft import * # Populate local __all__ namespace __all__ = [] @@ -16,3 +24,5 @@ __all__.extend(load_raw_binary.__all__) __all__.extend(load_spy_container.__all__) __all__.extend(save_spy_container.__all__) +__all__.extend(load_ft.__all__) + diff --git a/syncopy/io/load_ft.py b/syncopy/io/load_ft.py index 25d669b8a..fea66f4ac 100644 --- a/syncopy/io/load_ft.py +++ b/syncopy/io/load_ft.py @@ -12,7 +12,10 @@ from syncopy.shared.errors import (SPYTypeError, SPYValueError, SPYIOError, SPYInfo, SPYError, SPYWarning) -from syncopy import AnalogData +from syncopy.datatype import AnalogData + + +__all__ = ["load_ft_signals"] def load_ft_signals(filename, select_structures=None, **lm_kwargs): From 6b68de3daacac5d43cbdfceecaf02bcc7b3e395a Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Thu, 10 Feb 2022 11:58:38 +0100 Subject: [PATCH 026/166] NEW: Docstring and general-purpose importer - completed `read` function (manager that invokes file-format specific readers) - wrote docstrings On branch nwb Changes to be committed: modified: syncopy/io/_read_nwb.py modified: syncopy/io/read_external.py modified: syncopy/tests/local_spy.py --- syncopy/io/_read_nwb.py | 17 ++++++++-- syncopy/io/read_external.py | 66 +++++++++++++++++++++++++++++++++++-- syncopy/tests/local_spy.py | 2 +- 3 files changed, 80 insertions(+), 5 deletions(-) diff --git a/syncopy/io/_read_nwb.py b/syncopy/io/_read_nwb.py index 1c4f2260c..df72d3dc7 100644 --- a/syncopy/io/_read_nwb.py +++ b/syncopy/io/_read_nwb.py @@ -34,10 +34,23 @@ def read_nwb(filename, memuse=3000): """ - Coming soon... + Read contents of NWB files + Parameters + ---------- + filename : str + Name of (may include full path to) NWB file (e.g., `"/path/to/mydata.nwb"`). memuse : scalar - Approximate in-memory cache size (in MB) for writing data to disk + Approximate in-memory cache size (in MB) for reading data from disk + + Returns + ------- + angData : syncopy.AnalogData + Any NWB `TimeSeries`-like data is imported into an :class:`~syncopy.AnalogData` + object + evtData : syncopy.EventData + If the NWB file contains TTL pulse data, an additional :class:`~syncopy.EventData` + object is instantiated """ # Abort if NWB is not installed diff --git a/syncopy/io/read_external.py b/syncopy/io/read_external.py index f3ffe5988..8ddd82f3d 100644 --- a/syncopy/io/read_external.py +++ b/syncopy/io/read_external.py @@ -3,11 +3,73 @@ # Import data from 3rd party formats # +# Builtin/3rd party package imports +import os + +# Local imports +from syncopy.shared.parsers import io_parser +from syncopy.shared.errors import SPYTypeError, SPYValueError +from ._read_nwb import read_nwb + +supportedFormats = ["nwb"] + __all__ = ["read"] -def read(filename): +def read(filename, format=None): """ - Coming soon.... + Read data from non-native Syncopy file formats + + Parameters + ---------- + filename : str + Name of (may include full path to) file to read + format : None or str + If the external format cannot be inferred from `filename`, the `format` + specifier can be used to manually set it + + Returns + ------- + data : Syncopy data object(s) + Depending on `filename` on or more Syncopy data objects is returned + + Notes + ----- + This manager may be used as general purpose import function for reading + third party file formats. However, to leverage specific functionality of + the respective format-specific reading routines, please invoke the corresponding + functions directly. For instance, if you want to decrease the in-memory + footprint of reading NWB files, use :func:`~syncopy.io._read_nwb.read_nwb` + directly. + + See also + -------- + syncopy.io._read_nwb.read_nwb : read contents of NWB files """ + # Parse basal input args (thorough error checking is performed by the actual + # importer functions) + _, baseName = io_parser(filename, varname="filename", exists=True) + if not isinstance(format, (type(None), str)): + raise SPYTypeError(format, varname="format", expected="string") + + # Ensure we can actually process a provided `format` + if format is not None: + format = format.replace(".", "") + if format not in supportedFormats: + lgl = "one of the following supported formats " +\ + "".join(fmt + ", " for fmt in supportedFormats)[:-2] + raise SPYValueError(lgl, varname="format", actual=format) + + # Try to infer file-format from file extension + if format is None: + _, ext = os.path.splitext(baseName) + if ext == ".nwb": + format = "nwb" + + # Call appropriate importer + if format == "nwb": + read_nwb(filename) + + return + diff --git a/syncopy/tests/local_spy.py b/syncopy/tests/local_spy.py index 822c52c61..8a0f2cd79 100644 --- a/syncopy/tests/local_spy.py +++ b/syncopy/tests/local_spy.py @@ -29,7 +29,7 @@ nwbFilePath = "/home/fuertingers/Documents/job/SyNCoPy/Data/tt2.nwb" # nwbFilePath = "/home/fuertingers/Documents/job/SyNCoPy/Data/test.nwb" - a, b = spy.read_nwb(nwbFilePath) + xx = spy.read(nwbFilePath, format="xyz") nwbio = NWBHDF5IO(nwbFilePath, "r", load_namespaces=True) nwbfile = nwbio.read() From 4ba7dfd571edc92dba6802f1c5c128a604efb052 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 11 Feb 2022 17:19:12 +0100 Subject: [PATCH 027/166] CHG: Non standard FT-format - apparently ppl save additional (meta-information) fields into their FT data structures, added a simple attribute to hold this addition info. Purely explorative, however we might want to add something like this in general (say also for tags and such_ --- syncopy/io/load_ft.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/syncopy/io/load_ft.py b/syncopy/io/load_ft.py index fea66f4ac..3baf6823b 100644 --- a/syncopy/io/load_ft.py +++ b/syncopy/io/load_ft.py @@ -18,7 +18,10 @@ __all__ = ["load_ft_signals"] -def load_ft_signals(filename, select_structures=None, **lm_kwargs): +def load_ft_signals(filename, + select_structures=None, + add_fields=None, + **lm_kwargs): ''' Imports time-series data from Field Trip @@ -76,7 +79,7 @@ def load_ft_signals(filename, select_structures=None, **lm_kwargs): for key in struct_keys: structure = raw_dict[key] - data = _read_mat_structure(structure) + data = _read_mat_structure(structure, add_fields=add_fields) out_dict[key] = data # load only a subset @@ -95,7 +98,7 @@ def load_ft_signals(filename, select_structures=None, **lm_kwargs): return out_dict -def _read_mat_structure(structure): +def _read_mat_structure(structure, add_fields=None): ''' Local helper to parse a single FT structure @@ -111,7 +114,7 @@ def _read_mat_structure(structure): trial - trial time - time fsample - samplerate - cfg - ? + cfg - X Each trial in FT has nChannels x nSamples ordering, Syncopy has nSamples x nChannels @@ -135,23 +138,28 @@ def _read_mat_structure(structure): trials.append(trl.T.astype(np.float32)) # initialize AnalogData - data = AnalogData(trials, samplerate=structure['fsample']) - + adata = AnalogData(trials, samplerate=structure['fsample']) + adata.add_info = {} # get the channel ids channels = structure["label"] # set the channel ids - data.channel =list(channels.astype(str)) + adata.channel =list(channels.astype(str)) # update trialdefinition times_array = np.vstack(structure["time"]) # nTrials x nSamples - offsets = times_array[:, 0] * data.samplerate + offsets = times_array[:, 0] * adata.samplerate - trl_def = np.hstack([data.sampleinfo, offsets[:, None]]) - data.trialdefinition = trl_def + trl_def = np.hstack([adata.sampleinfo, offsets[:, None]]) + adata.trialdefinition = trl_def - return data + # write additional fields(non standard FT-format) + # into Syncopy config + afields = add_fields if add_fields is not None else range(0) + for field in afields: + adata.add_info[field] = structure[field] + return adata def _get_Matlab_version(header): From cafe82a54e694ae62d7cbd4b5f379f923674914a Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Mon, 14 Feb 2022 16:05:56 +0100 Subject: [PATCH 028/166] FIX: Repaired incorrect trialdefinition setting in Selector - fixed erroneous construction of `trialdefinition` arrays in `Selector` (resulting in `SpectralData` objects without time-axis). Closes #207 On branch fixes Changes to be committed: modified: syncopy/datatype/base_data.py --- syncopy/datatype/base_data.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/syncopy/datatype/base_data.py b/syncopy/datatype/base_data.py index f1f5701bf..bf1a32ba9 100644 --- a/syncopy/datatype/base_data.py +++ b/syncopy/datatype/base_data.py @@ -1685,6 +1685,9 @@ def trialdefinition(self, data): trlTime = data._get_time([trlno], toilim=[-np.inf, np.inf])[0] if isinstance(trlTime, list): stop = np.max(trlTime) + # Avoid creating empty arrays for "static" `SpectralData` objects + if stop == start == 0: + stop += 1 else: stop = trlTime.stop if step is None: From 2159431c40de3692d51f6fed34862056bb312021 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Mon, 14 Feb 2022 16:06:33 +0100 Subject: [PATCH 029/166] FIX: Respect conda permissions - do not attempt to run `conda clean` with cluster-wide conda environment - updated Windows runner and increased its memory On branch fixes Changes to be committed: modified: .gitlab-ci.yml --- .gitlab-ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1ddcedfcb..dd14970b5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -93,7 +93,6 @@ slurmtest: GIT_FETCH_EXTRA_FLAGS: --tags script: - source /opt/conda/etc/profile.d/conda.sh - - conda clean --all -y - conda env update -f syncopy.yml --prune - conda activate syncopy - export PYTHONPATH=$CI_PROJECT_DIR From 66d1ff32218ee5c5a97907ad0b9cd1e1e4eab8e8 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 14 Feb 2022 16:30:03 +0100 Subject: [PATCH 030/166] NEW: HDF5 MAT-File Reader for ft_datatype_raw - reading hdf data in trial chunks, assumption is that at least one single trial fits into RAM - attaching additional meta info missing, not sure how/if to support On branch ft-importer Changes to be committed: modified: syncopy/io/load_ft.py --- syncopy/io/load_ft.py | 303 ++++++++++++++++++++++++++++++++---------- 1 file changed, 233 insertions(+), 70 deletions(-) diff --git a/syncopy/io/load_ft.py b/syncopy/io/load_ft.py index 3baf6823b..1a52902a9 100644 --- a/syncopy/io/load_ft.py +++ b/syncopy/io/load_ft.py @@ -7,6 +7,8 @@ import numpy as np from scipy import io as sio import re +import h5py +from tqdm import tqdm # Local imports from syncopy.shared.errors import (SPYTypeError, SPYValueError, SPYIOError, SPYInfo, @@ -15,22 +17,28 @@ from syncopy.datatype import AnalogData -__all__ = ["load_ft_signals"] +__all__ = ["load_ft_raw"] -def load_ft_signals(filename, - select_structures=None, - add_fields=None, - **lm_kwargs): + +def load_ft_raw(filename, + select_structures=None, + add_fields=None, + mem_use=2000): ''' - Imports time-series data from Field Trip + Imports raw time-series data from Field Trip into potentially multiple `~syncopy.AnalogData` objects, - one for each structure. + one for each structure found within the MAT-file. + + Intended for both older MAT-file versions and + the newer 7.3, more info here: + + https://de.mathworks.com/help/matlab/import_export/mat-file-versions.html - Intended for Matlab versions < 7.3, since 7.3 Matlab - also uses hdf5. Here another local helper should - be implemented + For <7.3 the MAT-file gets loaded completely + into RAM, but its size is capped at 2GB. The 7.3 version is + in hdf5 format and can be read block-wise. The aim is to parse each FT data structure, which have the following fields (Syncopy analogon on the right): @@ -42,69 +50,196 @@ def load_ft_signals(filename, time - time fsample - samplerate cfg - ? - + The FT `cfg` contains a lot of meta data which at the moment we don't import into Syncopy. - + This is still experimental code, use with caution!! ''' - raw_dict = sio.loadmat(filename, - mat_dtype=True, - simplify_cells=True, - **lm_kwargs) - - bytes_ = raw_dict['__header__'] - header = bytes_.decode() - version = _get_Matlab_version(header) + # Required fields for the ft_datatype_raw + # see also https://www.fieldtriptoolbox.org/development/datastructure/ + req_fields_raw = ('time', 'trial', 'label', 'fsample') + version = _get_Matlab_version(filename) + msg = f"Reading MAT-File version {version} " + SPYInfo(msg) + + # new hdf container format if version >= 7.3: - raise NotImplementedError("Only Matlab < 7.3 is supported") - - struct_keys = [key for key in raw_dict.keys() if '__' not in key] + + h5File = h5py.File(filename, 'r') + struct_keys = [key for key in h5File.keys() if '#' not in key] + + struct_container = h5File + struct_reader = lambda struct: _read_hdf_structure(struct, + h5File=h5File, + mem_use=mem_use, + add_fields=add_fields) + + # old format <2GB, use scipy's MAT reader + else: + + if mem_use < 2000: + msg = "MAT-File version < 7.3 does not support lazy loading" + msg += f"\nReading {filename} might take up to 2GB of RAM, you requested only {mem_use / 1000}GB" + SPYWarning(msg) + + raw_dict = sio.loadmat(filename, + mat_dtype=True, + simplify_cells=True) + + struct_keys = [skey for skey in raw_dict.keys() if '__' not in skey] + + struct_container = raw_dict + struct_reader = lambda struct: _read_dict_structure(struct, + add_fields=add_fields) if len(struct_keys) == 0: SPYValueError(legal="At least one structure", varname=filename, actual="No structure found" ) - + msg = f"Found {len(struct_keys)} structure(s): {struct_keys}" SPYInfo(msg) out_dict = {} - - # load all structures - if select_structures is None: - for key in struct_keys: - - structure = raw_dict[key] - data = _read_mat_structure(structure, add_fields=add_fields) - out_dict[key] = data - + # load only a subset + if select_structures is not None: + keys = select_structures + # load all structures found else: - for key in select_structures: - if key not in struct_keys: - msg = f"Could not find structure `{key}` in {filename}" - SPYWarning(msg) - continue + keys = struct_keys - structure = raw_dict[key] + for skey in keys: + if skey not in struct_keys: + msg = f"Could not find structure `{skey}` in {filename}" + SPYWarning(msg) + continue + + structure = struct_container[skey] + # check that required fields are there + _check_req_fields(req_fields_raw, structure) + data = struct_reader(structure) + out_dict[skey] = data - data = _read_mat_structure(structure) - out_dict[key] = data - return out_dict -def _read_mat_structure(structure, add_fields=None): +def _read_hdf_structure(h5Group, h5File, mem_use, add_fields=None): + + ''' + Each Matlab structure contained in + a hdf5 MAT-File is a h5py Group object. + + Each key of this Group corresponds to + a field in the Matlab structure. + + This is the translation from FT to Syncopy: + + FT Syncopy + + label - channel + trial - trial + time - time + fsample - samplerate + cfg - X + + ''' + + # this should be fixed upstream such that + # the `defaultDimord` is indeed the default :) + AData = AnalogData(dimord=AnalogData._defaultDimord) + + # the only straightforward thing: + AData.samplerate = h5Group['fsample'][0, 0] + + # probably better to define an abstract mapping + # if we want to support more FT formats in the future + + # these are numpy arrays holding hdf5 object references + # e.i. one per trial, channel, time (per trial) + trl_refs = h5Group['trial'][:, 0] + time_refs = h5Group['time'][:, 0] + chan_refs = h5Group['label'][0, :] + + # -- retrieve shape information -- + nTrials = trl_refs.size + + # peek in 1st trial to determine single trial shape + # we only support equal trial lengths at this stage + nSamples, nChannels = h5File[trl_refs[0]].shape + nTotalSamples = nTrials * nSamples + + itemsize = h5File[trl_refs[0]].dtype.itemsize + # in Mbyte + trl_size = itemsize * nSamples * nChannels / 1e6 + + # assumption: single trial fits into RAM + if trl_size > mem_use: + msg = f"\nSingle trial is bigger than requested chache size of {mem_use}MB\n" + msg += f"Still trying to load {trl_size:.1f}MB trials.." + SPYWarning(msg) + maxLoadTrials = 1 + else: + maxLoadTrials = int(mem_use / trl_size) + + # chunks of the trial reference object array + nChunks = np.ceil(nTrials / maxLoadTrials).astype(int) + trl_chunks = np.array_split(trl_refs, nChunks) + + # -- IO process -- + + # create new hdf5 dataset for our AnalogData + # with the default dimord ['time', 'channel'] + h5FileOut = h5py.File(AData.filename, mode="w") + ADset = h5FileOut.create_dataset("data", + dtype=np.float32, + shape=[nTotalSamples, nChannels]) + + pbar = tqdm(total=nTrials, desc=f"loading {nTrials} trials") + SampleCounter = 0 # trial stacking + for ref_chunk in trl_chunks: + # one swipe + for tr in ref_chunk: + ADset[SampleCounter:nSamples, :] = h5File[tr] + SampleCounter += nSamples + pbar.update(1) + pbar.close() + + AData.data = ADset + + # -- trialdefinition -- + + sampleinfo = np.vstack([np.arange(nTrials), np.arange(1, nTrials + 1)]).T * nSamples + + offsets = [] + # we need to look into the time vectors for each trial + for time_r in time_refs: + offsets.append(h5File[time_r][0, 0]) + offsets = np.array(offsets) + + trl_def = np.hstack([sampleinfo, offsets[:, None]]) + AData.trialdefinition = trl_def + + # each channel label is an integer array with shape (X, 1), + # where `X` is the number of ascii encoded characters + channels = [''.join(map(chr, h5File[cr][:, 0])) for cr in chan_refs] + AData.channel = channels + + return AData + + +def _read_dict_structure(structure, add_fields=None): ''' Local helper to parse a single FT structure and return an `~syncopy.AnalogData` object - Intended for Matlab Version < 7.3 + Only for for Matlab data format version < 7.3 + which was opened via scipy.io.loadmat! This is the translation from FT to Syncopy: @@ -114,68 +249,96 @@ def _read_mat_structure(structure, add_fields=None): trial - trial time - time fsample - samplerate - cfg - X + cfg - X Each trial in FT has nChannels x nSamples ordering, Syncopy has nSamples x nChannels ''' - # nTrials = structure["trial"].shape[0] + # nTrials = structure["trial"].shape[0] trials = [] # 1st trial as reference - nChannels, nSamples = structure["trial"][0].shape + nChannels, nSamples = structure['trial'][0].shape # check equal trial lengths - for trl in structure["trial"]: + for trl in structure['trial']: if trl.shape[-1] != nSamples: lgl = 'Trials of equal lengths' actual = 'Trials of unequal lengths' raise SPYValueError(lgl, varname="load .mat", actual=actual) - + # channel x sample ordering in FT trials.append(trl.T.astype(np.float32)) - # initialize AnalogData - adata = AnalogData(trials, samplerate=structure['fsample']) - adata.add_info = {} + # initialize AnalogData + AData = AnalogData(trials, samplerate=structure['fsample']) + AData.add_info = {} # get the channel ids - channels = structure["label"] - # set the channel ids - adata.channel =list(channels.astype(str)) + channels = structure['label'] + # set the channel ids + AData.channel = list(channels.astype(str)) # update trialdefinition - times_array = np.vstack(structure["time"]) - + times_array = np.vstack(structure['time']) + # nTrials x nSamples - offsets = times_array[:, 0] * adata.samplerate + offsets = times_array[:, 0] * AData.samplerate - trl_def = np.hstack([adata.sampleinfo, offsets[:, None]]) - adata.trialdefinition = trl_def + trl_def = np.hstack([AData.sampleinfo, offsets[:, None]]) + AData.trialdefinition = trl_def # write additional fields(non standard FT-format) # into Syncopy config - afields = add_fields if add_fields is not None else range(0) + afields = add_fields if add_fields is not None else range(0) for field in afields: - adata.add_info[field] = structure[field] - return adata - + AData.add_info[field] = structure[field] + return AData + + +def _get_Matlab_version(filename): + + ''' + Peeks into the 1st line of a .mat file + and extracts the version information. + Works for both < 7.3 and newer MAT-files. + ''' -def _get_Matlab_version(header): + with open(filename, 'rb') as matfile: + line1 = next(matfile) + # relevant information + header = line1[:76].decode() # matches for example 'MATLAB 5.01' # with the version as only capture group - - pattern = re.compile("^MATLAB\s(\d*\.\d*)") + pattern = re.compile("^MATLAB\s(\d*\.\d*)") match = pattern.match(header) if not match: - lgl = 'Recognized .mat file' - actual = 'not recognized .mat file' - raise SPYValueError(lgl, 'load .mat', actual) + lgl = 'recognizable .mat file' + actual = 'can not recognize .mat file' + raise SPYValueError(lgl, filename, actual) version = float(match.group(1)) return version + + +def _check_req_fields(req_fields, structure): + + ''' + Just check the the minimal required fields + (aka keys in Python) are present in a + Matlab structure + + Works for both old-style (dict) and + new-style (hdf5 Group) MAT-file structures. + ''' + + for key in req_fields: + if key not in structure: + lgl = f"{key} present in MAT structure" + actual = f"{key} missing" + raise SPYValueError(lgl, 'MAT structure', actual) From 69a1b7323b8d2d35445230403453f5ae8f6a8649 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Mon, 14 Feb 2022 16:41:40 +0100 Subject: [PATCH 031/166] FIX: Do not raise exception if arithmetic operands have different numerical types - remove `SPYTypeError` that was raised if operand and base had unequal numerical types (real/complex) - adapted tests accordingly On branch fixes Changes to be committed: modified: syncopy/datatype/methods/arithmetic.py modified: syncopy/tests/test_basedata.py --- syncopy/datatype/methods/arithmetic.py | 10 +++++----- syncopy/tests/test_basedata.py | 10 ---------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/syncopy/datatype/methods/arithmetic.py b/syncopy/datatype/methods/arithmetic.py index 84810c9d1..532a8e9b4 100644 --- a/syncopy/datatype/methods/arithmetic.py +++ b/syncopy/datatype/methods/arithmetic.py @@ -156,7 +156,7 @@ def _parse_input(obj1, obj2, operator): raise SPYValueError("non-zero scalar for division", varname="operand", actual=str(operand)) # Ensure complex and real values are not mashed up - _check_complex_operand(baseTrials, operand, "scalar") + _check_complex_operand(baseTrials, operand, "scalar", operator) # Determine exact numeric type of operation's result opres_type = np.result_type(*(trl.dtype for trl in baseTrials), operand) @@ -172,7 +172,7 @@ def _parse_input(obj1, obj2, operator): operand = np.array(operand) # Ensure complex and real values are not mashed up - _check_complex_operand(baseTrials, operand, "array") + _check_complex_operand(baseTrials, operand, "array", operator) # Determine exact numeric type of the operation's result opres_type = np.result_type(*(trl.dtype for trl in baseTrials), operand.dtype) @@ -274,7 +274,7 @@ def _parse_input(obj1, obj2, operator): return baseObj, operand, operand_dat, opres_type, operand_idxs # Check for complexity in `operand` vs. `baseObj` -def _check_complex_operand(baseTrials, operand, opDimType): +def _check_complex_operand(baseTrials, operand, opDimType, operator): """ Local helper to determine if provided scalar/array and `baseObj` are both real/complex """ @@ -285,8 +285,8 @@ def _check_complex_operand(baseTrials, operand, opDimType): else: sameType = lambda dt : "complex" not in dt.name if not all(sameType(trl.dtype) for trl in baseTrials): - lgl = "{} of same mathematical type (real/complex)" - raise SPYTypeError(operand, varname="operand", expected=lgl.format(opDimType)) + wrng = "Operand is {} of different mathematical type (real/complex)" + SPYWarning(wrng.format(opDimType), caller=operator) return diff --git a/syncopy/tests/test_basedata.py b/syncopy/tests/test_basedata.py index 3907f8fa4..363a1c83c 100644 --- a/syncopy/tests/test_basedata.py +++ b/syncopy/tests/test_basedata.py @@ -452,16 +452,6 @@ def test_arithmetic(self): operation(dummy, np.inf) assert "'inf'; expected finite scalar" in str(spyval.value) - # Complex scalar (all test data are real) - with pytest.raises(SPYTypeError) as spytyp: - operation(dummy, complexNum) - assert "expected scalar of same mathematical type (real/complex)" in str(spytyp.value) - - # Array w/wrong numeric type - with pytest.raises(SPYTypeError) as spytyp: - operation(dummy, complexArr) - assert "array of same numerical type (real/complex) found ndarray" in str(spytyp.value) - # Syncopy object of different type with pytest.raises(SPYTypeError) as spytyp: operation(dummy, other) From dcfc7bb2b3cff520cbdb0e08e1172d009ac36bfe Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 14 Feb 2022 17:19:35 +0100 Subject: [PATCH 032/166] CHG: Read trial-by-trial for hdf5 MAT-File format - mem_use parameter now determines expected single trial size (times 2.5) Changes to be committed: modified: syncopy/io/load_ft.py --- syncopy/io/load_ft.py | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/syncopy/io/load_ft.py b/syncopy/io/load_ft.py index 1a52902a9..191bd40fd 100644 --- a/syncopy/io/load_ft.py +++ b/syncopy/io/load_ft.py @@ -17,7 +17,6 @@ from syncopy.datatype import AnalogData - __all__ = ["load_ft_raw"] @@ -178,17 +177,10 @@ def _read_hdf_structure(h5Group, h5File, mem_use, add_fields=None): trl_size = itemsize * nSamples * nChannels / 1e6 # assumption: single trial fits into RAM - if trl_size > mem_use: - msg = f"\nSingle trial is bigger than requested chache size of {mem_use}MB\n" + if trl_size > 0.4 * mem_use: + msg = f"\nSingle trial is at least 40% of the requested chache size of {mem_use}MB\n" msg += f"Still trying to load {trl_size:.1f}MB trials.." SPYWarning(msg) - maxLoadTrials = 1 - else: - maxLoadTrials = int(mem_use / trl_size) - - # chunks of the trial reference object array - nChunks = np.ceil(nTrials / maxLoadTrials).astype(int) - trl_chunks = np.array_split(trl_refs, nChunks) # -- IO process -- @@ -199,14 +191,13 @@ def _read_hdf_structure(h5Group, h5File, mem_use, add_fields=None): dtype=np.float32, shape=[nTotalSamples, nChannels]) - pbar = tqdm(total=nTrials, desc=f"loading {nTrials} trials") + pbar = tqdm(trl_refs, desc=f"loading {nTrials} trials") SampleCounter = 0 # trial stacking - for ref_chunk in trl_chunks: - # one swipe - for tr in ref_chunk: - ADset[SampleCounter:nSamples, :] = h5File[tr] - SampleCounter += nSamples - pbar.update(1) + + # one swipe per trial + for tr in pbar: + ADset[SampleCounter:nSamples, :] = h5File[tr] + SampleCounter += nSamples pbar.close() AData.data = ADset From bfa48ee3a94a892bc6c16d634dfb1fadd19903f9 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Mon, 14 Feb 2022 17:42:09 +0100 Subject: [PATCH 033/166] CHG: Modified selectdata messaging - do not print message on in-place selections but rather print some info if a data-selection actually triggers on-disk copying (closes #199) - amended docstrings of `_preview_trial` in `ContinousData` and `DiscreteData` to highlight that these helpers can be used to easily (and cheaply) pseudo- evaluate the effect of data selections On branch fixes Changes to be committed: modified: syncopy/datatype/continuous_data.py modified: syncopy/datatype/discrete_data.py modified: syncopy/datatype/methods/selectdata.py --- syncopy/datatype/continuous_data.py | 6 ++++++ syncopy/datatype/discrete_data.py | 6 ++++++ syncopy/datatype/methods/selectdata.py | 16 +++++++++++++++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/syncopy/datatype/continuous_data.py b/syncopy/datatype/continuous_data.py index 1df452754..52ce54f96 100644 --- a/syncopy/datatype/continuous_data.py +++ b/syncopy/datatype/continuous_data.py @@ -243,6 +243,12 @@ def _preview_trial(self, trialno): :meth:`syncopy.shared.computational_routine.ComputationalRoutine.computeFunction` to avoid loading actual trial-data into memory. + Notes + ----- + If an active in-place selection is found, the generated `FauxTrial` object + respects it (e.g., if only 2 of 10 channels are selected in-place, `faux_trl` + reports to only contain 2 channels) + See also -------- syncopy.datatype.base_data.FauxTrial : class definition and further details diff --git a/syncopy/datatype/discrete_data.py b/syncopy/datatype/discrete_data.py index c8ae89521..73e75b514 100644 --- a/syncopy/datatype/discrete_data.py +++ b/syncopy/datatype/discrete_data.py @@ -208,6 +208,12 @@ def _preview_trial(self, trialno): :meth:`syncopy.shared.computational_routine.ComputationalRoutine.computeFunction` to avoid loading actual trial-data into memory. + Notes + ----- + If an active in-place selection is found, the generated `FauxTrial` object + respects it (e.g., if only 2 of 10 channels are selected in-place, `faux_trl` + reports to only contain 2 channels) + See also -------- syncopy.datatype.base_data.FauxTrial : class definition and further details diff --git a/syncopy/datatype/methods/selectdata.py b/syncopy/datatype/methods/selectdata.py index f81f7d1d6..bb3113db2 100644 --- a/syncopy/datatype/methods/selectdata.py +++ b/syncopy/datatype/methods/selectdata.py @@ -3,6 +3,9 @@ # Syncopy data selection methods # +# Builtin/3rd party package imports +import numpy as np + # Local imports from syncopy.shared.parsers import data_parser from syncopy.shared.errors import SPYValueError, SPYTypeError, SPYInfo, SPYWarning @@ -302,9 +305,20 @@ def selectdata(data, trials=None, channels=None, channels_i=None, channels_j=Non # If an in-place selection was requested we're done if inplace: - SPYInfo("In-place selection attached to data object: {}".format(data._selection)) return + # Inform the user what's about to happen + fauxTrials = [data._preview_trial(trlno) for trlno in data._selection.trials] + fauxSizes = [np.prod(ftrl.shape)*ftrl.dtype.itemsize for ftrl in fauxTrials] + selectionSize = sum(fauxSizes) / 1024**2 + sUnit = "MB" + if selectionSize > 1000: + selectionSize /= 1024 + sUnit = "GB" + msg = "Copying {dsize:3.2f} {dunit:s} of data based on selection " +\ + "to create new {objkind:s} object on disk" + SPYInfo(msg.format(dsize=selectionSize, dunit=sUnit, objkind=data.__class__.__name__)) + # Create inventory of all available selectors and actually provided values # to create a bookkeeping dict for logging log_dct = {"inplace": inplace, "clear": clear} From c8b9a4e86d707efd09805a39e0d502cc55715a60 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Tue, 15 Feb 2022 11:00:44 +0100 Subject: [PATCH 034/166] NEW: Amended CHANGELOG - added introduced modifications to CHANGELOG On branch fixes Changes to be committed: modified: CHANGELOG.md --- CHANGELOG.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67aded280..a214ef812 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,21 +5,30 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +Bugfix release + ### NEW ### CHANGED - Repaired top-level imports: renamed `connectivity` to `connectivityanalysis` and the "connectivity" module is now called "nwanalysis" -- include `conda clean` in CD pipeline to avoid disk fillup by unused conda +- Included `conda clean` in CD pipeline to avoid disk fillup by unused conda packages/cache +- Inverted `selectdata` messaging policy: only actual on-disk copy operations + trigger a `SPYInfo` message (closes #197) ### REMOVED - Do not parse scalars using `numbers.Number`, use `numpy.number` instead to catch Boolean values +- Do not raise a `SPYTypeError` if an arithmetic operation is performed using + objects of different numerical types (real/complex; closes #199) ### DEPRECATED ### FIXED +- The `trialdefinition` arrays constructed by the `Selector` class were incorrect + for `SpectralData` objects without time-axis, resulting in "empty" trials. This + has been fixed (closes #207) -## [v0.2] - 2022-01-18 +## [v0.20] - 2022-01-18 Major Release ### NEW From 80f63634f5632414322bfce33008658d03aff390 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Tue, 15 Feb 2022 11:44:16 +0100 Subject: [PATCH 035/166] FIX: Modifications based on code review of PR #206 - use `dimord.index` instead of `_defaultDimord.index` for shape allocation - renamed `nBlocks` to `blockList` and amended in-code comment - renamed `read_nwb` to `load_nwb` - removed manager `read_external` - amended CHANGELOG On branch nwb Changes to be committed: modified: CHANGELOG.md modified: syncopy/io/__init__.py renamed: syncopy/io/_read_nwb.py -> syncopy/io/_load_nwb.py deleted: syncopy/io/read_external.py modified: syncopy/tests/local_spy.py --- CHANGELOG.md | 2 + syncopy/io/__init__.py | 9 +-- syncopy/io/{_read_nwb.py => _load_nwb.py} | 17 ++--- syncopy/io/read_external.py | 75 ----------------------- syncopy/tests/local_spy.py | 6 +- 5 files changed, 18 insertions(+), 91 deletions(-) rename syncopy/io/{_read_nwb.py => _load_nwb.py} (93%) delete mode 100644 syncopy/io/read_external.py diff --git a/CHANGELOG.md b/CHANGELOG.md index aca3fa968..33bf89a57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht ## [Unreleased] ### NEW +- Added loading functionality for NWB 2.0 files + ### CHANGED - Made plotting routines matplotlib 3.5 compatible diff --git a/syncopy/io/__init__.py b/syncopy/io/__init__.py index 07a94aa64..c2a0d15c4 100644 --- a/syncopy/io/__init__.py +++ b/syncopy/io/__init__.py @@ -4,18 +4,15 @@ # # Import __all__ routines from local modules -from . import (utils, load_spy_container, save_spy_container, - read_external, _read_nwb) +from . import (utils, load_spy_container, save_spy_container, _load_nwb) from .utils import * from .load_spy_container import * from .save_spy_container import * -from .read_external import * -from ._read_nwb import * +from ._load_nwb import * # Populate local __all__ namespace __all__ = [] __all__.extend(utils.__all__) __all__.extend(load_spy_container.__all__) __all__.extend(save_spy_container.__all__) -__all__.extend(read_external.__all__) -__all__.extend(_read_nwb.__all__) +__all__.extend(_load_nwb.__all__) diff --git a/syncopy/io/_read_nwb.py b/syncopy/io/_load_nwb.py similarity index 93% rename from syncopy/io/_read_nwb.py rename to syncopy/io/_load_nwb.py index df72d3dc7..9155d64a1 100644 --- a/syncopy/io/_read_nwb.py +++ b/syncopy/io/_load_nwb.py @@ -29,10 +29,10 @@ "or using pip:\n" +\ "\tpip install pynwb" -__all__ = ["read_nwb"] +__all__ = ["load_nwb"] -def read_nwb(filename, memuse=3000): +def load_nwb(filename, memuse=3000): """ Read contents of NWB files @@ -191,8 +191,8 @@ def read_nwb(filename, memuse=3000): # allocate a target dataset for reading the NWB data angData = AnalogData(dimord=AnalogData._defaultDimord) angShape = [None, None] - angShape[angData._defaultDimord.index("time")] = nSamples - angShape[angData._defaultDimord.index("channel")] = nChannels + angShape[angData.dimord.index("time")] = nSamples + angShape[angData.dimord.index("channel")] = nChannels h5ang = h5py.File(angData.filename, mode="w") angDset = h5ang.create_dataset("data", dtype=np.result_type(*dTypes), shape=angShape) @@ -208,10 +208,13 @@ def read_nwb(filename, memuse=3000): # Show dataset name in progress bar label pbar.set_description("Loading {} from disk".format(acqValue.name)) - # Given memory cap, compute how many data blocks can be grabbed per swipe + # Given memory cap, compute how many data blocks can be grabbed per swipe: + # `nSamp` is the no. of samples that can be loaded into memory without exceeding `memuse` + # `rem` is the no. of remaining samples, s. t. ``nSamp + rem = angDset.shape[0]` + # `blockList` is a list of samples to load per swipe, i.e., `[nSamp, nSamp, ..., rem]` nSamp = int(memuse / (np.prod(angDset.shape[1:]) * angDset.dtype.itemsize)) rem = int(angDset.shape[0] % nSamp) - nBlocks = [nSamp] * int(angDset.shape[0] // nSamp) + [rem] * int(rem > 0) + blockList = [nSamp] * int(angDset.shape[0] // nSamp) + [rem] * int(rem > 0) # If channel-specific gains are set, load them now if acqValue.channel_conversion is not None: @@ -220,7 +223,7 @@ def read_nwb(filename, memuse=3000): # Write data block-wise to `angDset` (use `del` to wipe blocks from memory) # Use 'unsafe' casting to allow `tmp` array conversion int -> float endChan = chanCounter + acqValue.data.shape[1] - for m, M in enumerate(tqdm(nBlocks, desc=pbarDesc, position=1, leave=False)): + for m, M in enumerate(tqdm(blockList, desc=pbarDesc, position=1, leave=False)): tmp = acqValue.data[m * nSamp: m * nSamp + M, :] if acqValue.channel_conversion is not None: np.multiply(tmp, gains, out=tmp, casting="unsafe") diff --git a/syncopy/io/read_external.py b/syncopy/io/read_external.py deleted file mode 100644 index 8ddd82f3d..000000000 --- a/syncopy/io/read_external.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Import data from 3rd party formats -# - -# Builtin/3rd party package imports -import os - -# Local imports -from syncopy.shared.parsers import io_parser -from syncopy.shared.errors import SPYTypeError, SPYValueError -from ._read_nwb import read_nwb - -supportedFormats = ["nwb"] - -__all__ = ["read"] - - -def read(filename, format=None): - """ - Read data from non-native Syncopy file formats - - Parameters - ---------- - filename : str - Name of (may include full path to) file to read - format : None or str - If the external format cannot be inferred from `filename`, the `format` - specifier can be used to manually set it - - Returns - ------- - data : Syncopy data object(s) - Depending on `filename` on or more Syncopy data objects is returned - - Notes - ----- - This manager may be used as general purpose import function for reading - third party file formats. However, to leverage specific functionality of - the respective format-specific reading routines, please invoke the corresponding - functions directly. For instance, if you want to decrease the in-memory - footprint of reading NWB files, use :func:`~syncopy.io._read_nwb.read_nwb` - directly. - - See also - -------- - syncopy.io._read_nwb.read_nwb : read contents of NWB files - """ - - # Parse basal input args (thorough error checking is performed by the actual - # importer functions) - _, baseName = io_parser(filename, varname="filename", exists=True) - if not isinstance(format, (type(None), str)): - raise SPYTypeError(format, varname="format", expected="string") - - # Ensure we can actually process a provided `format` - if format is not None: - format = format.replace(".", "") - if format not in supportedFormats: - lgl = "one of the following supported formats " +\ - "".join(fmt + ", " for fmt in supportedFormats)[:-2] - raise SPYValueError(lgl, varname="format", actual=format) - - # Try to infer file-format from file extension - if format is None: - _, ext = os.path.splitext(baseName) - if ext == ".nwb": - format = "nwb" - - # Call appropriate importer - if format == "nwb": - read_nwb(filename) - - return - diff --git a/syncopy/tests/local_spy.py b/syncopy/tests/local_spy.py index 8a0f2cd79..dd716e542 100644 --- a/syncopy/tests/local_spy.py +++ b/syncopy/tests/local_spy.py @@ -29,8 +29,8 @@ nwbFilePath = "/home/fuertingers/Documents/job/SyNCoPy/Data/tt2.nwb" # nwbFilePath = "/home/fuertingers/Documents/job/SyNCoPy/Data/test.nwb" - xx = spy.read(nwbFilePath, format="xyz") + xx = spy.load_nwb(nwbFilePath) - nwbio = NWBHDF5IO(nwbFilePath, "r", load_namespaces=True) - nwbfile = nwbio.read() + # nwbio = NWBHDF5IO(nwbFilePath, "r", load_namespaces=True) + # nwbfile = nwbio.read() From 44d673adc7a12f4f0e8cdd1a48a9f44fe2dc7864 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Tue, 15 Feb 2022 11:50:49 +0100 Subject: [PATCH 036/166] CHG: Added comment - included comment to clarify `EventData` instantiation On branch nwb Changes to be committed: modified: syncopy/io/_load_nwb.py --- syncopy/io/_load_nwb.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/syncopy/io/_load_nwb.py b/syncopy/io/_load_nwb.py index 9155d64a1..f1a447fc3 100644 --- a/syncopy/io/_load_nwb.py +++ b/syncopy/io/_load_nwb.py @@ -179,6 +179,9 @@ def load_nwb(filename, memuse=3000): h5evt = h5py.File(evtData.filename, mode="w") evtDset = h5evt.create_dataset("data", dtype=np.result_type(*ttlDtypes), shape=(ttlVals[0].data.size, 3)) + # Column 1: sample indices + # Column 2: TTL pulse values + # Column 3: TTL channel markers evtDset[:, 0] = ((ttlChans[0].timestamps[()] - tStarts[0]) / ttlChans[0].timestamps__resolution).astype(np.intp) evtDset[:, 1] = ttlVals[0].data[()] evtDset[:, 2] = ttlChans[0].data[()] From 6a99718daac2554ae241f2b0eab3a59d475db4ba Mon Sep 17 00:00:00 2001 From: tensionhead Date: Tue, 15 Feb 2022 17:33:29 +0100 Subject: [PATCH 037/166] WIP: Added docstring and .info attribute - prototypical .info attribute to store additional fields of the MAT-File structures Changes to be committed: modified: syncopy/io/load_ft.py --- syncopy/io/load_ft.py | 126 +++++++++++++++++++++++++++++++++++------- 1 file changed, 105 insertions(+), 21 deletions(-) diff --git a/syncopy/io/load_ft.py b/syncopy/io/load_ft.py index 191bd40fd..6a959840a 100644 --- a/syncopy/io/load_ft.py +++ b/syncopy/io/load_ft.py @@ -30,14 +30,10 @@ def load_ft_raw(filename, into potentially multiple `~syncopy.AnalogData` objects, one for each structure found within the MAT-file. - Intended for both older MAT-file versions and - the newer 7.3, more info here: - - https://de.mathworks.com/help/matlab/import_export/mat-file-versions.html - - For <7.3 the MAT-file gets loaded completely - into RAM, but its size is capped at 2GB. The 7.3 version is - in hdf5 format and can be read block-wise. + For MAT-File < v7.3 the MAT-file gets loaded completely + into RAM, but its size should be capped at 2GB. + The v7.3 is in hdf5 format and will be read in trial-by-trial, + this should be the default for MAT-Files exceeding 2GB. The aim is to parse each FT data structure, which have the following fields (Syncopy analogon on the right): @@ -47,24 +43,62 @@ def load_ft_raw(filename, label - channel trial - trial time - time + + optional: fsample - samplerate + cfg - ? The FT `cfg` contains a lot of meta data which at the moment we don't import into Syncopy. This is still experimental code, use with caution!! + + Parameters + ---------- + filename: str + Path to the MAT-File + select_structures: sequence or None, optional + Sequence of strings, one for each structure, + the default `None` will load all structures found + add_fields: sequence, optional + Additional MAT-File fields within each structure to + be imported. They can be accessed via the `AnalogData.info` attribute. + mem_use: int + The amount of RAM requested for the import process in MB. Note that < v7.3 MAT-File formats can only be loaded at once. For MAT-File v7.3 this should be at least twice the size of a single trial. + + Returns + ------- + out_dict: dict + Dictionary with keys being the names of the structures loaded from the MAT-File, + and as values the `~syncopy.AnalogData` datasets + + See also + -------- + MAT-File formats: https://de.mathworks.com/help/matlab/import_export/mat-file-versions.html + Field Trip datastructures: https://www.fieldtriptoolbox.org/development/datastructure/ + + Examples + -------- + Load the two structures from a MAT-File `example.mat`: + + dct = load_ft_raw('example.mat', select_structures=('Data_K', Data_KB')) + + Access the individual `~syncopy.AnalogData` datasets: + + data_kb = dct['Data_KB'] + data_k = dct['Data_K'] + ''' # Required fields for the ft_datatype_raw - # see also https://www.fieldtriptoolbox.org/development/datastructure/ - req_fields_raw = ('time', 'trial', 'label', 'fsample') + req_fields_raw = ('time', 'trial', 'label') version = _get_Matlab_version(filename) msg = f"Reading MAT-File version {version} " SPYInfo(msg) - # new hdf container format + # new hdf container format, use h5py if version >= 7.3: h5File = h5py.File(filename, 'r') @@ -119,7 +153,6 @@ def load_ft_raw(filename, continue structure = struct_container[skey] - # check that required fields are there _check_req_fields(req_fields_raw, structure) data = struct_reader(structure) out_dict[skey] = data @@ -127,7 +160,10 @@ def load_ft_raw(filename, return out_dict -def _read_hdf_structure(h5Group, h5File, mem_use, add_fields=None): +def _read_hdf_structure(h5Group, + h5File, + mem_use, + add_fields=None): ''' Each Matlab structure contained in @@ -143,18 +179,20 @@ def _read_hdf_structure(h5Group, h5File, mem_use, add_fields=None): label - channel trial - trial time - time + + optional: fsample - samplerate + cfg - X ''' + # for user info + struct_name = h5Group.name[1:] # this should be fixed upstream such that # the `defaultDimord` is indeed the default :) AData = AnalogData(dimord=AnalogData._defaultDimord) - # the only straightforward thing: - AData.samplerate = h5Group['fsample'][0, 0] - # probably better to define an abstract mapping # if we want to support more FT formats in the future @@ -164,6 +202,11 @@ def _read_hdf_structure(h5Group, h5File, mem_use, add_fields=None): time_refs = h5Group['time'][:, 0] chan_refs = h5Group['label'][0, :] + if 'fsample' in h5Group: + AData.samplerate = h5Group['fsample'][0, 0] + else: + AData.samplerate = _infer_fsample(h5File[time_refs[0]]) + # -- retrieve shape information -- nTrials = trl_refs.size @@ -191,7 +234,7 @@ def _read_hdf_structure(h5Group, h5File, mem_use, add_fields=None): dtype=np.float32, shape=[nTotalSamples, nChannels]) - pbar = tqdm(trl_refs, desc=f"loading {nTrials} trials") + pbar = tqdm(trl_refs, desc=f"{struct_name} - loading {nTrials} trials") SampleCounter = 0 # trial stacking # one swipe per trial @@ -204,7 +247,8 @@ def _read_hdf_structure(h5Group, h5File, mem_use, add_fields=None): # -- trialdefinition -- - sampleinfo = np.vstack([np.arange(nTrials), np.arange(1, nTrials + 1)]).T * nSamples + nTr_rng = np.arange(nTrials) + sampleinfo = np.vstack([nTr_rng, nTr_rng + 1]).T * nSamples offsets = [] # we need to look into the time vectors for each trial @@ -220,6 +264,26 @@ def _read_hdf_structure(h5Group, h5File, mem_use, add_fields=None): channels = [''.join(map(chr, h5File[cr][:, 0])) for cr in chan_refs] AData.channel = channels + # -- additional fields -- + + # this is the most experimental part + AData.info = {} + for field in add_fields: + if field not in h5Group: + msg = f"Could not find additional field {field} in {struct_name}" + SPYWarning(msg) + continue + + # again an array of hdf5 object references + af_refs = h5Group[field][:, 0] + + AData.info[field] = [] + for af_ref in af_refs: + # here would be more parsing needed + # if we want to generally (strings, numbers, ..) + # support this + AData.info[field] = h5File[af_ref] + return AData @@ -239,7 +303,10 @@ def _read_dict_structure(structure, add_fields=None): label - channel trial - trial time - time + + optional: fsample - samplerate + cfg - X Each trial in FT has nChannels x nSamples ordering, @@ -264,8 +331,14 @@ def _read_dict_structure(structure, add_fields=None): trials.append(trl.T.astype(np.float32)) # initialize AnalogData - AData = AnalogData(trials, samplerate=structure['fsample']) - AData.add_info = {} + if 'fsample' in structure: + samplerate = structure['fsample'] + else: + samplerate = _infer_fsample(structure['time'][0]) + + AData = AnalogData(trials, samplerate=samplerate) + + AData.info = {} # get the channel ids channels = structure['label'] # set the channel ids @@ -284,7 +357,7 @@ def _read_dict_structure(structure, add_fields=None): # into Syncopy config afields = add_fields if add_fields is not None else range(0) for field in afields: - AData.add_info[field] = structure[field] + AData.info[field] = structure[field] return AData @@ -333,3 +406,14 @@ def _check_req_fields(req_fields, structure): lgl = f"{key} present in MAT structure" actual = f"{key} missing" raise SPYValueError(lgl, 'MAT structure', actual) + + +def _infer_fsample(time_vector): + + ''' + Akin to `ft_datatype_raw` determine + the sampling frequency from the sampling + times + ''' + + return np.mean(np.diff(time_vector)) From 8deb12f8f54d0d15407efc23d72de53026299cec Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Wed, 16 Feb 2022 11:53:11 +0100 Subject: [PATCH 038/166] CHG: Mostly docstring reformating - included rst-tables in docstrings - removed unused imports On branch ft-importer Changes to be committed: modified: syncopy/io/load_ft.py --- syncopy/io/load_ft.py | 122 ++++++++++++++++++++---------------------- 1 file changed, 58 insertions(+), 64 deletions(-) diff --git a/syncopy/io/load_ft.py b/syncopy/io/load_ft.py index 6a959840a..e140f026b 100644 --- a/syncopy/io/load_ft.py +++ b/syncopy/io/load_ft.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Load data Field Trip .mat files +# Load data from Field Trip .mat files # # Builtin/3rd party package imports @@ -11,12 +11,9 @@ from tqdm import tqdm # Local imports -from syncopy.shared.errors import (SPYTypeError, SPYValueError, SPYIOError, SPYInfo, - SPYError, SPYWarning) - +from syncopy.shared.errors import SPYValueError, SPYInfo, SPYWarning from syncopy.datatype import AnalogData - __all__ = ["load_ft_raw"] @@ -25,9 +22,9 @@ def load_ft_raw(filename, add_fields=None, mem_use=2000): - ''' + """ Imports raw time-series data from Field Trip - into potentially multiple `~syncopy.AnalogData` objects, + into potentially multiple :class:`~syncopy.AnalogData` objects, one for each structure found within the MAT-file. For MAT-File < v7.3 the MAT-file gets loaded completely @@ -38,16 +35,15 @@ def load_ft_raw(filename, The aim is to parse each FT data structure, which have the following fields (Syncopy analogon on the right): - FT Syncopy - - label - channel - trial - trial - time - time - - optional: - fsample - samplerate - - cfg - ? + +--------------------+------------+ + |FT | Syncopy | + +--------------------+------------+ + | label | channel | + | trial | trial | + | time | time | + | fsample (optional) | samplerate | + | cfg | ? | + +--------------------+------------+ The FT `cfg` contains a lot of meta data which at the moment we don't import into Syncopy. @@ -57,7 +53,7 @@ def load_ft_raw(filename, Parameters ---------- filename: str - Path to the MAT-File + Path to the MAT-file select_structures: sequence or None, optional Sequence of strings, one for each structure, the default `None` will load all structures found @@ -65,13 +61,15 @@ def load_ft_raw(filename, Additional MAT-File fields within each structure to be imported. They can be accessed via the `AnalogData.info` attribute. mem_use: int - The amount of RAM requested for the import process in MB. Note that < v7.3 MAT-File formats can only be loaded at once. For MAT-File v7.3 this should be at least twice the size of a single trial. + The amount of RAM requested for the import process in MB. Note that < v7.3 + MAT-File formats can only be loaded at once. For MAT-File v7.3 this should + be at least twice the size of a single trial. Returns ------- out_dict: dict Dictionary with keys being the names of the structures loaded from the MAT-File, - and as values the `~syncopy.AnalogData` datasets + and its values being the :class:`~syncopy.AnalogData` datasets See also -------- @@ -80,16 +78,15 @@ def load_ft_raw(filename, Examples -------- - Load the two structures from a MAT-File `example.mat`: + Load two structures `'Data_K'` and `'Data_KB'` from a MAT-File `example.mat`: - dct = load_ft_raw('example.mat', select_structures=('Data_K', Data_KB')) + >>> dct = load_ft_raw('example.mat', select_structures=('Data_K', Data_KB')) - Access the individual `~syncopy.AnalogData` datasets: + Access the individual :class:`~syncopy.AnalogData` datasets: - data_kb = dct['Data_KB'] - data_k = dct['Data_K'] - - ''' + >>> data_kb = dct['Data_KB'] + >>> data_k = dct['Data_K'] + """ # Required fields for the ft_datatype_raw req_fields_raw = ('time', 'trial', 'label') @@ -117,7 +114,7 @@ def load_ft_raw(filename, msg = "MAT-File version < 7.3 does not support lazy loading" msg += f"\nReading {filename} might take up to 2GB of RAM, you requested only {mem_use / 1000}GB" SPYWarning(msg) - + raw_dict = sio.loadmat(filename, mat_dtype=True, simplify_cells=True) @@ -165,7 +162,7 @@ def _read_hdf_structure(h5Group, mem_use, add_fields=None): - ''' + """ Each Matlab structure contained in a hdf5 MAT-File is a h5py Group object. @@ -174,18 +171,17 @@ def _read_hdf_structure(h5Group, This is the translation from FT to Syncopy: - FT Syncopy - - label - channel - trial - trial - time - time - - optional: - fsample - samplerate - - cfg - X - - ''' + +--------------------+------------+ + | FT | Syncopy | + +--------------------+------------+ + | label | channel | + | trial | trial | + | time | time | + | fsample (optional) | samplerate | + | cfg | X | + +--------------------+------------+ + + """ # for user info struct_name = h5Group.name[1:] @@ -197,7 +193,7 @@ def _read_hdf_structure(h5Group, # if we want to support more FT formats in the future # these are numpy arrays holding hdf5 object references - # e.i. one per trial, channel, time (per trial) + # i.e. one per trial, channel, time (per trial) trl_refs = h5Group['trial'][:, 0] time_refs = h5Group['time'][:, 0] chan_refs = h5Group['label'][0, :] @@ -289,33 +285,32 @@ def _read_hdf_structure(h5Group, def _read_dict_structure(structure, add_fields=None): - ''' + """ Local helper to parse a single FT structure - and return an `~syncopy.AnalogData` object + and return an :class:`~syncopy.AnalogData` object Only for for Matlab data format version < 7.3 which was opened via scipy.io.loadmat! This is the translation from FT to Syncopy: - FT Syncopy - - label - channel - trial - trial - time - time - - optional: - fsample - samplerate - - cfg - X + +--------------------+------------+ + | FT | Syncopy | + +--------------------+------------+ + | label | channel | + | trial | trial | + | time | time | + | fsample (optional) | samplerate | + | cfg | X | + +--------------------+------------+ Each trial in FT has nChannels x nSamples ordering, Syncopy has nSamples x nChannels - ''' - + """ + # nTrials = structure["trial"].shape[0] trials = [] - + # 1st trial as reference nChannels, nSamples = structure['trial'][0].shape @@ -363,11 +358,11 @@ def _read_dict_structure(structure, add_fields=None): def _get_Matlab_version(filename): - ''' + """ Peeks into the 1st line of a .mat file and extracts the version information. Works for both < 7.3 and newer MAT-files. - ''' + """ with open(filename, 'rb') as matfile: line1 = next(matfile) @@ -376,7 +371,6 @@ def _get_Matlab_version(filename): # matches for example 'MATLAB 5.01' # with the version as only capture group - pattern = re.compile("^MATLAB\s(\d*\.\d*)") match = pattern.match(header) @@ -392,14 +386,14 @@ def _get_Matlab_version(filename): def _check_req_fields(req_fields, structure): - ''' + """ Just check the the minimal required fields (aka keys in Python) are present in a Matlab structure Works for both old-style (dict) and new-style (hdf5 Group) MAT-file structures. - ''' + """ for key in req_fields: if key not in structure: @@ -410,10 +404,10 @@ def _check_req_fields(req_fields, structure): def _infer_fsample(time_vector): - ''' + """ Akin to `ft_datatype_raw` determine the sampling frequency from the sampling times - ''' + """ return np.mean(np.diff(time_vector)) From 506a38701aae50a8beb20faaf6dc63b68def25b1 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Wed, 16 Feb 2022 16:24:48 +0100 Subject: [PATCH 039/166] NEW: Simple Sequence Parser and PR fixes - included input validation, hard mem_use checks raise exceptions - additional fields now get more parsing for hdf5 - renamed to `include_fields` in signature On branch ft-importer Changes to be committed: modified: syncopy/io/load_ft.py modified: syncopy/shared/parsers.py --- syncopy/io/load_ft.py | 117 ++++++++++++++++++++++++++++++-------- syncopy/shared/parsers.py | 64 ++++++++++++++++++++- 2 files changed, 156 insertions(+), 25 deletions(-) diff --git a/syncopy/io/load_ft.py b/syncopy/io/load_ft.py index e140f026b..bd2674e75 100644 --- a/syncopy/io/load_ft.py +++ b/syncopy/io/load_ft.py @@ -4,14 +4,16 @@ # # Builtin/3rd party package imports -import numpy as np -from scipy import io as sio import re import h5py +import os +import numpy as np +from scipy import io as sio from tqdm import tqdm # Local imports -from syncopy.shared.errors import SPYValueError, SPYInfo, SPYWarning +from syncopy.shared.errors import SPYValueError, SPYInfo, SPYWarning, SPYTypeError +from syncopy.shared.parsers import io_parser, sequence_parser, scalar_parser from syncopy.datatype import AnalogData __all__ = ["load_ft_raw"] @@ -19,7 +21,7 @@ def load_ft_raw(filename, select_structures=None, - add_fields=None, + include_fields=None, mem_use=2000): """ @@ -28,9 +30,9 @@ def load_ft_raw(filename, one for each structure found within the MAT-file. For MAT-File < v7.3 the MAT-file gets loaded completely - into RAM, but its size should be capped at 2GB. + into RAM, but its size should be capped by Matlab at 2GB. The v7.3 is in hdf5 format and will be read in trial-by-trial, - this should be the default for MAT-Files exceeding 2GB. + this should be the Matlab default for MAT-Files exceeding 2GB. The aim is to parse each FT data structure, which have the following fields (Syncopy analogon on the right): @@ -57,7 +59,7 @@ def load_ft_raw(filename, select_structures: sequence or None, optional Sequence of strings, one for each structure, the default `None` will load all structures found - add_fields: sequence, optional + include_fields: sequence, optional Additional MAT-File fields within each structure to be imported. They can be accessed via the `AnalogData.info` attribute. mem_use: int @@ -86,8 +88,30 @@ def load_ft_raw(filename, >>> data_kb = dct['Data_KB'] >>> data_k = dct['Data_K'] + + Load all structures from `example.mat` plus additional field `'chV1'`: + + >>> dct = load_ft_raw('example.mat', include_fields=('chV1',)) + """ + # -- Input validation -- + + io_parser(filename, isfile=True) + + if select_structures is not None: + sequence_parser(select_structures, + varname='select_structures', + content_type=str) + if include_fields is not None: + sequence_parser(include_fields, + varname='include_fields', + content_type=str) + + scalar_parser(mem_use, varname='mem_use', ntype="int_like", lims=[1, np.inf]) + + # -- MAT-File Format -- + # Required fields for the ft_datatype_raw req_fields_raw = ('time', 'trial', 'label') @@ -105,7 +129,7 @@ def load_ft_raw(filename, struct_reader = lambda struct: _read_hdf_structure(struct, h5File=h5File, mem_use=mem_use, - add_fields=add_fields) + include_fields=include_fields) # old format <2GB, use scipy's MAT reader else: @@ -114,6 +138,9 @@ def load_ft_raw(filename, msg = "MAT-File version < 7.3 does not support lazy loading" msg += f"\nReading {filename} might take up to 2GB of RAM, you requested only {mem_use / 1000}GB" SPYWarning(msg) + lgl = '2000 or more MB' + actual = f"{mem_use}" + raise SPYValueError(lgl, varname='mem_use', actual=actual) raw_dict = sio.loadmat(filename, mat_dtype=True, @@ -123,7 +150,7 @@ def load_ft_raw(filename, struct_container = raw_dict struct_reader = lambda struct: _read_dict_structure(struct, - add_fields=add_fields) + include_fields=include_fields) if len(struct_keys) == 0: SPYValueError(legal="At least one structure", @@ -134,6 +161,8 @@ def load_ft_raw(filename, msg = f"Found {len(struct_keys)} structure(s): {struct_keys}" SPYInfo(msg) + # -- IO Operations -- + out_dict = {} # load only a subset @@ -160,7 +189,7 @@ def load_ft_raw(filename, def _read_hdf_structure(h5Group, h5File, mem_use, - add_fields=None): + include_fields=None): """ Each Matlab structure contained in @@ -216,10 +245,13 @@ def _read_hdf_structure(h5Group, trl_size = itemsize * nSamples * nChannels / 1e6 # assumption: single trial fits into RAM - if trl_size > 0.4 * mem_use: + if trl_size >= 0.4 * mem_use: msg = f"\nSingle trial is at least 40% of the requested chache size of {mem_use}MB\n" msg += f"Still trying to load {trl_size:.1f}MB trials.." SPYWarning(msg) + lgl = f'{2.5 * trl_size} or more MB' + actual = f"{mem_use}" + raise SPYValueError(lgl, varname='mem_use', actual=actual) # -- IO process -- @@ -260,30 +292,45 @@ def _read_hdf_structure(h5Group, channels = [''.join(map(chr, h5File[cr][:, 0])) for cr in chan_refs] AData.channel = channels - # -- additional fields -- + # -- Additional Fields -- - # this is the most experimental part AData.info = {} - for field in add_fields: + afields = include_fields if include_fields is not None else range(0) + for field in afields: if field not in h5Group: msg = f"Could not find additional field {field} in {struct_name}" SPYWarning(msg) continue - # again an array of hdf5 object references - af_refs = h5Group[field][:, 0] + dset = h5Group[field] + # we only support fields pointing to + # directly to a dataset containing actual data + # and not references to larger objects + if isinstance(dset[0], h5py.Reference): + msg = f"Could not read additional field {field}\n" + msg += "Only simple fields holding str labels or 1D arrays are supported atm" + SPYWarning(msg) + continue - AData.info[field] = [] - for af_ref in af_refs: - # here would be more parsing needed - # if we want to generally (strings, numbers, ..) - # support this - AData.info[field] = h5File[af_ref] + # ASCII encoding via uint16 + if dset.dtype == np.uint16 and len(dset.shape) == 2: + AData.info[field] = _parse_MAT_hdf_strings(dset) + + # numerical data can be written + # directly as np.array into info dict + elif dset.dtype == np.float64: + AData.info[field] = dset[...] + + else: + msg = f"Could not read additional field {field}\n" + msg += "Unknown data type, only 1D numerical or string arrays/fields supported" + SPYWarning(msg) + continue return AData -def _read_dict_structure(structure, add_fields=None): +def _read_dict_structure(structure, include_fields=None): """ Local helper to parse a single FT structure @@ -350,7 +397,7 @@ def _read_dict_structure(structure, add_fields=None): # write additional fields(non standard FT-format) # into Syncopy config - afields = add_fields if add_fields is not None else range(0) + afields = include_fields if include_fields is not None else range(0) for field in afields: AData.info[field] = structure[field] return AData @@ -411,3 +458,25 @@ def _infer_fsample(time_vector): """ return np.mean(np.diff(time_vector)) + + +def _parse_MAT_hdf_strings(dataset): + + ''' + Expects a hdf5 dataset of shape (X, N), + where X is the number of characters in + a single string, and N is the number of strings. + + The entries themselves are are of integer type, + the ASCII encoding of strings in Matlab v7.3. + + Intended for small(!!) string datasets containing + for example some labels + ''' + + str_seq = [] + for i, ascii_arr in enumerate(dataset[...].T): + string = ''.join(map(chr, ascii_arr)) + str_seq.append(string) + + return np.array(str_seq) diff --git a/syncopy/shared/parsers.py b/syncopy/shared/parsers.py index 73f642b8f..3a8243ddc 100644 --- a/syncopy/shared/parsers.py +++ b/syncopy/shared/parsers.py @@ -688,4 +688,66 @@ def filename_parser(filename, is_in_valid_container=None): "tag": tag, "basename": basename, "extension": ext - } + } + + +def sequence_parser(sequence, content_type=None, varname=""): + + ''' + Check if input is of sequence (list, tuple, array..) + type. Intended for function arguments like + `add_fields = ['fieldA', 'fieldB']`. For numeric + sequences (aka arrays) better to use the `array_parser`. + + Parameters + ---------- + sequence: sequence type + The sequence to check + content_type: type + The type of the sequence contents, e.g. `str` + varname : str + Local variable name used in caller + + See also + -------- + array_parser : similar functionality for parsing array-like objects + + Examples + -------- + + seq1 = ['one', 'two', 'three'] + + This will be parsed, as we check only if + `seq1` is any sequence: + + sequence_parser(seq1) + + This will raise a `SPYTypeError` as the + actual content type is `str` + + sequence_parser(seq1, content_type=int) + + ''' + + # this does NOT capture str and dict + try: + iter(sequence) + except TypeError: + expected = 'sequence' + raise SPYTypeError(sequence, + varname=varname, + expected=expected) + + if isinstance(sequence, str) or isinstance(sequence, dict): + expected = 'sequence' + raise SPYTypeError(sequence, + varname=varname, + expected=expected) + + if content_type is not None: + for element in sequence: + if not isinstance(element, content_type): + expected = content_type.__name__ + raise SPYTypeError(element, + varname=f"element of {varname}", + expected=expected) From 043555d2db954c53188777d7d2b954003f6305c8 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Wed, 16 Feb 2022 16:33:15 +0100 Subject: [PATCH 040/166] FIX: Check for equal trial lengths during hdf5 import Changes to be committed: modified: syncopy/io/load_ft.py --- syncopy/io/load_ft.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/syncopy/io/load_ft.py b/syncopy/io/load_ft.py index bd2674e75..da14e2b2d 100644 --- a/syncopy/io/load_ft.py +++ b/syncopy/io/load_ft.py @@ -267,7 +267,12 @@ def _read_hdf_structure(h5Group, # one swipe per trial for tr in pbar: - ADset[SampleCounter:nSamples, :] = h5File[tr] + trl_array = h5File[tr] + if trl_array.shape != (nSamples, nChannels): + lgl = 'trials of equal lenghts' + actual = 'trials of unequal lengths' + raise SPYValueError(lgl, actual=actual) + ADset[SampleCounter:nSamples, :] = trl_array SampleCounter += nSamples pbar.close() From 288d4732e05ea6860cc48b01af521de2ebb4eec7 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Wed, 16 Feb 2022 22:03:25 +0100 Subject: [PATCH 041/166] CHG: Minor docstring modifications - changed wording of `include_fields` explainer a little On branch ft-importer Changes to be committed: modified: syncopy/io/load_ft.py --- syncopy/io/load_ft.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/syncopy/io/load_ft.py b/syncopy/io/load_ft.py index da14e2b2d..ec9d1af77 100644 --- a/syncopy/io/load_ft.py +++ b/syncopy/io/load_ft.py @@ -38,7 +38,7 @@ def load_ft_raw(filename, have the following fields (Syncopy analogon on the right): +--------------------+------------+ - |FT | Syncopy | + | FT | Syncopy | +--------------------+------------+ | label | channel | | trial | trial | @@ -61,7 +61,8 @@ def load_ft_raw(filename, the default `None` will load all structures found include_fields: sequence, optional Additional MAT-File fields within each structure to - be imported. They can be accessed via the `AnalogData.info` attribute. + be imported. They can be accessed via a purpose-generated `AnalogData.info` + attribute. mem_use: int The amount of RAM requested for the import process in MB. Note that < v7.3 MAT-File formats can only be loaded at once. For MAT-File v7.3 this should @@ -92,7 +93,7 @@ def load_ft_raw(filename, Load all structures from `example.mat` plus additional field `'chV1'`: >>> dct = load_ft_raw('example.mat', include_fields=('chV1',)) - + """ # -- Input validation -- @@ -308,7 +309,7 @@ def _read_hdf_structure(h5Group, continue dset = h5Group[field] - # we only support fields pointing to + # we only support fields pointing # directly to a dataset containing actual data # and not references to larger objects if isinstance(dset[0], h5py.Reference): @@ -336,7 +337,6 @@ def _read_hdf_structure(h5Group, def _read_dict_structure(structure, include_fields=None): - """ Local helper to parse a single FT structure and return an :class:`~syncopy.AnalogData` object @@ -467,7 +467,7 @@ def _infer_fsample(time_vector): def _parse_MAT_hdf_strings(dataset): - ''' + """ Expects a hdf5 dataset of shape (X, N), where X is the number of characters in a single string, and N is the number of strings. @@ -477,7 +477,7 @@ def _parse_MAT_hdf_strings(dataset): Intended for small(!!) string datasets containing for example some labels - ''' + """ str_seq = [] for i, ascii_arr in enumerate(dataset[...].T): From 27f2179d495a42cbcce8e3d87015666832bc2e7e Mon Sep 17 00:00:00 2001 From: tensionhead Date: Thu, 17 Feb 2022 09:22:22 +0100 Subject: [PATCH 042/166] CHG: Amended doc string example for include_fields Changes to be committed: modified: syncopy/io/load_ft.py --- syncopy/io/load_ft.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/syncopy/io/load_ft.py b/syncopy/io/load_ft.py index ec9d1af77..5ce1e0851 100644 --- a/syncopy/io/load_ft.py +++ b/syncopy/io/load_ft.py @@ -94,6 +94,9 @@ def load_ft_raw(filename, >>> dct = load_ft_raw('example.mat', include_fields=('chV1',)) + Access the additionally loaded field: + + >>> dct['Data_K'].info['chV1'] """ # -- Input validation -- From 60059951f377416d8b2d8ec4aacafd8b5d1af8f2 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Thu, 17 Feb 2022 09:44:57 +0100 Subject: [PATCH 043/166] CHG: Removed SPYWarnings before failing - now one SPYInfo for the = 0.4 * mem_use: - msg = f"\nSingle trial is at least 40% of the requested chache size of {mem_use}MB\n" - msg += f"Still trying to load {trl_size:.1f}MB trials.." - SPYWarning(msg) lgl = f'{2.5 * trl_size} or more MB' actual = f"{mem_use}" raise SPYValueError(lgl, varname='mem_use', actual=actual) @@ -261,6 +258,7 @@ def _read_hdf_structure(h5Group, # create new hdf5 dataset for our AnalogData # with the default dimord ['time', 'channel'] + # and our default data type np.float32 -> implicit casting! h5FileOut = h5py.File(AData.filename, mode="w") ADset = h5FileOut.create_dataset("data", dtype=np.float32, From ef8c968c0b6f2addbcdd0efea9fecf6e151c02d1 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Thu, 17 Feb 2022 12:26:46 +0100 Subject: [PATCH 044/166] CHG: Minor edits - included some fixme comments to resolve pending PR discussions On branch ft-importer Changes to be committed: modified: syncopy/io/load_ft.py --- syncopy/io/load_ft.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/syncopy/io/load_ft.py b/syncopy/io/load_ft.py index ab3fb7e98..cfa7cbd01 100644 --- a/syncopy/io/load_ft.py +++ b/syncopy/io/load_ft.py @@ -6,7 +6,6 @@ # Builtin/3rd party package imports import re import h5py -import os import numpy as np from scipy import io as sio from tqdm import tqdm @@ -376,6 +375,7 @@ def _read_dict_structure(structure, include_fields=None): raise SPYValueError(lgl, varname="load .mat", actual=actual) # channel x sample ordering in FT + # default data type np.float32 -> implicit casting! trials.append(trl.T.astype(np.float32)) # initialize AnalogData @@ -403,6 +403,7 @@ def _read_dict_structure(structure, include_fields=None): # write additional fields(non standard FT-format) # into Syncopy config + # FIXME: does this require similar error checking as in the hdf case? afields = include_fields if include_fields is not None else range(0) for field in afields: AData.info[field] = structure[field] @@ -480,6 +481,8 @@ def _parse_MAT_hdf_strings(dataset): for example some labels """ + # FIXME: a simple `for in ascii_arr in dataset` might do the trick as well + # (no need to enumerate)? str_seq = [] for i, ascii_arr in enumerate(dataset[...].T): string = ''.join(map(chr, ascii_arr)) From bc2097ec10a4adba18e5ed64f9730115d7692fe0 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 18 Feb 2022 13:55:33 +0100 Subject: [PATCH 045/166] FIX: Type in /io/__init__.py --- syncopy/io/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncopy/io/__init__.py b/syncopy/io/__init__.py index 7683a3e56..2d07af3c8 100644 --- a/syncopy/io/__init__.py +++ b/syncopy/io/__init__.py @@ -8,7 +8,7 @@ utils, load_spy_container, save_spy_container, - load_ft + load_ft, _load_nwb ) from .utils import * From 1964ddc856df7fb095ee302e518922436013530f Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 18 Feb 2022 14:25:37 +0100 Subject: [PATCH 046/166] CHG: Improved/Included doc for load_ft_raw - corrected the load_ft_raw table to be rendered properly with Sphinx - mentioned the possibility to load MAT-files directly into Syncopy - some minor corrections for the quickstart Changes to be committed: modified: doc/source/quickstart/quickstart.rst modified: doc/source/user/fieldtrip.rst modified: syncopy/io/load_ft.py --- doc/source/quickstart/quickstart.rst | 12 ++++++++---- doc/source/user/fieldtrip.rst | 2 ++ syncopy/io/load_ft.py | 10 +++++++--- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/doc/source/quickstart/quickstart.rst b/doc/source/quickstart/quickstart.rst index bccd6bdb5..9e67cb4ac 100644 --- a/doc/source/quickstart/quickstart.rst +++ b/doc/source/quickstart/quickstart.rst @@ -16,11 +16,13 @@ Here we want to quickly explore some standard analyses for analog data (e.g. MUA Preparations ============ -To start with a clean slate, let's construct a synthetic signal consisting of a damped harmonic and additive white noise: +To start with a clean slate, let's construct a synthetic dataset consisting of a damped harmonic and additive white noise: .. literalinclude:: /quickstart/damped_harm.py -With this we have white noise on both channels, and only channel 1 additionally got the damped harmonic signal. +With this we have a dataset of type :class:`~syncopy.AnalogData`, which is intended for holding time-series data like electrophys. measurements. + +To recap: we have generated a synthetic dataset white noise on both channels, and ``channel1`` additionally carries the damped harmonic signal. .. hint:: Further details about artificial data generation can be found at the :ref:`synth_data` section. @@ -84,6 +86,8 @@ The parameter ``foilim`` controls the *frequencies of interest limits*, so in t informing us, that for this dataset a spectral smoothing of 3Hz required 5 Slepian tapers. +The resulting new dataset ``fft_spectra`` is of type :class:`syncopy.SpectralData`, which is the general datatype storing the results of a time-frequency analysis. + .. hint:: Try typing ``fft_spectra.log`` into your interpreter and have a look at :doc:`Trace Your Steps: Data Logs ` to learn more about Syncopy's logging features @@ -130,6 +134,6 @@ To quickly inspect the results for each channel we can use:: .. image:: wavelet_spec.png :height: 250px -Again, we see a strong 30Hz signal in the 1st channel, and channel 2 is devoid of any rhythms. However now we also get information along the time axis, the dampening of the harmonic in channel 1 is clearly visible for later time points. +Again, we see a strong 30Hz signal in the 1st channel, and channel 2 is devoid of any rhythms. However, in contrast to the ``method=mtmfft`` call, now we also get information along the time axis. The dampening of the harmonic over time in channel 1 is clearly visible. -An improved method, the superlet transform, providing super-resolution time-frequency representations can be computed via ``method='superlet'``, see :func:`~syncopy.freqanalysis` for more details and examples. +An improved method, the superlet transform, providing super-resolution time-frequency representations can be computed via ``method='superlet'``, see :func:`~syncopy.freqanalysis` for more details. diff --git a/doc/source/user/fieldtrip.rst b/doc/source/user/fieldtrip.rst index a20dc2f7c..ab0c71ebf 100644 --- a/doc/source/user/fieldtrip.rst +++ b/doc/source/user/fieldtrip.rst @@ -151,6 +151,8 @@ Data created with Syncopy can be loaded into MATLAB using the `matlab-syncopy development and supports only a subset of data classes. Also, the MATLAB interface does not support loading data that do not fit into local memory. +MAT-Files can also be imported directly into Syncopy via :func:`~syncopy.load_ft_raw`, at the moment only the ``ft_datatype_raw`` is supported. + Exemplary Workflow: Roundtrip - FieldTrip to Syncopy and Back ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ For this illustrative example we start by generating synthetic data in FieldTrip diff --git a/syncopy/io/load_ft.py b/syncopy/io/load_ft.py index cfa7cbd01..6be935e98 100644 --- a/syncopy/io/load_ft.py +++ b/syncopy/io/load_ft.py @@ -38,11 +38,15 @@ def load_ft_raw(filename, +--------------------+------------+ | FT | Syncopy | - +--------------------+------------+ + +====================+============+ | label | channel | + +--------------------+------------+ | trial | trial | + +--------------------+------------+ | time | time | + +--------------------+------------+ | fsample (optional) | samplerate | + +--------------------+------------+ | cfg | ? | +--------------------+------------+ @@ -75,8 +79,8 @@ def load_ft_raw(filename, See also -------- - MAT-File formats: https://de.mathworks.com/help/matlab/import_export/mat-file-versions.html - Field Trip datastructures: https://www.fieldtriptoolbox.org/development/datastructure/ + `MAT-File formats `_ + `Field Trip datastructures `_ Examples -------- From 520f44895331fe2facc1fc565d26fab6a288525d Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Mon, 21 Feb 2022 10:01:27 +0100 Subject: [PATCH 047/166] FIX: Repair array parser - parse mixed-type string arrays (e.g., things like `[3, 's']`) correctly (closes #211) - included two new tests to verify the corrected parsing On branch fixes Changes to be committed: modified: syncopy/shared/parsers.py modified: syncopy/tests/test_parsers.py --- syncopy/shared/parsers.py | 12 ++++++++++-- syncopy/tests/test_parsers.py | 6 ++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/syncopy/shared/parsers.py b/syncopy/shared/parsers.py index 73f642b8f..23e250cb0 100644 --- a/syncopy/shared/parsers.py +++ b/syncopy/shared/parsers.py @@ -340,9 +340,17 @@ def array_parser(var, varname="", ntype=None, hasinf=None, hasnan=None, scalar_parser : similar functionality for parsing numeric scalars """ - # Make sure `var` is array-like and convert it to ndarray to simplify parsing + # Make sure `var` is array-like if not isinstance(var, (np.ndarray, list)): raise SPYTypeError(var, varname=varname, expected="array_like") + + # "Exotic" arrays (str et al.) must contain only elements of the same type + # (however, don't be too stingy with numeric arrays - `[2, 2.0, 3]`` is okay) + if ntype not in [None, "numeric", "int_like"]: + if np.unique([str(type(a)) for a in var]).size > 1: + raise SPYTypeError(var, varname=varname, expected="array elements of identical type") + + # Convert input to ndarray to simplify parsing arr = np.array(var) # If bounds-checking is requested but `ntype` is not set, use the @@ -366,7 +374,7 @@ def array_parser(var, varname="", ntype=None, hasinf=None, hasnan=None, raise SPYValueError(msg.format(dt="numeric"), varname=varname, actual=msg.format(dt=str(arr.dtype))) if ntype == "int_like": - if not np.all([np.round(a) == a for a in arr]): + if not np.array_equal(arr, np.round(arr)): raise SPYValueError(msg.format(dt=ntype), varname=varname) else: if not np.issubdtype(arr.dtype, np.dtype(ntype).type): diff --git a/syncopy/tests/test_parsers.py b/syncopy/tests/test_parsers.py index 8448b637e..b703ab3d0 100755 --- a/syncopy/tests/test_parsers.py +++ b/syncopy/tests/test_parsers.py @@ -172,6 +172,12 @@ def test_ntype(self): with pytest.raises(SPYValueError): array_parser(np.float32(self.time), varname="time", ntype='float64') + # invalid mixed-type arrays + with pytest.raises(SPYTypeError): + array_parser([3, 's'], varname="testarr", ntype='str') + with pytest.raises(SPYTypeError): + array_parser([3, 's'], varname="testarr", ntype='int') + def test_character_list(self): channels = np.array(["channel1", "channel2", "channel3"]) From 98169526611fd0680e41f02d2384c9a0813e3d88 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Mon, 21 Feb 2022 10:19:19 +0100 Subject: [PATCH 048/166] CHG: Updated CHANGELOG - amended CHANGELOG in preparation for bugfix release - set new `releaseVersion` to 0.21 On branch fixes Changes to be committed: modified: CHANGELOG.md modified: setup.py --- CHANGELOG.md | 4 +++- setup.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3196e9c2..74cf487fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,8 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht Bugfix release ### NEW -- Added loading functionality for NWB 2.0 files +- Added experimental loading functionality for NWB 2.0 files +- Added experimental loading functionality for Matlab mat files ### CHANGED - Made plotting routines matplotlib 3.5 compatible @@ -34,6 +35,7 @@ Bugfix release - The `trialdefinition` arrays constructed by the `Selector` class were incorrect for `SpectralData` objects without time-axis, resulting in "empty" trials. This has been fixed (closes #207) +- Repaired `array_parser` to adequately complain about mixed-type arrays (closes #211) ## [v0.20] - 2022-01-18 Major Release diff --git a/setup.py b/setup.py index 26828774f..f373aabaf 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ from conda2pip import conda2pip # Set release version by hand -releaseVersion = "0.2" +releaseVersion = "0.21" # Get necessary and optional package dependencies required, dev = conda2pip(return_lists=True) From 6ce4e34c137e73a6b4445e36954b8368b92ea652 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 21 Feb 2022 10:36:30 +0100 Subject: [PATCH 049/166] CHG: Renamed input_validators to input_processors - most of these routines validate the user supplied frontend arguments AND return values fitting to the downstream methods, hence 'processing' is more fitting than 'validating' modified: syncopy/io/load_ft.py modified: syncopy/nwanalysis/connectivity_analysis.py renamed: syncopy/shared/input_validators.py -> syncopy/shared/input_processors.py modified: syncopy/specest/freqanalysis.py new file: syncopy/tests/README.md --- syncopy/io/load_ft.py | 2 +- syncopy/nwanalysis/connectivity_analysis.py | 28 ++++---- ...nput_validators.py => input_processors.py} | 68 ++++++++++++------- syncopy/specest/freqanalysis.py | 33 +++++---- syncopy/tests/README.md | 23 +++++++ 5 files changed, 96 insertions(+), 58 deletions(-) rename syncopy/shared/{input_validators.py => input_processors.py} (86%) create mode 100644 syncopy/tests/README.md diff --git a/syncopy/io/load_ft.py b/syncopy/io/load_ft.py index 6be935e98..8015f7a1e 100644 --- a/syncopy/io/load_ft.py +++ b/syncopy/io/load_ft.py @@ -86,7 +86,7 @@ def load_ft_raw(filename, -------- Load two structures `'Data_K'` and `'Data_KB'` from a MAT-File `example.mat`: - >>> dct = load_ft_raw('example.mat', select_structures=('Data_K', Data_KB')) + >>> dct = load_ft_raw('example.mat', select_structures=('Data_K', 'Data_KB')) Access the individual :class:`~syncopy.AnalogData` datasets: diff --git a/syncopy/nwanalysis/connectivity_analysis.py b/syncopy/nwanalysis/connectivity_analysis.py index 200d1b1f2..adc0bc834 100644 --- a/syncopy/nwanalysis/connectivity_analysis.py +++ b/syncopy/nwanalysis/connectivity_analysis.py @@ -16,10 +16,10 @@ SPYInfo) from syncopy.shared.kwarg_decorators import (unwrap_cfg, unwrap_select, detect_parallel_client) -from syncopy.shared.input_validators import ( - validate_taper, - validate_foi, - validate_padding, +from syncopy.shared.input_processors import ( + process_taper, + process_foi, + process_padding, check_effective_parameters, check_passed_kwargs ) @@ -176,11 +176,11 @@ def connectivityanalysis(data, method="coh", keeptrials=False, output="abs", raise SPYValueError(legal=lgl, varname="pad_to_length", actual=actual) # the actual number of samples in case of later padding - nSamples = validate_padding(pad_to_length, lenTrials) + nSamples = process_padding(pad_to_length, lenTrials) # --- Basic foi sanitization --- - foi, foilim = validate_foi(foi, foilim, data.samplerate) + foi, foilim = process_foi(foi, foilim, data.samplerate) # only now set foi array for foilim in 1Hz steps if foilim is not None: @@ -223,14 +223,14 @@ def connectivityanalysis(data, method="coh", keeptrials=False, output="abs", foi = freqs # sanitize taper selection and retrieve dpss settings - taper_opt = validate_taper(taper, - tapsmofrq, - nTaper, - keeptapers=False, # ST_CSD's always average tapers - foimax=foi.max(), - samplerate=data.samplerate, - nSamples=nSamples, - output="pow") # ST_CSD's always have this unit/norm + taper_opt = process_taper(taper, + tapsmofrq, + nTaper, + keeptapers=False, # ST_CSD's always average tapers + foimax=foi.max(), + samplerate=data.samplerate, + nSamples=nSamples, + output="pow") # ST_CSD's always have this unit/norm log_dict["foi"] = foi log_dict["taper"] = taper diff --git a/syncopy/shared/input_validators.py b/syncopy/shared/input_processors.py similarity index 86% rename from syncopy/shared/input_validators.py rename to syncopy/shared/input_processors.py index bc185ddd1..333690dc9 100644 --- a/syncopy/shared/input_validators.py +++ b/syncopy/shared/input_processors.py @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- # -# Validators for user submitted frontend arguments like foi, taper, etc. +# Processing of user submitted frontend arguments like foi, taper, etc. +# The processors return values needed directly for the +# downstream method calls. # Input args are the parameters to check for validity + auxiliary parameters -# needed for the checks. +# needed for the checks, raise exceptions in case of invalid input. # # Builtin/3rd party package imports @@ -15,9 +17,27 @@ from syncopy.datatype.methods.padding import _nextpow2 -def validate_padding(pad_to_length, lenTrials): +def process_padding(pad_to_length, lenTrials): + """ - Simplified padding + Simplified padding interface, for all taper based methods + padding has to be done **before** tapering! + + Parameters + ---------- + pad_to_length : int, None or 'nextpow2' + Either an integer indicating the absolute length of + the trials after padding or `'nextpow2'` to pad all trials + to the nearest power of two. If `None`, no padding is to + be performed + lenTrials : sequence of int_like + Sequence holding all individual trial lengths + + Returns + ------- + abs_pad : int + Absolute length of all trials after padding + """ # supported padding options not_valid = False @@ -25,20 +45,14 @@ def validate_padding(pad_to_length, lenTrials): not_valid = True elif isinstance(pad_to_length, str) and pad_to_length not in availablePaddingOpt: not_valid = True - if isinstance(pad_to_length, bool): # bool is an int subclass, check for it separately... + # bool is an int subclass, have to check for it separately... + if isinstance(pad_to_length, bool): not_valid = True if not_valid: lgl = "`None`, 'nextpow2' or an integer like number" actual = f"{pad_to_length}" raise SPYValueError(legal=lgl, varname="pad_to_length", actual=actual) - # here we check for equal lengths trials in case of no user specified absolute padding length - # we do a rough 'maxlen' padding, nextpow2 will be overruled in this case - if lenTrials.min() != lenTrials.max() and not isinstance(pad_to_length, numbers.Number): - abs_pad = int(lenTrials.max()) - msg = f"Unequal trial lengths present, automatic padding to {abs_pad} samples" - SPYWarning(msg) - # zero padding of ALL trials the same way if isinstance(pad_to_length, numbers.Number): @@ -49,20 +63,22 @@ def validate_padding(pad_to_length, lenTrials): abs_pad = pad_to_length # or pad to optimal FFT lengths - # (not possible for unequal lengths trials) elif pad_to_length == 'nextpow2': - # after padding - abs_pad = _nextpow2(int(lenTrials.min())) - # no padding, equal lengths trials - elif pad_to_length is None: + abs_pad = _nextpow2(int(lenTrials.max())) + + # no padding in case of equal length trials + elif pad_to_length is None: abs_pad = int(lenTrials.max()) - + if lenTrials.min() != lenTrials.max(): + msg = f"Unequal trial lengths present, padding all trials to {abs_pad} samples" + SPYWarning(msg) + # `abs_pad` is now the (soon to be padded) signal length in samples return abs_pad -def validate_foi(foi, foilim, samplerate): +def process_foi(foi, foilim, samplerate): """ Parameters @@ -132,14 +148,14 @@ def validate_foi(foi, foilim, samplerate): return foi, foilim -def validate_taper(taper, - tapsmofrq, - nTaper, - keeptapers, - foimax, - samplerate, +def process_taper(taper, + tapsmofrq, + nTaper, + keeptapers, + foimax, + samplerate, nSamples, - output): + output): """ General taper validation and Slepian/dpss input sanitization. diff --git a/syncopy/specest/freqanalysis.py b/syncopy/specest/freqanalysis.py index 589d39d31..32847b381 100644 --- a/syncopy/specest/freqanalysis.py +++ b/syncopy/specest/freqanalysis.py @@ -16,10 +16,10 @@ from syncopy.shared.tools import best_match from syncopy.shared.const_def import spectralConversions -from syncopy.shared.input_validators import ( - validate_taper, - validate_foi, - validate_padding, +from syncopy.shared.input_processors import ( + process_taper, + process_foi, + process_padding, check_effective_parameters, check_passed_kwargs ) @@ -150,9 +150,8 @@ def freqanalysis(data, method='mtmfft', output='pow', to this absolute length. For instance ``pad_to_length = 2000`` pads all trials to an absolute length of 2000 samples, if and only if the longest trial contains at maximum 2000 samples. - Alternatively if all trials have the same initial lengths - setting `pad_to_length='nextpow2'` pads all trials to - the next power of two. + Alternatively `pad_to_length='nextpow2'` pads all trials to + the next power of two of the longest trial. If `None` and trials have unequal lengths all trials are padded to match the longest trial. polyremoval : int or None @@ -333,7 +332,7 @@ def freqanalysis(data, method='mtmfft', output='pow', if method == 'mtmfft': # the actual number of samples in case of later padding - minSampleNum = validate_padding(pad_to_length, lenTrials) + minSampleNum = process_padding(pad_to_length, lenTrials) else: minSampleNum = lenTrials.min() @@ -343,7 +342,7 @@ def freqanalysis(data, method='mtmfft', output='pow', # Shortcut to data sampling interval dt = 1 / data.samplerate - foi, foilim = validate_foi(foi, foilim, data.samplerate) + foi, foilim = process_foi(foi, foilim, data.samplerate) # see also https://docs.obspy.org/_modules/obspy/signal/detrend.html#polynomial if polyremoval is not None: @@ -473,14 +472,14 @@ def freqanalysis(data, method='mtmfft', output='pow', raise SPYValueError(legal=lgl, varname="foi/foilim", actual=act) # sanitize taper selection and retrieve dpss settings - taper_opt = validate_taper(taper, - tapsmofrq, - nTaper, - keeptapers, - foimax=foi.max(), - samplerate=data.samplerate, - nSamples=minSampleNum, - output=output) + taper_opt = process_taper(taper, + tapsmofrq, + nTaper, + keeptapers, + foimax=foi.max(), + samplerate=data.samplerate, + nSamples=minSampleNum, + output=output) # Update `log_dct` w/method-specific options log_dct["taper"] = taper diff --git a/syncopy/tests/README.md b/syncopy/tests/README.md new file mode 100644 index 000000000..1667be489 --- /dev/null +++ b/syncopy/tests/README.md @@ -0,0 +1,23 @@ +## Syncopy Testing Routines + +Frontends and general architecture, for explicit backend methods see `/backend` subdirectory. + +### Run all + +Just launch the `run_tests.sh` script. + +### Manually start specific tests + +Assuming you are in this `/test` directory, +amend your Python path with the `/syncopy` module directory: + +```bash +export PYTHONPATH=../../ +``` + +To run all connectivity tests except the parallel routines: + +```bash +pytest -v test_connectivity.py -k 'not parallel' +``` + From 7176bd387fb5471816083fba17199840ce6980a2 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Mon, 21 Feb 2022 18:06:50 +0100 Subject: [PATCH 050/166] CHG: Include info message for I/O operations related to arithmetic ops - added message to inform about the amount of data is about to be moved around as a result of an arithmetic operation - encapsulated size estimation of active selection in separate function (used by `selectdata` and `_perform_computation`) On branch fixes Changes to be committed: modified: syncopy/datatype/methods/arithmetic.py modified: syncopy/datatype/methods/selectdata.py modified: syncopy/tests/local_spy.py --- syncopy/datatype/methods/arithmetic.py | 16 +++++++++++++++- syncopy/datatype/methods/selectdata.py | 13 ++++++++++--- syncopy/tests/local_spy.py | 2 ++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/syncopy/datatype/methods/arithmetic.py b/syncopy/datatype/methods/arithmetic.py index 532a8e9b4..6972584a6 100644 --- a/syncopy/datatype/methods/arithmetic.py +++ b/syncopy/datatype/methods/arithmetic.py @@ -9,8 +9,9 @@ # Local imports from syncopy import __acme__ +from .selectdata import _get_selection_size from syncopy.shared.parsers import data_parser -from syncopy.shared.errors import SPYValueError, SPYTypeError, SPYWarning +from syncopy.shared.errors import SPYValueError, SPYTypeError, SPYWarning, SPYInfo from syncopy.shared.computational_routine import ComputationalRoutine from syncopy.shared.kwarg_decorators import unwrap_io from syncopy.shared.computational_routine import ComputationalRoutine @@ -365,6 +366,19 @@ def _perform_computation(baseObj, else: raise SPYValueError("supported arithmetic operator", actual=operator) + # Inform about the amount of data is about to be moved around + operandSize = _get_selection_size(baseObj) + sUnit = "MB" + if operandSize > 1000: + operandSize /= 1024 + sUnit = "GB" + msg = "Allocating {dsize:3.2f} {dunit:s} {objkind:s} object on disk for " +\ + "result of {op:s} operation" + SPYInfo(msg.format(dsize=operandSize, + dunit=sUnit, + objkind=out.__class__.__name__, + op=operator), caller=operator) + # If ACME is available, try to attach (already running) parallel computing client parallel = False if __acme__: diff --git a/syncopy/datatype/methods/selectdata.py b/syncopy/datatype/methods/selectdata.py index bb3113db2..0bd058587 100644 --- a/syncopy/datatype/methods/selectdata.py +++ b/syncopy/datatype/methods/selectdata.py @@ -308,9 +308,7 @@ def selectdata(data, trials=None, channels=None, channels_i=None, channels_j=Non return # Inform the user what's about to happen - fauxTrials = [data._preview_trial(trlno) for trlno in data._selection.trials] - fauxSizes = [np.prod(ftrl.shape)*ftrl.dtype.itemsize for ftrl in fauxTrials] - selectionSize = sum(fauxSizes) / 1024**2 + selectionSize = _get_selection_size(data) sUnit = "MB" if selectionSize > 1000: selectionSize /= 1024 @@ -338,6 +336,15 @@ def selectdata(data, trials=None, channels=None, channels_i=None, channels_j=Non return out if new_out else None +def _get_selection_size(data): + """ + Local helper routine for computing the on-disk size of an active data-selection + """ + fauxTrials = [data._preview_trial(trlno) for trlno in data._selection.trials] + fauxSizes = [np.prod(ftrl.shape)*ftrl.dtype.itemsize for ftrl in fauxTrials] + return sum(fauxSizes) / 1024**2 + + @unwrap_io def _selectdata(trl, noCompute=False, chunkShape=None): if noCompute: diff --git a/syncopy/tests/local_spy.py b/syncopy/tests/local_spy.py index 1a7a8c799..5f48a4905 100644 --- a/syncopy/tests/local_spy.py +++ b/syncopy/tests/local_spy.py @@ -26,6 +26,8 @@ # Prepare code to be executed using, e.g., iPython's `%run` magic command if __name__ == "__main__": + sys.exit() + nwbFilePath = "/home/fuertingers/Documents/job/SyNCoPy/Data/tt2.nwb" # nwbFilePath = "/home/fuertingers/Documents/job/SyNCoPy/Data/test.nwb" From aa944b324f1661fe2307d72cb16a531193fd1974 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Tue, 22 Feb 2022 16:09:15 +0100 Subject: [PATCH 051/166] CHG: Improved additional MAT-File fields reading - if a additional field is requested, print all fields found in the respective MAT-File Changes to be committed: modified: syncopy/io/load_ft.py --- syncopy/io/load_ft.py | 101 ++++++++++++++++++++++++------------------ 1 file changed, 57 insertions(+), 44 deletions(-) diff --git a/syncopy/io/load_ft.py b/syncopy/io/load_ft.py index 8015f7a1e..9b7bc8917 100644 --- a/syncopy/io/load_ft.py +++ b/syncopy/io/load_ft.py @@ -17,6 +17,9 @@ __all__ = ["load_ft_raw"] +# Required fields for the ft_datatype_raw +req_fields_raw = ('time', 'trial', 'label') + def load_ft_raw(filename, select_structures=None, @@ -119,9 +122,6 @@ def load_ft_raw(filename, # -- MAT-File Format -- - # Required fields for the ft_datatype_raw - req_fields_raw = ('time', 'trial', 'label') - version = _get_Matlab_version(filename) msg = f"Reading MAT-File version {version} " SPYInfo(msg) @@ -165,7 +165,7 @@ def load_ft_raw(filename, actual="No structure found" ) - msg = f"Found {len(struct_keys)} structure(s): {struct_keys}" + msg = f"Found {len(struct_keys)} structure(s): {struct_keys} in {filename}" SPYInfo(msg) # -- IO Operations -- @@ -303,39 +303,42 @@ def _read_hdf_structure(h5Group, AData.channel = channels # -- Additional Fields -- - - AData.info = {} - afields = include_fields if include_fields is not None else range(0) - for field in afields: - if field not in h5Group: - msg = f"Could not find additional field {field} in {struct_name}" - SPYWarning(msg) - continue - - dset = h5Group[field] - # we only support fields pointing - # directly to a dataset containing actual data - # and not references to larger objects - if isinstance(dset[0], h5py.Reference): - msg = f"Could not read additional field {field}\n" - msg += "Only simple fields holding str labels or 1D arrays are supported atm" - SPYWarning(msg) - continue - - # ASCII encoding via uint16 - if dset.dtype == np.uint16 and len(dset.shape) == 2: - AData.info[field] = _parse_MAT_hdf_strings(dset) - - # numerical data can be written - # directly as np.array into info dict - elif dset.dtype == np.float64: - AData.info[field] = dset[...] - - else: - msg = f"Could not read additional field {field}\n" - msg += "Unknown data type, only 1D numerical or string arrays/fields supported" - SPYWarning(msg) - continue + if include_fields is not None: + AData.info = {} + # additional fields in MAT-File + afields = [k for k in h5Group.keys() if k not in req_fields_raw] + msg = f"Found following additional fields: {afields}" + SPYInfo(msg, caller='load_ft_raw') + for field in include_fields: + if field not in h5Group: + msg = f"Could not find additional field {field} in {struct_name}" + SPYWarning(msg, caller='load_ft_raw') + continue + + dset = h5Group[field] + # we only support fields pointing + # directly to a dataset containing actual data + # and not references to larger objects + if isinstance(dset[0], h5py.Reference): + msg = f"Could not read additional field {field}\n" + msg += "Only simple fields holding str labels or 1D arrays are supported atm" + SPYWarning(msg) + continue + + # ASCII encoding via uint16 + if dset.dtype == np.uint16 and len(dset.shape) == 2: + AData.info[field] = _parse_MAT_hdf_strings(dset) + + # numerical data can be written + # directly as np.array into info dict + elif dset.dtype == np.float64: + AData.info[field] = dset[...] + + else: + msg = f"Could not read additional field {field}\n" + msg += "Unknown data type, only 1D numerical or string arrays/fields supported" + SPYWarning(msg) + continue return AData @@ -390,7 +393,6 @@ def _read_dict_structure(structure, include_fields=None): AData = AnalogData(trials, samplerate=samplerate) - AData.info = {} # get the channel ids channels = structure['label'] # set the channel ids @@ -405,12 +407,23 @@ def _read_dict_structure(structure, include_fields=None): trl_def = np.hstack([AData.sampleinfo, offsets[:, None]]) AData.trialdefinition = trl_def - # write additional fields(non standard FT-format) - # into Syncopy config - # FIXME: does this require similar error checking as in the hdf case? - afields = include_fields if include_fields is not None else range(0) - for field in afields: - AData.info[field] = structure[field] + # -- Additional Fields -- + + if include_fields is not None: + AData.info = {} + # additional fields in MAT-File + afields = [k for k in structure.keys() if k not in req_fields_raw] + msg = f"Found following additional fields: {afields}" + SPYInfo(msg, caller='load_ft_raw') + + for field in include_fields: + if field not in structure: + msg = f"Could not find additional field {field}" + SPYWarning(msg, caller='load_ft_raw') + continue + + AData.info[field] = structure[field] + return AData From 5ed85cbba55593860b3c76a6ed6011739d313313 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Tue, 22 Feb 2022 16:28:03 +0100 Subject: [PATCH 052/166] FIX : Trial-by-trial writing for hdf5 - always increment both sides of a slice :D Changes to be committed: modified: syncopy/io/load_ft.py --- syncopy/io/load_ft.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncopy/io/load_ft.py b/syncopy/io/load_ft.py index 9b7bc8917..eeb1dbfec 100644 --- a/syncopy/io/load_ft.py +++ b/syncopy/io/load_ft.py @@ -277,7 +277,7 @@ def _read_hdf_structure(h5Group, lgl = 'trials of equal lenghts' actual = 'trials of unequal lengths' raise SPYValueError(lgl, actual=actual) - ADset[SampleCounter:nSamples, :] = trl_array + ADset[SampleCounter:SampleCounter + nSamples, :] = trl_array SampleCounter += nSamples pbar.close() From 4ca3dc4e3edbb83ba659ba166c855352cfcb84ea Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 25 Feb 2022 14:19:19 +0100 Subject: [PATCH 053/166] FIX: Return correct trialtimes for SpikeData - return a list of arrays with length nTrials instead of a generator with length nSpikes - fixes #218 with the solution provided by @atlaie Changes to be committed: modified: syncopy/datatype/base_data.py modified: syncopy/datatype/discrete_data.py --- syncopy/datatype/base_data.py | 1 + syncopy/datatype/discrete_data.py | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/syncopy/datatype/base_data.py b/syncopy/datatype/base_data.py index bf1a32ba9..c948978ae 100644 --- a/syncopy/datatype/base_data.py +++ b/syncopy/datatype/base_data.py @@ -593,6 +593,7 @@ def sampleinfo(self, sinfo): @property def _t0(self): + """ These are the trigger offsets """ if self._trialdefinition is not None: return self._trialdefinition[:, 2] else: diff --git a/syncopy/datatype/discrete_data.py b/syncopy/datatype/discrete_data.py index 73e75b514..50ab90dec 100644 --- a/syncopy/datatype/discrete_data.py +++ b/syncopy/datatype/discrete_data.py @@ -182,9 +182,10 @@ def trials(self): def trialtime(self): """list(:class:`numpy.ndarray`): trigger-relative sample times in s""" if self.samplerate is not None and self.sampleinfo is not None: - return [((t + self._t0[tk]) / self.samplerate \ - for t in range(0, int(self.sampleinfo[tk, 1] - self.sampleinfo[tk, 0]))) \ - for tk in self.trialid] + return [ + (trl[:, 0] - self._t0[tk] - self.sampleinfo[tk, 0]) / self.samplerate + for tk, trl in enumerate(self.trials) + ] # Helper function that grabs a single trial def _get_trial(self, trialno): From 91376cb623c648f4ccb6b498b849e6efbc96d960 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 25 Feb 2022 15:54:13 +0100 Subject: [PATCH 054/166] NEW: Support tapers other than dpss with additional parameters - tapers like 'kaiser' need additional parameters which now can be passed from the fronted via the new `taper_opt` parameter - no changes in backend needed as here a `taper_opt` was already present On branch general_tapers Changes to be committed: modified: syncopy/nwanalysis/connectivity_analysis.py modified: syncopy/shared/const_def.py modified: syncopy/shared/input_processors.py modified: syncopy/specest/freqanalysis.py --- syncopy/nwanalysis/connectivity_analysis.py | 8 +- syncopy/shared/const_def.py | 6 +- syncopy/shared/input_processors.py | 87 +++++++++++++-------- syncopy/specest/freqanalysis.py | 17 ++-- 4 files changed, 71 insertions(+), 47 deletions(-) diff --git a/syncopy/nwanalysis/connectivity_analysis.py b/syncopy/nwanalysis/connectivity_analysis.py index adc0bc834..3aec673c6 100644 --- a/syncopy/nwanalysis/connectivity_analysis.py +++ b/syncopy/nwanalysis/connectivity_analysis.py @@ -36,7 +36,7 @@ @detect_parallel_client def connectivityanalysis(data, method="coh", keeptrials=False, output="abs", foi=None, foilim=None, pad_to_length=None, - polyremoval=None, taper="hann", tapsmofrq=None, + polyremoval=None, taper="hann", taper_opt=None, tapsmofrq=None, nTaper=None, out=None, **kwargs): """ @@ -224,6 +224,7 @@ def connectivityanalysis(data, method="coh", keeptrials=False, output="abs", # sanitize taper selection and retrieve dpss settings taper_opt = process_taper(taper, + taper_opt, tapsmofrq, nTaper, keeptapers=False, # ST_CSD's always average tapers @@ -234,10 +235,11 @@ def connectivityanalysis(data, method="coh", keeptrials=False, output="abs", log_dict["foi"] = foi log_dict["taper"] = taper - # only dpss returns non-empty taper_opt dict - if taper_opt: + if taper_opt and taper == 'dpss': log_dict["nTaper"] = taper_opt["Kmax"] log_dict["tapsmofrq"] = tapsmofrq + elif taper_opt: + log_dict["taper_opt"] = taper_opt check_effective_parameters(ST_CrossSpectra, defaults, lcls) # parallel computation over trials diff --git a/syncopy/shared/const_def.py b/syncopy/shared/const_def.py index 4fb55049c..b15307ae5 100644 --- a/syncopy/shared/const_def.py +++ b/syncopy/shared/const_def.py @@ -20,10 +20,8 @@ #: available tapers of :func:`~syncopy.freqanalysis` and :func:`~syncopy.connectivity` all_windows = windows.__all__ -all_windows.remove("exponential") # not symmetric -all_windows.remove("hanning") # deprecated -all_windows.remove("gaussian") # we don't support taper with args -all_windows.remove("kaiser") # we don't support taper with args +all_windows.remove("exponential") # not symmetric +all_windows.remove("hanning") # deprecated availableTapers = all_windows availablePaddingOpt = [None, 'nextpow2'] diff --git a/syncopy/shared/input_processors.py b/syncopy/shared/input_processors.py index 333690dc9..a6dd4ef90 100644 --- a/syncopy/shared/input_processors.py +++ b/syncopy/shared/input_processors.py @@ -4,12 +4,14 @@ # The processors return values needed directly for the # downstream method calls. # Input args are the parameters to check for validity + auxiliary parameters -# needed for the checks, raise exceptions in case of invalid input. +# needed for the checks. Processors raise exceptions in case of invalid input. # # Builtin/3rd party package imports import numpy as np import numbers +from inspect import signature +from scipy.signal import windows from syncopy.shared.errors import SPYValueError, SPYWarning, SPYInfo from syncopy.shared.parsers import scalar_parser, array_parser @@ -18,26 +20,26 @@ def process_padding(pad_to_length, lenTrials): - + """ Simplified padding interface, for all taper based methods padding has to be done **before** tapering! Parameters - ---------- + ---------- pad_to_length : int, None or 'nextpow2' - Either an integer indicating the absolute length of + Either an integer indicating the absolute length of the trials after padding or `'nextpow2'` to pad all trials - to the nearest power of two. If `None`, no padding is to + to the nearest power of two. If `None`, no padding is to be performed lenTrials : sequence of int_like Sequence holding all individual trial lengths Returns - ------- + ------- abs_pad : int Absolute length of all trials after padding - + """ # supported padding options not_valid = False @@ -45,8 +47,8 @@ def process_padding(pad_to_length, lenTrials): not_valid = True elif isinstance(pad_to_length, str) and pad_to_length not in availablePaddingOpt: not_valid = True - # bool is an int subclass, have to check for it separately... - if isinstance(pad_to_length, bool): + # bool is an int subclass, have to check for it separately... + if isinstance(pad_to_length, bool): not_valid = True if not_valid: lgl = "`None`, 'nextpow2' or an integer like number" @@ -65,14 +67,14 @@ def process_padding(pad_to_length, lenTrials): # or pad to optimal FFT lengths elif pad_to_length == 'nextpow2': abs_pad = _nextpow2(int(lenTrials.max())) - + # no padding in case of equal length trials elif pad_to_length is None: abs_pad = int(lenTrials.max()) if lenTrials.min() != lenTrials.max(): msg = f"Unequal trial lengths present, padding all trials to {abs_pad} samples" SPYWarning(msg) - + # `abs_pad` is now the (soon to be padded) signal length in samples return abs_pad @@ -149,18 +151,21 @@ def process_foi(foi, foilim, samplerate): def process_taper(taper, + taper_opt, tapsmofrq, nTaper, keeptapers, foimax, samplerate, - nSamples, + nSamples, output): """ General taper validation and Slepian/dpss input sanitization. - The default is to max out `nTaper` to achieve the desired frequency - smoothing bandwidth. For details about the Slepion settings see + + For `taper='dpss'` the default is to max out `nTaper` to achieve + the desired frequency smoothing bandwidth. + For details about the Slepion settings see "The Effective Bandwidth of a Multitaper Spectral Estimator, A. T. Walden, E. J. McCoy and D. B. Percival" @@ -169,10 +174,13 @@ def process_taper(taper, ---------- taper : str Windowing function, one of :data:`~syncopy.shared.const_def.availableTapers` + taper_opt : dict or None + Dictionary holding additional keywords for tapers which have additional + parameters like for example :func:`~scipy.signal.windows.kaiser` tapsmofrq : float or None Taper smoothing bandwidth for `taper='dpss'` nTaper : int_like or None - Number of tapers to use for multi-tapering (not recommended) + Number of tapers to use for multi-tapering with `taper='dpss'` (not recommended) Other Parameters ---------------- @@ -188,10 +196,11 @@ def process_taper(taper, Returns ------- - dpss_opt : dict + taper_opt : dict For multi-tapering (`taper='dpss'`) contains the parameters `NW` and `Kmax` for `scipy.signal.windows.dpss`. - For all other tapers this is an empty dictionary. + For other tapers these are the additional parameters or + an empty dictionary. """ # See if taper choice is supported @@ -199,8 +208,13 @@ def process_taper(taper, lgl = "'" + "or '".join(opt + "' " for opt in availableTapers) raise SPYValueError(legal=lgl, varname="taper", actual=taper) - # Warn user about DPSS only settings + if not isinstance(taper_opt, (dict, type(None))): + lgl = "dict or None" + actual = type(taper_opt) + raise SPYValueError(lgl, "taper_opt", actual) + if taper != "dpss": + # Warn user about DPSS only settings if tapsmofrq is not None: msg = "`tapsmofrq` is only used if `taper` is `dpss`!" SPYWarning(msg) @@ -211,8 +225,27 @@ def process_taper(taper, msg = "`keeptapers` is only used if `taper` is `dpss`!" SPYWarning(msg) - # empty dpss_opt, only Slepians have options - return {} + if taper_opt is not None: + # availableTapers are given by windows.__all__ + parameters = signature(getattr(windows, taper)).parameters + supported_kws = list(parameters.keys()) + # 'M' is the kw for the window length + # for all of scipy's windows + supported_kws.remove('M') + + for key in taper_opt: + if key not in supported_kws: + lgl = f"one of {supported_kws} for `taper='{taper}'`" + raise SPYValueError(lgl, "taper_opt key", key) + # all supplied keys are fine + return taper_opt + else: + # taper_opt was None + return {} + + if taper == "dpss" and taper_opt is not None: + msg = "For multi-tapering with `taper='dpss'` use `tapsmofrq` and `nTaper` to control frequency smoothing, `taper_opt` has no effect" + SPYWarning(msg) # direct mtm estimate (averaging) only valid for spectral power if taper == "dpss" and not keeptapers and output != "pow": @@ -244,16 +277,6 @@ def process_taper(taper, lgl = "smoothing bandwidth in Hz, typical values are in the range 1-10Hz" raise SPYValueError(legal=lgl, varname="tapsmofrq", actual=tapsmofrq) - # Try to derive "sane" settings by using 3/4 octave - # smoothing of highest `foi` - # following Hill et al. "Oscillatory Synchronization in Large-Scale - # Cortical Networks Predicts Perception", Neuron, 2011 - # FIX ME: This "sane setting" seems quite excessive (huuuge bwidths) - - # tapsmofrq = (foimax * 2**(3 / 4 / 2) - foimax * 2**(-3 / 4 / 2)) / 2 - # msg = f'Automatic setting of `tapsmofrq` to {tapsmofrq:.2f}' - # SPYInfo(msg) - # -------------------------------------------- # set parameters for scipy.signal.windows.dpss NW = tapsmofrq * nSamples / (2 * samplerate) @@ -266,7 +289,7 @@ def process_taper(taper, if nTaper is None: msg = f'Using {Kmax} taper(s) for multi-tapering' SPYInfo(msg) - dpss_opt = {'NW' : NW, 'Kmax' : Kmax} + dpss_opt = {'NW': NW, 'Kmax': Kmax} return dpss_opt elif nTaper is not None: @@ -285,7 +308,7 @@ def process_taper(taper, ''' SPYWarning(msg) - dpss_opt = {'NW' : NW, 'Kmax' : nTaper} + dpss_opt = {'NW': NW, 'Kmax': nTaper} return dpss_opt diff --git a/syncopy/specest/freqanalysis.py b/syncopy/specest/freqanalysis.py index 32847b381..633b91aff 100644 --- a/syncopy/specest/freqanalysis.py +++ b/syncopy/specest/freqanalysis.py @@ -48,8 +48,8 @@ @detect_parallel_client def freqanalysis(data, method='mtmfft', output='pow', keeptrials=True, foi=None, foilim=None, - pad_to_length=None, polyremoval=None, - taper="hann", tapsmofrq=None, nTaper=None, keeptapers=False, + pad_to_length=None, polyremoval=None, taper="hann", + taper_opt=None, tapsmofrq=None, nTaper=None, keeptapers=False, toi="all", t_ftimwin=None, wavelet="Morlet", width=6, order=None, order_max=None, order_min=1, c_1=3, adaptive=False, out=None, **kwargs): @@ -471,8 +471,9 @@ def freqanalysis(data, method='mtmfft', output='pow', act = "empty frequency selection" raise SPYValueError(legal=lgl, varname="foi/foilim", actual=act) - # sanitize taper selection and retrieve dpss settings + # sanitize taper selection and/or retrieve dpss settings taper_opt = process_taper(taper, + taper_opt, tapsmofrq, nTaper, keeptapers, @@ -483,10 +484,11 @@ def freqanalysis(data, method='mtmfft', output='pow', # Update `log_dct` w/method-specific options log_dct["taper"] = taper - # only dpss returns non-empty taper_opt dict - if taper_opt: + if taper_opt and taper == 'dpss': log_dct["nTaper"] = taper_opt["Kmax"] log_dct["tapsmofrq"] = tapsmofrq + elif taper_opt: + log_dct["taper_opt"] = taper_opt # ------------------------------------------------------- # Now, prepare explicit compute-classes for chosen method @@ -628,13 +630,12 @@ def freqanalysis(data, method='mtmfft', output='pow', else: soi = [slice(None)] * numTrials - # Collect keyword args for `mtmconvol` in dictionary method_kwargs = {"samplerate": data.samplerate, "nperseg": nperseg, "noverlap": noverlap, - "taper" : taper, - "taper_opt" : taper_opt} + "taper": taper, + "taper_opt": taper_opt} # Set up compute-class specestMethod = MultiTaperFFTConvol( From f83188af815fe93ac84d2e18f4462cc326557c1f Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 25 Feb 2022 16:03:03 +0100 Subject: [PATCH 055/166] FIX: Amended frontend doc strings On branch general_tapers Changes to be committed: modified: syncopy/nwanalysis/connectivity_analysis.py modified: syncopy/specest/freqanalysis.py --- syncopy/nwanalysis/connectivity_analysis.py | 4 ++++ syncopy/specest/freqanalysis.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/syncopy/nwanalysis/connectivity_analysis.py b/syncopy/nwanalysis/connectivity_analysis.py index 3aec673c6..6f29e1387 100644 --- a/syncopy/nwanalysis/connectivity_analysis.py +++ b/syncopy/nwanalysis/connectivity_analysis.py @@ -115,6 +115,10 @@ def connectivityanalysis(data, method="coh", keeptrials=False, output="abs", taper : str Only valid if `method` is `'coh'` or `'granger'`. Windowing function, one of :data:`~syncopy.specest.const_def.availableTapers` + taper_opt : dict or None + Dictionary with keys for additional taper parameters. + For example :func:`~scipy.signal.windows.kaiser` has + the additional parameter 'beta'. tapsmofrq : float Only valid if `method` is `'coh'` or `'granger'` and `taper` is `'dpss'`. The amount of spectral smoothing through multi-tapering (Hz). diff --git a/syncopy/specest/freqanalysis.py b/syncopy/specest/freqanalysis.py index 633b91aff..4af46746b 100644 --- a/syncopy/specest/freqanalysis.py +++ b/syncopy/specest/freqanalysis.py @@ -165,6 +165,10 @@ def freqanalysis(data, method='mtmfft', output='pow', taper : str Only valid if `method` is `'mtmfft'` or `'mtmconvol'`. Windowing function, one of :data:`~syncopy.shared.const_def.availableTapers` (see below). + taper_opt : dict or None + Dictionary with keys for additional taper parameters. + For example :func:`~scipy.signal.windows.kaiser` has + the additional parameter 'beta'. tapsmofrq : float Only valid if `method` is `'mtmfft'` or `'mtmconvol'` and `taper` is `'dpss'`. The amount of spectral smoothing through multi-tapering (Hz). From aae976f44bd3b2ed56df0be25d6b187bfbe59597 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 25 Feb 2022 17:51:23 +0100 Subject: [PATCH 056/166] CHG: Demeaning after tapering (only) for Granger and additional frontend checks - Granger-Geweke needs full frequency range [0, Nyquist], this gets now enforced in the connectivity fronted - optional demeaning after tapering for mtmfft, only needed for Granger so far, actually gives problems for coherence estimates at the 0-frequency - resolves #194 On branch granger_improvements Changes to be committed: modified: syncopy/nwanalysis/ST_compRoutines.py modified: syncopy/nwanalysis/connectivity_analysis.py modified: syncopy/nwanalysis/csd.py modified: syncopy/specest/mtmfft.py modified: syncopy/tests/local_spy.py modified: syncopy/tests/test_connectivity.py --- syncopy/nwanalysis/ST_compRoutines.py | 5 ++- syncopy/nwanalysis/connectivity_analysis.py | 20 +++++++++ syncopy/nwanalysis/csd.py | 5 ++- syncopy/specest/mtmfft.py | 21 +++++---- syncopy/tests/local_spy.py | 50 ++++----------------- syncopy/tests/test_connectivity.py | 27 ++++++++--- 6 files changed, 70 insertions(+), 58 deletions(-) diff --git a/syncopy/nwanalysis/ST_compRoutines.py b/syncopy/nwanalysis/ST_compRoutines.py index 564092427..096ec414c 100644 --- a/syncopy/nwanalysis/ST_compRoutines.py +++ b/syncopy/nwanalysis/ST_compRoutines.py @@ -27,6 +27,7 @@ def cross_spectra_cF(trl_dat, foi=None, taper="hann", taper_opt=None, + demean_taper=False, polyremoval=False, timeAxis=0, chunkShape=None, @@ -77,6 +78,8 @@ def cross_spectra_cF(trl_dat, `'Kmax'` and `'NW'`. For further details, please refer to the `SciPy docs `_ + demean_taper : bool + Set to `True` to perform de-meaning after tapering polyremoval : int or None Order of polynomial used for de-trending data in the time domain prior to spectral analysis. A value of 0 corresponds to subtracting the mean @@ -153,7 +156,7 @@ def cross_spectra_cF(trl_dat, dat = detrend(dat, type='linear', axis=0, overwrite_data=True) CS_ij = csd(dat, samplerate, nSamples, taper=taper, taper_opt=taper_opt) - + # where does freqs go/come from - # we will eventually solve this issue.. return CS_ij[None, freq_idx, ...] diff --git a/syncopy/nwanalysis/connectivity_analysis.py b/syncopy/nwanalysis/connectivity_analysis.py index adc0bc834..a67754444 100644 --- a/syncopy/nwanalysis/connectivity_analysis.py +++ b/syncopy/nwanalysis/connectivity_analysis.py @@ -195,6 +195,19 @@ def connectivityanalysis(data, method="coh", keeptrials=False, output="abs", "pad_to_length": pad_to_length} # --- Setting up specific Methods --- + if method == 'granger': + + if foi is not None or foilim is not None: + lgl = "no foi specification for Granger analysis" + actual = "foi or foilim specification" + raise SPYValueError(lgl, 'foi/foilim', actual) + + nChannels = len(data.channel) + nTrials = len(lenTrials) + # warn user if this ratio is not small + if nChannels / nTrials > 0.1: + msg = "Multi-channel Granger analysis can be numerically unstable, it is recommended to have at least 10 times the number of trials compared to the number of channels. Try calculating in sub-groups of fewer channels!" + SPYWarning(msg) if method in ['coh', 'granger']: @@ -240,11 +253,18 @@ def connectivityanalysis(data, method="coh", keeptrials=False, output="abs", log_dict["tapsmofrq"] = tapsmofrq check_effective_parameters(ST_CrossSpectra, defaults, lcls) + + if method == 'granger': + demean_taper = True + else: + demean_taper = False + # parallel computation over trials st_compRoutine = ST_CrossSpectra(samplerate=data.samplerate, nSamples=nSamples, taper=taper, taper_opt=taper_opt, + demean_taper=demean_taper, polyremoval=polyremoval, timeAxis=timeAxis, foi=foi) diff --git a/syncopy/nwanalysis/csd.py b/syncopy/nwanalysis/csd.py index bd9bbb770..3411324f6 100644 --- a/syncopy/nwanalysis/csd.py +++ b/syncopy/nwanalysis/csd.py @@ -19,6 +19,7 @@ def csd(trl_dat, nSamples=None, taper="hann", taper_opt=None, + demean_taper=False, norm=False, fullOutput=False): @@ -62,6 +63,8 @@ def csd(trl_dat, `'Kmax'` and `'NW'`. For further details, please refer to the `SciPy docs `_ + demean_taper : bool + Set to `True` to perform de-meaning after tapering norm : bool, optional Set to `True` to normalize for a single-trial coherence measure. Only meaningful in a multi-taper (``taper = "dpss"``) setup and if no @@ -91,7 +94,7 @@ def csd(trl_dat, # compute the individual spectra # specs have shape (nTapers x nFreq x nChannels) - specs, freqs = mtmfft(trl_dat, samplerate, nSamples, taper, taper_opt) + specs, freqs = mtmfft(trl_dat, samplerate, nSamples, taper, taper_opt, demean_taper) # outer product along channel axes # has shape (nTapers x nFreq x nChannels x nChannels) diff --git a/syncopy/specest/mtmfft.py b/syncopy/specest/mtmfft.py index 0431caef1..5e3d5a988 100644 --- a/syncopy/specest/mtmfft.py +++ b/syncopy/specest/mtmfft.py @@ -8,7 +8,12 @@ from scipy import signal -def mtmfft(data_arr, samplerate, nSamples=None, taper="hann", taper_opt=None): +def mtmfft(data_arr, + samplerate, + nSamples=None, + taper="hann", + taper_opt=None, + demean_taper=False): """ (Multi-)tapered fast Fourier transform. Returns full complex Fourier transform for each taper. @@ -33,12 +38,8 @@ def mtmfft(data_arr, samplerate, nSamples=None, taper="hann", taper_opt=None): `'Kmax'` and `'NW'`. For further details, please refer to the `SciPy docs `_ - n : int or None - Number of points along transformation axis in the input to use. - If `n` is smaller than the length of the input, the input is cropped. - If it is larger, the input is padded with zeros. If `n` is not given, - the length of the input along the axis specified by `axis` is used. - + demean_taper : bool + Set to `True` to perform de-meaning after tapering Returns ------- @@ -97,9 +98,13 @@ def mtmfft(data_arr, samplerate, nSamples=None, taper="hann", taper_opt=None): for taperIdx, win in enumerate(windows): win = np.tile(win, (nChannels, 1)).T + tapered = data_arr * win + # de-mean again after tapering - needed for Granger! + if demean_taper: + tapered = tapered - tapered.mean(axis=0) # real fft takes only 'half the energy'/positive frequencies, # multiply by 2 to correct for this - ftr[taperIdx] = 2 * np.fft.rfft(data_arr * win, n=nSamples, axis=0) + ftr[taperIdx] = 2 * np.fft.rfft(tapered, n=nSamples, axis=0) # normalization ftr[taperIdx] /= np.sqrt(nSamples) diff --git a/syncopy/tests/local_spy.py b/syncopy/tests/local_spy.py index 5f48a4905..0e2c21658 100644 --- a/syncopy/tests/local_spy.py +++ b/syncopy/tests/local_spy.py @@ -20,52 +20,20 @@ from syncopy.tests.misc import generate_artificial_data from syncopy.tests import synth_data -from pynwb import NWBHDF5IO # Prepare code to be executed using, e.g., iPython's `%run` magic command if __name__ == "__main__": - sys.exit() + mock_up = np.arange(24).reshape((8, 3)) + ad1 = spy.AnalogData([mock_up] * 5) - nwbFilePath = "/home/fuertingers/Documents/job/SyNCoPy/Data/tt2.nwb" - # nwbFilePath = "/home/fuertingers/Documents/job/SyNCoPy/Data/test.nwb" - - xx = spy.load_nwb(nwbFilePath) - - - # nwbio = NWBHDF5IO(nwbFilePath, "r", load_namespaces=True) - # nwbfile = nwbio.read() - - # AR(2) Network test data - AdjMat = synth_data.mk_RandomAdjMat(nChannels) - trls = [100 * synth_data.AR2_network(AdjMat) for _ in range(nTrials)] - tdat1 = spy.AnalogData(trls, samplerate=fs) - - # phase difusion test data - f1, f2 = 10, 40 + nTrials = 50 + nSamples = 200 trls = [] for _ in range(nTrials): - - p1 = synth_data.phase_diffusion(f1, eps=.01, nChannels=nChannels, nSamples=nSamples) - p2 = synth_data.phase_diffusion(f2, eps=0.001, nChannels=nChannels, nSamples=nSamples) - trls.append( - 1 * np.cos(p1) + 1 * np.cos(p2) + 0.6 * np.random.randn( - nSamples, nChannels)) - - tdat2 = spy.AnalogData(trls, samplerate=1000) - - - # Test stuff within here... - data1 = generate_artificial_data(nTrials=5, nChannels=16, equidistant=False, inmemory=False) - data2 = generate_artificial_data(nTrials=5, nChannels=16, equidistant=True, inmemory=False) - - - - # client = spy.esi_cluster_setup(interactive=False) - # data1 + data2 - - # sys.exit() - # spec = spy.freqanalysis(artdata, method="mtmfft", taper="dpss", output="pow") - - + # defaults AR(2) parameters yield 40Hz peak + trls.append(synth_data.AR2_network(None, nSamples=nSamples)) + ad1 = spy.AnalogData(trls, samplerate=200) + gr = spy.connectivityanalysis(ad1, method='granger', taper='dpss', tapsmofrq=3, + foilim=[0, 100]) diff --git a/syncopy/tests/test_connectivity.py b/syncopy/tests/test_connectivity.py index 063d26ee6..055770e5c 100644 --- a/syncopy/tests/test_connectivity.py +++ b/syncopy/tests/test_connectivity.py @@ -67,7 +67,7 @@ class TestGranger: def test_gr_solution(self, **kwargs): Gcaus = ca(self.data, method='granger', taper='dpss', - tapsmofrq=3, foi=self.foi, **kwargs) + tapsmofrq=3, foi=None, **kwargs) # check all channel combinations with coupling for i, j in zip(*self.cpl_idx): @@ -105,12 +105,21 @@ def test_gr_selections(self): def test_gr_foi(self): - call = lambda foi, foilim: ca(self.data, - method='granger', - foi=foi, - foilim=foilim) + try: + ca(self.data, + method='granger', + foi=np.arange(0, 70) + ) + except SPYValueError as err: + assert 'no foi specification' in str(err) - run_foi_test(call, foilim=[0, 70]) + try: + ca(self.data, + method='granger', + foilim=[0, 70] + ) + except SPYValueError as err: + assert 'no foi specification' in str(err) def test_gr_cfg(self): @@ -149,6 +158,7 @@ def test_gr_polyremoval(self): # remove the constant again self.data = self.data - 10 + class TestCoherence: nSamples = 1500 @@ -202,6 +212,8 @@ def test_coh_solution(self, **kwargs): null_idx *= (res.freq < self.f2 - 5) | (res.freq > self.f2 + 5) assert np.all(res.data[0, null_idx, 0, 1] < 0.1) + plot_coh(res, 0, 1, label="channel 0-1") + def test_coh_selections(self): selections = mk_selection_dicts(self.nTrials, @@ -463,7 +475,6 @@ def run_cfg_test(call, method, positivity=True): cfg = get_defaults(ca) cfg.method = method - cfg.foilim = [0, 70] cfg.taper = 'parzen' cfg.output = 'abs' @@ -615,6 +626,7 @@ def plot_coh(res, i, j, label=''): ax.set_xlabel('frequency (Hz)') ax.set_ylabel('coherence $|CSD|^2$') ax.plot(res.freq, res.data[0, :, i, j], label=label) + ax.legend() def plot_corr(res, i, j, label=''): @@ -623,6 +635,7 @@ def plot_corr(res, i, j, label=''): ax.set_xlabel('lag (s)') ax.set_ylabel('Correlation') ax.plot(res.time[0], res.data[:, 0, i, j], label=label) + ax.legend() if __name__ == '__main__': From 80cdca7120ced3481bdfb1d67fe2507dea2ede1f Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 28 Feb 2022 10:27:05 +0100 Subject: [PATCH 057/166] NEW: load_ft_raw tests - new pytest mark to test if run on esi fs - added `list_only` kw to load_ft_raw to allow just peeking into .mat files On branch ft-importer-tests Changes to be committed: modified: syncopy/io/load_ft.py modified: syncopy/tests/test_spyio.py --- syncopy/io/load_ft.py | 22 +++++++-- syncopy/tests/test_spyio.py | 91 ++++++++++++++++++++++++++++++++++++- 2 files changed, 106 insertions(+), 7 deletions(-) diff --git a/syncopy/io/load_ft.py b/syncopy/io/load_ft.py index eeb1dbfec..55c7365f7 100644 --- a/syncopy/io/load_ft.py +++ b/syncopy/io/load_ft.py @@ -22,6 +22,7 @@ def load_ft_raw(filename, + list_only=False, select_structures=None, include_fields=None, mem_use=2000): @@ -62,6 +63,9 @@ def load_ft_raw(filename, ---------- filename: str Path to the MAT-file + list_only: bool, optional + Set to `True` to return only a list containing the names + of the structures found select_structures: sequence or None, optional Sequence of strings, one for each structure, the default `None` will load all structures found @@ -103,6 +107,11 @@ def load_ft_raw(filename, Access the additionally loaded field: >>> dct['Data_K'].info['chV1'] + + Just peek into the MAT-File and get a list of the contained structures: + + >>> load_ft_raw('example.mat', list_only=True) + >>> ['Data_K', 'Data_KB'] """ # -- Input validation -- @@ -159,15 +168,18 @@ def load_ft_raw(filename, struct_reader = lambda struct: _read_dict_structure(struct, include_fields=include_fields) + msg = f"Found {len(struct_keys)} structure(s): {struct_keys} in {filename}" + SPYInfo(msg) + + if list_only: + return struct_keys + if len(struct_keys) == 0: SPYValueError(legal="At least one structure", varname=filename, actual="No structure found" ) - msg = f"Found {len(struct_keys)} structure(s): {struct_keys} in {filename}" - SPYInfo(msg) - # -- IO Operations -- out_dict = {} @@ -320,7 +332,7 @@ def _read_hdf_structure(h5Group, # directly to a dataset containing actual data # and not references to larger objects if isinstance(dset[0], h5py.Reference): - msg = f"Could not read additional field {field}\n" + msg = f"Could not read additional field '{field}'\n" msg += "Only simple fields holding str labels or 1D arrays are supported atm" SPYWarning(msg) continue @@ -335,7 +347,7 @@ def _read_hdf_structure(h5Group, AData.info[field] = dset[...] else: - msg = f"Could not read additional field {field}\n" + msg = f"Could not read additional field '{field}'\n" msg += "Unknown data type, only 1D numerical or string arrays/fields supported" SPYWarning(msg) continue diff --git a/syncopy/tests/test_spyio.py b/syncopy/tests/test_spyio.py index 9bc8b0d43..ac422723b 100644 --- a/syncopy/tests/test_spyio.py +++ b/syncopy/tests/test_spyio.py @@ -17,12 +17,23 @@ # Local imports from syncopy.datatype import AnalogData -from syncopy.io import save, load +from syncopy.io import save, load, load_ft_raw from syncopy.shared.filetypes import FILE_EXT -from syncopy.shared.errors import SPYValueError, SPYIOError, SPYError +from syncopy.shared.errors import ( + SPYValueError, + SPYIOError, + SPYError, + SPYTypeError +) import syncopy.datatype as swd from syncopy.tests.misc import generate_artificial_data + +# Decorator to detect if test data dir is available +on_esi = os.path.isdir('/cs/scratch/syncopy') +skip_no_esi = pytest.mark.skipif(not on_esi, reason="ESI fs not available") + + class TestSpyIO(): # Allocate test-datasets for AnalogData, SpectralData, SpikeData and EventData objects @@ -452,3 +463,79 @@ def test_save_mmap(self): # Delete all open references to file objects b4 closing tmp dir del dmap, adata + + +@skip_no_esi +class Test_FT_Importer: + + """At the moment only ft_datatype_raw is supported""" + + mat_file_dir = '/cs/scratch/syncopy/MAT-Files' + + def test_read_hdf(self): + """Test MAT-File v73 reader, uses h5py""" + + mat_name = 'Mohsen-v73.mat' + fname = os.path.join(self.mat_file_dir, mat_name) + + dct = load_ft_raw(fname) + assert 'Data_K' in dct + AData = dct['Data_K'] + + assert isinstance(AData, AnalogData) + assert len(AData.trials) == 393 + assert len(AData.channel) == 218 + + # list only structure names + slist = load_ft_raw(fname, list_only=True) + assert 'Data_K' in slist + assert 'Data_KB' in slist + + # additional fields of Matlab structures + # get attached to .info dict + # hdf reader does NOT support nested fields + dct = load_ft_raw(fname, include_fields=('chV1',)) + AData2 = dct['Data_K'] + assert 'chV1' in AData2.info + assert len(AData2.info['chV1']) == 30 + assert isinstance(AData2.info['chV1'][0], str) + + # test loading a subset of structures + dct = load_ft_raw(fname, select_structures=('Data_KB',)) + assert 'Data_KB' in dct + assert 'Data_K' not in dct + + # test str sequence parsing + try: + dct = load_ft_raw(fname, select_structures=(3, 'sth')) + except SPYTypeError as err: + assert 'expected str found int' in str(err) + + try: + dct = load_ft_raw(fname, include_fields=(3, 'sth')) + except SPYTypeError as err: + assert 'expected str found int' in str(err) + + def test_read_dict(self): + """Test MAT-File v7 reader, based on scipy.io.loadmat""" + + mat_name = 'MohsenK-v7.mat' + fname = os.path.join(self.mat_file_dir, mat_name) + + dct = load_ft_raw(fname) + assert 'Data_K' in dct + AData = dct['Data_K'] + + assert isinstance(AData, AnalogData) + assert len(AData.trials) == 393 + assert len(AData.channel) == 218 + + slist = load_ft_raw(fname, list_only=True) + assert 'Data_K' in slist + + # additional fields of Matlab structures + # get attached to .info dict + # here nested structures are supported (but dis-encouraged) + dct = load_ft_raw(fname, include_fields=('ch',)) + AData2 = dct['Data_K'] + assert 'ch' in AData2.info From f96c69988a57ed4d515c2d6473bf5ff0fe5b4a87 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 28 Feb 2022 15:10:55 +0100 Subject: [PATCH 058/166] CHG: Multitapering enabled via tapsmofrq setting - as soon as `tapsmofrq` is set implicitly use 'dpss' tapers - more intense argument parsing for tapers with additional parameters On branch general_tapers Changes to be committed: modified: syncopy/nwanalysis/connectivity_analysis.py modified: syncopy/shared/const_def.py modified: syncopy/shared/input_processors.py modified: syncopy/specest/freqanalysis.py modified: syncopy/tests/test_connectivity.py modified: syncopy/tests/test_specest.py --- syncopy/nwanalysis/connectivity_analysis.py | 44 ++++---- syncopy/shared/const_def.py | 1 + syncopy/shared/input_processors.py | 116 +++++++++++--------- syncopy/specest/freqanalysis.py | 45 ++++---- syncopy/tests/test_connectivity.py | 8 +- syncopy/tests/test_specest.py | 17 +-- 6 files changed, 123 insertions(+), 108 deletions(-) diff --git a/syncopy/nwanalysis/connectivity_analysis.py b/syncopy/nwanalysis/connectivity_analysis.py index 6f29e1387..d9e7a9c80 100644 --- a/syncopy/nwanalysis/connectivity_analysis.py +++ b/syncopy/nwanalysis/connectivity_analysis.py @@ -36,8 +36,8 @@ @detect_parallel_client def connectivityanalysis(data, method="coh", keeptrials=False, output="abs", foi=None, foilim=None, pad_to_length=None, - polyremoval=None, taper="hann", taper_opt=None, tapsmofrq=None, - nTaper=None, out=None, **kwargs): + polyremoval=None, tapsmofrq=None, nTaper=None, + taper="hann", taper_opt=None, out=None, **kwargs): """ Perform connectivity analysis of Syncopy :class:`~syncopy.AnalogData` objects @@ -112,22 +112,22 @@ def connectivityanalysis(data, method="coh", keeptrials=False, output="abs", the next power of two. If `None` and trials have unequal lengths all trials are padded to match the longest trial. - taper : str + tapsmofrq : float or None + Only valid if `method` is `'coh'` or `'granger'`. + Enables multi-tapering and sets the amount of spectral + smoothing with slepian tapers in Hz. + nTaper : int or None + Only valid if `method` is `'coh'` or `'granger'` and `tapsmofrq` is set. + Number of orthogonal tapers to use for multi-tapering. It is not recommended to set the number + of tapers manually! Leave at `None` for the optimal number to be set automatically. + taper : str or None, optional Only valid if `method` is `'coh'` or `'granger'`. Windowing function, one of :data:`~syncopy.specest.const_def.availableTapers` + For multi-tapering with slepian tapers use `tapsmofrq` directly. taper_opt : dict or None Dictionary with keys for additional taper parameters. For example :func:`~scipy.signal.windows.kaiser` has - the additional parameter 'beta'. - tapsmofrq : float - Only valid if `method` is `'coh'` or `'granger'` and `taper` is `'dpss'`. - The amount of spectral smoothing through multi-tapering (Hz). - Note that smoothing frequency specifications are one-sided, - i.e., 4 Hz smoothing means plus-minus 4 Hz, i.e., a 8 Hz smoothing box. - nTaper : int or None - Only valid if `method` is `'coh'` or `'granger'` and ``taper = 'dpss'``. - Number of orthogonal tapers to use. It is not recommended to set the number - of tapers manually! Leave at `None` for the optimal number to be set automatically. + the additional parameter 'beta'. For multi-tapering use `tapsmofrq` directly. Examples -------- @@ -227,15 +227,15 @@ def connectivityanalysis(data, method="coh", keeptrials=False, output="abs", foi = freqs # sanitize taper selection and retrieve dpss settings - taper_opt = process_taper(taper, - taper_opt, - tapsmofrq, - nTaper, - keeptapers=False, # ST_CSD's always average tapers - foimax=foi.max(), - samplerate=data.samplerate, - nSamples=nSamples, - output="pow") # ST_CSD's always have this unit/norm + taper, taper_opt = process_taper(taper, + taper_opt, + tapsmofrq, + nTaper, + keeptapers=False, # ST_CSD's always average tapers + foimax=foi.max(), + samplerate=data.samplerate, + nSamples=nSamples, + output="pow") # ST_CSD's always have this unit/norm log_dict["foi"] = foi log_dict["taper"] = taper diff --git a/syncopy/shared/const_def.py b/syncopy/shared/const_def.py index b15307ae5..c703304b3 100644 --- a/syncopy/shared/const_def.py +++ b/syncopy/shared/const_def.py @@ -22,6 +22,7 @@ all_windows = windows.__all__ all_windows.remove("exponential") # not symmetric all_windows.remove("hanning") # deprecated +all_windows.remove("dpss") # activated via `tapsmofrq` availableTapers = all_windows availablePaddingOpt = [None, 'nextpow2'] diff --git a/syncopy/shared/input_processors.py b/syncopy/shared/input_processors.py index a6dd4ef90..a457424e0 100644 --- a/syncopy/shared/input_processors.py +++ b/syncopy/shared/input_processors.py @@ -163,8 +163,8 @@ def process_taper(taper, """ General taper validation and Slepian/dpss input sanitization. - For `taper='dpss'` the default is to max out `nTaper` to achieve - the desired frequency smoothing bandwidth. + For multi-tapering with slepian tapers the default is to max out + `nTaper` to achieve the desired frequency smoothing bandwidth. For details about the Slepion settings see "The Effective Bandwidth of a Multitaper Spectral Estimator, @@ -178,9 +178,9 @@ def process_taper(taper, Dictionary holding additional keywords for tapers which have additional parameters like for example :func:`~scipy.signal.windows.kaiser` tapsmofrq : float or None - Taper smoothing bandwidth for `taper='dpss'` + Taper smoothing bandwidth for multi-tapering with 'dpss' window nTaper : int_like or None - Number of tapers to use for multi-tapering with `taper='dpss'` (not recommended) + Number of tapers to use for multi-tapering (not recommended) Other Parameters ---------------- @@ -196,13 +196,23 @@ def process_taper(taper, Returns ------- + taper : str or None + The user supplied taper taper_opt : dict - For multi-tapering (`taper='dpss'`) contains the - parameters `NW` and `Kmax` for `scipy.signal.windows.dpss`. + For multi-tapering contains the + keys `NW` and `Kmax` for `scipy.signal.windows.dpss`. For other tapers these are the additional parameters or - an empty dictionary. + an empty dictionary in case selected taper has no further args. """ + if taper == 'dpss': + lgl = "set `tapsmofrq` parameter directly for multi-tapering" + raise SPYValueError(legal=lgl, varname='taper', actual=taper) + + # no tapering at all + if taper is None and tapsmofrq is None: + return None, {} + # See if taper choice is supported if taper not in availableTapers: lgl = "'" + "or '".join(opt + "' " for opt in availableTapers) @@ -213,75 +223,83 @@ def process_taper(taper, actual = type(taper_opt) raise SPYValueError(lgl, "taper_opt", actual) - if taper != "dpss": - # Warn user about DPSS only settings - if tapsmofrq is not None: - msg = "`tapsmofrq` is only used if `taper` is `dpss`!" - SPYWarning(msg) + # -- no multi-tapering -- + if tapsmofrq is None: if nTaper is not None: - msg = "`nTaper` is only used if `taper` is `dpss`!" + msg = "`nTaper` is only used for multi-tapering!" SPYWarning(msg) if keeptapers: - msg = "`keeptapers` is only used if `taper` is `dpss`!" + msg = "`keeptapers` is only used for multi-tapering!" SPYWarning(msg) + # availableTapers are given by windows.__all__ + parameters = signature(getattr(windows, taper)).parameters + supported_kws = list(parameters.keys()) + # 'M' is the kw for the window length + # for all of scipy's windows + supported_kws.remove('M') + supported_kws.remove('sym') + if taper_opt is not None: - # availableTapers are given by windows.__all__ - parameters = signature(getattr(windows, taper)).parameters - supported_kws = list(parameters.keys()) - # 'M' is the kw for the window length - # for all of scipy's windows - supported_kws.remove('M') + + if len(supported_kws) == 0: + lgl = f"`None`, taper '{taper}' has no additional parameters" + raise SPYValueError(lgl, varname='taper_opt', actual=taper_opt) for key in taper_opt: if key not in supported_kws: lgl = f"one of {supported_kws} for `taper='{taper}'`" raise SPYValueError(lgl, "taper_opt key", key) + for key in supported_kws: + if key not in taper_opt: + lgl = f"additional parameter '{key}' for `taper='{taper}'`" + raise SPYValueError(lgl, "taper_opt", None) # all supplied keys are fine - return taper_opt + return taper, taper_opt + + elif len(supported_kws) > 0: + lgl = f"additional parameters for taper '{taper}': {supported_kws}" + raise SPYValueError(lgl, varname='taper_opt', actual=taper_opt) else: - # taper_opt was None - return {} + # taper_opt was None and taper needs no additional parameters + return taper, {} - if taper == "dpss" and taper_opt is not None: - msg = "For multi-tapering with `taper='dpss'` use `tapsmofrq` and `nTaper` to control frequency smoothing, `taper_opt` has no effect" - SPYWarning(msg) + # -- multi-tapering -- + else: + if taper != 'hann': + lgl = "`None` for multi-tapering, just set `tapsmofrq`" + raise SPYValueError(lgl, varname='taper', actual=taper) - # direct mtm estimate (averaging) only valid for spectral power - if taper == "dpss" and not keeptapers and output != "pow": - lgl = "'pow', the only valid option for taper averaging" - raise SPYValueError(legal=lgl, varname="output", actual=output) + if taper_opt is not None: + msg = "For multi-tapering use `tapsmofrq` and `nTaper` to control frequency smoothing, `taper_opt` has no effect" + SPYWarning(msg) - # Set/get `tapsmofrq` if we're working w/Slepian tapers - elif taper == "dpss": + # direct mtm estimate (averaging) only valid for spectral power + if not keeptapers and output != "pow": + lgl = "'pow', the only valid option for taper averaging" + raise SPYValueError(legal=lgl, varname="output", actual=output) # --- minimal smoothing bandwidth --- # --- such that Kmax/nTaper is at least 1 minBw = 2 * samplerate / nSamples # ----------------------------------- - # user set tapsmofrq directly - if tapsmofrq is not None: - try: - scalar_parser(tapsmofrq, varname="tapsmofrq", lims=[0, np.inf]) - except Exception as exc: - raise exc - - if tapsmofrq < minBw: - msg = f'Setting tapsmofrq to the minimal attainable bandwidth of {minBw:.2f}Hz' - SPYInfo(msg) - tapsmofrq = minBw - - # we now enforce a user submitted smoothing bw - else: + try: + scalar_parser(tapsmofrq, varname="tapsmofrq", lims=[0, np.inf]) + except Exception as exc: lgl = "smoothing bandwidth in Hz, typical values are in the range 1-10Hz" raise SPYValueError(legal=lgl, varname="tapsmofrq", actual=tapsmofrq) + if tapsmofrq < minBw: + msg = f'Setting tapsmofrq to the minimal attainable bandwidth of {minBw:.2f}Hz' + SPYInfo(msg) + tapsmofrq = minBw + # -------------------------------------------- # set parameters for scipy.signal.windows.dpss NW = tapsmofrq * nSamples / (2 * samplerate) # from the minBw setting NW always is at least 1 - Kmax = int(2 * NW - 1) # optimal number of tapers + Kmax = int(2 * NW - 1) # optimal number of tapers # -------------------------------------------- # the recommended way: @@ -290,7 +308,7 @@ def process_taper(taper, msg = f'Using {Kmax} taper(s) for multi-tapering' SPYInfo(msg) dpss_opt = {'NW': NW, 'Kmax': Kmax} - return dpss_opt + return 'dpss', dpss_opt elif nTaper is not None: try: @@ -309,7 +327,7 @@ def process_taper(taper, SPYWarning(msg) dpss_opt = {'NW': NW, 'Kmax': nTaper} - return dpss_opt + return 'dpss', dpss_opt def check_effective_parameters(CR, defaults, lcls): diff --git a/syncopy/specest/freqanalysis.py b/syncopy/specest/freqanalysis.py index 4af46746b..a564f8230 100644 --- a/syncopy/specest/freqanalysis.py +++ b/syncopy/specest/freqanalysis.py @@ -162,24 +162,25 @@ def freqanalysis(data, method='mtmfft', output='pow', If `polyremoval` is `None`, no de-trending is performed. Note that for spectral estimation de-meaning is very advisable and hence also the default. - taper : str + tapsmofrq : float or None + Only valid if `method` is `'mtmfft'` or `'mtmconvol'` + Enables multi-tapering and sets the amount of spectral + smoothing with slepian tapers in Hz. + nTaper : int or None + Only valid if `method` is `'mtmfft'` or `'mtmconvol'` and `tapsmofrq` is set. + Number of orthogonal tapers to use for multi-tapering. It is not recommended to set the number + of tapers manually! Leave at `None` for the optimal number to be set automatically. + taper : str or None, optional Only valid if `method` is `'mtmfft'` or `'mtmconvol'`. Windowing function, - one of :data:`~syncopy.shared.const_def.availableTapers` (see below). + one of :data:`~syncopy.specest.const_def.availableTapers` + For multi-tapering with slepian tapers use `tapsmofrq` directly. taper_opt : dict or None Dictionary with keys for additional taper parameters. For example :func:`~scipy.signal.windows.kaiser` has - the additional parameter 'beta'. - tapsmofrq : float - Only valid if `method` is `'mtmfft'` or `'mtmconvol'` and `taper` is `'dpss'`. - The amount of spectral smoothing through multi-tapering (Hz). - Note that smoothing frequency specifications are one-sided, - i.e., 4 Hz smoothing means plus-minus 4 Hz, i.e., a 8 Hz smoothing box. - nTaper : int or None - Only valid if `method` is `'mtmfft'` or `'mtmconvol'` and `taper='dpss'`. - Number of orthogonal tapers to use. It is not recommended to set the number - of tapers manually! Leave at `None` for the optimal number to be set automatically. + the additional parameter 'beta'. For multi-tapering use `tapsmofrq` directly. keeptapers : bool - Only valid if `method` is `'mtmfft'` or `'mtmconvol'`. + Only valid if `method` is `'mtmfft'` or `'mtmconvol'` and multi-tapering enabled + via setting `tapsmofrq`. If `True`, return spectral estimates for each taper. Otherwise power spectrum is averaged across tapers, if and only if `output` is `pow`. @@ -476,15 +477,15 @@ def freqanalysis(data, method='mtmfft', output='pow', raise SPYValueError(legal=lgl, varname="foi/foilim", actual=act) # sanitize taper selection and/or retrieve dpss settings - taper_opt = process_taper(taper, - taper_opt, - tapsmofrq, - nTaper, - keeptapers, - foimax=foi.max(), - samplerate=data.samplerate, - nSamples=minSampleNum, - output=output) + taper, taper_opt = process_taper(taper, + taper_opt, + tapsmofrq, + nTaper, + keeptapers, + foimax=foi.max(), + samplerate=data.samplerate, + nSamples=minSampleNum, + output=output) # Update `log_dct` w/method-specific options log_dct["taper"] = taper diff --git a/syncopy/tests/test_connectivity.py b/syncopy/tests/test_connectivity.py index 063d26ee6..a434128f8 100644 --- a/syncopy/tests/test_connectivity.py +++ b/syncopy/tests/test_connectivity.py @@ -66,7 +66,7 @@ class TestGranger: def test_gr_solution(self, **kwargs): - Gcaus = ca(self.data, method='granger', taper='dpss', + Gcaus = ca(self.data, method='granger', tapsmofrq=3, foi=self.foi, **kwargs) # check all channel combinations with coupling @@ -180,7 +180,6 @@ def test_coh_solution(self, **kwargs): method='coh', foilim=[5, 60], output='pow', - taper='dpss', tapsmofrq=1.5, **kwargs) @@ -464,7 +463,10 @@ def run_cfg_test(call, method, positivity=True): cfg.method = method cfg.foilim = [0, 70] - cfg.taper = 'parzen' + # test general tapers with + # additional parameters + cfg.taper = 'kaiser' + cfg.taper_opt = {'beta': 2} cfg.output = 'abs' result = call(cfg) diff --git a/syncopy/tests/test_specest.py b/syncopy/tests/test_specest.py index d00fc26ba..0d32189d7 100644 --- a/syncopy/tests/test_specest.py +++ b/syncopy/tests/test_specest.py @@ -189,7 +189,7 @@ def test_allocout(self): # keep trials but throw away tapers out = SpectralData(dimord=SpectralData._defaultDimord) - freqanalysis(self.adata, method="mtmfft", taper="dpss", + freqanalysis(self.adata, method="mtmfft", tapsmofrq=3, keeptapers=False, output="pow", out=out) assert out.sampleinfo.shape == (self.nTrials, 2) assert out.taper.size == 1 @@ -197,7 +197,6 @@ def test_allocout(self): # re-use `cfg` from above and additionally throw away `tapers` cfg.dataset = self.adata cfg.out = SpectralData(dimord=SpectralData._defaultDimord) - cfg.taper = "dpss" cfg.tapsmofrq = 3 cfg.output = "pow" cfg.keeptapers = False @@ -258,12 +257,12 @@ def test_dpss(self): # ensure default setting results in single taper spec = freqanalysis(self.adata, method="mtmfft", - taper="dpss", tapsmofrq=3, output="pow", select=select) + tapsmofrq=3, output="pow", select=select) assert spec.taper.size == 1 assert spec.channel.size == len(chanList) # specify tapers - spec = freqanalysis(self.adata, method="mtmfft", taper="dpss", + spec = freqanalysis(self.adata, method="mtmfft", tapsmofrq=7, keeptapers=True, select=select) assert spec.channel.size == len(chanList) @@ -273,7 +272,6 @@ def test_dpss(self): timeAxis = artdata.dimord.index("time") cfg = StructDict() cfg.method = "mtmfft" - cfg.taper = "dpss" cfg.tapsmofrq = 9.3 cfg.output = "pow" @@ -374,7 +372,7 @@ def test_vdata(self): vdata = VirtualData([dmap, dmap]) avdata = AnalogData(vdata, samplerate=self.fs, trialdefinition=self.trialdefinition) - spec = freqanalysis(avdata, method="mtmfft", taper="dpss", + spec = freqanalysis(avdata, method="mtmfft", tapsmofrq=3, keeptapers=False, output="abs", pad="relative", padlength=npad) assert (np.diff(avdata.sampleinfo)[0][0] + npad) / 2 + 1 == spec.freq.size @@ -397,7 +395,6 @@ def test_parallel(self, testcluster): # now create uniform `cfg` for remaining SLURM tests cfg = StructDict() cfg.method = "mtmfft" - cfg.taper = "dpss" cfg.tapsmofrq = 9.3 cfg.output = "pow" @@ -535,7 +532,7 @@ def test_tf_allocout(self): # keep trials but throw away tapers out = SpectralData(dimord=SpectralData._defaultDimord) - freqanalysis(self.tfData, method="mtmconvol", taper="dpss", tapsmofrq=3, + freqanalysis(self.tfData, method="mtmconvol", tapsmofrq=3, keeptapers=False, output="pow", toi=0.0, t_ftimwin=1.0, out=out) assert out.sampleinfo.shape == (self.nTrials, 2) @@ -544,7 +541,6 @@ def test_tf_allocout(self): # re-use `cfg` from above and additionally throw away `tapers` cfg.dataset = self.tfData cfg.out = SpectralData(dimord=SpectralData._defaultDimord) - cfg.taper = "dpss" cfg.tapsmofrq = 3 cfg.keeptapers = False cfg.output = "pow" @@ -696,7 +692,6 @@ def test_tf_toi(self): # Test correct time-array assembly for ``toi = "all"`` (cut down data signifcantly # to not overflow memory here); same for ``toi = 1.0``` - cfg.taper = "dpss" cfg.tapsmofrq = 10 cfg.keeptapers = True cfg.select = {"trials": [0], "channels": [0], "toilim": [-0.5, 0.5]} @@ -737,7 +732,6 @@ def test_tf_irregular_trials(self): # also make sure ``toi = "all"`` works under any circumstance cfg = get_defaults(freqanalysis) cfg.method = "mtmconvol" - cfg.taper = "dpss" cfg.tapsmofrq = 10 cfg.t_ftimwin = 1.0 cfg.output = "pow" @@ -848,7 +842,6 @@ def test_tf_parallel(self, testcluster): # equidistant trial spacing, keep tapers cfg.output = "abs" - cfg.taper = "dpss" cfg.tapsmofrq = 10 cfg.keeptapers = True artdata = generate_artificial_data(nTrials=self.nTrials, nChannels=self.nChannels, From 050a9509631443f42e6bd0ef3c578cb6db70124b Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 28 Feb 2022 15:18:16 +0100 Subject: [PATCH 059/166] CHG: PR fixes --- syncopy/nwanalysis/connectivity_analysis.py | 7 +------ syncopy/specest/mtmfft.py | 6 +++--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/syncopy/nwanalysis/connectivity_analysis.py b/syncopy/nwanalysis/connectivity_analysis.py index a67754444..42a3f70cd 100644 --- a/syncopy/nwanalysis/connectivity_analysis.py +++ b/syncopy/nwanalysis/connectivity_analysis.py @@ -254,17 +254,12 @@ def connectivityanalysis(data, method="coh", keeptrials=False, output="abs", check_effective_parameters(ST_CrossSpectra, defaults, lcls) - if method == 'granger': - demean_taper = True - else: - demean_taper = False - # parallel computation over trials st_compRoutine = ST_CrossSpectra(samplerate=data.samplerate, nSamples=nSamples, taper=taper, taper_opt=taper_opt, - demean_taper=demean_taper, + demean_taper=(method == 'granger'), polyremoval=polyremoval, timeAxis=timeAxis, foi=foi) diff --git a/syncopy/specest/mtmfft.py b/syncopy/specest/mtmfft.py index 5e3d5a988..da1c2b347 100644 --- a/syncopy/specest/mtmfft.py +++ b/syncopy/specest/mtmfft.py @@ -98,13 +98,13 @@ def mtmfft(data_arr, for taperIdx, win in enumerate(windows): win = np.tile(win, (nChannels, 1)).T - tapered = data_arr * win + win *= data_arr # de-mean again after tapering - needed for Granger! if demean_taper: - tapered = tapered - tapered.mean(axis=0) + win -= win.mean(axis=0) # real fft takes only 'half the energy'/positive frequencies, # multiply by 2 to correct for this - ftr[taperIdx] = 2 * np.fft.rfft(tapered, n=nSamples, axis=0) + ftr[taperIdx] = 2 * np.fft.rfft(win, n=nSamples, axis=0) # normalization ftr[taperIdx] /= np.sqrt(nSamples) From 9778e25a72bbd5bdabb6c180850c4950968c715a Mon Sep 17 00:00:00 2001 From: Gregor Moenke Date: Mon, 28 Feb 2022 15:27:30 +0100 Subject: [PATCH 060/166] anonymize data --- syncopy/tests/test_spyio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/syncopy/tests/test_spyio.py b/syncopy/tests/test_spyio.py index ac422723b..a5324e3ea 100644 --- a/syncopy/tests/test_spyio.py +++ b/syncopy/tests/test_spyio.py @@ -475,7 +475,7 @@ class Test_FT_Importer: def test_read_hdf(self): """Test MAT-File v73 reader, uses h5py""" - mat_name = 'Mohsen-v73.mat' + mat_name = 'matdata-v73.mat' fname = os.path.join(self.mat_file_dir, mat_name) dct = load_ft_raw(fname) @@ -519,7 +519,7 @@ def test_read_hdf(self): def test_read_dict(self): """Test MAT-File v7 reader, based on scipy.io.loadmat""" - mat_name = 'MohsenK-v7.mat' + mat_name = 'matdataK-v7.mat' fname = os.path.join(self.mat_file_dir, mat_name) dct = load_ft_raw(fname) From f2d3ad536ad1286509a9dcca61faa4f87ee56916 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 28 Feb 2022 17:38:34 +0100 Subject: [PATCH 061/166] FIX: Adapt connectivity test - granger no longer allows for specifying foi/foilim On branch general_tapers Changes to be committed: modified: syncopy/tests/test_connectivity.py --- syncopy/tests/test_connectivity.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/syncopy/tests/test_connectivity.py b/syncopy/tests/test_connectivity.py index bfc39e8e7..217945767 100644 --- a/syncopy/tests/test_connectivity.py +++ b/syncopy/tests/test_connectivity.py @@ -476,7 +476,8 @@ def run_cfg_test(call, method, positivity=True): cfg = get_defaults(ca) cfg.method = method - cfg.foilim = [0, 70] + if method != 'granger': + cfg.foilim = [0, 70] # test general tapers with # additional parameters cfg.taper = 'kaiser' From 79450a55d411dc9560ee09bd3540c81edf735382 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Mon, 28 Feb 2022 20:15:15 +0100 Subject: [PATCH 062/166] WIP: First draft of scalar-selection support - allow scalars for single-element selections - restrict channel-pair selections to single elements or slices (addresses #216) On branch fix_selectcsdata Changes to be committed: modified: syncopy/datatype/base_data.py modified: syncopy/datatype/continuous_data.py modified: syncopy/tests/local_spy.py --- syncopy/datatype/base_data.py | 17 +++++++++++++++-- syncopy/datatype/continuous_data.py | 6 ++++-- syncopy/tests/local_spy.py | 9 +++++++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/syncopy/datatype/base_data.py b/syncopy/datatype/base_data.py index c948978ae..c4608a546 100644 --- a/syncopy/datatype/base_data.py +++ b/syncopy/datatype/base_data.py @@ -1861,11 +1861,15 @@ def _selection_setter(self, data, select, dataprop, selectkey): hasnan = False hasinf = False + # Convert 'all' selections to take-all `None` (see next if below) and + # put single-string selections into a list; same for single-scalar selections if isinstance(selection, str): if selection == "all": selection = None else: - raise SPYValueError(legal="'all'", varname=vname, actual=selection) + selection = [selection] + elif np.issubdtype(type(selection), np.number): + selection = [selection] # Take entire inventory sitting in `dataprop` if selection is None: @@ -1942,11 +1946,18 @@ def _selection_setter(self, data, select, dataprop, selectkey): if dataprop in ["unit", "eventid"]: setattr(self, selector, getattr(data, "_get_" + dataprop)(self.trials, idxList)) else: + # be careful w/pairwise channel selections in `CrossSpectralData` objects + if dataprop in ["channel_i", "channel_j"]: + if len(idxList) > 1: + err = "Multi-channel-pair selections not supported" + raise NotImplementedError(err) + idxList = idxList[0] # if possible, convert range-arrays (`[0, 1, 2, 3]`) to slices for better performance - if len(idxList) > 1: + elif len(idxList) > 1: steps = np.diff(idxList) if steps.min() == steps.max() == 1: idxList = slice(idxList[0], idxList[-1] + 1, 1) + setattr(self, selector, idxList) else: @@ -2109,6 +2120,8 @@ def __str__(self): ppdict[attr] = "{0:d} {1:s}{2:s}, ".format(len(val), attr, "s" if not attr.endswith("s") else "") + elif np.issubdtype(type(val), np.number): + ppdict[attr] = "one {0:s}, ".format(attr) else: ppdict[attr] = "" diff --git a/syncopy/datatype/continuous_data.py b/syncopy/datatype/continuous_data.py index 52ce54f96..fdbeab5ad 100644 --- a/syncopy/datatype/continuous_data.py +++ b/syncopy/datatype/continuous_data.py @@ -291,7 +291,7 @@ def _preview_trial(self, trialno): dims.pop(self._stackingDim) for dim in dims: sel = getattr(self._selection, dim) - if sel: + if sel is not None: dimIdx = self.dimord.index(dim) idx[dimIdx] = sel if isinstance(sel, slice): @@ -308,8 +308,10 @@ def _preview_trial(self, trialno): delta = 1 shp[dimIdx] = int(np.ceil((end - begin) / delta)) idx[dimIdx] = slice(begin, end, delta) - else: + elif isinstance(sel, list): shp[dimIdx] = len(sel) + else: + shp[dimIdx] = 1 return FauxTrial(shp, tuple(idx), self.data.dtype, self.dimord) diff --git a/syncopy/tests/local_spy.py b/syncopy/tests/local_spy.py index 5f48a4905..a08b5435e 100644 --- a/syncopy/tests/local_spy.py +++ b/syncopy/tests/local_spy.py @@ -26,6 +26,15 @@ # Prepare code to be executed using, e.g., iPython's `%run` magic command if __name__ == "__main__": + mock_up = np.ones((10, 2)) + ad1 = spy.AnalogData([2 * mock_up, mock_up]) + + cs1 = spy.connectivityanalysis(ad1) + + cs1.show(channels_i = 0, channels_j = 1).shape + # cs1.show(channels_i = [0], channels_j = [1]).shape + + sys.exit() nwbFilePath = "/home/fuertingers/Documents/job/SyNCoPy/Data/tt2.nwb" From f64985d6ff85deb5175a6f59b6cbdc02b184ce84 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Tue, 1 Mar 2022 19:38:38 +0100 Subject: [PATCH 063/166] WIP: Started refactoring Selector class - use select keys that are identically named as their corresponding class attributes (e.g., ``select={'channel':...}``) - allow trial selections using NumPy arrays (closes #180) - consistently support scalar selectors (e.g., ``select={'channel':'channel01'}``) (addresses #39) - check anonymous keywords of `selectdata` to filter for typos like ``trails=[0,1]`` (closes #223) - todo: modify tests and include new tests for introduced functionality On branch fix_selectcsdata Changes to be committed: modified: syncopy/datatype/base_data.py modified: syncopy/datatype/methods/selectdata.py modified: syncopy/shared/computational_routine.py modified: syncopy/shared/kwarg_decorators.py modified: syncopy/shared/tools.py modified: syncopy/tests/local_spy.py modified: syncopy/tests/test_computationalroutine.py modified: syncopy/tests/test_connectivity.py modified: syncopy/tests/test_continuousdata.py modified: syncopy/tests/test_discretedata.py modified: syncopy/tests/test_selectdata.py --- syncopy/datatype/base_data.py | 83 ++++++++-------- syncopy/datatype/methods/selectdata.py | 104 +++++++++++++-------- syncopy/shared/computational_routine.py | 7 ++ syncopy/shared/kwarg_decorators.py | 5 + syncopy/shared/tools.py | 90 +++++++++--------- syncopy/tests/local_spy.py | 2 +- syncopy/tests/test_computationalroutine.py | 16 ++-- syncopy/tests/test_connectivity.py | 4 +- syncopy/tests/test_continuousdata.py | 46 +++++---- syncopy/tests/test_discretedata.py | 6 +- syncopy/tests/test_selectdata.py | 9 ++ 11 files changed, 218 insertions(+), 154 deletions(-) diff --git a/syncopy/datatype/base_data.py b/syncopy/datatype/base_data.py index c4608a546..3195cc8e2 100644 --- a/syncopy/datatype/base_data.py +++ b/syncopy/datatype/base_data.py @@ -1346,24 +1346,24 @@ class Selector(): select : dict or :class:`~syncopy.shared.tools.StructDict` or None or str Dictionary or :class:`~syncopy.shared.tools.StructDict` with keys specifying data selectors. **Note**: some keys are only valid for certain types - of Syncopy objects, e.g., "freqs" is not a valid selector for an + of Syncopy objects, e.g., "freq" is not a valid selector for an :class:`~syncopy.AnalogData` object. Supported keys are (please see :func:`~syncopy.selectdata` for a detailed description of each selector) * 'trials' : list (integers) - * 'channels' : list (integers or strings), slice or range + * 'channel' : list (integers or strings), slice or range * 'toi' : list (floats) * 'toilim' : list (floats [tmin, tmax]) * 'foi' : list (floats) * 'foilim' : list (floats [fmin, fmax]) - * 'tapers' : list (integers or strings), slice or range - * 'units' : list (integers or strings), slice or range - * 'eventids' : list (integers), slice or range + * 'taper' : list (integers or strings), slice or range + * 'unit' : list (integers or strings), slice or range + * 'eventid' : list (integers), slice or range Any property of `data` that is not specifically accessed via one of the above keys is taken as is, e.g., ``select = {'trials': [1, 2]}`` selects the entire contents of trials no. 2 and 3, while - ``select = {'channels': range(0, 50)}`` selects the first 50 channels + ``select = {'channel': range(0, 50)}`` selects the first 50 channels of `data` across all defined trials. Consequently, if `select` is `None` or if ``select = "all"`` the entire contents of `data` is selected. @@ -1411,7 +1411,7 @@ class Selector(): Whenever possible, this class performs extensive input parsing to ensure consistency of provided selectors. Some exceptions to this rule include `toi` and `toilim`: depending on the size of `data` and the number of - defined trials, `data.time` might be a list of arrays of substantial + defined trials, `data.time` might generate a list of arrays of substantial size. To not overflow memory and slow down computations, neither `toi` nor `toilim` is checked for consistency with respect to `data.time`, i.e., the code does not verify that min/max of `toi`/`toilim` are within the @@ -1469,8 +1469,8 @@ def __init__(self, data, select): varname="select", actual=select) if not isinstance(select, dict): raise SPYTypeError(select, "select", expected="dict") - supported = ["trials", "channels", "channels_i", "channels_j", "toi", - "toilim", "foi", "foilim", "tapers", "units", "eventids"] + supported = ["trials", "channel", "channel_i", "channel_j", "toi", + "toilim", "foi", "foilim", "taper", "unit", "eventid"] if not set(select.keys()).issubset(supported): lgl = "dict with one or all of the following keys: '" +\ "'".join(opt + "', " for opt in supported)[:-2] @@ -1538,6 +1538,8 @@ def trials(self, dataselect): raise SPYValueError(legal="'all' or `None` or list/array", varname=vname, actual=trials) if trials is not None: + if np.issubdtype(type(trials), np.number): + trials = [trials] try: array_parser(trials, varname=vname, ntype="int_like", hasinf=False, hasnan=False, lims=[0, len(data.trials)], dims=1) @@ -1549,7 +1551,7 @@ def trials(self, dataselect): raise SPYValueError(legal=lgl, varname=vname, actual=act) else: trials = trlList - self._trials = trials + self._trials = list(trials) # ensure `trials` is a list cf. #180 @property def channel(self): @@ -1559,14 +1561,14 @@ def channel(self): @channel.setter def channel(self, dataselect): data, select = dataselect - chanSpec = select.get("channels") + chanSpec = select.get("channel") if self._dataClass == "CrossSpectralData": if chanSpec is not None: lgl = "`channel_i` and/or `channel_j` selectors for `CrossSpectralData`" - raise SPYValueError(legal=lgl, varname="select: channels", actual=data.__class__.__name__) + raise SPYValueError(legal=lgl, varname="select: channel", actual=data.__class__.__name__) else: return - self._selection_setter(data, select, "channel", "channels") + self._selection_setter(data, select, "channel") @property def channel_i(self): @@ -1576,7 +1578,7 @@ def channel_i(self): @channel_i.setter def channel_i(self, dataselect): data, select = dataselect - self._selection_setter(data, select, "channel_i", "channels_i") + self._selection_setter(data, select, "channel_i") @property def channel_j(self): @@ -1586,7 +1588,7 @@ def channel_j(self): @channel_j.setter def channel_j(self, dataselect): data, select = dataselect - self._selection_setter(data, select, "channel_j", "channels_j") + self._selection_setter(data, select, "channel_j") @property def time(self): @@ -1627,6 +1629,8 @@ def time(self, dataselect): raise SPYValueError(legal="'all' or `None` or list/array", varname=vname, actual=timeSpec) if timeSpec is not None: + if np.issubdtype(type(timeSpec), np.number): + timeSpec = [timeSpec] try: array_parser(timeSpec, varname=vname, hasinf=checkInf, hasnan=False, dims=1) except Exception as exc: @@ -1754,6 +1758,8 @@ def freq(self, dataselect): raise SPYValueError(legal="'all' or `None` or list/array", varname=vname, actual=freqSpec) if freqSpec is not None: + if np.issubdtype(type(freqSpec), np.number): + freqSpec = [freqSpec] try: array_parser(freqSpec, varname=vname, hasinf=checkInf, hasnan=False, lims=[data.freq.min(), data.freq.max()], dims=1) @@ -1780,7 +1786,7 @@ def taper(self): @taper.setter def taper(self, dataselect): data, select = dataselect - self._selection_setter(data, select, "taper", "tapers") + self._selection_setter(data, select, "taper") @property def unit(self): @@ -1790,7 +1796,7 @@ def unit(self): @unit.setter def unit(self, dataselect): data, select = dataselect - self._selection_setter(data, select, "unit", "units") + self._selection_setter(data, select, "unit") @property def eventid(self): @@ -1800,10 +1806,10 @@ def eventid(self): @eventid.setter def eventid(self, dataselect): data, select = dataselect - self._selection_setter(data, select, "eventid", "eventids") + self._selection_setter(data, select, "eventid") # Helper function to process provided selections - def _selection_setter(self, data, select, dataprop, selectkey): + def _selection_setter(self, data, select, selectkey): """ Converts user-provided selection key-words to indexing lists/slices @@ -1815,10 +1821,9 @@ def _selection_setter(self, data, select, dataprop, selectkey): Python dictionary or Syncopy :class:`StructDict` formatted for data selection. See :class:`Selector` for a list of valid key-value pairs. - dataprop : str - Name of property in `data` to select from selectkey : str - Name of key in `select` holding selection pertinent to `dataprop` + Name of key in `select` holding selection pertinent to identically + named property in `data` Returns ------- @@ -1841,8 +1846,8 @@ def _selection_setter(self, data, select, dataprop, selectkey): # Unpack input and perform error-checking selection = select.get(selectkey) - target = getattr(data, dataprop, None) - selector = "_{}".format(dataprop) + target = getattr(data, selectkey, None) + selector = "_{}".format(selectkey) vname = "select: {}".format(selectkey) if selection is not None and target is None: lgl = "Syncopy data object with {}".format(selectkey) @@ -1871,9 +1876,9 @@ def _selection_setter(self, data, select, dataprop, selectkey): elif np.issubdtype(type(selection), np.number): selection = [selection] - # Take entire inventory sitting in `dataprop` + # Take entire inventory sitting in `selectkey` if selection is None: - if dataprop in ["unit", "eventid"]: + if selectkey in ["unit", "eventid"]: setattr(self, selector, [slice(None, None, 1)] * len(self.trials)) else: setattr(self, selector, slice(None, None, 1)) @@ -1901,7 +1906,7 @@ def _selection_setter(self, data, select, dataprop, selectkey): # The 2d-arrays in `DiscreteData` objects require some additional hand-holding # performed by the respective `_get_unit` and `_get_eventid` class methods - if dataprop in ["unit", "eventid"]: + if selectkey in ["unit", "eventid"]: if selection.start is selection.stop is None: setattr(self, selector, [slice(None, None, 1)] * len(self.trials)) else: @@ -1911,7 +1916,7 @@ def _selection_setter(self, data, select, dataprop, selectkey): selection = list(target[selection]) else: selection = list(selection) - setattr(self, selector, getattr(data, "_get_" + dataprop)(self.trials, selection)) + setattr(self, selector, getattr(data, "_get_" + selectkey)(self.trials, selection)) else: if selection.start is selection.stop is None: setattr(self, selector, slice(None, None, 1)) @@ -1935,7 +1940,7 @@ def _selection_setter(self, data, select, dataprop, selectkey): else: targetArr = np.arange(target.size) if not set(selection).issubset(targetArr): - lgl = "list/array of {} existing names or indices".format(dataprop) + lgl = "list/array of {} existing names or indices".format(selectkey) raise SPYValueError(legal=lgl, varname=vname) # Preserve order and duplicates of selection - don't use `np.isin` here! @@ -1943,21 +1948,23 @@ def _selection_setter(self, data, select, dataprop, selectkey): for sel in selection: idxList += list(np.where(targetArr == sel)[0]) - if dataprop in ["unit", "eventid"]: - setattr(self, selector, getattr(data, "_get_" + dataprop)(self.trials, idxList)) + if selectkey in ["unit", "eventid"]: + setattr(self, selector, getattr(data, "_get_" + selectkey)(self.trials, idxList)) else: - # be careful w/pairwise channel selections in `CrossSpectralData` objects - if dataprop in ["channel_i", "channel_j"]: - if len(idxList) > 1: - err = "Multi-channel-pair selections not supported" - raise NotImplementedError(err) - idxList = idxList[0] # if possible, convert range-arrays (`[0, 1, 2, 3]`) to slices for better performance - elif len(idxList) > 1: + if len(idxList) > 1: steps = np.diff(idxList) if steps.min() == steps.max() == 1: idxList = slice(idxList[0], idxList[-1] + 1, 1) + # be careful w/pairwise list-channel selections in `CrossSpectralData` objects + # (that could not be converted to slices above) + if isinstance(idxList, list) and selectkey in ["channel_i", "channel_j"]: + if len(idxList) > 1: + err = "Multi-channel-pair selections not supported" + raise NotImplementedError(err) + idxList = idxList[0] + setattr(self, selector, idxList) else: diff --git a/syncopy/datatype/methods/selectdata.py b/syncopy/datatype/methods/selectdata.py index 0bd058587..a282b6863 100644 --- a/syncopy/datatype/methods/selectdata.py +++ b/syncopy/datatype/methods/selectdata.py @@ -17,9 +17,22 @@ @unwrap_cfg @detect_parallel_client -def selectdata(data, trials=None, channels=None, channels_i=None, channels_j=None, - toi=None, toilim=None, foi=None, foilim=None, tapers=None, units=None, - eventids=None, out=None, inplace=False, clear=False, **kwargs): +def selectdata(data, + trials=None, + channel=None, + channel_i=None, + channel_j=None, + toi=None, + toilim=None, + foi=None, + foilim=None, + taper=None, + unit=None, + eventid=None, + out=None, + inplace=False, + clear=False, + **kwargs): """ Create a new Syncopy object from a selection @@ -50,34 +63,34 @@ def selectdata(data, trials=None, channels=None, channels_i=None, channels_j=Non List of Syncopy data objects and respective valid data selectors: - :class:`~syncopy.AnalogData` : trials, channels, toi/toilim + :class:`~syncopy.AnalogData` : trials, channel, toi/toilim Examples - >>> spy.selectdata(data, trials=[0, 3, 5], channels=["channel01", "channel02"]) + >>> spy.selectdata(data, trials=[0, 3, 5], channel=["channel01", "channel02"]) >>> cfg = spy.StructDict() >>> cfg.trials = [5, 3, 0]; cfg.toilim = [0.25, 0.5] >>> spy.selectdata(cfg, data) - :class:`~syncopy.SpectralData` : trials, channels, toi/toilim, foi/foilim, tapers + :class:`~syncopy.SpectralData` : trials, channel, toi/toilim, foi/foilim, taper Examples - >>> spy.selectdata(data, trials=[0, 3, 5], channels=["channel01", "channel02"]) + >>> spy.selectdata(data, trials=[0, 3, 5], channel=["channel01", "channel02"]) >>> cfg = spy.StructDict() - >>> cfg.foi = [30, 40, 50]; cfg.tapers = slice(2, 4) + >>> cfg.foi = [30, 40, 50]; cfg.taper = slice(2, 4) >>> spy.selectdata(cfg, data) - :class:`~syncopy.EventData` : trials, toi/toilim, eventids + :class:`~syncopy.EventData` : trials, toi/toilim, eventid Examples - >>> spy.selectdata(data, toilim=[-1, 2.5], eventids=[0, 1]) + >>> spy.selectdata(data, toilim=[-1, 2.5], eventid=[0, 1]) >>> cfg = spy.StructDict() - >>> cfg.trials = [0, 0, 1, 0]; cfg.eventids = slice(2, None) + >>> cfg.trials = [0, 0, 1, 0]; cfg.eventid = slice(2, None) >>> spy.selectdata(cfg, data) - :class:`~syncopy.SpikeData` : trials, toi/toilim, units, channels + :class:`~syncopy.SpikeData` : trials, toi/toilim, unit, channel Examples - >>> spy.selectdata(data, toilim=[-1, 2.5], units=range(0, 10)) + >>> spy.selectdata(data, toilim=[-1, 2.5], unit=range(0, 10)) >>> cfg = spy.StructDict() >>> cfg.toi = [1.25, 3.2]; cfg.trials = [0, 1, 2, 3] >>> spy.selectdata(cfg, data) @@ -85,7 +98,7 @@ def selectdata(data, trials=None, channels=None, channels_i=None, channels_j=Non **Note** Any property that is not specifically accessed via one of the provided selectors is taken as is, e.g., ``spy.selectdata(data, trials=[1, 2])`` selects the entire contents of trials no. 2 and 3, while - ``spy.selectdata(data, channels=range(0, 50))`` selects the first 50 channels + ``spy.selectdata(data, channel=range(0, 50))`` selects the first 50 channels of `data` across all defined trials. Consequently, if no keywords are specified, the entire contents of `data` is selected. @@ -103,18 +116,18 @@ def selectdata(data, trials=None, channels=None, channels_i=None, channels_j=Non repetitions and need not be sorted (e.g., ``trials = [0, 1, 0, 0, 2]`` is valid) but must be finite and not NaN. If `trials` is `None`, or ``trials = "all"`` all trials are selected. - channels : list (integers or strings), slice, range or None or "all" + channel : list (integers or strings), slice, range, str, int, None or "all" Channel-selection; can be a list of channel names (``['channel3', 'channel1']``), a list of channel indices (``[3, 5]``), a slice (``slice(3, 10)``) or range (``range(3, 10)``). Note that following Python conventions, channels are counted starting at zero, and range and slice selections are half-open intervals of the form `[low, high)`, i.e., low is included , high is - excluded. Thus, ``channels = [0, 1, 2]`` or ``channels = slice(0, 3)`` + excluded. Thus, ``channel = [0, 1, 2]`` or ``channel = slice(0, 3)`` selects the first up to (and including) the third channel. Selections can be unsorted and may include repetitions but must match exactly, be finite - and not NaN. If `channels` is `None`, or ``channels = "all"`` all channels + and not NaN. If `channel` is `None`, or ``channel = "all"`` all channels are selected. - toi : list (floats) or None or "all" + toi : list (floats), float, None or "all" Time-points to be selected (in seconds) in each trial. Timing is expected to be on a by-trial basis (e.g., relative to trigger onsets). Selections can be approximate, unsorted and may include repetitions but must be @@ -130,7 +143,7 @@ def selectdata(data, trials=None, channels=None, channels_i=None, channels_j=Non `tmin` and `tmax` are included in the selection. If `toilim` is `None` or ``toilim = "all"``, the entire time-span in each trial is selected. - foi : list (floats) or None or "all" + foi : list (floats), float, None or "all" Frequencies to be selected (in Hz). Selections can be approximate, unsorted and may include repetitions but must be finite and not NaN. Fuzzy matching is performed for approximate selections (i.e., selected frequencies are @@ -143,33 +156,33 @@ def selectdata(data, trials=None, channels=None, channels_i=None, channels_j=Non but may be unbounded (e.g., ``[-np.inf, 60.5]`` is valid). Edges `fmin` and `fmax` are included in the selection. If `foilim` is `None` or ``foilim = "all"``, all frequencies are selected. - tapers : list (integers or strings), slice, range or None or "all" + taper : list (integers or strings), slice, range, str, int, None or "all" Taper-selection; can be a list of taper names (``['dpss-win-1', 'dpss-win-3']``), a list of taper indices (``[3, 5]``), a slice (``slice(3, 10)``) or range (``range(3, 10)``). Note that following Python conventions, tapers are counted starting at zero, and range and slice selections are half-open intervals of the form `[low, high)`, i.e., low is included , high is - excluded. Thus, ``tapers = [0, 1, 2]`` or ``tapers = slice(0, 3)`` selects + excluded. Thus, ``taper = [0, 1, 2]`` or ``taper = slice(0, 3)`` selects the first up to (and including) the third taper. Selections can be unsorted and may include repetitions but must match exactly, be finite and not NaN. - If `tapers` is `None` or ``tapers = "all"``, all tapers are selected. - units : list (integers or strings), slice, range or None or "all" + If `taper` is `None` or ``taper = "all"``, all tapers are selected. + unit : list (integers or strings), slice, range, str, int, None or "all" Unit-selection; can be a list of unit names (``['unit10', 'unit3']``), a list of unit indices (``[3, 5]``), a slice (``slice(3, 10)``) or range (``range(3, 10)``). Note that following Python conventions, units are counted starting at zero, and range and slice selections are half-open intervals of the form `[low, high)`, i.e., low is included , high is - excluded. Thus, ``units = [0, 1, 2]`` or ``units = slice(0, 3)`` selects + excluded. Thus, ``unit = [0, 1, 2]`` or ``unit = slice(0, 3)`` selects the first up to (and including) the third unit. Selections can be unsorted and may include repetitions but must match exactly, be finite and not NaN. - If `units` is `None` or ``units = "all"``, all units are selected. - eventids : list (integers), slice, range or None or "all" + If `unit` is `None` or ``unit = "all"``, all units are selected. + eventid : list (integers), slice, range, int, None or "all" Event-ID-selection; can be a list of event-id codes (``[2, 0, 1]``), slice (``slice(0, 2)``) or range (``range(0, 2)``). Note that following Python conventions, range and slice selections are half-open intervals of the form `[low, high)`, i.e., low is included , high is excluded. Selections can be unsorted and may include repetitions but must match exactly, be - finite and not NaN. If `eventids` is `None` or ``eventids = "all"``, all + finite and not NaN. If `eventid` is `None` or ``eventid = "all"``, all events are selected. inplace : bool If `inplace` is `True` **no** new object is created. Instead the provided @@ -271,22 +284,33 @@ def selectdata(data, trials=None, channels=None, channels_i=None, channels_j=Non lgl = "no output object for in-place selection" raise SPYValueError(lgl, varname="out", actual=out.__class__.__name__) - # FIXME: remove once tests are in place (cf #165) - if channels_i is not None or channels_j is not None: - SPYWarning("CrossSpectralData channel selection currently untested and experimental!") - - # Collect provided keywords in dict + # Collect provided selection keywords in dict selectDict = {"trials": trials, - "channels": channels, - "channels_i": channels_i, - "channels_j": channels_j, + "channel": channel, + "channel_i": channel_i, + "channel_j": channel_j, "toi": toi, "toilim": toilim, "foi": foi, "foilim": foilim, - "tapers": tapers, - "units": units, - "eventids": eventids} + "taper": taper, + "unit": unit, + "eventid": eventid} + + # The only valid anonymous kw is "parallel" (i.e., filter out typos like 'trails' etc.) + if len(kwargs) > 0: + if list(kwargs.keys()) != ["parallel"]: + kwargs.pop("parallel", None) + expected = list(selectDict.keys()) + ["out", "inplace", "clear", "parallel"] + lgl = "dict with one or all of the following keys: '" +\ + "'".join(opt + "', " for opt in expected)[:-2] + act = "dict with keys '" +\ + "'".join(key + "', " for key in kwargs.keys())[:-2] + raise SPYValueError(legal=lgl, varname="kwargs", actual=act) + + # FIXME: remove once tests are in place (cf #165) + if channel_i is not None or channel_j is not None: + SPYWarning("CrossSpectralData channel selection currently experimental!") # First simplest case: determine whether we just need to clear an existing selection if clear: @@ -360,7 +384,7 @@ def process_metadata(self, data, out): # Get/set timing-related selection modifiers out.trialdefinition = data._selection.trialdefinition - # if data._selection._timeShuffle: # FIXME: should be implemented done the road + # if data._selection._timeShuffle: # FIXME: should be implemented down the road # out.time = data._selection.timepoints if data._selection._samplerate: out.samplerate = data.samplerate @@ -369,4 +393,6 @@ def process_metadata(self, data, out): for prop in data._selection._dimProps: selection = getattr(data._selection, prop) if selection is not None: + if np.issubdtype(type(selection), np.number): + selection = [selection] setattr(out, prop, getattr(data, prop)[selection]) diff --git a/syncopy/shared/computational_routine.py b/syncopy/shared/computational_routine.py index 6d66f1854..fe2bbb42c 100644 --- a/syncopy/shared/computational_routine.py +++ b/syncopy/shared/computational_routine.py @@ -449,6 +449,8 @@ def initialize(self, data, out_stackingdim, chan_per_worker=None, keeptrials=Tru ingrid = list(grd) sigrid = [] for sk, sel in enumerate(grd): + if np.issubdtype(type(sel), np.number): + sel = [sel] if isinstance(sel, list): selarr = np.array(sel, dtype=np.intp) else: # sel is a slice @@ -909,6 +911,11 @@ def compute_sequential(self, data, out): # Perform computation res = self.computeFunction(arr, *argv, **self.cfg) + # In case scalar selections have been performed, explicitly assign + # desired output shape to re-create "lost" singleton dimensions + # (use an explicit `shape` assignment here to avoid copies) + res.shape = self.targetShapes[nblock] + # Either write result to `outgrid` location in `target` or add it up if self.keeptrials: target[outgrid] = res diff --git a/syncopy/shared/kwarg_decorators.py b/syncopy/shared/kwarg_decorators.py index a4f588399..d2de64bbf 100644 --- a/syncopy/shared/kwarg_decorators.py +++ b/syncopy/shared/kwarg_decorators.py @@ -625,6 +625,11 @@ def wrapper_io(trl_dat, *wrkargs, **kwargs): # Now, actually call wrapped function res = func(arr, *wrkargs, **kwargs) + # In case scalar selections have been performed, explicitly assign + # desired output shape to re-create "lost" singleton dimensions + # (use an explicit `shape` assignment here to avoid copies) + res.shape = outshape + # === STEP 3 === write result to disk # Write result to multiple stand-alone HDF files or use a mutex to write to a # common single file (sequentially) diff --git a/syncopy/shared/tools.py b/syncopy/shared/tools.py index 36a1f382b..a246f1070 100644 --- a/syncopy/shared/tools.py +++ b/syncopy/shared/tools.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -# +# # Auxiliaries used across all of Syncopy -# +# # Builtin/3rd party package imports import numpy as np @@ -22,7 +22,7 @@ class StructDict(dict): cfg.a = [0, 25] """ - + def __init__(self, *args, **kwargs): """ Create a child-class of dict whose attributes are its keys @@ -30,10 +30,10 @@ def __init__(self, *args, **kwargs): """ super().__init__(*args, **kwargs) self.__dict__ = self - + def __repr__(self): return self.__str__() - + def __str__(self): if self.keys(): ppStr = "Syncopy StructDict\n\n" @@ -50,88 +50,92 @@ def __str__(self): def best_match(source, selection, span=False, tol=None, squash_duplicates=False): """ Find matching elements in a given 1d-array/list - + Parameters ---------- source : NumPy 1d-array/list Reference array whose elements are to be matched by `selection` selection: NumPy 1d-array/list - Array of query-values whose closest matches are to be found in `source`. - Note that `source` and `selection` need not be the same length. + Array of query-values whose closest matches are to be found in `source`. + Note that `source` and `selection` need not be the same length. span : bool - If `True`, `selection` is interpreted as (closed) interval ``[lo, hi]`` and - `source` is queried for all elements contained in the interval, i.e., - ``lo <= src <= hi for src in source`` (typically used for - `toilim`/`foilim`-like selections). + If `True`, `selection` is interpreted as (closed) interval ``[lo, hi]`` and + `source` is queried for all elements contained in the interval, i.e., + ``lo <= src <= hi for src in source`` (typically used for + `toilim`/`foilim`-like selections). tol : None or float If `None` for each component of `selection` the closest value in `source` is selected, e.g., for ``source = [10, 20]`` and ``selection = [-50, 0, 50]`` - the closest values are `[10, 10, 20]`. - If not `None`, ensures values in `selection` do not deviate further - than `tol` from `source`. If any element `sel` of `selection` is outside - a `tol`-neighborhood around `source`, i.e., - ``np.abs(sel - source).max() >= tol``, - a :class:`~syncopy.shared.errors.SPYValueError` is raised. + the closest values are `[10, 10, 20]`. + If not `None`, ensures values in `selection` do not deviate further + than `tol` from `source`. If any element `sel` of `selection` is outside + a `tol`-neighborhood around `source`, i.e., + ``np.abs(sel - source).max() >= tol``, + a :class:`~syncopy.shared.errors.SPYValueError` is raised. squash_duplicates : bool - If `True`, identical matches are removed from the result. - + If `True`, identical matches are removed from the result. + Returns ------- values : NumPy 1darray Values of `source` that most closely match given elements in `selection` idx : NumPy 1darray - Indices of `values` with respect to `source`, such that, + Indices of `values` with respect to `source`, such that, ``source[idx] == values`` - + Notes ----- - This is an auxiliary method that is intended purely for internal use. Thus, - no error checking is performed. - + This is an auxiliary method that is intended purely for internal use. Thus, + no error checking is performed. + Examples -------- Exact matching, ordered `source` and `selection`: - + >>> best_match(np.arange(10), [2,5]) (array([2, 5]), array([2, 5])) - + Inexact matching, ordered `source` and `selection`: - + >>> source = np.arange(10) >>> selection = np.array([1.5, 1.5, 2.2, 6.2, 8.8]) >>> best_match(source, selection) (array([2, 2, 2, 6, 9]), array([2, 2, 2, 6, 9])) - + Inexact matching, unordered `source` and `selection`: - + >>> source = np.array([2.2, 1.5, 1.5, 6.2, 8.8]) >>> selection = np.array([1.9, 9., 1., -0.4, 1.2, 0.2, 9.3]) >>> best_match(source, selection) (array([2.2, 8.8, 1.5, 1.5, 1.5, 1.5, 8.8]), array([0, 4, 1, 1, 1, 1, 4])) - + Same as above, but ignore duplicate matches - - >>> best_match(source, selection, squash_duplicates=True) + + >>> best_match(source, selection, squash_duplicates=True) (array([2.2, 8.8, 1.5]), array([0, 4, 1])) - + Interval-matching: - + >>> best_match(np.arange(10), [2.9, 6.1], span=True) (array([3, 4, 5, 6]), array([3, 4, 5, 6])) """ - + # Make `source` a NumPy array if necessary if isinstance(source, list): source = np.array(source) - + + # If `selection` is a scalar, convert it to 1-element list + if np.issubdtype(type(selection), np.number): + selection = [selection] + # Ensure selection is within `tol` bounds from `source` if tol is not None: if not np.all([np.all((np.abs(source - value)) < tol) for value in selection]): lgl = "all elements of `selection` to be within a {0:2.4f}-band around `source`" act = "values in `selection` deviating further than given tolerance " +\ "of {0:2.4f} from source" - raise SPYValueError(legal=lgl.format(tol), - varname="selection", + raise SPYValueError(legal=lgl.format(tol), + varname="selection", actual=act.format(tol)) # Do not perform O(n) potentially unnecessary sort operations... @@ -139,7 +143,7 @@ def best_match(source, selection, span=False, tol=None, squash_duplicates=False) # Interval-selections are a lot easier than discrete time-points... if span: - idx = np.intersect1d(np.where(source >= selection[0])[0], + idx = np.intersect1d(np.where(source >= selection[0])[0], np.where(source <= selection[1])[0]) else: issorted = True @@ -148,14 +152,14 @@ def best_match(source, selection, span=False, tol=None, squash_duplicates=False) orig = np.array(source, copy=True) idx_orig = np.argsort(orig) source = orig[idx_orig] - idx = np.searchsorted(source, selection, side="left") + idx = np.searchsorted(source, selection, side="left") leftNbrs = np.abs(selection - source[np.maximum(idx - 1, np.zeros(idx.shape, dtype=np.intp))]) rightNbrs = np.abs(selection - source[np.minimum(idx, np.full(idx.shape, source.size - 1, dtype=np.intp))]) shiftLeft = ((idx == source.size) | (leftNbrs < rightNbrs)) idx[shiftLeft] -= 1 # Account for potentially unsorted selections (and thus unordered `idx`) - if squash_duplicates: + if squash_duplicates: _, xdi = np.unique(idx.astype(np.intp), return_index=True) idx = idx[np.sort(xdi)] @@ -186,7 +190,7 @@ def get_defaults(obj): Examples -------- To see the default input arguments of :meth:`syncopy.freqanalysis` use - + >>> spy.get_defaults(spy.freqanalysis) """ diff --git a/syncopy/tests/local_spy.py b/syncopy/tests/local_spy.py index a08b5435e..e249c1346 100644 --- a/syncopy/tests/local_spy.py +++ b/syncopy/tests/local_spy.py @@ -31,7 +31,7 @@ cs1 = spy.connectivityanalysis(ad1) - cs1.show(channels_i = 0, channels_j = 1).shape + cs1.show(channel_i = 0, channel_j = 1).shape # cs1.show(channels_i = [0], channels_j = [1]).shape diff --git a/syncopy/tests/test_computationalroutine.py b/syncopy/tests/test_computationalroutine.py index df2f0ed7b..38b4744f7 100644 --- a/syncopy/tests/test_computationalroutine.py +++ b/syncopy/tests/test_computationalroutine.py @@ -116,9 +116,9 @@ class TestComputationalRoutine(): # Data selections to be tested w/`sigdata` sigdataSelections = [None, {"trials": [3, 1, 0], - "channels": ["channel" + str(i) for i in range(12, 28)][::-1]}, + "channel": ["channel" + str(i) for i in range(12, 28)][::-1]}, {"trials": [0, 1, 2], - "channels": range(0, int(nChannels / 2)), + "channel": range(0, int(nChannels / 2)), "toilim": [-0.25, 0.25]}] # Data selections to be tested w/`artdata` generated below (use fixed but arbitrary @@ -126,10 +126,10 @@ class TestComputationalRoutine(): seed = np.random.RandomState(13) artdataSelections = [None, {"trials": [3, 1, 0], - "channels": ["channel" + str(i) for i in range(12, 28)][::-1], + "channel": ["channel" + str(i) for i in range(12, 28)][::-1], "toi": None}, {"trials": [0, 1, 2], - "channels": range(0, int(nChannels / 2)), + "channel": range(0, int(nChannels / 2)), "toilim": [-0.5, 0.6]}] # Error tolerances and respective quality metrics (depend on data selection!) @@ -193,10 +193,10 @@ def test_sequential_equidistant(self): def test_sequential_nonequidistant(self): for overlapping in [False, True]: nonequidata = generate_artificial_data(nTrials=self.nTrials, - nChannels=self.nChannels, - equidistant=False, - overlapping=overlapping, - inmemory=False) + nChannels=self.nChannels, + equidistant=False, + overlapping=overlapping, + inmemory=False) # unsorted, w/repetitions toi = self.seed.choice(nonequidata.time[0], int(nonequidata.time[0].size)) diff --git a/syncopy/tests/test_connectivity.py b/syncopy/tests/test_connectivity.py index 063d26ee6..c8bbd845a 100644 --- a/syncopy/tests/test_connectivity.py +++ b/syncopy/tests/test_connectivity.py @@ -584,7 +584,7 @@ def mk_selection_dicts(nTrials, nChannels, toi_min, toi_max): sel_dct = {} sel_dct['trials'] = comb[0] - sel_dct['channels'] = comb[1] + sel_dct['channel'] = comb[1] sel_dct['toi'] = comb[2] selections.append(sel_dct) @@ -592,7 +592,7 @@ def mk_selection_dicts(nTrials, nChannels, toi_min, toi_max): sel_dct = {} sel_dct['trials'] = comb[0] - sel_dct['channels'] = comb[1] + sel_dct['channel'] = comb[1] sel_dct['toilim'] = comb[2] selections.append(sel_dct) diff --git a/syncopy/tests/test_continuousdata.py b/syncopy/tests/test_continuousdata.py index 84d0006bd..6e2c50aae 100644 --- a/syncopy/tests/test_continuousdata.py +++ b/syncopy/tests/test_continuousdata.py @@ -41,13 +41,15 @@ ] chanSelections = [ ["channel03", "channel01", "channel01", "channel02"], # string selection w/repetition + unordered - [4, 2, 2, 5, 5], # repetition + unorderd + [4, 2, 2, 5, 5], # repetition + unordered range(5, 8), # narrow range - slice(-2, None) # negative-start slice + slice(-2, None), # negative-start slice + "channel02", # str selection + 1 # scalar selection ] toiSelections = [ "all", # non-type-conform string - [0.6], # single inexact match + 0.6, # single inexact match [-0.2, 0.6, 0.9, 1.1, 1.3, 1.6, 1.8, 2.2, 2.45, 3.] # unordered, inexact, repetions ] toilimSelections = [ @@ -57,7 +59,7 @@ ] foiSelections = [ "all", # non-type-conform string - [2.6], # single inexact match + 2.6, # single inexact match [1.1, 1.9, 2.1, 3.9, 9.2, 11.8, 12.9, 5.1, 13.8] # unordered, inexact, repetions ] foilimSelections = [ @@ -67,6 +69,8 @@ ] taperSelections = [ ["TestTaper_03", "TestTaper_01", "TestTaper_01", "TestTaper_02"], # string selection w/repetition + unordered + "TestTaper_03", # singe str + 0, # scalar selection [0, 1, 1, 2, 3], # preserve repetition, don't convert to slice range(2, 5), # narrow range slice(0, 5, 2), # slice w/non-unitary step-size @@ -490,7 +494,7 @@ def test_object_padding(self): # real thing: pad object with standing channel selection res = padding(adata, "zero", pad="absolute", padlength=total_time,unit="time", - create_new=True, select={"trials": trialSel, "channels": chanSel}) + create_new=True, select={"trials": trialSel, "channel": chanSel}) for tk, trl in enumerate(res.trials): adataTrl = adata.trials[trialSel[tk]] nSamples = pad_list[trialSel[tk]]["pad_width"][timeAxis, :].sum() + adataTrl.shape[timeAxis] @@ -529,7 +533,7 @@ def test_object_padding(self): # same as above, but this time w/swapped dimensions res2 = padding(adata2, "zero", pad="absolute", padlength=total_time, unit="time", - create_new=True, select={"trials": trialSel, "channels": chanSel}) + create_new=True, select={"trials": trialSel, "channel": chanSel}) pad_list2 = padding(adata2, "zero", pad="absolute", padlength=total_time, unit="time", create_new=False) for tk, trl in enumerate(res2.trials): @@ -567,7 +571,7 @@ def test_object_padding(self): equidistant=False, overlapping=True, inmemory=False, dimord=adata2.dimord) res3 = padding(adata3, "zero", pad="absolute", padlength=total_time, unit="time", - create_new=True, select={"trials": trialSel, "channels": chanSel}) + create_new=True, select={"trials": trialSel, "channel": chanSel}) pad_list3 = padding(adata3, "zero", pad="absolute", padlength=total_time, unit="time", create_new=False) for tk, trl in enumerate(res3.trials): @@ -606,7 +610,7 @@ def test_dataselection(self): for timeSel in timeSelections: kwdict = {} kwdict["trials"] = trialSel - kwdict["channels"] = chanSel + kwdict["channel"] = chanSel kwdict[timeSel[0]] = timeSel[1] cfg = StructDict(kwdict) # data selection via class-method + `Selector` instance for indexing @@ -658,7 +662,7 @@ def test_ang_arithmetic(self): # Now the most complicated case: user-defined subset selections are present kwdict = {} kwdict["trials"] = trialSelections[1] - kwdict["channels"] = chanSelections[3] + kwdict["channel"] = chanSelections[3] kwdict[timeSelections[4][0]] = timeSelections[4][1] _selection_op_tests(dummy, ymmud, dummy2, ymmud2, kwdict, operation) @@ -821,10 +825,10 @@ def test_sd_dataselection(self): for taperSel in taperSelections: kwdict = {} kwdict["trials"] = trialSel - kwdict["channels"] = chanSel + kwdict["channel"] = chanSel kwdict[timeSel[0]] = timeSel[1] kwdict[freqSel[0]] = freqSel[1] - kwdict["tapers"] = taperSel + kwdict["taper"] = taperSel cfg = StructDict(kwdict) # data selection via class-method + `Selector` instance for indexing selected = obj.selectdata(**kwdict) @@ -888,10 +892,10 @@ def test_sd_arithmetic(self): # Now the most complicated case: user-defined subset selections are present kwdict = {} kwdict["trials"] = trialSelections[1] - kwdict["channels"] = chanSelections[3] + kwdict["channel"] = chanSelections[3] kwdict[timeSelections[4][0]] = timeSelections[4][1] kwdict[freqSelections[4][0]] = freqSelections[4][1] - kwdict["tapers"] = taperSelections[2] + kwdict["taper"] = taperSelections[2] _selection_op_tests(dummy, ymmud, dummy2, ymmud2, kwdict, operation) # # Go through full selection stack - WARNING: this takes > 1 hour @@ -1047,14 +1051,14 @@ def test_csd_dataselection(self): chanJdx = obj.dimord.index("channel_j") freqIdx = obj.dimord.index("freq") for trialSel in trialSelections: - for chaniSel in chanSelections: - for chanjSel in chanSelections: + for chaniSel in chanSelections[2:]: + for chanjSel in chanSelections[2:]: for timeSel in timeSelections: for freqSel in freqSelections: kwdict = {} kwdict["trials"] = trialSel - kwdict["channels_i"] = chaniSel - kwdict["channels_j"] = chanjSel + kwdict["channel_i"] = chaniSel + kwdict["channel_j"] = chanjSel kwdict[timeSel[0]] = timeSel[1] kwdict[freqSel[0]] = freqSel[1] cfg = StructDict(kwdict) @@ -1065,11 +1069,13 @@ def test_csd_dataselection(self): idx[chanIdx] = selector.channel_i idx[chanJdx] = selector.channel_j idx[freqIdx] = selector.freq + jdx = [[elem] if np.issubdtype(type(elem), np.number) else elem for elem in idx] + idx = jdx for tk, trialno in enumerate(selector.trials): idx[timeIdx] = selector.time[tk] indexed = obj.trials[trialno][idx[0], ...][:, idx[1], ...][:, :, idx[2], :][..., idx[3]] assert np.array_equal(selected.trials[tk].squeeze(), - indexed.squeeze()) + indexed.squeeze()) cfg.data = obj cfg.out = CrossSpectralData(dimord=obj.dimord) # data selection via package function and `cfg`: ensure equality @@ -1115,8 +1121,8 @@ def test_csd_arithmetic(self): # Now the most complicated case: user-defined subset selections are present kwdict = {} kwdict["trials"] = trialSelections[1] - kwdict["channels_i"] = chanSelections[3] - kwdict["channels_j"] = chanSelections[2] + kwdict["channel_i"] = chanSelections[3] + kwdict["channel_j"] = chanSelections[4] kwdict[timeSelections[4][0]] = timeSelections[4][1] kwdict[freqSelections[4][0]] = freqSelections[4][1] _selection_op_tests(dummy, ymmud, dummy2, ymmud2, kwdict, operation) diff --git a/syncopy/tests/test_discretedata.py b/syncopy/tests/test_discretedata.py index 6d745a8f6..8aeff25b3 100644 --- a/syncopy/tests/test_discretedata.py +++ b/syncopy/tests/test_discretedata.py @@ -197,8 +197,8 @@ def test_dataselection(self): for timeSel in timeSelections: kwdict = {} kwdict["trials"] = trialSel - kwdict["channels"] = chanSel - kwdict["units"] = unitSel + kwdict["channel"] = chanSel + kwdict["unit"] = unitSel kwdict[timeSel[0]] = timeSel[1] cfg = StructDict(kwdict) # data selection via class-method + `Selector` instance for indexing @@ -518,7 +518,7 @@ def test_ed_dataselection(self): for timeSel in timeSelections: kwdict = {} kwdict["trials"] = trialSel - kwdict["eventids"] = eventidSel + kwdict["eventid"] = eventidSel kwdict[timeSel[0]] = timeSel[1] cfg = StructDict(kwdict) # data selection via class-method + `Selector` instance for indexing diff --git a/syncopy/tests/test_selectdata.py b/syncopy/tests/test_selectdata.py index ae9a9fdb9..8697a6fb1 100644 --- a/syncopy/tests/test_selectdata.py +++ b/syncopy/tests/test_selectdata.py @@ -50,6 +50,8 @@ class TestSelector(): selectDict["channel"] = {"valid": (["channel03", "channel01"], ["channel03", "channel01", "channel01", "channel02"], # repetition ["channel01", "channel01", "channel02", "channel03"], # preserve repetition + "channel03", # string -> scalar + 0, # scalar [4, 2, 5], [4, 2, 2, 5, 5], # repetition [0, 0, 1, 2, 3], # preserve repetition, don't convert to slice @@ -68,6 +70,8 @@ class TestSelector(): "result": ([2, 0], [2, 0, 0, 1], [0, 0, 1, 2], + [2], + [0], [4, 2, 5], [4, 2, 2, 5, 5], [0, 0, 1, 2, 3], @@ -112,6 +116,7 @@ class TestSelector(): slice(None), None, "all", + 0, # scalar slice(0, 5), slice(3, None), slice(2, 4), @@ -127,6 +132,7 @@ class TestSelector(): slice(None, None, 1), slice(None, None, 1), slice(None, None, 1), + [0], slice(0, 5, 1), slice(3, None, 1), slice(2, 4, 1), @@ -165,6 +171,8 @@ class TestSelector(): slice(None), None, "all", + "unit3", # string -> scalar + 4, # scalar slice(0, 5), slice(3, None), slice(2, 4), @@ -200,6 +208,7 @@ class TestSelector(): slice(None), None, "all", + 1, # scalar slice(0, 2), slice(1, None), slice(0, 1), From ea14a440faeea66a58f55fc339f47c1140b4c1c0 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Wed, 2 Mar 2022 15:54:02 +0100 Subject: [PATCH 064/166] WIP: Scalar indexing gymnastics - NumPy auto-squeezes arrays when using scalar indices. This results in inconsistent shapes of resulting arrays making everything an order of magnitude more complicated... On branch fix_selectcsdata Changes to be committed: modified: syncopy/datatype/methods/arithmetic.py modified: syncopy/shared/computational_routine.py --- syncopy/datatype/methods/arithmetic.py | 5 +++++ syncopy/shared/computational_routine.py | 20 ++++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/syncopy/datatype/methods/arithmetic.py b/syncopy/datatype/methods/arithmetic.py index 6972584a6..6af668935 100644 --- a/syncopy/datatype/methods/arithmetic.py +++ b/syncopy/datatype/methods/arithmetic.py @@ -480,6 +480,9 @@ def arithmetic_cF(base_dat, operand_dat, operand_idx, operation=None, opres_type if isinstance(operand_dat, dict): with h5py.File(operand_dat["filename"], "r") as h5f: operand = h5f[operand_dat["dsetname"]][operand_idx] + # enforce original shape in case `operand_idx` contained scalar + # selections that squeezed the array + operand.shape = chunkShape else: operand = operand_dat @@ -513,4 +516,6 @@ def process_metadata(self, baseObj, out): for prop in baseObj._selection._dimProps: selection = getattr(baseObj._selection, prop) if selection is not None: + if np.issubdtype(type(selection), np.number): + selection = [selection] setattr(out, prop, getattr(baseObj, prop)[selection]) diff --git a/syncopy/shared/computational_routine.py b/syncopy/shared/computational_routine.py index fe2bbb42c..56fcd5725 100644 --- a/syncopy/shared/computational_routine.py +++ b/syncopy/shared/computational_routine.py @@ -335,9 +335,10 @@ def initialize(self, data, out_stackingdim, chan_per_worker=None, keeptrials=Tru trial = trials[0] trlArg0 = tuple(arg[0] if isinstance(arg, (list, tuple, np.ndarray)) and len(arg) == self.numTrials \ else arg for arg in self.argv) - chunkShape0 = chk_arr[0, :] + chunkShape0 = tuple(chk_arr[0, :]) lyt = [slice(0, stop) for stop in chunkShape0] sourceLayout = [] + sourceShapes = [] targetLayout = [] targetShapes = [] c_blocks = [1] @@ -383,6 +384,7 @@ def initialize(self, data, out_stackingdim, chan_per_worker=None, keeptrials=Tru targetLayout.append(tuple(lyt)) targetShapes.append(tuple([slc.stop - slc.start for slc in lyt])) sourceLayout.append(trial.idx) + sourceShapes.append(trial.shape) chanstack += res[outchanidx] blockstack += block @@ -391,6 +393,10 @@ def initialize(self, data, out_stackingdim, chan_per_worker=None, keeptrials=Tru targetLayout.append(tuple(lyt)) targetShapes.append(chunkShape0) sourceLayout.append(trial.idx) + sourceShapes.append(trial.shape) + + # if data._selection.channel_i == slice(-2, None, 1): + # import pdb; pdb.set_trace() # Construct dimensional layout of output stacking = targetLayout[0][stackingDim].stop @@ -406,6 +412,7 @@ def initialize(self, data, out_stackingdim, chan_per_worker=None, keeptrials=Tru targetLayout.append(tuple(lyt)) targetShapes.append(tuple([slc.stop - slc.start for slc in lyt])) sourceLayout.append(trial.idx) + sourceShapes.append(trial.shape) else: chanstack = 0 blockstack = 0 @@ -421,6 +428,7 @@ def initialize(self, data, out_stackingdim, chan_per_worker=None, keeptrials=Tru targetLayout.append(tuple(lyt)) targetShapes.append(tuple([slc.stop - slc.start for slc in lyt])) sourceLayout.append(trial.idx) + sourceShapes.append(trial.shape) chanstack += res[outchanidx] blockstack += block @@ -471,6 +479,7 @@ def initialize(self, data, out_stackingdim, chan_per_worker=None, keeptrials=Tru # Store determined shapes and grid layout self.sourceLayout = sourceLayout + self.sourceShapes = sourceShapes self.sourceSelectors = sourceSelectors self.targetLayout = targetLayout self.targetShapes = targetShapes @@ -909,12 +918,19 @@ def compute_sequential(self, data, out): arr = np.vstack(stacks)[ingrid] # Perform computation + try: + arr.shape = self.sourceShapes[nblock] + except: + import pdb; pdb.set_trace() res = self.computeFunction(arr, *argv, **self.cfg) # In case scalar selections have been performed, explicitly assign # desired output shape to re-create "lost" singleton dimensions # (use an explicit `shape` assignment here to avoid copies) - res.shape = self.targetShapes[nblock] + try: + res.shape = self.targetShapes[nblock] + except: + import pdb; pdb.set_trace() # Either write result to `outgrid` location in `target` or add it up if self.keeptrials: From fefffe65571a99ec7a08160fefc67acf5adaab58 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Wed, 2 Mar 2022 16:49:13 +0100 Subject: [PATCH 065/166] WIP: Preprocessing: Butterworth filters - interfaced to scipy's butterworth filter designer and sos filtering routines On branch preprocessing Changes to be committed: new file: syncopy/preproc/filteringCRs.py --- syncopy/preproc/filteringCRs.py | 113 ++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 syncopy/preproc/filteringCRs.py diff --git a/syncopy/preproc/filteringCRs.py b/syncopy/preproc/filteringCRs.py new file mode 100644 index 000000000..86bfd1364 --- /dev/null +++ b/syncopy/preproc/filteringCRs.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# +# computeFunctions and -Routines for parallel calculation +# of common preprocessing steps like IIR filtering +# + +# Builtin/3rd party package imports +import numpy as np +import scipy.signal as sci +from inspect import signature + +# syncopy imports +from syncopy.shared.tools import best_match +from syncopy.shared.computational_routine import ComputationalRoutine +from syncopy.shared.kwarg_decorators import unwrap_io + + +@unwrap_io +def but_filtering_cF(dat, + samplerate=1, + filter_type='lp', + freq=None, + order=None, + direction='twopass', + polyremoval=None, + timeAxis=0, + noCompute=False + ): + """ + Provides basic filtering of signals with IIR (Butterworth) + filters. Supported are low-pass, high-pass, + band-pass and band-stop (Notch) filtering. + + dat : (N, K) :class:`numpy.ndarray` + Uniformly sampled multi-channel time-series data + The 1st dimension is interpreted as the time axis, + columns represent individual channels. + Dimensions can be transposed to `(K, N)` with the `timeAxis` parameter + filter_type : {'lp', 'hp', 'bp, 'bs'}, optional + Select type of filter, either low-pass `'lp'`, + high-pass `'hp'`, band-pass `'bp'` or band-stop (Notch) `'bs'`. + freq : float or array_like + Cut-off frequency for low- and high-pass filters or sequence + of two frequencies for band-stop and band-pass filter. + order : int + Order of the filter. Higher orders yield a sharper transition width + or 'roll off' of the filter. + direction : {'twopass', 'onepass'} + Filter direction: + `'twopass'` - zero-phase forward and reverse filter + `'onepass'` - forward filter, introduces group delays + polyremoval : int or None + Order of polynomial used for de-trending data in the time domain prior + to filtering. A value of 0 corresponds to subtracting the mean + ("de-meaning"), ``polyremoval = 1`` removes linear trends (subtracting the + least squares fit of a linear polynomial). + timeAxis : int, optional + Index of running time axis in `dat` (0 or 1) + noCompute : bool + Preprocessing flag. If `True`, do not perform actual calculation but + instead return expected shape and :class:`numpy.dtype` of output + array. + + Returns + ------- + filtered : (N, K) :class:`~numpy.ndarray` + The filtered signals + + Notes + ----- + This method is intended to be used as + :meth:`~syncopy.shared.computational_routine.ComputationalRoutine.computeFunction` + inside a :class:`~syncopy.shared.computational_routine.ComputationalRoutine`. + Thus, input parameters are presumed to be forwarded from a parent metafunction. + Consequently, this function does **not** perform any error checking and operates + under the assumption that all inputs have been externally validated and cross-checked. + + """ + + # attach dummy channel axis in case only a + # single signal/channel is the input + if dat.ndim < 2: + dat = dat[:, np.newaxis] + + # Re-arrange array if necessary and get dimensional information + if timeAxis != 0: + dat = dat.T # does not copy but creates view of `dat` + else: + dat = dat + + # filtering does not change the shape + outShape = dat.shape + if noCompute: + return outShape, np.float32 + + # detrend + if polyremoval == 0: + # SciPy's overwrite_data not working for type='constant' :/ + dat = sci.detrend(dat, type='constant', axis=0, overwrite_data=True) + elif polyremoval == 1: + dat = sci.detrend(dat, type='linear', axis=0, overwrite_data=True) + + # design the butterworth filter with "second-order-sections" output + sos = sci.butter(order, freq, filter_type, fs=samplerate, output='sos') + + # do the filtering + if direction == 'twopass': + filtered = sci.sosfiltfilt(sos, dat, axis=0) + return filtered + + elif direction == 'onepass': + filtered = sci.sosfilt(sos, dat, axis=0) + return filtered From c6b24ad4acb429e070617f2f69c8725dcfe78bec Mon Sep 17 00:00:00 2001 From: tensionhead Date: Wed, 2 Mar 2022 17:45:57 +0100 Subject: [PATCH 066/166] FIX: remove 'dpss' setting for granger test --- syncopy/tests/test_connectivity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncopy/tests/test_connectivity.py b/syncopy/tests/test_connectivity.py index 217945767..5d13b9a52 100644 --- a/syncopy/tests/test_connectivity.py +++ b/syncopy/tests/test_connectivity.py @@ -67,7 +67,7 @@ class TestGranger: def test_gr_solution(self, **kwargs): - Gcaus = ca(self.data, method='granger', taper='dpss', + Gcaus = ca(self.data, method='granger', tapsmofrq=3, foi=None, **kwargs) From 0cf41645e83a51b50f909535b03958633294d23c Mon Sep 17 00:00:00 2001 From: Gregor Moenke Date: Wed, 2 Mar 2022 17:37:06 +0000 Subject: [PATCH 067/166] separate frontend tests for slurm --- .gitlab-ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dd14970b5..8604bd5b7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -96,7 +96,9 @@ slurmtest: - conda env update -f syncopy.yml --prune - conda activate syncopy - export PYTHONPATH=$CI_PROJECT_DIR - - srun -p DEV --mem=8000m -c 4 pytest + - srun -p DEV --mem=8000m -c 4 pytest -v test_specest.py + - srun -p DEV --mem=8000m -c 4 pytest -v test_connectivity.py + - srun -p DEV --mem=8000m -c 4 pytest --ignore=test_specest.py --ignore=test_connectivity.py pypitest: stage: upload From 6cf518328b8c38e662cc368ad4dff4dd7fbfe051 Mon Sep 17 00:00:00 2001 From: Gregor Moenke Date: Wed, 2 Mar 2022 17:52:54 +0000 Subject: [PATCH 068/166] Update .gitlab-ci.yml --- .gitlab-ci.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8604bd5b7..dd97508c6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,6 +4,8 @@ stages: - upload - deploy +variables: + TEST_DIR: "$CI_PROJECT_DIR/syncopy/tests" intellinux: stage: tox allow_failure: true @@ -96,8 +98,8 @@ slurmtest: - conda env update -f syncopy.yml --prune - conda activate syncopy - export PYTHONPATH=$CI_PROJECT_DIR - - srun -p DEV --mem=8000m -c 4 pytest -v test_specest.py - - srun -p DEV --mem=8000m -c 4 pytest -v test_connectivity.py + - srun -p DEV --mem=8000m -c 4 pytest -v $TEST_DIR/test_specest.py + - srun -p DEV --mem=8000m -c 4 pytest -v $TEST_DIR/test_connectivity.py - srun -p DEV --mem=8000m -c 4 pytest --ignore=test_specest.py --ignore=test_connectivity.py pypitest: From ec73a1bd7418972da4099404fc098b93a6fedad0 Mon Sep 17 00:00:00 2001 From: Gregor Moenke Date: Wed, 2 Mar 2022 17:57:13 +0000 Subject: [PATCH 069/166] define global TEST_DIR --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dd97508c6..63352ab27 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,6 +6,7 @@ stages: variables: TEST_DIR: "$CI_PROJECT_DIR/syncopy/tests" + intellinux: stage: tox allow_failure: true From 74947bc903e966a1657878c4d72a848af149eaa6 Mon Sep 17 00:00:00 2001 From: Gregor Moenke Date: Wed, 2 Mar 2022 17:58:49 +0000 Subject: [PATCH 070/166] FIX: update ignore path --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 63352ab27..468f8b0d8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -101,7 +101,7 @@ slurmtest: - export PYTHONPATH=$CI_PROJECT_DIR - srun -p DEV --mem=8000m -c 4 pytest -v $TEST_DIR/test_specest.py - srun -p DEV --mem=8000m -c 4 pytest -v $TEST_DIR/test_connectivity.py - - srun -p DEV --mem=8000m -c 4 pytest --ignore=test_specest.py --ignore=test_connectivity.py + - srun -p DEV --mem=8000m -c 4 pytest --ignore=$TEST_DIR/test_specest.py --ignore=$TEST_DIR/test_connectivity.py pypitest: stage: upload From 6b1472ef48595cebc0101305118defcda71e4e8a Mon Sep 17 00:00:00 2001 From: tensionhead Date: Thu, 3 Mar 2022 10:50:57 +0100 Subject: [PATCH 071/166] CHG: Set default order=6 for butterworth filter Changes to be committed: modified: syncopy/preproc/filteringCRs.py --- syncopy/preproc/filteringCRs.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/syncopy/preproc/filteringCRs.py b/syncopy/preproc/filteringCRs.py index 86bfd1364..1ffd4302b 100644 --- a/syncopy/preproc/filteringCRs.py +++ b/syncopy/preproc/filteringCRs.py @@ -20,7 +20,7 @@ def but_filtering_cF(dat, samplerate=1, filter_type='lp', freq=None, - order=None, + order=6, direction='twopass', polyremoval=None, timeAxis=0, @@ -42,9 +42,10 @@ def but_filtering_cF(dat, freq : float or array_like Cut-off frequency for low- and high-pass filters or sequence of two frequencies for band-stop and band-pass filter. - order : int - Order of the filter. Higher orders yield a sharper transition width - or 'roll off' of the filter. + order : int, optional + Order of the filter, default is 6. + Higher orders yield a sharper transition width + or less 'roll off' of the filter, but are more computationally expensive. direction : {'twopass', 'onepass'} Filter direction: `'twopass'` - zero-phase forward and reverse filter From 11c173367d12039da00486ebd2ce236505749e03 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Thu, 3 Mar 2022 14:12:01 +0100 Subject: [PATCH 072/166] FIX: Repair DiscreteData's trialtime property - the recently modified `trialtime` prop of the `DiscreteData` class produced unordered times containing repetitions. This has been fixed by reverting to the previous implementation which itself is now modified to not output a list of nSamples but nTrials generators On branch fix_selectcsdata Changes to be committed: modified: syncopy/datatype/discrete_data.py --- syncopy/datatype/discrete_data.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/syncopy/datatype/discrete_data.py b/syncopy/datatype/discrete_data.py index 50ab90dec..5f58aa594 100644 --- a/syncopy/datatype/discrete_data.py +++ b/syncopy/datatype/discrete_data.py @@ -182,10 +182,9 @@ def trials(self): def trialtime(self): """list(:class:`numpy.ndarray`): trigger-relative sample times in s""" if self.samplerate is not None and self.sampleinfo is not None: - return [ - (trl[:, 0] - self._t0[tk] - self.sampleinfo[tk, 0]) / self.samplerate - for tk, trl in enumerate(self.trials) - ] + return [((t + self._t0[tk]) / self.samplerate \ + for t in range(0, int(self.sampleinfo[tk, 1] - self.sampleinfo[tk, 0]))) \ + for tk in np.unique(self.trialid)] # Helper function that grabs a single trial def _get_trial(self, trialno): @@ -269,7 +268,8 @@ def _get_time(self, trials, toi=None, toilim=None): for trlno in trials: thisTrial = self.data[self.trialid == trlno, self.dimord.index("sample")] trlSample = np.arange(*self.sampleinfo[trlno, :]) - trlTime = np.array(list(allTrials[np.where(self.trialid == trlno)[0][0]])) + trlTime = np.array(list(allTrials[trlno])) + # trlTime = np.array(list(allTrials[np.where(self.trialid == trlno)[0][0]])) minSample = trlSample[np.where(trlTime >= toilim[0])[0][0]] maxSample = trlSample[np.where(trlTime <= toilim[1])[0][-1]] selSample, _ = best_match(trlSample, [minSample, maxSample], span=True) @@ -287,7 +287,7 @@ def _get_time(self, trials, toi=None, toilim=None): for trlno in trials: thisTrial = self.data[self.trialid == trlno, self.dimord.index("sample")] trlSample = np.arange(*self.sampleinfo[trlno, :]) - trlTime = np.array(list(allTrials[np.where(self.trialid == trlno)[0][0]])) + trlTime = np.array(list(allTrials[trlno])) _, selSample = best_match(trlTime, toi) for k, idx in enumerate(selSample): if np.abs(trlTime[idx - 1] - toi[k]) < np.abs(trlTime[idx] - toi[k]): From c443938952016494ba731f253dd004ac9dd6eae1 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Thu, 3 Mar 2022 14:51:59 +0100 Subject: [PATCH 073/166] CHG: More selectdata modifications - enforce shape conversation in `CommputationalRoutine` (i.e., do not collapse array in case of scalar selections) - included warning message in case operand of arithmetic ops has active in-place selection present On branch fix_selectcsdata Changes to be committed: modified: syncopy/datatype/methods/arithmetic.py modified: syncopy/shared/computational_routine.py modified: syncopy/shared/kwarg_decorators.py modified: syncopy/tests/test_decorators.py modified: syncopy/tests/test_selectdata.py modified: syncopy/tests/test_specest.py --- syncopy/datatype/methods/arithmetic.py | 6 ++++- syncopy/shared/computational_routine.py | 22 ++++++++--------- syncopy/shared/kwarg_decorators.py | 6 +++++ syncopy/tests/test_decorators.py | 8 +++---- syncopy/tests/test_selectdata.py | 23 ++++++++++++------ syncopy/tests/test_specest.py | 32 ++++++++++++------------- 6 files changed, 58 insertions(+), 39 deletions(-) diff --git a/syncopy/datatype/methods/arithmetic.py b/syncopy/datatype/methods/arithmetic.py index 6af668935..02e529003 100644 --- a/syncopy/datatype/methods/arithmetic.py +++ b/syncopy/datatype/methods/arithmetic.py @@ -212,8 +212,12 @@ def _parse_input(obj1, obj2, operator): raise SPYValueError(lgl, varname="operand", actual=act.format(baseSr, opndSr)) - # If only a subset of `operand` is selected, adjust for this + # If only a subset of `operand` is selected, adjust for this (and warn + # that arbitrarily ugly things might happen with mis-matched selections) if operand._selection is not None: + wrng = "Found existing in-place selection in operand. " +\ + "Shapes and trial counts of base and operand objects have to match up!" + SPYWarning(wrng, caller=operator) opndTrialList = operand._selection.trials else: opndTrialList = list(range(len(operand.trials))) diff --git a/syncopy/shared/computational_routine.py b/syncopy/shared/computational_routine.py index 56fcd5725..a85cd4994 100644 --- a/syncopy/shared/computational_routine.py +++ b/syncopy/shared/computational_routine.py @@ -162,6 +162,10 @@ def __init__(self, *argv, **kwargs): # indices are ABSOLUTE, i.e., wrt entire dataset, not just current trial! self.sourceLayout = None + # list of shape-tuples of input trial-chunks (necessary to restore shape of + # arrays that got inflated by scalar selection tuples)) + self.sourceShapes = None + # list of index-tuples for re-ordering NumPy arrays extracted w/`self.sourceLayout` # >>> can be unordered w/repetitions <<< # indices are RELATIVE, i.e., wrt current trial! @@ -395,9 +399,6 @@ def initialize(self, data, out_stackingdim, chan_per_worker=None, keeptrials=Tru sourceLayout.append(trial.idx) sourceShapes.append(trial.shape) - # if data._selection.channel_i == slice(-2, None, 1): - # import pdb; pdb.set_trace() - # Construct dimensional layout of output stacking = targetLayout[0][stackingDim].stop for tk in range(1, self.numTrials): @@ -612,6 +613,7 @@ class method (starting with the word `"compute_"`). "infile": data.filename, "indset": data.data.name, "ingrid": self.sourceLayout[chk], + "inshape": self.sourceShapes[chk], "sigrid": self.sourceSelectors[chk], "fancy": self.useFancyIdx, "vdsdir": self.virtualDatasetDir, @@ -917,20 +919,18 @@ def compute_sequential(self, data, out): shape=(self.hdr[fk]["M"], self.hdr[fk]["N"]))[idx]) arr = np.vstack(stacks)[ingrid] + # Ensure input array shape was not inflated by scalar selection + # tuple, e.g., ``e=np.ones((2,2)); e[0,:].shape = (2,)`` not ``(1,2)`` + # (use an explicit `shape` assignment here to avoid copies) + arr.shape = self.sourceShapes[nblock] + # Perform computation - try: - arr.shape = self.sourceShapes[nblock] - except: - import pdb; pdb.set_trace() res = self.computeFunction(arr, *argv, **self.cfg) # In case scalar selections have been performed, explicitly assign # desired output shape to re-create "lost" singleton dimensions # (use an explicit `shape` assignment here to avoid copies) - try: - res.shape = self.targetShapes[nblock] - except: - import pdb; pdb.set_trace() + res.shape = self.targetShapes[nblock] # Either write result to `outgrid` location in `target` or add it up if self.keeptrials: diff --git a/syncopy/shared/kwarg_decorators.py b/syncopy/shared/kwarg_decorators.py index d2de64bbf..882a1a350 100644 --- a/syncopy/shared/kwarg_decorators.py +++ b/syncopy/shared/kwarg_decorators.py @@ -575,6 +575,7 @@ def wrapper_io(trl_dat, *wrkargs, **kwargs): infilename = trl_dat["infile"] indset = trl_dat["indset"] ingrid = trl_dat["ingrid"] + inshape = trl_dat["inshape"] sigrid = trl_dat["sigrid"] fancy = trl_dat["fancy"] vdsdir = trl_dat["vdsdir"] @@ -622,6 +623,11 @@ def wrapper_io(trl_dat, *wrkargs, **kwargs): arr = np.vstack(dsets) # === STEP 2 === perform computation + # Ensure input array shape was not inflated by scalar selection + # tuple, e.g., ``e=np.ones((2,2)); e[0,:].shape = (2,)`` not ``(1,2)`` + # (use an explicit `shape` assignment here to avoid copies) + arr.shape = inshape + # Now, actually call wrapped function res = func(arr, *wrkargs, **kwargs) diff --git a/syncopy/tests/test_decorators.py b/syncopy/tests/test_decorators.py index 2ab462f9f..9fc4c4795 100644 --- a/syncopy/tests/test_decorators.py +++ b/syncopy/tests/test_decorators.py @@ -207,25 +207,25 @@ def test_varargin(self): # data positional + select keyword fnameList = group_objects(*self.dataObjs[:letterIdx + 1], - select={"channels": [letter]}) + select={"channel": [letter]}) assert groupList == fnameList # data positional + cfg w/select cfg = StructDict() - cfg.select = {"channels": [letter]} + cfg.select = {"channel": [letter]} fnameList = group_objects(*self.dataObjs[:letterIdx + 1], cfg) assert groupList == fnameList # cfg w/data + select cfg = StructDict() cfg.data = self.dataObjs[:letterIdx + 1] - cfg.select = {"channels": [letter]} + cfg.select = {"channel": [letter]} fnameList = group_objects(cfg) assert groupList == fnameList # invalid selection with pytest.raises(SPYValueError) as exc: - group_objects(*self.dataObjs, select={"channels": ["Z"]}) + group_objects(*self.dataObjs, select={"channel": ["Z"]}) assert "expected list/array of channel existing names or indices" in str(exc.value) # data does not only contain Syncopy objects diff --git a/syncopy/tests/test_selectdata.py b/syncopy/tests/test_selectdata.py index 8697a6fb1..e1c2c748f 100644 --- a/syncopy/tests/test_selectdata.py +++ b/syncopy/tests/test_selectdata.py @@ -354,9 +354,18 @@ def test_general(self): selects = list(range(getattr(discrete, prop).size))[selection] elif isinstance(selection, range): selects = list(selection) - elif isinstance(selection, str) or selection is None: - selects = [None] - else: # selection is list/ndarray + elif isinstance(selection, str): + if selection == "all": + selects = [None] + else: + selection = [selection] + elif np.issubdtype(type(selection), np.number): + selection = [selection] + + # elif isinstance(selection, str) or selection is None: + # selects = [None] + if isinstance(selection, (list, np.ndarray)): + # else: # selection is list/ndarray if isinstance(selection[0], str): avail = getattr(discrete, prop) else: @@ -456,9 +465,9 @@ def test_general(self): solution = slice(start, stop, step) # once we're sure `Selector` works, actually select data - selection = Selector(dummy, {prop + "s": sel}) + selection = Selector(dummy, {prop : sel}) assert getattr(selection, prop) == solution - selected = selectdata(dummy, {prop + "s": sel}) + selected = selectdata(dummy, {prop : sel}) # process `unit` and `enventid` if prop in selection._byTrialProps: @@ -491,12 +500,12 @@ def test_general(self): # ensure invalid selection trigger expected errors for ik, isel in enumerate(self.selectDict[prop]["invalid"]): with pytest.raises(self.selectDict[prop]["errors"][ik]): - Selector(dummy, {prop + "s": isel}) + Selector(dummy, {prop : isel}) else: # ensure objects that don't have a `prop` attribute complain with pytest.raises(SPYValueError): - Selector(dummy, {prop + "s": [0]}) + Selector(dummy, {prop : [0]}) # ensure invalid `toi` + `toilim` specifications trigger expected errors if hasattr(dummy, "time") or hasattr(dummy, "trialtime"): diff --git a/syncopy/tests/test_specest.py b/syncopy/tests/test_specest.py index d00fc26ba..ce5dfa7e3 100644 --- a/syncopy/tests/test_specest.py +++ b/syncopy/tests/test_specest.py @@ -126,9 +126,9 @@ class TestMTMFFT(): # Data selections to be tested w/data generated based on `sig` sigdataSelections = [None, {"trials": [3, 1, 0], - "channels": ["channel" + str(i) for i in range(12, 28)][::-1]}, + "channel": ["channel" + str(i) for i in range(12, 28)][::-1]}, {"trials": [0, 1, 2], - "channels": range(0, int(nChannels / 2)), + "channel": range(0, int(nChannels / 2)), "toilim": [0.25, 0.75]}] # Data selections to be tested w/`artdata` generated below (use fixed but arbitrary @@ -136,10 +136,10 @@ class TestMTMFFT(): seed = np.random.RandomState(13) artdataSelections = [None, {"trials": [3, 1, 0], - "channels": ["channel" + str(i) for i in range(10, 15)][::-1], + "channel": ["channel" + str(i) for i in range(10, 15)][::-1], "toi": None}, {"trials": [0, 1, 2], - "channels": range(0, 8), + "channel": range(0, 8), "toilim": [-0.5, 0.6]}] # Error tolerances for target amplitudes (depend on data selection!) @@ -479,9 +479,9 @@ class TestMTMConvol(): # Data selection dict for the above object dataSelections = [None, {"trials": [1, 2, 0], - "channels": ["channel" + str(i) for i in range(2, 6)][::-1]}, + "channel": ["channel" + str(i) for i in range(2, 6)][::-1]}, {"trials": [0, 2], - "channels": range(0, nChan2), + "channel": range(0, nChan2), "toilim": [-20, 60.8]}] def test_tf_output(self): @@ -612,7 +612,7 @@ def test_tf_solution(self): chanNo = chan if select: if "toilim" not in select.keys(): - chanNo = np.where(self.tfData.channel == select["channels"][chan])[0][0] + chanNo = np.where(self.tfData.channel == select["channel"][chan])[0][0] if chanNo % 2: modIdx = self.odd[(-1)**trlNo] else: @@ -699,7 +699,7 @@ def test_tf_toi(self): cfg.taper = "dpss" cfg.tapsmofrq = 10 cfg.keeptapers = True - cfg.select = {"trials": [0], "channels": [0], "toilim": [-0.5, 0.5]} + cfg.select = {"trials": [0], "channel": [0], "toilim": [-0.5, 0.5]} cfg.toi = "all" cfg.t_ftimwin = 0.05 tfSpec = freqanalysis(cfg, self.tfData) @@ -889,9 +889,9 @@ class TestWavelet(): # Set up in-place data-selection dicts for the constructed object dataSelections = [None, {"trials": [1, 2, 0], - "channels": ["channel" + str(i) for i in range(2, 4)][::-1]}, + "channel": ["channel" + str(i) for i in range(2, 4)][::-1]}, {"trials": [0, 2], - "channels": range(0, int(nChannels / 2)), + "channel": range(0, int(nChannels / 2)), "toilim": [-20, 60.8]}] @skip_low_mem @@ -965,7 +965,7 @@ def test_wav_solution(self): chanNo = chan if select: if "toilim" not in select.keys(): - chanNo = np.where(self.tfData.channel == select["channels"][chan])[0][0] + chanNo = np.where(self.tfData.channel == select["channel"][chan])[0][0] if chanNo % 2: modIdx = self.odd[(-1)**trlNo] else: @@ -1034,7 +1034,7 @@ def test_wav_toi(self): # Test correct time-array assembly for ``toi = "all"`` (cut down data signifcantly # to not overflow memory here) - cfg.select = {"trials": [0], "channels": [0], "toilim": [-0.5, 0.5]} + cfg.select = {"trials": [0], "channel": [0], "toilim": [-0.5, 0.5]} cfg.toi = "all" tfSpec = freqanalysis(cfg, self.tfData) dt = 1/self.tfData.samplerate @@ -1173,9 +1173,9 @@ class TestSuperlet(): # Set up in-place data-selection dicts for the constructed object dataSelections = [None, {"trials": [1, 2, 0], - "channels": ["channel" + str(i) for i in range(2, 4)][::-1]}, + "channel": ["channel" + str(i) for i in range(2, 4)][::-1]}, {"trials": [0, 2], - "channels": range(0, int(nChannels / 2)), + "channel": range(0, int(nChannels / 2)), "toilim": [-20, 60.8]}] @skip_low_mem @@ -1248,7 +1248,7 @@ def test_slet_solution(self): chanNo = chan if select: if "toilim" not in select.keys(): - chanNo = np.where(self.tfData.channel == select["channels"][chan])[0][0] + chanNo = np.where(self.tfData.channel == select["channel"][chan])[0][0] if chanNo % 2: modIdx = self.odd[(-1)**trlNo] else: @@ -1317,7 +1317,7 @@ def test_slet_toi(self): # Test correct time-array assembly for ``toi = "all"`` (cut down data signifcantly # to not overflow memory here) - cfg.select = {"trials": [0], "channels": [0], "toilim": [-0.5, 0.5]} + cfg.select = {"trials": [0], "channel": [0], "toilim": [-0.5, 0.5]} cfg.toi = "all" tfSpec = freqanalysis(cfg, self.tfData) dt = 1/self.tfData.samplerate From c8cb33e4f6cb7a26f129fb44c0c9435d04c0d275 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Thu, 3 Mar 2022 16:04:39 +0100 Subject: [PATCH 074/166] NEW: Preprocessing frontend - only butterworth (IIR) filtering so far Changes to be committed: modified: syncopy/__init__.py modified: syncopy/nwanalysis/ST_compRoutines.py modified: syncopy/nwanalysis/connectivity_analysis.py new file: syncopy/preproc/__init__.py renamed: syncopy/preproc/filteringCRs.py -> syncopy/preproc/butterworthCR.py new file: syncopy/preproc/preprocessing.py modified: syncopy/tests/local_spy.py --- syncopy/__init__.py | 2 + syncopy/nwanalysis/ST_compRoutines.py | 1 - syncopy/nwanalysis/connectivity_analysis.py | 5 + syncopy/preproc/__init__.py | 11 ++ .../{filteringCRs.py => butterworthCR.py} | 49 ++++- syncopy/preproc/preprocessing.py | 180 ++++++++++++++++++ syncopy/tests/local_spy.py | 16 +- 7 files changed, 249 insertions(+), 15 deletions(-) create mode 100644 syncopy/preproc/__init__.py rename syncopy/preproc/{filteringCRs.py => butterworthCR.py} (75%) create mode 100644 syncopy/preproc/preprocessing.py diff --git a/syncopy/__init__.py b/syncopy/__init__.py index e3202cd3b..11dca5a53 100644 --- a/syncopy/__init__.py +++ b/syncopy/__init__.py @@ -102,6 +102,7 @@ from .nwanalysis import * from .statistics import * from .plotting import * +from .preproc import * # Register session __session__ = datatype.base_data.SessionLogger() @@ -126,3 +127,4 @@ __all__.extend(nwanalysis.__all__) __all__.extend(statistics.__all__) __all__.extend(plotting.__all__) +__all__.extend(preproc.__all__) diff --git a/syncopy/nwanalysis/ST_compRoutines.py b/syncopy/nwanalysis/ST_compRoutines.py index 096ec414c..00588aed1 100644 --- a/syncopy/nwanalysis/ST_compRoutines.py +++ b/syncopy/nwanalysis/ST_compRoutines.py @@ -182,7 +182,6 @@ class ST_CrossSpectra(ComputationalRoutine): computeFunction = staticmethod(cross_spectra_cF) - backends = [csd] # 1st argument,the data, gets omitted valid_kws = list(signature(cross_spectra_cF).parameters.keys())[1:] # hardcode some parameter names which got digested from the frontend diff --git a/syncopy/nwanalysis/connectivity_analysis.py b/syncopy/nwanalysis/connectivity_analysis.py index 82feb2c29..842f47971 100644 --- a/syncopy/nwanalysis/connectivity_analysis.py +++ b/syncopy/nwanalysis/connectivity_analysis.py @@ -129,6 +129,11 @@ def connectivityanalysis(data, method="coh", keeptrials=False, output="abs", For example :func:`~scipy.signal.windows.kaiser` has the additional parameter 'beta'. For multi-tapering use `tapsmofrq` directly. + Returns + ------- + out : `~syncopy.CrossSpectralData` + The analyis result with dims ['time', 'freq', 'channel_i', channel_j'] + Examples -------- Coming soon... diff --git a/syncopy/preproc/__init__.py b/syncopy/preproc/__init__.py new file mode 100644 index 000000000..2e05ec49c --- /dev/null +++ b/syncopy/preproc/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# +# Populate namespace with preprocessing frontend +# + +# Import __all__ routines from local modules +from .preprocessing import preprocessing + +# Populate local __all__ namespace +# with the user-exposed frontend +__all__ = ['preprocessing'] diff --git a/syncopy/preproc/filteringCRs.py b/syncopy/preproc/butterworthCR.py similarity index 75% rename from syncopy/preproc/filteringCRs.py rename to syncopy/preproc/butterworthCR.py index 1ffd4302b..f1739dee9 100644 --- a/syncopy/preproc/filteringCRs.py +++ b/syncopy/preproc/butterworthCR.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # computeFunctions and -Routines for parallel calculation -# of common preprocessing steps like IIR filtering +# of IIR Filter operations with the Butterworth filter # # Builtin/3rd party package imports @@ -10,7 +10,6 @@ from inspect import signature # syncopy imports -from syncopy.shared.tools import best_match from syncopy.shared.computational_routine import ComputationalRoutine from syncopy.shared.kwarg_decorators import unwrap_io @@ -24,7 +23,8 @@ def but_filtering_cF(dat, direction='twopass', polyremoval=None, timeAxis=0, - noCompute=False + noCompute=False, + chunkShape=None ): """ Provides basic filtering of signals with IIR (Butterworth) @@ -78,11 +78,6 @@ def but_filtering_cF(dat, """ - # attach dummy channel axis in case only a - # single signal/channel is the input - if dat.ndim < 2: - dat = dat[:, np.newaxis] - # Re-arrange array if necessary and get dimensional information if timeAxis != 0: dat = dat.T # does not copy but creates view of `dat` @@ -112,3 +107,41 @@ def but_filtering_cF(dat, elif direction == 'onepass': filtered = sci.sosfilt(sos, dat, axis=0) return filtered + + +class But_Filtering(ComputationalRoutine): + + """ + Compute class that performs filtering with butterworth filters + of :class:`~syncopy.AnalogData` objects + + Sub-class of :class:`~syncopy.shared.computational_routine.ComputationalRoutine`, + see :doc:`/developer/compute_kernels` for technical details on Syncopy's compute + classes and metafunctions. + + See also + -------- + syncopy.preprocessing : parent metafunction + """ + + computeFunction = staticmethod(but_filtering_cF) + + # 1st argument,the data, gets omitted + valid_kws = list(signature(but_filtering_cF).parameters.keys())[1:] + + def process_metadata(self, data, out): + + # Some index gymnastics to get trial begin/end "samples" + if data._selection is not None: + chanSec = data._selection.channel + trl = data._selection.trialdefinition + for row in range(trl.shape[0]): + trl[row, :2] = [row, row + 1] + else: + chanSec = slice(None) + trl = data.trialdefinition + + out.trialdefinition = trl + + out.samplerate = data.samplerate + out.channel = np.array(data.channel[chanSec]) diff --git a/syncopy/preproc/preprocessing.py b/syncopy/preproc/preprocessing.py new file mode 100644 index 000000000..d05c625d4 --- /dev/null +++ b/syncopy/preproc/preprocessing.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +# +# Syncopy preprocessing frontend +# + +# Builtin/3rd party package imports +import numpy as np + +# Syncopy imports +from syncopy import AnalogData +from syncopy.shared.parsers import data_parser, scalar_parser, array_parser +from syncopy.shared.tools import get_defaults +from syncopy.shared.errors import SPYValueError, SPYTypeError, SPYWarning, SPYInfo +from syncopy.shared.kwarg_decorators import (unwrap_cfg, unwrap_select, + detect_parallel_client) + +from syncopy.shared.input_processors import ( + check_effective_parameters, + check_passed_kwargs, + process_padding +) + +from .butterworthCR import But_Filtering + +availableFilters = ('but', 'firws') +availableFilterTypes = ('lp', 'hp', 'bp', 'bs') +availableDirections = ('twopass', 'onepass') + + +@unwrap_cfg +@unwrap_select +@detect_parallel_client +def preprocessing(data, + filter_class = 'but', + filter_type='lp', + freq=None, + order=6, + direction='twopass', + polyremoval=None, + **kwargs + ): + """ + Filtering of time continuous raw data with IIR and FIR filters + + data : `~syncopy.AnalogData` + A non-empty Syncopy :class:`~syncopy.AnalogData` object + filter_class : {'but', 'firws'} + Butterworth (IIR) or windowed sinc (FIR) + filter_type : {'lp', 'hp', 'bp', 'bs'}, optional + Select type of filter, either low-pass `'lp'`, + high-pass `'hp'`, band-pass `'bp'` or band-stop (Notch) `'bs'`. + freq : float or array_like + Cut-off frequency for low- and high-pass filters or sequence + of two frequencies for band-stop and band-pass filter. + order : int, optional + Order of the filter, default is 6. + Higher orders yield a sharper transition width + or less 'roll off' of the filter, but are more computationally expensive. + direction : {'twopass', 'onepass'} + Filter direction: + `'twopass'` - zero-phase forward and reverse filter + `'onepass'` - forward filter, introduces group delays + polyremoval : int or None, optional + Order of polynomial used for de-trending data in the time domain prior + to filtering. A value of 0 corresponds to subtracting the mean + ("de-meaning"), ``polyremoval = 1`` removes linear trends (subtracting the + least squares fit of a linear polynomial). + + Returns + ------- + filtered : `~syncopy.AnalogData` + The filtered dataset with the same shape and dimord as the input `data` + """ + + # -- Basic input parsing -- + + # Make sure our one mandatory input object can be processed + try: + data_parser(data, varname="data", dataclass="AnalogData", + writable=None, empty=False) + except Exception as exc: + raise exc + timeAxis = data.dimord.index("time") + + # Get everything of interest in local namespace + defaults = get_defaults(preprocessing) + lcls = locals() + # check for ineffective additional kwargs + check_passed_kwargs(lcls, defaults, frontend_name="preprocessing") + # Ensure a valid computational method was selected + + if filter_class not in availableFilters: + lgl = "'" + "or '".join(opt + "' " for opt in availableFilters) + raise SPYValueError(legal=lgl, varname="filter_class", actual=filter_class) + + if not isinstance(filter_type, str) or filter_type not in availableFilterTypes: + lgl = f"one of {availableFilterTypes}" + act = filter_type + raise SPYValueError(lgl, 'filter_type', filter_type) + + # check `freq` setting + if filter_type in ('lp', 'hp'): + scalar_parser(freq, varname='freq', lims=[0, data.samplerate / 2]) + elif filter_type in ('bp', 'bs'): + array_parser(freq, varname='freq', hasinf=False, hasnan=False, + lims=[0, data.samplerate / 2], dims=(2,)) + # filter order + scalar_parser(order, varname='order', lims=[0, 100], ntype='int_like') + + # filter direction + if not isinstance(direction, str) or direction not in availableDirections: + lgl = "'" + "or '".join(opt + "' " for opt in availableDirections) + raise SPYValueError(legal=lgl, varname="direction", actual=direction) + + # check polyremoval + if polyremoval is not None: + scalar_parser(polyremoval, varname="polyremoval", ntype="int_like", lims=[0, 1]) + + # -- get trial info + + # if a subset selection is present + # get sampleinfo and check for equidistancy + if data._selection is not None: + sinfo = data._selection.trialdefinition[:, :2] + trialList = data._selection.trials + # user picked discrete set of time points + if isinstance(data._selection.time[0], list): + lgl = "equidistant time points (toi) or time slice (toilim)" + actual = "non-equidistant set of time points" + raise SPYValueError(legal=lgl, varname="select", actual=actual) + else: + trialList = list(range(len(data.trials))) + sinfo = data.sampleinfo + lenTrials = np.diff(sinfo).squeeze() + + # check for equidistant sampling as needed for filtering + if not all([np.allclose(np.diff(time), 1 / data.samplerate) for time in data.time]): + lgl = "equidistant sampling in time" + act = "non-equidistant sampling" + raise SPYValueError(lgl, varname="data", actual=act) + + # -- Method calls + + # Prepare keyword dict for logging (use `lcls` to get actually provided + # keyword values, not defaults set above) + log_dict = {"filter_class": filter_class, + "filter_type": filter_type, + "freq": freq, + "order": order, + "direction": direction, + "polyremoval": polyremoval, + } + + if filter_class == 'but': + check_effective_parameters(But_Filtering, defaults, lcls) + + filterMethod = But_Filtering(samplerate=data.samplerate, + filter_type=filter_type, + freq=freq, + order=order, + direction=direction, + polyremoval=polyremoval, + timeAxis=timeAxis) + + if filter_class == 'firws': + raise NotImplementedError('FIR coming soon..') + + # ------------------------------------ + # Call the chosen ComputationalRoutine + # ------------------------------------ + + out = AnalogData(dimord=data.dimord) + # Perform actual computation + filterMethod.initialize(data, + out._stackingDim, + chan_per_worker=kwargs.get("chan_per_worker"), + keeptrials=True) + filterMethod.compute(data, out, parallel=kwargs.get("parallel"), log_dict=log_dict) + + return out diff --git a/syncopy/tests/local_spy.py b/syncopy/tests/local_spy.py index 0e2c21658..6209c7e91 100644 --- a/syncopy/tests/local_spy.py +++ b/syncopy/tests/local_spy.py @@ -29,11 +29,15 @@ ad1 = spy.AnalogData([mock_up] * 5) nTrials = 50 - nSamples = 200 + nSamples = 500 + fs = 500 + f1, f2 = 20, 40 + A1, A2 = 2, 3 trls = [] for _ in range(nTrials): - # defaults AR(2) parameters yield 40Hz peak - trls.append(synth_data.AR2_network(None, nSamples=nSamples)) - ad1 = spy.AnalogData(trls, samplerate=200) - gr = spy.connectivityanalysis(ad1, method='granger', taper='dpss', tapsmofrq=3, - foilim=[0, 100]) + sig1 = A1 * np.cos(f1 * 2 * np.pi * np.arange(nSamples) / fs) + sig1 += A2 * np.cos(f2 * 2 * np.pi * np.arange(nSamples) / fs) + sig2 = np.random.randn(nSamples) + trls.append(np.vstack([sig1, sig2]).T) + ad1 = spy.AnalogData(trls, samplerate=500) + #spy.preprocessing(ad1, filter_class='d') From f5491f4a287287c2ede44542c714200dc9aa6c55 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Fri, 4 Mar 2022 12:11:22 +0100 Subject: [PATCH 075/166] NEW: Amended selection tests - included tests for scalar selections (closes #39) - included tests for trial selections via NumPy arrays (closes #180) - included tests for proper parsing of selection keywords (closes #223) On branch fix_selectcsdata Changes to be committed: modified: syncopy/tests/test_continuousdata.py modified: syncopy/tests/test_selectdata.py --- syncopy/tests/test_continuousdata.py | 2 +- syncopy/tests/test_selectdata.py | 25 ++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/syncopy/tests/test_continuousdata.py b/syncopy/tests/test_continuousdata.py index 6e2c50aae..9f77cbbd4 100644 --- a/syncopy/tests/test_continuousdata.py +++ b/syncopy/tests/test_continuousdata.py @@ -91,7 +91,7 @@ def _base_op_tests(dummy, ymmud, dummy2, ymmud2, dummyC, operation): ymmudOperands = [ymmudArr, ymmudArr.tolist()] # Ensure trial counts are properly vetted - dummy2.selectdata(trials=[0], inplace=True) + dummy2.selectdata(trials=0, inplace=True) with pytest.raises(SPYValueError) as spyval: operation(dummy, dummy2) assert "Syncopy object with same number of trials (selected)" in str (spyval.value) diff --git a/syncopy/tests/test_selectdata.py b/syncopy/tests/test_selectdata.py index e1c2c748f..e2d3c21c4 100644 --- a/syncopy/tests/test_selectdata.py +++ b/syncopy/tests/test_selectdata.py @@ -430,6 +430,24 @@ def test_general(self): assert selected.trialdefinition.shape == (2, 4) assert np.array_equal(selected.trialdefinition[:, -1], dummy.trialdefinition[[3, 1], -1]) + # scalar selection + selection = Selector(dummy, {"trials": 2}) + assert selection.trials == [2] + selected = selectdata(dummy, trials=2) + assert np.array_equal(selected.trials[0], dummy.trials[2]) + assert selected.trialdefinition.shape == (1, 4) + assert np.array_equal(selected.trialdefinition[:, -1], dummy.trialdefinition[[2], -1]) + + # array selection + selection = Selector(dummy, {"trials": np.array([3, 1])}) + assert selection.trials == [3, 1] + selected = selectdata(dummy, trials=[3, 1]) + assert np.array_equal(selected.trials[0], dummy.trials[3]) + assert np.array_equal(selected.trials[1], dummy.trials[1]) + assert selected.trialdefinition.shape == (2, 4) + assert np.array_equal(selected.trialdefinition[:, -1], dummy.trialdefinition[[3, 1], -1]) + + # select all for trlSec in [None, "all"]: selection = Selector(dummy, {"trials": trlSec}) assert selection.trials == list(range(len(dummy.trials))) @@ -438,11 +456,11 @@ def test_general(self): assert np.array_equal(trl, dummy.trials[tk]) assert np.array_equal(selected.trialdefinition, dummy.trialdefinition) + # invalid trials with pytest.raises(SPYValueError): Selector(dummy, {"trials": [-1, 9]}) # test "simple" property setters handled by `_selection_setter` - # for prop in ["eventid"]: for prop in ["channel", "taper", "unit", "eventid"]: if hasattr(dummy, prop): expected = self.selectDict[prop]["result"] @@ -464,6 +482,11 @@ def test_general(self): else: solution = slice(start, stop, step) + # ensure typos in selectino keywords are caught + with pytest.raises(SPYValueError) as spv: + Selector(dummy, {prop + "x": sel}) + assert "expected dict with one or all of the following keys:" in str(spv.value) + # once we're sure `Selector` works, actually select data selection = Selector(dummy, {prop : sel}) assert getattr(selection, prop) == solution From 02984ab05225a41d8e5a0a2940bc4edbc685bc75 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Fri, 4 Mar 2022 13:36:16 +0100 Subject: [PATCH 076/166] NEW/CHG: Modified show output - new keyword `squeeze` in `show` strips singleton dimensions from array output by default - the corresponding info message has been modified accordingly (depending on `squeeze` being `True` or `False`) - included two little tests (only for `AnalogData`) - the core functionality of `show` is provided by `selectdata` anyway. Closes #225 On branch fix_selectcsdata Changes to be committed: modified: syncopy/datatype/methods/show.py modified: syncopy/tests/test_selectdata.py --- syncopy/datatype/methods/show.py | 58 ++++++++++++++++++++++---------- syncopy/tests/test_selectdata.py | 7 ++-- 2 files changed, 45 insertions(+), 20 deletions(-) diff --git a/syncopy/datatype/methods/show.py b/syncopy/datatype/methods/show.py index a5ac125d7..ff5676361 100644 --- a/syncopy/datatype/methods/show.py +++ b/syncopy/datatype/methods/show.py @@ -7,14 +7,14 @@ import numpy as np # Local imports -from syncopy.shared.errors import SPYInfo +from syncopy.shared.errors import SPYInfo, SPYTypeError from syncopy.shared.kwarg_decorators import unwrap_cfg __all__ = ["show"] @unwrap_cfg -def show(data, **kwargs): +def show(data, squeeze=True, **kwargs): """ Show (partial) contents of Syncopy object @@ -40,6 +40,10 @@ def show(data, **kwargs): determines which keywords can be used. Some keywords are only valid for certain types of Syncopy objects, e.g., "freqs" is not a valid selector for an :class:`~syncopy.AnalogData` object. + squeeze : bool + If `True` (default) any singleton dimensions are removed from the output + array, i.e., the shape of the returned array does not contain ones (e.g., + ``arr.shape = (2,)`` not ``arr.shape = (1,2,1,1)``). **kwargs : keywords Valid data selectors (e.g., `trials`, `channels`, `toi` etc.). Please refer to :func:`~syncopy.selectdata` for a full list of available data @@ -70,21 +74,26 @@ def show(data, **kwargs): Show the contents of `'channel02'` across all trials: - >>> spy.show(adata, channels=['channel02']) - Syncopy INFO: In-place selection attached to data object: Syncopy AnalogData selector with 1 channels, all times, 10 trials - Syncopy INFO: Showing 1 channels, all times, 10 trials - Out[11]: - array([[1.627 ], - [1.7906], - [1.1757], - ..., - [1.1498], - [0.7753], - [1.0457]], dtype=float32) + >>> spy.show(adata, channel='channel02') + Syncopy INFO: Showing all times 10 trials + Out[2]: array([1.0871, 0.7267, 0.2816, ..., 1.0273, 0.893 , 0.7226], dtype=float32) Note that this is equivalent to - >>> adata.show(channels=['channel02']) + >>> adata.show(channel='channel02') + + To preserve singleton dimensions use ``squeeze=False``: + + >>> adata.show(channel='channel02', squeeze=False) + Out[3]: + array([[1.0871], + [0.7267], + [0.2816], + ..., + [1.0273], + [0.893 ], + [0.7226]], dtype=float32) + See also -------- @@ -96,11 +105,26 @@ def show(data, **kwargs): SPYInfo("Empty object, nothing to show") return + # Parse single method-specific keyword + if not isinstance(squeeze, bool): + raise SPYTypeError(squeeze, varname="squeeze", expected="True or False") + # Leverage `selectdata` to sanitize input and perform subset picking data.selectdata(inplace=True, **kwargs) + # Truncate info message by removing any squeezed dimensions (if necessary) + msg = data._selection.__str__().partition("with")[-1] + if squeeze: + removeKeys = ["one", "1 "] + selectionTxt = np.array(msg.split(",")) + txtMask = [all(qualifier not in selTxt for qualifier in removeKeys) for selTxt in selectionTxt] + msg = "".join(selectionTxt[txtMask]) + transform_out = np.squeeze + else: + transform_out = lambda x : x + SPYInfo("Showing{}".format(msg)) + # Use an object's `_preview_trial` method fetch required indexing tuples - SPYInfo("Showing{}".format(data._selection.__str__().partition("with")[-1])) idxList = [] for trlno in data._selection.trials: idxList.append(data._preview_trial(trlno).idx) @@ -126,6 +150,6 @@ def show(data, **kwargs): # If possible slice underlying dataset only once, otherwise return a list # of arrays corresponding to selected trials if all(si == True for si in singleIdx): - return data.data[tuple(returnIdx)] + return transform_out(data.data[tuple(returnIdx)]) else: - return [data.data[idx] for idx in idxList] + return [transform_out(data.data[idx]) for idx in idxList] diff --git a/syncopy/tests/test_selectdata.py b/syncopy/tests/test_selectdata.py index e2d3c21c4..769e47b3e 100644 --- a/syncopy/tests/test_selectdata.py +++ b/syncopy/tests/test_selectdata.py @@ -362,10 +362,7 @@ def test_general(self): elif np.issubdtype(type(selection), np.number): selection = [selection] - # elif isinstance(selection, str) or selection is None: - # selects = [None] if isinstance(selection, (list, np.ndarray)): - # else: # selection is list/ndarray if isinstance(selection[0], str): avail = getattr(discrete, prop) else: @@ -415,6 +412,10 @@ def test_general(self): ang.selectdata(trials=[3, 1], clear=True) assert "no data selectors if `clear = True`" in str(spyval.value) + # show full/squeezed arrays + assert len(ang.show(channel=0).shape) == 1 + assert len(ang.show(channel=0, squeeze=False).shape) == 2 + # go through all data-classes defined above for dclass in self.classes: dummy = getattr(spd, dclass)(data=self.data[dclass], From 9ff224892f87fb46cb4ca95468a98985bd8d2301 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Fri, 4 Mar 2022 13:51:46 +0100 Subject: [PATCH 077/166] CHG: Make DiscreteData trial times more consistent - instead of returning a list of `nTrials` generators, unfold those to return `nTrials` NumPy arrays (similar to `ContinuousData`) On branch fix_selectcsdata Changes to be committed: modified: syncopy/datatype/discrete_data.py --- syncopy/datatype/discrete_data.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/syncopy/datatype/discrete_data.py b/syncopy/datatype/discrete_data.py index 5f58aa594..1dd065fc6 100644 --- a/syncopy/datatype/discrete_data.py +++ b/syncopy/datatype/discrete_data.py @@ -182,8 +182,8 @@ def trials(self): def trialtime(self): """list(:class:`numpy.ndarray`): trigger-relative sample times in s""" if self.samplerate is not None and self.sampleinfo is not None: - return [((t + self._t0[tk]) / self.samplerate \ - for t in range(0, int(self.sampleinfo[tk, 1] - self.sampleinfo[tk, 0]))) \ + return [np.array([(t + self._t0[tk]) / self.samplerate \ + for t in range(0, int(self.sampleinfo[tk, 1] - self.sampleinfo[tk, 0]))]) \ for tk in np.unique(self.trialid)] # Helper function that grabs a single trial @@ -268,8 +268,7 @@ def _get_time(self, trials, toi=None, toilim=None): for trlno in trials: thisTrial = self.data[self.trialid == trlno, self.dimord.index("sample")] trlSample = np.arange(*self.sampleinfo[trlno, :]) - trlTime = np.array(list(allTrials[trlno])) - # trlTime = np.array(list(allTrials[np.where(self.trialid == trlno)[0][0]])) + trlTime = allTrials[trlno] minSample = trlSample[np.where(trlTime >= toilim[0])[0][0]] maxSample = trlSample[np.where(trlTime <= toilim[1])[0][-1]] selSample, _ = best_match(trlSample, [minSample, maxSample], span=True) @@ -287,7 +286,7 @@ def _get_time(self, trials, toi=None, toilim=None): for trlno in trials: thisTrial = self.data[self.trialid == trlno, self.dimord.index("sample")] trlSample = np.arange(*self.sampleinfo[trlno, :]) - trlTime = np.array(list(allTrials[trlno])) + trlTime = allTrials[trlno] _, selSample = best_match(trlTime, toi) for k, idx in enumerate(selSample): if np.abs(trlTime[idx - 1] - toi[k]) < np.abs(trlTime[idx] - toi[k]): From 7568700eadd833a1bc9db5464d863565241ba456 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Fri, 4 Mar 2022 14:52:08 +0100 Subject: [PATCH 078/166] CHG: Renamed _selection to selection - do not hide active in-place selections in a "hidden" property; rather expose these to the user - included `sampleinfo` property (analogous to `BaseData`) in `Selector` class (closes #173) On branch fix_selectcsdata Changes to be committed: modified: syncopy/datatype/base_data.py modified: syncopy/datatype/continuous_data.py modified: syncopy/datatype/discrete_data.py modified: syncopy/datatype/methods/arithmetic.py modified: syncopy/datatype/methods/padding.py modified: syncopy/datatype/methods/selectdata.py modified: syncopy/datatype/methods/show.py modified: syncopy/nwanalysis/AV_compRoutines.py modified: syncopy/nwanalysis/ST_compRoutines.py modified: syncopy/nwanalysis/connectivity_analysis.py modified: syncopy/plotting/_plot_analog.py modified: syncopy/plotting/_plot_spectral.py modified: syncopy/plotting/spy_plotting.py modified: syncopy/shared/computational_routine.py modified: syncopy/shared/kwarg_decorators.py modified: syncopy/specest/compRoutines.py modified: syncopy/specest/freqanalysis.py modified: syncopy/tests/test_computationalroutine.py modified: syncopy/tests/test_continuousdata.py --- syncopy/datatype/base_data.py | 20 +- syncopy/datatype/continuous_data.py | 6 +- syncopy/datatype/discrete_data.py | 4 +- syncopy/datatype/methods/arithmetic.py | 34 +- syncopy/datatype/methods/padding.py | 8 +- syncopy/datatype/methods/selectdata.py | 29 +- syncopy/datatype/methods/show.py | 6 +- syncopy/nwanalysis/AV_compRoutines.py | 24 +- syncopy/nwanalysis/ST_compRoutines.py | 18 +- syncopy/nwanalysis/connectivity_analysis.py | 8 +- syncopy/plotting/_plot_analog.py | 272 ++++++++-------- syncopy/plotting/_plot_spectral.py | 324 ++++++++++---------- syncopy/plotting/spy_plotting.py | 6 +- syncopy/shared/computational_routine.py | 12 +- syncopy/shared/kwarg_decorators.py | 10 +- syncopy/specest/compRoutines.py | 24 +- syncopy/specest/freqanalysis.py | 10 +- syncopy/tests/test_computationalroutine.py | 6 +- syncopy/tests/test_continuousdata.py | 10 +- 19 files changed, 424 insertions(+), 407 deletions(-) diff --git a/syncopy/datatype/base_data.py b/syncopy/datatype/base_data.py index 3195cc8e2..1d471bcdb 100644 --- a/syncopy/datatype/base_data.py +++ b/syncopy/datatype/base_data.py @@ -559,12 +559,12 @@ def mode(self, md): self._mode = md @property - def _selection(self): + def selection(self): """Data selection specified by :class:`Selector`""" return self._selector - @_selection.setter - def _selection(self, select): + @selection.setter + def selection(self, select): if select is None: self._selector = None else: @@ -870,7 +870,7 @@ def __eq__(self, other): return True # If in-place selections are present, abort - if self._selection is not None or other._selection is not None: + if self.selection is not None or other.selection is not None: err = "Cannot perform object comparison with existing in-place selection" raise SPYError(err) @@ -1711,6 +1711,18 @@ def trialdefinition(self, data): counter += nSamples self._trialdefinition = trlDef + @property + def sampleinfo(self): + """nTrials x 2 :class:`numpy.ndarray` of [start, end] sample indices""" + if self._trialdefinition is not None: + return self._trialdefinition[:, :2] + else: + return None + + @sampleinfo.setter + def sampleinfo(self, sinfo): + raise SPYError("Cannot set sampleinfo. Use `Selector.trialdefinition` instead.") + @property def timepoints(self): """len(self.trials) list of lists encoding actual (not sample indices!) diff --git a/syncopy/datatype/continuous_data.py b/syncopy/datatype/continuous_data.py index fdbeab5ad..79ad6a521 100644 --- a/syncopy/datatype/continuous_data.py +++ b/syncopy/datatype/continuous_data.py @@ -262,10 +262,10 @@ def _preview_trial(self, trialno): idx[self._stackingDim] = slice(start, stop) # process existing data selections - if self._selection is not None: + if self.selection is not None: # time-selection is most delicate due to trial-offset - tsel = self._selection.time[self._selection.trials.index(trialno)] + tsel = self.selection.time[self.selection.trials.index(trialno)] if isinstance(tsel, slice): if tsel.start is not None: tstart = tsel.start @@ -290,7 +290,7 @@ def _preview_trial(self, trialno): dims = list(self.dimord) dims.pop(self._stackingDim) for dim in dims: - sel = getattr(self._selection, dim) + sel = getattr(self.selection, dim) if sel is not None: dimIdx = self.dimord.index(dim) idx[dimIdx] = sel diff --git a/syncopy/datatype/discrete_data.py b/syncopy/datatype/discrete_data.py index 1dd065fc6..21da49a31 100644 --- a/syncopy/datatype/discrete_data.py +++ b/syncopy/datatype/discrete_data.py @@ -223,8 +223,8 @@ def _preview_trial(self, trialno): trialIdx = np.where(self.trialid == trialno)[0] nCol = len(self.dimord) idx = [trialIdx.tolist(), slice(0, nCol)] - if self._selection is not None: # selections are harmonized, just take `.time` - idx[0] = trialIdx[self._selection.time[self._selection.trials.index(trialno)]].tolist() + if self.selection is not None: # selections are harmonized, just take `.time` + idx[0] = trialIdx[self.selection.time[self.selection.trials.index(trialno)]].tolist() shp = [len(idx[0]), nCol] return FauxTrial(shp, tuple(idx), self.data.dtype, self.dimord) diff --git a/syncopy/datatype/methods/arithmetic.py b/syncopy/datatype/methods/arithmetic.py index 02e529003..f84a70d08 100644 --- a/syncopy/datatype/methods/arithmetic.py +++ b/syncopy/datatype/methods/arithmetic.py @@ -137,10 +137,10 @@ def _parse_input(obj1, obj2, operator): # If no active selection is present, create a "fake" all-to-all selection # to harmonize processing down the road (and attach `_cleanup` attribute for later removal) - if baseObj._selection is None: + if baseObj.selection is None: baseObj.selectdata(inplace=True) - baseObj._selection._cleanup = True - baseTrialList = baseObj._selection.trials + baseObj.selection._cleanup = True + baseTrialList = baseObj.selection.trials # Use the `_preview_trial` functionality of Syncopy objects to get each trial's # shape and dtype (existing selections are taken care of automatically) @@ -214,11 +214,11 @@ def _parse_input(obj1, obj2, operator): # If only a subset of `operand` is selected, adjust for this (and warn # that arbitrarily ugly things might happen with mis-matched selections) - if operand._selection is not None: + if operand.selection is not None: wrng = "Found existing in-place selection in operand. " +\ "Shapes and trial counts of base and operand objects have to match up!" SPYWarning(wrng, caller=operator) - opndTrialList = operand._selection.trials + opndTrialList = operand.selection.trials else: opndTrialList = list(range(len(operand.trials))) @@ -257,12 +257,12 @@ def _parse_input(obj1, obj2, operator): else False for sel in trl.idx): lgl = "Syncopy object with ordered unreverberated subset selection" act = "Syncopy object with selection {}" - raise SPYValueError(lgl, varname="operand", actual=act.format(operand._selection)) + raise SPYValueError(lgl, varname="operand", actual=act.format(operand.selection)) if sum(isinstance(sel, slice) for sel in trl.idx) > 1 and \ sum(isinstance(sel, list) for sel in trl.idx) > 1: lgl = "Syncopy object without selections requiring advanced indexing" act = "Syncopy object with selection {}" - raise SPYValueError(lgl, varname="operand", actual=act.format(operand._selection)) + raise SPYValueError(lgl, varname="operand", actual=act.format(operand.selection)) # Propagate indices for fetching data from operand operand_idxs = [trl.idx for trl in opndTrials] @@ -344,12 +344,12 @@ def _perform_computation(baseObj, # Prepare logging info in dictionary: we know that `baseObj` is definitely # a Syncopy data object, operand may or may not be; account for this if "BaseData" in str(operand.__class__.__mro__): - opSel = operand._selection + opSel = operand.selection else: opSel = None log_dct = {"operator": operator, "base": baseObj.__class__.__name__, - "base selection": baseObj._selection, + "base selection": baseObj.selection, "operand": operand.__class__.__name__, "operand selection": opSel} @@ -423,8 +423,8 @@ def _perform_computation(baseObj, lock.release() # Delete any created subset selections - if hasattr(baseObj._selection, "_cleanup"): - baseObj._selection = None + if hasattr(baseObj.selection, "_cleanup"): + baseObj.selection = None return out @@ -510,15 +510,15 @@ class SpyArithmetic(ComputationalRoutine): def process_metadata(self, baseObj, out): # Get/set timing-related selection modifiers - out.trialdefinition = baseObj._selection.trialdefinition - # if baseObj._selection._timeShuffle: # FIXME: should be implemented done the road - # out.time = baseObj._selection.timepoints - if baseObj._selection._samplerate: + out.trialdefinition = baseObj.selection.trialdefinition + # if baseObj.selection._timeShuffle: # FIXME: should be implemented done the road + # out.time = baseObj.selection.timepoints + if baseObj.selection._samplerate: out.samplerate = baseObj.samplerate # Get/set dimensional attributes changed by selection - for prop in baseObj._selection._dimProps: - selection = getattr(baseObj._selection, prop) + for prop in baseObj.selection._dimProps: + selection = getattr(baseObj.selection, prop) if selection is not None: if np.issubdtype(type(selection), np.number): selection = [selection] diff --git a/syncopy/datatype/methods/padding.py b/syncopy/datatype/methods/padding.py index 0984a33c3..602b9d342 100644 --- a/syncopy/datatype/methods/padding.py +++ b/syncopy/datatype/methods/padding.py @@ -296,8 +296,8 @@ def padding(data, padtype, pad="absolute", padlength=None, prepadlength=None, # accounting for in-place selections); to not repeat this later, save relevant # quantities in tmp attributes (all prefixed by `'_pad'`) if spydata: - if data._selection is not None: - trialList = data._selection.trials + if data.selection is not None: + trialList = data.selection.trials data._pad_sinfo = np.zeros((len(trialList), 2)) data._pad_t0 = np.zeros((len(trialList),)) for tk, trlno in enumerate(trialList): @@ -305,11 +305,11 @@ def padding(data, padtype, pad="absolute", padlength=None, prepadlength=None, tsel = trl.idx[timeAxis] if isinstance(tsel, list): lgl = "Syncopy AnalogData object with no or channe/trial selection" - raise SPYValueError(lgl, varname="data", actual=data._selection) + raise SPYValueError(lgl, varname="data", actual=data.selection) else: data._pad_sinfo[tk, :] = [trl.idx[timeAxis].start, trl.idx[timeAxis].stop] data._pad_t0[tk] = data._t0[trlno] - data._pad_channel = data.channel[data._selection.channel] + data._pad_channel = data.channel[data.selection.channel] else: trialList = list(range(len(data.trials))) data._pad_sinfo = data.sampleinfo diff --git a/syncopy/datatype/methods/selectdata.py b/syncopy/datatype/methods/selectdata.py index a282b6863..78c3fc24d 100644 --- a/syncopy/datatype/methods/selectdata.py +++ b/syncopy/datatype/methods/selectdata.py @@ -186,9 +186,14 @@ def selectdata(data, events are selected. inplace : bool If `inplace` is `True` **no** new object is created. Instead the provided - selection is stored in the input object's `_selection` attribute for later + selection is stored in the input object's `selection` attribute for later use. By default `inplace` is `False` and all calls to `selectdata` create a new Syncopy data object. + clear : bool + If `True` remove any active in-place selection. Note that in-place + selections can also be removed manually by assinging `None` to the + `selection` property, i.e., ``mydata.selection = None`` is equivalent + to ``spy.selectdata(mydata, clear=True)`` or ``mydata.selectdata(clear=True)`` Returns ------- @@ -317,15 +322,15 @@ def selectdata(data, if any(value is not None for value in selectDict.values()): lgl = "no data selectors if `clear = True`" raise SPYValueError(lgl, varname="select", actual=selectDict) - if data._selection is None: + if data.selection is None: SPYInfo("No in-place selection found. ") else: - data._selection = None + data.selection = None SPYInfo("In-place selection cleared") return # Pass provided selections on to `Selector` class which performs error checking - data._selection = selectDict + data.selection = selectDict # If an in-place selection was requested we're done if inplace: @@ -354,7 +359,7 @@ def selectdata(data, log_dict=log_dct) # Wipe data-selection slot to not alter input object - data._selection = None + data.selection = None # Either return newly created output object or simply quit return out if new_out else None @@ -364,7 +369,7 @@ def _get_selection_size(data): """ Local helper routine for computing the on-disk size of an active data-selection """ - fauxTrials = [data._preview_trial(trlno) for trlno in data._selection.trials] + fauxTrials = [data._preview_trial(trlno) for trlno in data.selection.trials] fauxSizes = [np.prod(ftrl.shape)*ftrl.dtype.itemsize for ftrl in fauxTrials] return sum(fauxSizes) / 1024**2 @@ -383,15 +388,15 @@ class DataSelection(ComputationalRoutine): def process_metadata(self, data, out): # Get/set timing-related selection modifiers - out.trialdefinition = data._selection.trialdefinition - # if data._selection._timeShuffle: # FIXME: should be implemented down the road - # out.time = data._selection.timepoints - if data._selection._samplerate: + out.trialdefinition = data.selection.trialdefinition + # if data.selection._timeShuffle: # FIXME: should be implemented down the road + # out.time = data.selection.timepoints + if data.selection._samplerate: out.samplerate = data.samplerate # Get/set dimensional attributes changed by selection - for prop in data._selection._dimProps: - selection = getattr(data._selection, prop) + for prop in data.selection._dimProps: + selection = getattr(data.selection, prop) if selection is not None: if np.issubdtype(type(selection), np.number): selection = [selection] diff --git a/syncopy/datatype/methods/show.py b/syncopy/datatype/methods/show.py index ff5676361..d60713dca 100644 --- a/syncopy/datatype/methods/show.py +++ b/syncopy/datatype/methods/show.py @@ -113,7 +113,7 @@ def show(data, squeeze=True, **kwargs): data.selectdata(inplace=True, **kwargs) # Truncate info message by removing any squeezed dimensions (if necessary) - msg = data._selection.__str__().partition("with")[-1] + msg = data.selection.__str__().partition("with")[-1] if squeeze: removeKeys = ["one", "1 "] selectionTxt = np.array(msg.split(",")) @@ -126,7 +126,7 @@ def show(data, squeeze=True, **kwargs): # Use an object's `_preview_trial` method fetch required indexing tuples idxList = [] - for trlno in data._selection.trials: + for trlno in data.selection.trials: idxList.append(data._preview_trial(trlno).idx) # Perform some slicing/list-selection gymnastics: ensure that selections @@ -145,7 +145,7 @@ def show(data, squeeze=True, **kwargs): returnIdx[sk] = slice(selectors[0].start, selectors[-1].stop) # Reset in-place subset selection - data._selection = None + data.selection = None # If possible slice underlying dataset only once, otherwise return a list # of arrays corresponding to selected trials diff --git a/syncopy/nwanalysis/AV_compRoutines.py b/syncopy/nwanalysis/AV_compRoutines.py index 67ab6081b..7fe501aff 100644 --- a/syncopy/nwanalysis/AV_compRoutines.py +++ b/syncopy/nwanalysis/AV_compRoutines.py @@ -152,10 +152,10 @@ def pre_check(self): def process_metadata(self, data, out): # Some index gymnastics to get trial begin/end "samples" - if data._selection is not None: - chanSec_i = data._selection.channel_i - chanSec_j = data._selection.channel_j - trl = data._selection.trialdefinition + if data.selection is not None: + chanSec_i = data.selection.channel_i + chanSec_j = data.selection.channel_j + trl = data.selection.trialdefinition for row in range(trl.shape[0]): trl[row, :2] = [row, row + 1] else: @@ -293,10 +293,10 @@ def pre_check(self): def process_metadata(self, data, out): # Get trialdef array + channels from source - if data._selection is not None: - chanSec_i = data._selection.channel_i - chanSec_j = data._selection.channel_j - trl = data._selection.trialdefinition + if data.selection is not None: + chanSec_i = data.selection.channel_i + chanSec_j = data.selection.channel_j + trl = data.selection.trialdefinition else: chanSec_i = slice(None) chanSec_j = slice(None) @@ -472,10 +472,10 @@ def pre_check(self): def process_metadata(self, data, out): # Some index gymnastics to get trial begin/end "samples" - if data._selection is not None: - chanSec_i = data._selection.channel_i - chanSec_j = data._selection.channel_j - trl = data._selection.trialdefinition + if data.selection is not None: + chanSec_i = data.selection.channel_i + chanSec_j = data.selection.channel_j + trl = data.selection.trialdefinition for row in range(trl.shape[0]): trl[row, :2] = [row, row + 1] else: diff --git a/syncopy/nwanalysis/ST_compRoutines.py b/syncopy/nwanalysis/ST_compRoutines.py index 564092427..396f63d80 100644 --- a/syncopy/nwanalysis/ST_compRoutines.py +++ b/syncopy/nwanalysis/ST_compRoutines.py @@ -153,7 +153,7 @@ def cross_spectra_cF(trl_dat, dat = detrend(dat, type='linear', axis=0, overwrite_data=True) CS_ij = csd(dat, samplerate, nSamples, taper=taper, taper_opt=taper_opt) - + # where does freqs go/come from - # we will eventually solve this issue.. return CS_ij[None, freq_idx, ...] @@ -188,9 +188,9 @@ class ST_CrossSpectra(ComputationalRoutine): def process_metadata(self, data, out): # Some index gymnastics to get trial begin/end "samples" - if data._selection is not None: - chanSec = data._selection.channel - trl = data._selection.trialdefinition + if data.selection is not None: + chanSec = data.selection.channel + trl = data.selection.trialdefinition for row in range(trl.shape[0]): trl[row, :2] = [row, row + 1] else: @@ -308,7 +308,7 @@ def cross_covariance_cF(trl_dat, dat = detrend(dat, type='constant', axis=0, overwrite_data=True) elif polyremoval == 1: detrend(dat, type='linear', axis=0, overwrite_data=True) - + # re-normalize output for different effective overlaps norm_overlap = np.arange(nSamples, nSamples // 2, step = -1) @@ -362,9 +362,9 @@ def process_metadata(self, data, out): # Get trialdef array + channels from source: note, since lags are encoded # in time-axis, trial offsets etc. are bogus anyway: simply take max-sample # counts / 2 to fit lags - if data._selection is not None: - chanSec = data._selection.channel - trl = np.ceil(data._selection.trialdefinition / 2) + if data.selection is not None: + chanSec = data.selection.channel + trl = np.ceil(data.selection.trialdefinition / 2) else: chanSec = slice(None) trl = np.ceil(data.trialdefinition / 2) @@ -374,7 +374,7 @@ def process_metadata(self, data, out): if not self.keeptrials: trl = trl[[0], :] - + # set 1st entry of time axis to the 0-lag trl[:, 2] = 0 out.trialdefinition = trl diff --git a/syncopy/nwanalysis/connectivity_analysis.py b/syncopy/nwanalysis/connectivity_analysis.py index adc0bc834..c1cc09ded 100644 --- a/syncopy/nwanalysis/connectivity_analysis.py +++ b/syncopy/nwanalysis/connectivity_analysis.py @@ -151,11 +151,11 @@ def connectivityanalysis(data, method="coh", keeptrials=False, output="abs", # if a subset selection is present # get sampleinfo and check for equidistancy - if data._selection is not None: - sinfo = data._selection.trialdefinition[:, :2] - trialList = data._selection.trials + if data.selection is not None: + sinfo = data.selection.trialdefinition[:, :2] + trialList = data.selection.trials # user picked discrete set of time points - if isinstance(data._selection.time[0], list): + if isinstance(data.selection.time[0], list): lgl = "equidistant time points (toi) or time slice (toilim)" actual = "non-equidistant set of time points" raise SPYValueError(legal=lgl, varname="select", actual=actual) diff --git a/syncopy/plotting/_plot_analog.py b/syncopy/plotting/_plot_analog.py index 0361897c2..459cc6903 100644 --- a/syncopy/plotting/_plot_analog.py +++ b/syncopy/plotting/_plot_analog.py @@ -1,15 +1,15 @@ # -*- coding: utf-8 -*- -# +# # Outsourced plotting class methods from respective parent classes -# +# # Builtin/3rd party package imports import numpy as np -import os +import os # Local imports from syncopy.shared.errors import SPYValueError, SPYTypeError, SPYWarning -from syncopy.plotting.spy_plotting import (pltConfig, _layout_subplot_panels, +from syncopy.plotting.spy_plotting import (pltConfig, _layout_subplot_panels, _prep_toilim_avg, _setup_figure, _prep_plots) # Conditional matplotlib import @@ -24,52 +24,52 @@ def singlepanelplot(self, trials="all", channels="all", toilim=None, avg_channel title=None, grid=None, fig=None, **kwargs): """ Plot contents of :class:`~syncopy.AnalogData` objects using single-panel figure(s) - - Please refer to :func:`syncopy.singlepanelplot` for detailed usage information. - + + Please refer to :func:`syncopy.singlepanelplot` for detailed usage information. + Examples -------- - Use :func:`~syncopy.tests.misc.generate_artificial_data` to create two synthetic - :class:`~syncopy.AnalogData` objects. - + Use :func:`~syncopy.tests.misc.generate_artificial_data` to create two synthetic + :class:`~syncopy.AnalogData` objects. + >>> from syncopy.tests.misc import generate_artificial_data - >>> adata = generate_artificial_data(nTrials=10, nChannels=32) - >>> bdata = generate_artificial_data(nTrials=5, nChannels=16) - + >>> adata = generate_artificial_data(nTrials=10, nChannels=32) + >>> bdata = generate_artificial_data(nTrials=5, nChannels=16) + Plot an average of the first 16 channels, averaged across trials 2, 4, and 6: - + >>> fig = spy.singlepanelplot(adata, channels=range(16), trials=[2, 4, 6]) - - Overlay average of latter half of channels, averaged across trials 1, 3, 5: + + Overlay average of latter half of channels, averaged across trials 1, 3, 5: >>> fig = spy.singlepanelplot(adata, channels=range(16,32), trials=[1, 3, 5], fig=fig) - + Do not average channels: - + >>> fig = spy.singlepanelplot(adata, channels=range(16,32), trials=[1, 3, 5], avg_channels=False) - + Plot `adata` and `bdata` simultaneously in two separate figures: - + >>> fig1, fig2 = spy.singlepanelplot(adata, bdata, overlay=False) - - Overlay `adata` and `bdata`; use channel and trial selections that are valid + + Overlay `adata` and `bdata`; use channel and trial selections that are valid for both datasets: - + >>> fig3 = spy.singlepanelplot(adata, bdata, channels=range(16), trials=[1, 2, 3]) - + See also -------- syncopy.singlepanelplot : visualize Syncopy data objects using single-panel plots """ - + # Collect input arguments in dict `inputArgs` and process them inputArgs = locals() inputArgs.pop("self") dimArrs, dimCounts, idx, timeIdx, chanIdx = _prep_analog_plots(self, "singlepanelplot", **inputArgs) (nTrials, nChan) = dimCounts (trList, chArr) = dimArrs - - # If we're overlaying a multi-channel plot, ensure settings match up; also, + + # If we're overlaying a multi-channel plot, ensure settings match up; also, # do not try to overlay on top of multi-panel plots if hasattr(fig, "multipanelplot"): lgl = "single-panel figure generated by `singleplot`" @@ -82,11 +82,11 @@ def singlepanelplot(self, trials="all", channels="all", toilim=None, avg_channel raise SPYValueError(legal=lgl, varname="channels/avg_channels", actual=act) if nChan != len(fig.chanOffsets): lgl = "channel-count matching existing multi-channel panels in figure" - act = "{} channels per panel but {} channels for plotting".format(len(fig.chanOffsets), + act = "{} channels per panel but {} channels for plotting".format(len(fig.chanOffsets), nChan) raise SPYValueError(legal=lgl, varname="channels/channels per panel", actual=act) - # Ensure provided timing selection can actually be averaged (leverage + # Ensure provided timing selection can actually be averaged (leverage # the fact that `toilim` selections exclusively generate slices) if nTrials > 0: tLengths = _prep_toilim_avg(self) @@ -103,31 +103,31 @@ def singlepanelplot(self, trials="all", channels="all", toilim=None, avg_channel fig, ax = _setup_figure(1, xLabel=xLabel, grid=grid) fig.analogPlot = True else: - ax, = fig.get_axes() + ax, = fig.get_axes() - # Single-channel panel + # Single-channel panel if avg_channels: - + # Set up pieces of generic figure titles if nChan > 1: chanTitle = "Average of {} channels".format(nChan) else: chanTitle = chArr[0] - + # Plot entire timecourse if nTrials == 0: - + # Do not fetch entire dataset at once, but channel by channel - chanSec = np.arange(self.channel.size)[self._selection.channel] + chanSec = np.arange(self.channel.size)[self.selection.channel] pltArr = np.zeros((self.data.shape[timeIdx],), dtype=self.data.dtype) for chan in chanSec: idx[chanIdx] = chan pltArr += self.data[tuple(idx)].squeeze() pltArr /= nChan - + # The actual plotting command... ax.plot(pltArr) - + # Set plot title depending on dataset overlay if fig.objCount == 0: if title is None: @@ -142,20 +142,20 @@ def singlepanelplot(self, trials="all", channels="all", toilim=None, avg_channel # Average across trials else: - + # Compute channel-/trial-average time-course: 2D array with slice/list # selection does not require fancy indexing - no need to check this here pltArr = np.zeros((tLengths[0],), dtype=self.data.dtype) for k, trlno in enumerate(trList): - idx[timeIdx] = self._selection.time[k] + idx[timeIdx] = self.selection.time[k] pltArr += self._get_trial(trlno)[tuple(idx)].mean(axis=chanIdx).squeeze() pltArr /= nTrials - + # The actual plotting command is literally one line... - time = self.time[trList[0]][self._selection.time[0]] + time = self.time[trList[0]][self.selection.time[0]] ax.plot(time, pltArr, label=os.path.basename(self.filename)) ax.set_xlim([time[0], time[-1]]) - + # Set plot title depending on dataset overlay if fig.objCount == 0: if title is None: @@ -172,18 +172,18 @@ def singlepanelplot(self, trials="all", channels="all", toilim=None, avg_channel if title is None: title = overlayTitle.format(len(handles)) # ax.set_title(title, size=pltConfig["singleTitleSize"]) - + # Multi-channel panel else: # "Raw" data, do not respect any trials if nTrials == 0: - + # If required, compute max amplitude across provided channels if not hasattr(fig, "chanOffsets"): maxAmps = np.zeros((nChan,), dtype=self.data.dtype) tickOffsets = maxAmps.copy() - chanSec = np.arange(self.channel.size)[self._selection.channel] + chanSec = np.arange(self.channel.size)[self.selection.channel] for k, chan in enumerate(chanSec): idx[chanIdx] = chan pltArr = np.abs(self.data[tuple(idx)].squeeze()) @@ -192,7 +192,7 @@ def singlepanelplot(self, trials="all", channels="all", toilim=None, avg_channel fig.chanOffsets = np.cumsum([0] + [maxAmps.max()] * (nChan - 1)) fig.tickOffsets = fig.chanOffsets + tickOffsets.mean() - # Do not plot all at once but cycle through channels to not overflow memory + # Do not plot all at once but cycle through channels to not overflow memory for k, chan in enumerate(chanSec): idx[chanIdx] = chan ax.plot(self.data[tuple(idx)].squeeze() + fig.chanOffsets[k], @@ -200,7 +200,7 @@ def singlepanelplot(self, trials="all", channels="all", toilim=None, avg_channel label=os.path.basename(self.filename)) if grid is not None: ax.grid(grid) - + # Set plot title depending on dataset overlay if fig.objCount == 0: if title is None: @@ -213,7 +213,7 @@ def singlepanelplot(self, trials="all", channels="all", toilim=None, avg_channel # ax.set_title(title, size=pltConfig["singleTitleSize"]) else: handles, labels = ax.get_legend_handles_labels() - ax.legend(handles[ : : (nChan + 1)], + ax.legend(handles[ : : (nChan + 1)], labels[ : : (nChan + 1)]) if title is None: title = overlayTitle.format(len(handles)) @@ -222,10 +222,10 @@ def singlepanelplot(self, trials="all", channels="all", toilim=None, avg_channel # Average across trial(s) else: - # Compute trial-average + # Compute trial-average pltArr = np.zeros((tLengths[0], nChan), dtype=self.data.dtype) for k, trlno in enumerate(trList): - idx[timeIdx] = self._selection.time[k] + idx[timeIdx] = self.selection.time[k] pltArr += np.swapaxes(self._get_trial(trlno)[tuple(idx)], timeIdx, 0) pltArr /= nTrials @@ -235,78 +235,78 @@ def singlepanelplot(self, trials="all", channels="all", toilim=None, avg_channel fig.tickOffsets = fig.chanOffsets + np.abs(pltArr).mean() # Plot the entire trial-averaged array at once - time = self.time[trList[0]][self._selection.time[0]] - ax.plot(time, + time = self.time[trList[0]][self.selection.time[0]] + ax.plot(time, (pltArr + fig.chanOffsets.reshape(1, nChan)).reshape(time.size, nChan), color=plt.rcParams["axes.prop_cycle"].by_key()["color"][fig.objCount], label=os.path.basename(self.filename)) if grid is not None: ax.grid(grid) - + # Set plot title depending on dataset overlay if fig.objCount == 0: if title is None: - title = "{0} channels {1}across {2} trials".format(nChan, + title = "{0} channels {1}across {2} trials".format(nChan, "averaged " if nTrials > 1 else "", nTrials) # ax.set_title(title, size=pltConfig["singleTitleSize"]) else: handles, labels = ax.get_legend_handles_labels() - ax.legend(handles[ : : (nChan + 1)], + ax.legend(handles[ : : (nChan + 1)], labels[ : : (nChan + 1)]) if title is None: title = overlayTitle.format(len(handles)) # ax.set_title(title, size=pltConfig["singleTitleSize"]) - + # Increment overlay-counter and draw figure fig.objCount += 1 plt.draw() - self._selection = None + self.selection = None return fig -def multipanelplot(self, trials="all", channels="all", toilim=None, avg_channels=False, +def multipanelplot(self, trials="all", channels="all", toilim=None, avg_channels=False, avg_trials=True, title=None, grid=None, fig=None, **kwargs): """ Plot contents of :class:`~syncopy.AnalogData` objects using multi-panel figure(s) - - Please refer to :func:`syncopy.multipanelplot` for detailed usage information. - + + Please refer to :func:`syncopy.multipanelplot` for detailed usage information. + Examples -------- - Use :func:`~syncopy.tests.misc.generate_artificial_data` to create two synthetic - :class:`~syncopy.AnalogData` objects. - + Use :func:`~syncopy.tests.misc.generate_artificial_data` to create two synthetic + :class:`~syncopy.AnalogData` objects. + >>> from syncopy.tests.misc import generate_artificial_data - >>> adata = generate_artificial_data(nTrials=10, nChannels=32) - >>> bdata = generate_artificial_data(nTrials=5, nChannels=16) - + >>> adata = generate_artificial_data(nTrials=10, nChannels=32) + >>> bdata = generate_artificial_data(nTrials=5, nChannels=16) + Show overview of first 5 channels, averaged across trials 2, 4, and 6: - + >>> fig = spy.multipanelplot(adata, channels=range(5), trials=[2, 4, 6]) - - Overlay last 5 channels, averaged across trials 1, 3, 5: + + Overlay last 5 channels, averaged across trials 1, 3, 5: >>> fig = spy.multipanelplot(adata, channels=range(27, 32), trials=[1, 3, 5], fig=fig) - + Do not average trials: - + >>> fig = spy.multipanelplot(adata, channels=range(27, 32), trials=[1, 3, 5], avg_trials=False) - + Plot `adata` and `bdata` simultaneously in two separate figures: - + >>> fig1, fig2 = spy.multipanelplot(adata, bdata, channels=range(5), overlay=False) - - Overlay `adata` and `bdata`; use channel and trial selections that are valid + + Overlay `adata` and `bdata`; use channel and trial selections that are valid for both datasets: - + >>> fig3 = spy.multipanelplot(adata, bdata, channels=range(5), trials=[1, 2, 3], avg_trials=False) - + See also -------- syncopy.multipanelplot : visualize Syncopy data objects using multi-panel plots """ - + # Collect input arguments in dict `inputArgs` and process them inputArgs = locals() inputArgs.pop("self") @@ -320,7 +320,7 @@ def multipanelplot(self, trials="all", channels="all", toilim=None, avg_channels if avg_trials: msg = "`trials` is `None` but `avg_trials` is `True`. " +\ "Cannot perform trial averaging without trial specification - " +\ - "setting ``avg_trials = False``. " + "setting ``avg_trials = False``. " SPYWarning(msg) avg_trials = False if avg_channels: @@ -337,7 +337,7 @@ def multipanelplot(self, trials="all", channels="all", toilim=None, avg_channels if hasattr(fig, "nTrialPanels"): if nTrials != fig.nTrialPanels: lgl = "number of trials to plot matching existing panels in figure" - act = "{} panels but {} trials for plotting".format(fig.nTrialPanels, + act = "{} panels but {} trials for plotting".format(fig.nTrialPanels, nTrials) raise SPYValueError(legal=lgl, varname="trials/figure panels", actual=act) if avg_trials: @@ -355,7 +355,7 @@ def multipanelplot(self, trials="all", channels="all", toilim=None, avg_channels if hasattr(fig, "nChanPanels"): if nChan != fig.nChanPanels: lgl = "number of channels to plot matching existing panels in figure" - act = "{} panels but {} channels for plotting".format(fig.nChanPanels, + act = "{} panels but {} channels for plotting".format(fig.nChanPanels, nChan) raise SPYValueError(legal=lgl, varname="channels/figure panels", actual=act) if avg_channels: @@ -373,7 +373,7 @@ def multipanelplot(self, trials="all", channels="all", toilim=None, avg_channels raise SPYValueError(legal=lgl, varname="channels/avg_channels", actual=act) if nChan != len(fig.chanOffsets): lgl = "channel-count matching existing multi-channel panels in figure" - act = "{} channels per panel but {} channels for plotting".format(len(fig.chanOffsets), + act = "{} channels per panel but {} channels for plotting".format(len(fig.chanOffsets), nChan) raise SPYValueError(legal=lgl, varname="channels/channels per panel", actual=act) @@ -382,10 +382,10 @@ def multipanelplot(self, trials="all", channels="all", toilim=None, avg_channels # Either construct subplot panel layout/vet provided layout or fetch existing if fig is None: - + # Determine no. of required panels if avg_trials and not avg_channels: - npanels = nChan + npanels = nChan elif not avg_trials and avg_channels: npanels = nTrials elif not avg_trials and not avg_channels: @@ -395,8 +395,8 @@ def multipanelplot(self, trials="all", channels="all", toilim=None, avg_channels "single-panel plot. Please use `singlepanelplot` instead" SPYWarning(msg) return - - # Although, `_setup_figure` can call `_layout_subplot_panels` for us, we + + # Although, `_setup_figure` can call `_layout_subplot_panels` for us, we # need `nrow` and `ncol` below, so do it here if nTrials > 0: xLabel = "Time (s)" @@ -412,11 +412,11 @@ def multipanelplot(self, trials="all", channels="all", toilim=None, avg_channels else: ax_arr = fig.get_axes() nrow, ncol = ax_arr[0].numRows, ax_arr[0].numCols - + # Panels correspond to channels if avg_trials and not avg_channels: - - # Ensure provided timing selection can actually be averaged (leverage + + # Ensure provided timing selection can actually be averaged (leverage # the fact that `toilim` selections exclusively generate slices) tLengths = _prep_toilim_avg(self) @@ -424,17 +424,17 @@ def multipanelplot(self, trials="all", channels="all", toilim=None, avg_channels # selection does not require fancy indexing - no need to check this here pltArr = np.zeros((tLengths[0], nChan), dtype=self.data.dtype) for k, trlno in enumerate(trList): - idx[timeIdx] = self._selection.time[k] + idx[timeIdx] = self.selection.time[k] pltArr += np.swapaxes(self._get_trial(trlno)[tuple(idx)], timeIdx, 0) pltArr /= nTrials - + # Cycle through channels and plot trial-averaged time-courses (time- # axis must be identical for all channels, set up `idx` just once) - idx[timeIdx] = self._selection.time[0] - time = self.time[trList[k]][self._selection.time[0]] + idx[timeIdx] = self.selection.time[0] + time = self.time[trList[k]][self.selection.time[0]] for k, chan in enumerate(chArr): ax_arr[k].plot(time, pltArr[:, k], label=os.path.basename(self.filename)) - + # If we're overlaying datasets, adjust panel- and sup-titles: include # legend in top-right axis (note: `ax_arr` is row-major flattened) if fig.objCount == 0: @@ -456,15 +456,15 @@ def multipanelplot(self, trials="all", channels="all", toilim=None, avg_channels if title is None: title = overlayTitle.format(len(handles)) fig.suptitle(title, size=pltConfig["singleTitleSize"]) - + # Panels correspond to trials elif not avg_trials and avg_channels: - + # Cycle through panels to plot by-trial channel-averages for k, trlno in enumerate(trList): - idx[timeIdx] = self._selection.time[k] - time = self.time[trList[k]][self._selection.time[k]] - ax_arr[k].plot(time, + idx[timeIdx] = self.selection.time[k] + time = self.time[trList[k]][self.selection.time[k]] + ax_arr[k].plot(time, self._get_trial(trlno)[tuple(idx)].mean(axis=chanIdx).squeeze(), label=os.path.basename(self.filename)) @@ -492,15 +492,15 @@ def multipanelplot(self, trials="all", channels="all", toilim=None, avg_channels # Panels correspond to channels (if `trials` is `None`) otherwise trials elif not avg_trials and not avg_channels: - + # Plot each channel in separate panel if nTrials == 0: - chanSec = np.arange(self.channel.size)[self._selection.channel] + chanSec = np.arange(self.channel.size)[self.selection.channel] for k, chan in enumerate(chanSec): idx[chanIdx] = chan ax_arr[k].plot(self.data[tuple(idx)].squeeze(), label=os.path.basename(self.filename)) - + # If we're overlaying datasets, adjust panel- and sup-titles: include # legend in top-right axis (note: `ax_arr` is row-major flattened) if fig.objCount == 0: @@ -519,7 +519,7 @@ def multipanelplot(self, trials="all", channels="all", toilim=None, avg_channels if title is None: title = overlayTitle.format(len(handles)) fig.suptitle(title, size=pltConfig["singleTitleSize"]) - + # Each trial gets its own panel w/multiple channels per panel else: @@ -528,20 +528,20 @@ def multipanelplot(self, trials="all", channels="all", toilim=None, avg_channels maxAmps = np.zeros((nTrials,), dtype=self.data.dtype) tickOffsets = maxAmps.copy() for k, trlno in enumerate(trList): - idx[timeIdx] = self._selection.time[k] + idx[timeIdx] = self.selection.time[k] pltArr = np.abs(self._get_trial(trlno)[tuple(idx)]) maxAmps[k] = pltArr.max() tickOffsets[k] = pltArr.mean() fig.chanOffsets = np.cumsum([0] + [maxAmps.max()] * (nChan - 1)) fig.tickOffsets = fig.chanOffsets + tickOffsets.mean() - + # Cycle through panels to plot by-trial multi-channel time-courses for k, trlno in enumerate(trList): - idx[timeIdx] = self._selection.time[k] - time = self.time[trList[k]][self._selection.time[k]] + idx[timeIdx] = self.selection.time[k] + time = self.time[trList[k]][self.selection.time[k]] pltArr = np.swapaxes(self._get_trial(trlno)[tuple(idx)], timeIdx, 0) - ax_arr[k].plot(time, - (pltArr + fig.chanOffsets.reshape(1, nChan)).reshape(time.size, nChan), + ax_arr[k].plot(time, + (pltArr + fig.chanOffsets.reshape(1, nChan)).reshape(time.size, nChan), color=plt.rcParams["axes.prop_cycle"].by_key()["color"][fig.objCount], label=os.path.basename(self.filename)) @@ -566,16 +566,16 @@ def multipanelplot(self, trials="all", channels="all", toilim=None, avg_channels ax_arr[0].set_yticklabels([" "] * chArr.size) ax = ax_arr[ncol - 1] handles, labels = ax.get_legend_handles_labels() - ax.legend(handles[ : : (nChan + 1)], + ax.legend(handles[ : : (nChan + 1)], labels[ : : (nChan + 1)]) if title is None: title = overlayTitle.format(len(handles)) fig.suptitle(title, size=pltConfig["singleTitleSize"]) - + # Increment overlay-counter, draw figure and wipe data-selection slot fig.objCount += 1 plt.draw() - self._selection = None + self.selection = None return fig @@ -586,57 +586,57 @@ def _prep_analog_plots(self, name, **inputArgs): Parameters ---------- self : :class:`~syncopy.AnalogData` object - Syncopy :class:`~syncopy.AnalogData` object that is being processed by + Syncopy :class:`~syncopy.AnalogData` object that is being processed by the respective :meth:`.singlepanelplot` or :meth:`.multipanelplot` class methods - defined in this module. + defined in this module. name : str Name of caller (i.e., "singlepanelplot" or "multipanelplot") inputArgs : dict Input arguments of caller (i.e., :meth:`.singlepanelplot` or :meth:`.multipanelplot`) collected in dictionary - + Returns ------- dimArrs : tuple - Tuple containing (in this order) `trList`, list of (selected) + Tuple containing (in this order) `trList`, list of (selected) trials to visualize and `chArr`, 1D :class:`numpy.ndarray` of channel specifiers - based on provided user selection. Note that `"all"` and `None` selections - are converted to arrays ready for indexing. + based on provided user selection. Note that `"all"` and `None` selections + are converted to arrays ready for indexing. dimCounts : tuple Tuple holding sizes of corresponding selection arrays comprised - in `dimArrs`. Elements are `nTrials`, number of (selected) trials and `nChan`, - number of (selected) channels. + in `dimArrs`. Elements are `nTrials`, number of (selected) trials and `nChan`, + number of (selected) channels. idx : list - Three element indexing list (respecting non-default `dimord`s) intended - for use with trial-array data. + Three element indexing list (respecting non-default `dimord`s) intended + for use with trial-array data. timeIdx : int - Position of time-axis within indexing list `idx` (either 0 or 1). + Position of time-axis within indexing list `idx` (either 0 or 1). chanIdx : int - Position of channel-axis within indexing list `idx` (either 0 or 1). - + Position of channel-axis within indexing list `idx` (either 0 or 1). + Notes ----- This is an auxiliary method that is intended purely for internal use. Please refer to the user-exposed methods :func:`~syncopy.singlepanelplot` and/or - :func:`~syncopy.multipanelplot` to actually generate plots of Syncopy data objects. - + :func:`~syncopy.multipanelplot` to actually generate plots of Syncopy data objects. + See also -------- :meth:`syncopy.plotting.spy_plotting._prep_plots` : General basic input parsing for all Syncopy plotting routines """ - + # Basic sanity checks for all plotting routines w/any Syncopy object _prep_plots(self, name, **inputArgs) - + # Ensure our binary flags are actually binary if not isinstance(inputArgs["avg_channels"], bool): raise SPYTypeError(inputArgs["avg_channels"], varname="avg_channels", expected="bool") if not isinstance(inputArgs.get("avg_trials", True), bool): raise SPYTypeError(inputArgs["avg_trials"], varname="avg_trials", expected="bool") - # Pass provided selections on to `Selector` class which performs error + # Pass provided selections on to `Selector` class which performs error # checking and generates required indexing arrays - self._selection = {"trials": inputArgs["trials"], + self.selection = {"trials": inputArgs["trials"], "channels": inputArgs["channels"], "toilim": inputArgs["toilim"]} @@ -656,10 +656,10 @@ def _prep_analog_plots(self, name, **inputArgs): lgl = "`trials` to be not `None` to perform timing selection" act = "`toilim` was provided but `trials` is `None`" raise SPYValueError(legal=lgl, varname="trials/toilim", actual=act) - else: - trList = self._selection.trials + else: + trList = self.selection.trials nTrials = len(trList) - chArr = self.channel[self._selection.channel] + chArr = self.channel[self.selection.channel] nChan = chArr.size # Collect arrays and counts in tuples @@ -670,6 +670,6 @@ def _prep_analog_plots(self, name, **inputArgs): idx = [slice(None), slice(None)] chanIdx = self.dimord.index("channel") timeIdx = self.dimord.index("time") - idx[chanIdx] = self._selection.channel + idx[chanIdx] = self.selection.channel return dimArrs, dimCounts, idx, timeIdx, chanIdx diff --git a/syncopy/plotting/_plot_spectral.py b/syncopy/plotting/_plot_spectral.py index aa0cd477f..6cd6184aa 100644 --- a/syncopy/plotting/_plot_spectral.py +++ b/syncopy/plotting/_plot_spectral.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -# +# # Outsourced plotting class methods from respective parent classes -# +# # Builtin/3rd party package imports import os @@ -10,7 +10,7 @@ # Local imports from syncopy.shared.errors import SPYValueError, SPYError, SPYTypeError, SPYWarning from syncopy.shared.parsers import scalar_parser -from syncopy.plotting.spy_plotting import (pltErrMsg, pltConfig, _prep_toilim_avg, +from syncopy.plotting.spy_plotting import (pltErrMsg, pltConfig, _prep_toilim_avg, _setup_figure, _setup_colorbar, _prep_plots) # Conditional matplotlib import @@ -24,57 +24,57 @@ __all__ = [] -def singlepanelplot(self, trials="all", channels="all", tapers="all", - toilim=None, foilim=None, avg_channels=False, avg_tapers=True, - interp="spline36", cmap="plasma", vmin=None, vmax=None, +def singlepanelplot(self, trials="all", channels="all", tapers="all", + toilim=None, foilim=None, avg_channels=False, avg_tapers=True, + interp="spline36", cmap="plasma", vmin=None, vmax=None, title=None, grid=None, fig=None, **kwargs): """ Plot contents of :class:`~syncopy.SpectralData` objects using single-panel figure(s) - - Please refer to :func:`syncopy.singlepanelplot` for detailed usage information. - + + Please refer to :func:`syncopy.singlepanelplot` for detailed usage information. + Examples -------- - Show frequency range 30-80 Hz of channel `'ecog_mua2'` averaged across + Show frequency range 30-80 Hz of channel `'ecog_mua2'` averaged across trials 2, 4, and 6: - + >>> fig = spy.singlepanelplot(freqData, trials=[2, 4, 6], channels=["ecog_mua2"], foilim=[30, 80]) - + Overlay channel `'ecog_mua3'` with same settings: - + >>> fig2 = spy.singlepanelplot(freqData, trials=[2, 4, 6], channels=['ecog_mua3'], foilim=[30, 80], fig=fig) - - Plot time-frequency contents of channel `'ecog_mua1'` present in both objects - `tfData1` and `tfData2` using the 'viridis' colormap, a plot grid, manually + + Plot time-frequency contents of channel `'ecog_mua1'` present in both objects + `tfData1` and `tfData2` using the 'viridis' colormap, a plot grid, manually defined lower and upper color value limits and no interpolation - + >>> fig1, fig2 = spy.singlepanelplot(tfData1, tfData2, channels=['ecog_mua1'], - cmap="viridis", vmin=0.25, vmax=0.95, + cmap="viridis", vmin=0.25, vmax=0.95, interp=None, grid=True, overlay=False) - - Note that overlay plotting is **not** supported for time-frequency objects. - + + Note that overlay plotting is **not** supported for time-frequency objects. + See also -------- syncopy.singlepanelplot : visualize Syncopy data objects using single-panel plots """ - + # Collect input arguments in dict `inputArgs` and process them inputArgs = locals() inputArgs.pop("self") - (dimArrs, - dimCounts, - isTimeFrequency, - complexConversion, - pltDtype, + (dimArrs, + dimCounts, + isTimeFrequency, + complexConversion, + pltDtype, dataLbl) = _prep_spectral_plots(self, "singlepanelplot", **inputArgs) (nTrials, nChan, nFreq, nTap) = dimCounts (trList, chArr, freqArr, tpArr) = dimArrs - - # If we're overlaying, ensure data and plot type match up - if hasattr(fig, "objCount"): + + # If we're overlaying, ensure data and plot type match up + if hasattr(fig, "objCount"): if isTimeFrequency: msg = "Overlay plotting not supported for time-frequency data" raise SPYError(msg) @@ -89,16 +89,16 @@ def singlepanelplot(self, trials="all", channels="all", tapers="all", # No time-frequency shenanigans: this is a simple power-spectrum (line-plot) if not isTimeFrequency: - + # Generic titles for figures overlayTitle = "Overlay of {} datasets" - + # Either create new figure or fetch existing if fig is None: fig, ax = _setup_figure(1, xLabel="Frequency (Hz)", yLabel=dataLbl, grid=grid) fig.spectralPlot = True else: - ax, = fig.get_axes() + ax, = fig.get_axes() # Average across channels, tapers or both using local helper func nTime = 1 @@ -108,21 +108,21 @@ def singlepanelplot(self, trials="all", channels="all", tapers="all", return if avg_channels and not avg_tapers: panelTitle = "{} tapers averaged across {} channels and {} trials".format(nTap, nChan, nTrials) - pltArr = _compute_pltArr(self, nFreq, nTap, nTime, complexConversion, pltDtype, + pltArr = _compute_pltArr(self, nFreq, nTap, nTime, complexConversion, pltDtype, avg1="channel") if avg_tapers and not avg_channels: panelTitle = "{} channels averaged across {} tapers and {} trials".format(nChan, nTap, nTrials) - pltArr = _compute_pltArr(self, nFreq, nChan, nTime, complexConversion, pltDtype, + pltArr = _compute_pltArr(self, nFreq, nChan, nTime, complexConversion, pltDtype, avg1="taper") if avg_tapers and avg_channels: panelTitle = "Average of {} channels, {} tapers and {} trials".format(nChan, nTap, nTrials) - pltArr = _compute_pltArr(self, nFreq, 1, nTime, complexConversion, pltDtype, + pltArr = _compute_pltArr(self, nFreq, 1, nTime, complexConversion, pltDtype, avg1="taper", avg2="channel") # Perform the actual plotting ax.plot(freqArr, np.log10(pltArr), label=os.path.basename(self.filename)) ax.set_xlim([freqArr[0], freqArr[-1]]) - + # Set plot title depending on dataset overlay if fig.objCount == 0: if title is None: @@ -134,31 +134,31 @@ def singlepanelplot(self, trials="all", channels="all", tapers="all", if title is None: title = overlayTitle.format(len(handles)) # ax.set_title(title, size=pltConfig["singleTitleSize"]) - + else: - - # For a single-panel TF visualization, we need to average across both tapers + channels + + # For a single-panel TF visualization, we need to average across both tapers + channels if not avg_channels and (not avg_tapers and nTap > 1): msg = "Single-panel time-frequency visualization requires averaging " +\ "across both tapers and channels" SPYWarning(msg) return - + # Compute (and verify) length of selected time intervals and assemble array for plotting panelTitle = "Average of {} channels, {} tapers and {} trials".format(nChan, nTap, nTrials) tLengths = _prep_toilim_avg(self) nTime = tLengths[0] - pltArr = _compute_pltArr(self, nFreq, 1, nTime, complexConversion, pltDtype, + pltArr = _compute_pltArr(self, nFreq, 1, nTime, complexConversion, pltDtype, avg1="taper", avg2="channel") # Prepare figure - fig, ax, cax = _setup_figure(1, xLabel="Time (s)", yLabel="Frequency (Hz)", + fig, ax, cax = _setup_figure(1, xLabel="Time (s)", yLabel="Frequency (Hz)", include_colorbar=True, grid=grid) fig.spectralPlot = True - + # Use `imshow` to render array as image - time = self.time[trList[0]][self._selection.time[0]] - ax.imshow(pltArr, origin="lower", interpolation=interp, + time = self.time[trList[0]][self.selection.time[0]] + ax.imshow(pltArr, origin="lower", interpolation=interp, cmap=cmap, vmin=vmin, vmax=vmax, extent=(time[0], time[-1], freqArr[0], freqArr[-1]), aspect="auto") cbar = _setup_colorbar(fig, ax, cax, label=dataLbl.replace(" (dB)", "")) @@ -169,7 +169,7 @@ def singlepanelplot(self, trials="all", channels="all", tapers="all", # Increment overlay-counter and draw figure fig.objCount += 1 plt.draw() - self._selection = None + self.selection = None return fig @@ -179,31 +179,31 @@ def multipanelplot(self, trials="all", channels="all", tapers="all", toilim=None title=None, grid=None, fig=None, **kwargs): """ Plot contents of :class:`~syncopy.SpectralData` objects using multi-panel figure(s) - - Please refer to :func:`syncopy.multipanelplot` for detailed usage information. - + + Please refer to :func:`syncopy.multipanelplot` for detailed usage information. + Examples -------- - Use 16 panels to show frequency range 30-80 Hz of first 16 channels in `freqData` + Use 16 panels to show frequency range 30-80 Hz of first 16 channels in `freqData` averaged across trials 2, 4, and 6: - + >>> fig = spy.multipanelplot(freqData, trials=[2, 4, 6], channels=range(16), foilim=[30, 80], panels="channels") - + Same settings, but each panel represents a trial: - + >>> fig = spy.multipanelplot(freqData, trials=[2, 4, 6], channels=range(16), - foilim=[30, 80], panels="trials", avg_trials=False, + foilim=[30, 80], panels="trials", avg_trials=False, avg_channels=True) - - Plot time-frequency contents of channels `'ecog_mua1'` and `'ecog_mua2'` of - `tfData` - + + Plot time-frequency contents of channels `'ecog_mua1'` and `'ecog_mua2'` of + `tfData` + >>> fig = spy.multipanelplot(tfData, channels=['ecog_mua1', 'ecog_mua2']) - - Note that multi-panel overlay plotting is **not** supported for + + Note that multi-panel overlay plotting is **not** supported for :class:`~syncopy.SpectralData` objects. - + See also -------- syncopy.multipanelplot : visualize Syncopy data objects using multi-panel plots @@ -212,21 +212,21 @@ def multipanelplot(self, trials="all", channels="all", tapers="all", toilim=None # Collect input arguments in dict `inputArgs` and process them inputArgs = locals() inputArgs.pop("self") - (dimArrs, - dimCounts, - isTimeFrequency, - complexConversion, - pltDtype, + (dimArrs, + dimCounts, + isTimeFrequency, + complexConversion, + pltDtype, dataLbl) = _prep_spectral_plots(self, "multipanelplot", **inputArgs) (nTrials, nChan, nFreq, nTap) = dimCounts (trList, chArr, freqArr, tpArr) = dimArrs # No overlaying here... - if hasattr(fig, "objCount"): + if hasattr(fig, "objCount"): msg = "Overlays of multi-panel `SpectralData` plots not supported" raise SPYError(msg) - - # Ensure panel-specification makes sense and is compatible w/averaging selection + + # Ensure panel-specification makes sense and is compatible w/averaging selection if not isinstance(panels, str): raise SPYTypeError(panels, varname="panels", expected="str") if panels not in availablePanels: @@ -256,7 +256,7 @@ def multipanelplot(self, trials="all", channels="all", tapers="all", toilim=None "two out of three dimensions (tapers, channels trials)" SPYWarning(msg) return - + # Prepare figure (same for all cases) if panels == "channels": npanels = nChan @@ -264,7 +264,7 @@ def multipanelplot(self, trials="all", channels="all", tapers="all", toilim=None npanels = nTrials else: # ``panels == "tapers"`` npanels = nTap - + # Construct subplot panel layout or vet provided layout nrow = kwargs.get("nrow", None) ncol = kwargs.get("ncol", None) @@ -272,15 +272,15 @@ def multipanelplot(self, trials="all", channels="all", tapers="all", toilim=None fig, ax_arr = _setup_figure(npanels, nrow=nrow, ncol=ncol, xLabel="Frequency (Hz)", yLabel=dataLbl, grid=grid, - include_colorbar=False, + include_colorbar=False, sharex=True, sharey=True) else: fig, ax_arr, cax = _setup_figure(npanels, nrow=nrow, ncol=ncol, xLabel="Time (s)", yLabel="Frequency (Hz)", grid=grid, - include_colorbar=True, + include_colorbar=True, sharex=True, sharey=True) - + # Monkey-patch object-counter to newly created figure fig.spectralPlot = True @@ -291,14 +291,14 @@ def multipanelplot(self, trials="all", channels="all", tapers="all", toilim=None nTime = 1 N = 1 - # For each panel stratification, set corresponding positional and - # keyword args for iteratively calling `_compute_pltArr` + # For each panel stratification, set corresponding positional and + # keyword args for iteratively calling `_compute_pltArr` if panels == "channels": - + panelVar = "channel" panelValues = chArr panelTitles = chArr - + if not avg_trials and avg_tapers: avgDim1 = "taper" avgDim2 = None @@ -320,13 +320,13 @@ def multipanelplot(self, trials="all", channels="all", tapers="all", toilim=None innerValues = ["all"] majorTitle = " Average of {} tapers and {} trials".format(nTap, nTrials) showLegend = False - + elif panels == "trials": - + panelVar = "trial" panelValues = trList panelTitles = ["Trial #{}".format(trlno) for trlno in trList] - + if not avg_channels and avg_tapers: avgDim1 = "taper" avgDim2 = None @@ -348,13 +348,13 @@ def multipanelplot(self, trials="all", channels="all", tapers="all", toilim=None innerValues = ["all"] majorTitle = " Average of {} channels and {} tapers".format(nChan, nTap) showLegend = False - + else: # panels = "tapers" - + panelVar = "taper" panelValues = tpArr panelTitles = ["Taper #{}".format(tpno) for tpno in tpArr] - + if not avg_trials and avg_channels: avgDim1 = "channel" avgDim2 = None @@ -378,14 +378,14 @@ def multipanelplot(self, trials="all", channels="all", tapers="all", toilim=None showLegend = False # Loop over panels, within each panel, loop over `innerValues` to (potentially) - # plot multiple spectra per panel + # plot multiple spectra per panel kwargs = {"avg1": avgDim1, "avg2": avgDim2} for panelCount, panelVal in enumerate(panelValues): kwargs[panelVar] = panelVal for innerVal in innerValues: kwargs[innerVar] = innerVal pltArr = _compute_pltArr(self, nFreq, N, nTime, complexConversion, pltDtype, **kwargs) - ax_arr[panelCount].plot(freqArr, np.log10(pltArr), + ax_arr[panelCount].plot(freqArr, np.log10(pltArr), label=innerVar.capitalize() + " " + str(innerVal)) ax_arr[panelCount].set_title(panelTitles[panelCount], size=pltConfig["multiTitleSize"]) if showLegend: @@ -396,13 +396,13 @@ def multipanelplot(self, trials="all", channels="all", tapers="all", toilim=None # Now, multi-panel time-frequency visualizations else: - + # Compute (and verify) length of selected time intervals tLengths = _prep_toilim_avg(self) nTime = tLengths[0] - time = self.time[trList[0]][self._selection.time[0]] + time = self.time[trList[0]][self.selection.time[0]] N = 1 - + if panels == "channels": panelVar = "channel" panelValues = chArr @@ -410,7 +410,7 @@ def multipanelplot(self, trials="all", channels="all", tapers="all", toilim=None majorTitle = " Average of {} tapers and {} trials".format(nTap, nTrials) avgDim1 = "taper" avgDim2 = None - + elif panels == "trials": panelVar = "trial" panelValues = trList @@ -428,7 +428,7 @@ def multipanelplot(self, trials="all", channels="all", tapers="all", toilim=None avgDim2 = None # Loop over panels, within each panel, loop over `innerValues` to (potentially) - # plot multiple spectra per panel + # plot multiple spectra per panel kwargs = {"avg1": avgDim1, "avg2": avgDim2} vmins = [] vmaxs = [] @@ -437,8 +437,8 @@ def multipanelplot(self, trials="all", channels="all", tapers="all", toilim=None pltArr = _compute_pltArr(self, nFreq, N, nTime, complexConversion, pltDtype, **kwargs) vmins.append(pltArr.min()) vmaxs.append(pltArr.max()) - ax_arr[panelCount].imshow(pltArr, origin="lower", interpolation=interp, cmap=cmap, - extent=(time[0], time[-1], freqArr[0], freqArr[-1]), + ax_arr[panelCount].imshow(pltArr, origin="lower", interpolation=interp, cmap=cmap, + extent=(time[0], time[-1], freqArr[0], freqArr[-1]), aspect="auto") ax_arr[panelCount].set_title(panelTitles[panelCount], size=pltConfig["multiTitleSize"]) @@ -447,7 +447,7 @@ def multipanelplot(self, trials="all", channels="all", tapers="all", toilim=None vmin = min(vmins) if vmax is None: vmax = max(vmaxs) - cbar = _setup_colorbar(fig, ax_arr, cax, label=dataLbl.replace(" (dB)", ""), + cbar = _setup_colorbar(fig, ax_arr, cax, label=dataLbl.replace(" (dB)", ""), outline=False, vmin=vmin, vmax=vmax) if title is None: fig.suptitle(majorTitle, size=pltConfig["singleTitleSize"]) @@ -455,52 +455,52 @@ def multipanelplot(self, trials="all", channels="all", tapers="all", toilim=None # Increment overlay-counter and draw figure fig.objCount += 1 plt.draw() - self._selection = None + self.selection = None return fig - + def _compute_pltArr(self, nFreq, N, nTime, complexConversion, pltDtype, - avg1="channel", avg2=None, trial="all", channel="all", + avg1="channel", avg2=None, trial="all", channel="all", freq="all", taper="all"): """ Local helper that extracts/averages data from :class:`~syncopy.SpectralData` object - + Parameters ---------- self : :class:`~syncopy.SpectralData` object - Syncopy :class:`~syncopy.SpectralData` object that is being processed by + Syncopy :class:`~syncopy.SpectralData` object that is being processed by the respective :meth:`.singlepanelplot` or :meth:`.multipanelplot` class methods - defined in this module. + defined in this module. nFreq : int Number of frequencies of interest N : int - Size of free dimension post averaging. Depending on `avg1` and `avg2` + Size of free dimension post averaging. Depending on `avg1` and `avg2` can be either `nChan`, `nTap` or 1 nTime : int Number of time-points of interest. If object does not contain time-frequency data, `nTime` has to be 1 complexConversion : callable - Automatically set by :meth:`~syncopy.plotting._plot_spectral._prep_spectral_plots` - to (potentially) convert complex Fourier coefficients to float. + Automatically set by :meth:`~syncopy.plotting._plot_spectral._prep_spectral_plots` + to (potentially) convert complex Fourier coefficients to float. pltDtype : str or :class:`numpy.dtype` - Automatically set by :meth:`~syncopy.plotting._plot_spectral._prep_spectral_plots`: - numeric type of (potentially converted) complex Fourier coefficients. + Automatically set by :meth:`~syncopy.plotting._plot_spectral._prep_spectral_plots`: + numeric type of (potentially converted) complex Fourier coefficients. avg1 : str or None - First dimension for averaging. If `None`, no mean-value is computed. Otherwise, - `avg1` can be either `"channel"` or `"taper"`. + First dimension for averaging. If `None`, no mean-value is computed. Otherwise, + `avg1` can be either `"channel"` or `"taper"`. avg2 : str or None - Second dimension for averaging. If `None`, no mean-value is computed. Otherwise, - `avg2` can be either `"channel"` or `"taper"`. + Second dimension for averaging. If `None`, no mean-value is computed. Otherwise, + `avg2` can be either `"channel"` or `"taper"`. trial : str or list - Either list of trial indices or `"all"`; set by + Either list of trial indices or `"all"`; set by :meth:`~syncopy.plotting._plot_spectral._prep_spectral_plots` channel : str or :class:`numpy.ndarray` - Either array of channel specifiers or `"all"`; set by + Either array of channel specifiers or `"all"`; set by :meth:`~syncopy.plotting._plot_spectral._prep_spectral_plots` freq : str or :class:`numpy.ndarray` - Either array of frequency specifiers or `"all"`; set by + Either array of frequency specifiers or `"all"`; set by :meth:`~syncopy.plotting._plot_spectral._prep_spectral_plots` taper : str or :class:`numpy.ndarray` - Either array of taper specifiers or `"all"`; set by + Either array of taper specifiers or `"all"`; set by :meth:`~syncopy.plotting._plot_spectral._prep_spectral_plots` Returns @@ -510,44 +510,44 @@ def _compute_pltArr(self, nFreq, N, nTime, complexConversion, pltDtype, input object contains time-frequency data, `pltArr` is a three-dimensional array of shape ``(nFreq, nTime, N)``, otherwise `pltArr` is two-dimensional with shape ``(nFreq, N)`` for ``N > 1``, or a one-dimensional ``(nFreq,)`` - array if ``N = 1``. + array if ``N = 1``. Notes ----- This is an auxiliary method that is intended purely for internal use. Please refer to the user-exposed methods :func:`~syncopy.singlepanelplot` and/or - :func:`~syncopy.multipanelplot` to actually generate plots of Syncopy data objects. + :func:`~syncopy.multipanelplot` to actually generate plots of Syncopy data objects. """ - + # Prepare indexing list respecting potential non-default `dimord`s idx = [slice(None), slice(None), slice(None), slice(None)] timeIdx = self.dimord.index("time") chanIdx = self.dimord.index("channel") freqIdx = self.dimord.index("freq") taperIdx = self.dimord.index("taper") - + if trial == "all": - trList = self._selection.trials + trList = self.selection.trials else: trList = [trial] nTrls = len(trList) - useFancy = self._selection._useFancy + useFancy = self.selection._useFancy if channel == "all": - idx[chanIdx] = self._selection.channel + idx[chanIdx] = self.selection.channel else: idx[chanIdx] = np.where(self.channel == channel)[0] useFancy = True - if freq == "all": - idx[freqIdx] = self._selection.freq + if freq == "all": + idx[freqIdx] = self.selection.freq else: idx[freqIdx] = np.where(self.freq == freq)[0] useFancy = True if taper == "all": - idx[taperIdx] = self._selection.taper + idx[taperIdx] = self.selection.taper else: idx[taperIdx] = [taper] useFancy = True - + if nTime == 1: pltArr = np.zeros((nFreq, N), dtype=pltDtype).squeeze() # `squeeze` in case `N = 1` else: @@ -555,7 +555,7 @@ def _compute_pltArr(self, nFreq, N, nTime, complexConversion, pltDtype, for tk, trlno in enumerate(trList): trlArr = complexConversion(self._get_trial(trlno)) - idx[timeIdx] = self._selection.time[tk] + idx[timeIdx] = self.selection.time[tk] if not useFancy: trlArr = trlArr[tuple(idx)] else: @@ -571,58 +571,58 @@ def _compute_pltArr(self, nFreq, N, nTime, complexConversion, pltDtype, def _prep_spectral_plots(self, name, **inputArgs): """ Local helper that performs sanity checks and sets up data selection - + Parameters ---------- self : :class:`~syncopy.SpectralData` object - Syncopy :class:`~syncopy.SpectralData` object that is being processed by + Syncopy :class:`~syncopy.SpectralData` object that is being processed by the respective :meth:`.singlepanelplot` or :meth:`.multipanelplot` class methods - defined in this module. + defined in this module. name : str Name of caller (i.e., "singlepanelplot" or "multipanelplot") inputArgs : dict Input arguments of caller (i.e., :meth:`.singlepanelplot` or :meth:`.multipanelplot`) collected in dictionary - + Returns ------- dimArrs : tuple - Four-element tuple containing (in this order): `trList`, list of (selected) + Four-element tuple containing (in this order): `trList`, list of (selected) trials to visualize, `chArr`, 1D :class:`numpy.ndarray` of channel specifiers - based on provided user selection, `freqArr`, 1D :class:`numpy.ndarray` of - frequency specifiers based on provided user selection, `tpArr`, - 1D :class:`numpy.ndarray` of taper specifiers based on provided user selection. + based on provided user selection, `freqArr`, 1D :class:`numpy.ndarray` of + frequency specifiers based on provided user selection, `tpArr`, + 1D :class:`numpy.ndarray` of taper specifiers based on provided user selection. Note that `"all"` and `None` selections are converted to arrays ready for - indexing. + indexing. dimCounts : tuple Four-element tuple holding sizes of corresponding selection arrays comprised - in `dimArrs`. Elements are (in this order): number of (selected) trials - `nTrials`, number of (selected) channels `nChan`, number of (selected) - frequencies `nFreq`, number of (selected) tapers `nTap`. + in `dimArrs`. Elements are (in this order): number of (selected) trials + `nTrials`, number of (selected) channels `nChan`, number of (selected) + frequencies `nFreq`, number of (selected) tapers `nTap`. isTimeFrequency : bool If `True`, input object contains time-frequency data, `False` otherwise complexConversion : callable - Lambda function that performs complex-to-float conversion of Fourier - coefficients (if necessary). + Lambda function that performs complex-to-float conversion of Fourier + coefficients (if necessary). pltDtype : str or :class:`numpy.dtype` - Numeric type of (potentially converted) complex Fourier coefficients. + Numeric type of (potentially converted) complex Fourier coefficients. dataLbl : str - Caption for y-axis or colorbar (depending on value of `isTimeFrequency`). - + Caption for y-axis or colorbar (depending on value of `isTimeFrequency`). + Notes ----- This is an auxiliary method that is intended purely for internal use. Please refer to the user-exposed methods :func:`~syncopy.singlepanelplot` and/or - :func:`~syncopy.multipanelplot` to actually generate plots of Syncopy data objects. - + :func:`~syncopy.multipanelplot` to actually generate plots of Syncopy data objects. + See also -------- :meth:`syncopy.plotting.spy_plotting._prep_plots` : General basic input parsing for all Syncopy plotting routines """ - + # Basic sanity checks for all plotting routines w/any Syncopy object _prep_plots(self, name, **inputArgs) - + # Ensure our binary flags are actually binary if not isinstance(inputArgs["avg_channels"], bool): raise SPYTypeError(inputArgs["avg_channels"], varname="avg_channels", expected="bool") @@ -630,15 +630,15 @@ def _prep_spectral_plots(self, name, **inputArgs): raise SPYTypeError(inputArgs["avg_tapers"], varname="avg_tapers", expected="bool") if not isinstance(inputArgs.get("avg_trials", True), bool): raise SPYTypeError(inputArgs["avg_trials"], varname="avg_trials", expected="bool") - - # Pass provided selections on to `Selector` class which performs error + + # Pass provided selections on to `Selector` class which performs error # checking and generates required indexing arrays - self._selection = {"trials": inputArgs["trials"], - "channels": inputArgs["channels"], + self.selection = {"trials": inputArgs["trials"], + "channels": inputArgs["channels"], "tapers": inputArgs["tapers"], "toilim": inputArgs["toilim"], "foilim": inputArgs["foilim"]} - + # Ensure any optional keywords controlling plotting appearance make sense if inputArgs["title"] is not None: if not isinstance(inputArgs["title"], str): @@ -648,13 +648,13 @@ def _prep_spectral_plots(self, name, **inputArgs): raise SPYTypeError(inputArgs["grid"], varname="grid", expected="bool") # Get trial/channel/taper count and collect quantities in tuple - trList = self._selection.trials + trList = self.selection.trials nTrials = len(trList) - chArr = self.channel[self._selection.channel] + chArr = self.channel[self.selection.channel] nChan = chArr.size - freqArr = self.freq[self._selection.freq] + freqArr = self.freq[self.selection.freq] nFreq = freqArr.size - tpArr = np.arange(self.taper.size)[self._selection.taper] + tpArr = np.arange(self.taper.size)[self.selection.taper] nTap = tpArr.size dimCounts = (nTrials, nChan, nFreq, nTap) dimArrs = (trList, chArr, freqArr, tpArr) @@ -663,20 +663,20 @@ def _prep_spectral_plots(self, name, **inputArgs): isTimeFrequency = False if any([t.size > 1 for t in self.time]): isTimeFrequency = True - + # Ensure provided min/max range for plotting TF data makes sense vminmax = False if inputArgs.get("vmin", None) is not None: try: scalar_parser(inputArgs["vmin"], varname="vmin") except Exception as exc: - raise exc + raise exc vminmax = True if inputArgs.get("vmax", None) is not None: try: scalar_parser(inputArgs["vmax"], varname="vmax") except Exception as exc: - raise exc + raise exc vminmax = True if inputArgs.get("vmin", None) and inputArgs.get("vmax", None): if inputArgs["vmin"] >= inputArgs["vmax"]: @@ -686,8 +686,8 @@ def _prep_spectral_plots(self, name, **inputArgs): if vminmax and not isTimeFrequency: msg = "`vmin` and `vmax` is only used for time-frequency visualizations" SPYWarning(msg) - - # Check for complex entries in data and set datatype for plotting arrays + + # Check for complex entries in data and set datatype for plotting arrays # constructed below (always use floats w/same precision as data) if "complex" in self.data.dtype.name: msg = "Found complex Fourier coefficients - visualization will use absolute values." @@ -699,5 +699,5 @@ def _prep_spectral_plots(self, name, **inputArgs): complexConversion = lambda x: x pltDtype = self.data.dtype dataLbl = "Power (dB)" - + return dimArrs, dimCounts, isTimeFrequency, complexConversion, pltDtype, dataLbl diff --git a/syncopy/plotting/spy_plotting.py b/syncopy/plotting/spy_plotting.py index d58c2fa69..105c8fc76 100644 --- a/syncopy/plotting/spy_plotting.py +++ b/syncopy/plotting/spy_plotting.py @@ -561,8 +561,8 @@ def _prep_toilim_avg(self): :func:`~syncopy.multipanelplot` : visualize Syncopy objects using multi-panel figure(s) """ - tLengths = np.zeros((len(self._selection.trials),), dtype=np.intp) - for k, tsel in enumerate(self._selection.time): + tLengths = np.zeros((len(self.selection.trials),), dtype=np.intp) + for k, tsel in enumerate(self.selection.time): if not isinstance(tsel, slice): msg = "Cannot average `toilim` selection. Please check `.time` property for consistency. " raise SPYError(msg) @@ -570,7 +570,7 @@ def _prep_toilim_avg(self): if start is None: start = 0 if stop is None: - stop = self._get_time([self._selection.trials[k]], + stop = self._get_time([self.selection.trials[k]], toilim=[-np.inf, np.inf])[0].stop tLengths[k] = stop - start diff --git a/syncopy/shared/computational_routine.py b/syncopy/shared/computational_routine.py index a85cd4994..c65eadccf 100644 --- a/syncopy/shared/computational_routine.py +++ b/syncopy/shared/computational_routine.py @@ -142,7 +142,7 @@ def __init__(self, *argv, **kwargs): # list of dicts encoding header info of raw binary input files (experimental!) self.hdr = None - # list of trial numbers to process (either `data.trials` or `data._selection.trials`) + # list of trial numbers to process (either `data.trials` or `data.selection.trials`) self.trialList = None # number of trials to process (shortcut for `len(self.trialList)`) @@ -269,9 +269,9 @@ def initialize(self, data, out_stackingdim, chan_per_worker=None, keeptrials=Tru # Determine if data-selection was provided; if so, extract trials and check # whether selection requires fancy array indexing - if data._selection is not None: - self.trialList = data._selection.trials - self.useFancyIdx = data._selection._useFancy + if data.selection is not None: + self.trialList = data.selection.trials + self.useFancyIdx = data.selection._useFancy else: self.trialList = list(range(len(data.trials))) self.useFancyIdx = False @@ -328,8 +328,8 @@ def initialize(self, data, out_stackingdim, chan_per_worker=None, keeptrials=Tru msg = "trial-averaging does not support channel-block parallelization!" SPYWarning(msg) chan_per_worker = None - if data._selection is not None: - if chan_per_worker is not None and data._selection.channel != slice(None, None, 1): + if data.selection is not None: + if chan_per_worker is not None and data.selection.channel != slice(None, None, 1): msg = "channel selection and simultaneous channel-block " +\ "parallelization not yet supported!" SPYWarning(msg) diff --git a/syncopy/shared/kwarg_decorators.py b/syncopy/shared/kwarg_decorators.py index 882a1a350..87e8ab988 100644 --- a/syncopy/shared/kwarg_decorators.py +++ b/syncopy/shared/kwarg_decorators.py @@ -352,19 +352,19 @@ def wrapper_select(*args, **kwargs): # Either extract `select` from input kws and cycle through positional # argument to apply in-place selection to all Syncopy objects, or clean - # any unintended leftovers in `_selection` if no `select` keyword was provided + # any unintended leftovers in `selection` if no `select` keyword was provided select = kwargs.get("select", None) for obj in args: - if hasattr(obj, "_selection"): - obj._selection = select + if hasattr(obj, "selection"): + obj.selection = select # Call function with modified data object(s) res = func(*args, **kwargs) # Wipe data-selection slot to not alter user objects for obj in args: - if hasattr(obj, "_selection"): - obj._selection = None + if hasattr(obj, "selection"): + obj.selection = None return res diff --git a/syncopy/specest/compRoutines.py b/syncopy/specest/compRoutines.py index 3271155e4..cd770ffac 100644 --- a/syncopy/specest/compRoutines.py +++ b/syncopy/specest/compRoutines.py @@ -190,9 +190,9 @@ class MultiTaperFFT(ComputationalRoutine): def process_metadata(self, data, out): # Some index gymnastics to get trial begin/end "samples" - if data._selection is not None: - chanSec = data._selection.channel - trl = data._selection.trialdefinition + if data.selection is not None: + chanSec = data.selection.channel + trl = data.selection.trialdefinition for row in range(trl.shape[0]): trl[row, :2] = [row, row + 1] else: @@ -414,9 +414,9 @@ class MultiTaperFFTConvol(ComputationalRoutine): def process_metadata(self, data, out): # Get trialdef array + channels from source - if data._selection is not None: - chanSec = data._selection.channel - trl = data._selection.trialdefinition + if data.selection is not None: + chanSec = data.selection.channel + trl = data.selection.trialdefinition else: chanSec = slice(None) trl = data.trialdefinition @@ -584,9 +584,9 @@ class WaveletTransform(ComputationalRoutine): def process_metadata(self, data, out): # Get trialdef array + channels from source - if data._selection is not None: - chanSec = data._selection.channel - trl = data._selection.trialdefinition + if data.selection is not None: + chanSec = data.selection.channel + trl = data.selection.trialdefinition else: chanSec = slice(None) trl = data.trialdefinition @@ -748,9 +748,9 @@ class SuperletTransform(ComputationalRoutine): def process_metadata(self, data, out): # Get trialdef array + channels from source - if data._selection is not None: - chanSec = data._selection.channel - trl = data._selection.trialdefinition + if data.selection is not None: + chanSec = data.selection.channel + trl = data.selection.trialdefinition else: chanSec = slice(None) trl = data.trialdefinition diff --git a/syncopy/specest/freqanalysis.py b/syncopy/specest/freqanalysis.py index 32847b381..fdbd6770b 100644 --- a/syncopy/specest/freqanalysis.py +++ b/syncopy/specest/freqanalysis.py @@ -306,9 +306,9 @@ def freqanalysis(data, method='mtmfft', output='pow', # If only a subset of `data` is to be processed, make some necessary adjustments # of the sampleinfo and trial lengths - if data._selection is not None: - sinfo = data._selection.trialdefinition[:, :2] - trialList = data._selection.trials + if data.selection is not None: + sinfo = data.selection.trialdefinition[:, :2] + trialList = data.selection.trials else: trialList = list(range(len(data.trials))) sinfo = data.sampleinfo @@ -370,8 +370,8 @@ def freqanalysis(data, method='mtmfft', output='pow', # Get start/end timing info respecting potential in-place selection if toi is None: raise SPYTypeError(toi, varname="toi", expected="scalar or array-like or 'all'") - if data._selection is not None: - tStart = data._selection.trialdefinition[:, 2] / data.samplerate + if data.selection is not None: + tStart = data.selection.trialdefinition[:, 2] / data.samplerate else: tStart = data._t0 / data.samplerate tEnd = tStart + lenTrials / data.samplerate diff --git a/syncopy/tests/test_computationalroutine.py b/syncopy/tests/test_computationalroutine.py index 38b4744f7..9867da693 100644 --- a/syncopy/tests/test_computationalroutine.py +++ b/syncopy/tests/test_computationalroutine.py @@ -39,9 +39,9 @@ class LowPassFilter(ComputationalRoutine): computeFunction = staticmethod(lowpass) def process_metadata(self, data, out): - if data._selection is not None: - chanSec = data._selection.channel - trl = data._selection.trialdefinition + if data.selection is not None: + chanSec = data.selection.channel + trl = data.selection.trialdefinition else: chanSec = slice(None) trl = np.zeros((len(self.trialList), 3), dtype=int) diff --git a/syncopy/tests/test_continuousdata.py b/syncopy/tests/test_continuousdata.py index 9f77cbbd4..526af3d8b 100644 --- a/syncopy/tests/test_continuousdata.py +++ b/syncopy/tests/test_continuousdata.py @@ -95,7 +95,7 @@ def _base_op_tests(dummy, ymmud, dummy2, ymmud2, dummyC, operation): with pytest.raises(SPYValueError) as spyval: operation(dummy, dummy2) assert "Syncopy object with same number of trials (selected)" in str (spyval.value) - dummy2._selection = None + dummy2.selection = None # Scalar algebra must be commutative (except for pow) for operand in scalarOperands: @@ -178,10 +178,10 @@ def _selection_op_tests(dummy, ymmud, dummy2, ymmud2, kwdict, operation): selected.trials[tk])) # Very important: clear manually set selections for next iteration - dummy._selection = None - dummy2._selection = None - ymmud._selection = None - ymmud2._selection = None + dummy.selection = None + dummy2.selection = None + ymmud.selection = None + ymmud2.selection = None class TestAnalogData(): From 4fe378f399f4465408bfa188ac26c2644652f82f Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Fri, 4 Mar 2022 14:56:59 +0100 Subject: [PATCH 079/166] CHG: Updated CHANGELOG - modified CHANGELOG in preparation for upcoming release On branch fix_selectcsdata Changes to be committed: modified: CHANGELOG.md --- CHANGELOG.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74cf487fb..ccd96e009 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,21 @@ Bugfix release ### NEW - Added experimental loading functionality for NWB 2.0 files - Added experimental loading functionality for Matlab mat files +- Added support for "scalar" selections, i.e., things like `selectdata(trials=0)` + or `data.selectdata(channels='mychannel')` ### CHANGED +- Renamed `_selection` class property to `selection` - Made plotting routines matplotlib 3.5 compatible +- The output of `show` is now automatically squeezed (i.e., singleton dimensions + are removed from the returned array). ### REMOVED +- Do not parse scalars using `numbers.Number`, use `numpy.number` instead to + catch Boolean values +- Do not raise a `SPYTypeError` if an arithmetic operation is performed using + objects of different numerical types (real/complex; closes #199) + ### DEPRECATED - Removed loading code for ESI binary format that is no longer supported - Repaired top-level imports: renamed `connectivity` to `connectivityanalysis` @@ -24,13 +34,6 @@ Bugfix release - Inverted `selectdata` messaging policy: only actual on-disk copy operations trigger a `SPYInfo` message (closes #197) -### REMOVED -- Do not parse scalars using `numbers.Number`, use `numpy.number` instead to - catch Boolean values -- Do not raise a `SPYTypeError` if an arithmetic operation is performed using - objects of different numerical types (real/complex; closes #199) - -### DEPRECATED ### FIXED - The `trialdefinition` arrays constructed by the `Selector` class were incorrect for `SpectralData` objects without time-axis, resulting in "empty" trials. This From 5abfed488f159b70cb1db2e124dc24b848a824ba Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 4 Mar 2022 16:06:21 +0100 Subject: [PATCH 080/166] NEW: FIR filters for preprocessing - new firws.py backend for windowed sinc filter design and zerophase correction - FT voodoo magic for setting default firws order not (yet) implemented - kaiser window beta setting for FIR still missing On branch preprocessing Your branch is up to date with 'origin/preprocessing'. Changes to be committed: modified: syncopy/preproc/butterworthCR.py new file: syncopy/preproc/firws.py new file: syncopy/preproc/firwsCR.py modified: syncopy/preproc/preprocessing.py modified: syncopy/shared/input_processors.py modified: syncopy/tests/local_spy.py --- syncopy/preproc/butterworthCR.py | 147 -------------- syncopy/preproc/compRoutines.py | 307 +++++++++++++++++++++++++++++ syncopy/preproc/firws.py | 235 ++++++++++++++++++++++ syncopy/preproc/preprocessing.py | 107 +++++++--- syncopy/shared/input_processors.py | 8 +- syncopy/tests/local_spy.py | 1 - 6 files changed, 630 insertions(+), 175 deletions(-) delete mode 100644 syncopy/preproc/butterworthCR.py create mode 100644 syncopy/preproc/compRoutines.py create mode 100644 syncopy/preproc/firws.py diff --git a/syncopy/preproc/butterworthCR.py b/syncopy/preproc/butterworthCR.py deleted file mode 100644 index f1739dee9..000000000 --- a/syncopy/preproc/butterworthCR.py +++ /dev/null @@ -1,147 +0,0 @@ -# -*- coding: utf-8 -*- -# -# computeFunctions and -Routines for parallel calculation -# of IIR Filter operations with the Butterworth filter -# - -# Builtin/3rd party package imports -import numpy as np -import scipy.signal as sci -from inspect import signature - -# syncopy imports -from syncopy.shared.computational_routine import ComputationalRoutine -from syncopy.shared.kwarg_decorators import unwrap_io - - -@unwrap_io -def but_filtering_cF(dat, - samplerate=1, - filter_type='lp', - freq=None, - order=6, - direction='twopass', - polyremoval=None, - timeAxis=0, - noCompute=False, - chunkShape=None - ): - """ - Provides basic filtering of signals with IIR (Butterworth) - filters. Supported are low-pass, high-pass, - band-pass and band-stop (Notch) filtering. - - dat : (N, K) :class:`numpy.ndarray` - Uniformly sampled multi-channel time-series data - The 1st dimension is interpreted as the time axis, - columns represent individual channels. - Dimensions can be transposed to `(K, N)` with the `timeAxis` parameter - filter_type : {'lp', 'hp', 'bp, 'bs'}, optional - Select type of filter, either low-pass `'lp'`, - high-pass `'hp'`, band-pass `'bp'` or band-stop (Notch) `'bs'`. - freq : float or array_like - Cut-off frequency for low- and high-pass filters or sequence - of two frequencies for band-stop and band-pass filter. - order : int, optional - Order of the filter, default is 6. - Higher orders yield a sharper transition width - or less 'roll off' of the filter, but are more computationally expensive. - direction : {'twopass', 'onepass'} - Filter direction: - `'twopass'` - zero-phase forward and reverse filter - `'onepass'` - forward filter, introduces group delays - polyremoval : int or None - Order of polynomial used for de-trending data in the time domain prior - to filtering. A value of 0 corresponds to subtracting the mean - ("de-meaning"), ``polyremoval = 1`` removes linear trends (subtracting the - least squares fit of a linear polynomial). - timeAxis : int, optional - Index of running time axis in `dat` (0 or 1) - noCompute : bool - Preprocessing flag. If `True`, do not perform actual calculation but - instead return expected shape and :class:`numpy.dtype` of output - array. - - Returns - ------- - filtered : (N, K) :class:`~numpy.ndarray` - The filtered signals - - Notes - ----- - This method is intended to be used as - :meth:`~syncopy.shared.computational_routine.ComputationalRoutine.computeFunction` - inside a :class:`~syncopy.shared.computational_routine.ComputationalRoutine`. - Thus, input parameters are presumed to be forwarded from a parent metafunction. - Consequently, this function does **not** perform any error checking and operates - under the assumption that all inputs have been externally validated and cross-checked. - - """ - - # Re-arrange array if necessary and get dimensional information - if timeAxis != 0: - dat = dat.T # does not copy but creates view of `dat` - else: - dat = dat - - # filtering does not change the shape - outShape = dat.shape - if noCompute: - return outShape, np.float32 - - # detrend - if polyremoval == 0: - # SciPy's overwrite_data not working for type='constant' :/ - dat = sci.detrend(dat, type='constant', axis=0, overwrite_data=True) - elif polyremoval == 1: - dat = sci.detrend(dat, type='linear', axis=0, overwrite_data=True) - - # design the butterworth filter with "second-order-sections" output - sos = sci.butter(order, freq, filter_type, fs=samplerate, output='sos') - - # do the filtering - if direction == 'twopass': - filtered = sci.sosfiltfilt(sos, dat, axis=0) - return filtered - - elif direction == 'onepass': - filtered = sci.sosfilt(sos, dat, axis=0) - return filtered - - -class But_Filtering(ComputationalRoutine): - - """ - Compute class that performs filtering with butterworth filters - of :class:`~syncopy.AnalogData` objects - - Sub-class of :class:`~syncopy.shared.computational_routine.ComputationalRoutine`, - see :doc:`/developer/compute_kernels` for technical details on Syncopy's compute - classes and metafunctions. - - See also - -------- - syncopy.preprocessing : parent metafunction - """ - - computeFunction = staticmethod(but_filtering_cF) - - # 1st argument,the data, gets omitted - valid_kws = list(signature(but_filtering_cF).parameters.keys())[1:] - - def process_metadata(self, data, out): - - # Some index gymnastics to get trial begin/end "samples" - if data._selection is not None: - chanSec = data._selection.channel - trl = data._selection.trialdefinition - for row in range(trl.shape[0]): - trl[row, :2] = [row, row + 1] - else: - chanSec = slice(None) - trl = data.trialdefinition - - out.trialdefinition = trl - - out.samplerate = data.samplerate - out.channel = np.array(data.channel[chanSec]) diff --git a/syncopy/preproc/compRoutines.py b/syncopy/preproc/compRoutines.py new file mode 100644 index 000000000..6d68b3ca9 --- /dev/null +++ b/syncopy/preproc/compRoutines.py @@ -0,0 +1,307 @@ +# -*- coding: utf-8 -*- +# +# computeFunctions and -Routines for parallel calculation +# of FIR and IIR Filter operations +# + +# Builtin/3rd party package imports +import numpy as np +import scipy.signal as sci +from inspect import signature + +# syncopy imports +from syncopy.shared.computational_routine import ComputationalRoutine +from syncopy.shared.kwarg_decorators import unwrap_io + +# backend imports +from .firws import design_wsinc, apply_fir, minphaserceps + + +@unwrap_io +def sinc_filtering_cF(dat, + samplerate=1, + filter_type='lp', + freq=None, + order=None, + window="hamming", + direction='onepass-zerophase', + polyremoval=None, + timeAxis=0, + noCompute=False, + chunkShape=None + ): + """ + Provides basic filtering of signals with FIR (windowed sinc) + filters. Supported are low-pass, high-pass, + band-pass and band-stop (Notch) filtering. + + dat : (N, K) :class:`numpy.ndarray` + Uniformly sampled multi-channel time-series data + The 1st dimension is interpreted as the time axis, + columns represent individual channels. + Dimensions can be transposed to `(K, N)` with the `timeAxis` parameter + samplerate : float + Sampling frequency in Hz + filter_type : {'lp', 'hp', 'bp, 'bs'}, optional + Select type of filter, either low-pass `'lp'`, + high-pass `'hp'`, band-pass `'bp'` or band-stop (Notch) `'bs'`. + freq : float or array_like + Cut-off frequency for low- and high-pass filters or sequence + of two frequencies for band-stop and band-pass filter. + order : int, optional + Order of the filter, or length of the windowed sinc. The default + `None` will create a filter of maximal order which is the number of + samples in the trial. + Higher orders yield a sharper transition width + or less 'roll off' of the filter, but are more computationally expensive. + window : {"hamming", "hann", "blackmann", "kaiser"} + The type of taper to use for the sinc function + direction : {'twopass', 'onepass', 'onepass-zerophase'} + Filter direction: + `'twopass'` - zero-phase forward and reverse filter, default for 'but' + `'onepass'` - forward filter, introduces group delays + `'onepass-zerophase' - forward filter with zerophase correction, default for 'firws' + polyremoval : int or None + Order of polynomial used for de-trending data in the time domain prior + to filtering. A value of 0 corresponds to subtracting the mean + ("de-meaning"), ``polyremoval = 1`` removes linear trends (subtracting the + least squares fit of a linear polynomial). + timeAxis : int, optional + Index of running time axis in `dat` (0 or 1) + noCompute : bool + Preprocessing flag. If `True`, do not perform actual calculation but + instead return expected shape and :class:`numpy.dtype` of output + array. + + Returns + ------- + filtered : (N, K) :class:`~numpy.ndarray` + The filtered signals + + Notes + ----- + This method is intended to be used as + :meth:`~syncopy.shared.computational_routine.ComputationalRoutine.computeFunction` + inside a :class:`~syncopy.shared.computational_routine.ComputationalRoutine`. + Thus, input parameters are presumed to be forwarded from a parent metafunction. + Consequently, this function does **not** perform any error checking and operates + under the assumption that all inputs have been externally validated and cross-checked. + + """ + + # Re-arrange array if necessary and get dimensional information + if timeAxis != 0: + dat = dat.T # does not copy but creates view of `dat` + else: + dat = dat + + # filtering does not change the shape + outShape = dat.shape + if noCompute: + return outShape, np.float32 + + # detrend + if polyremoval == 0: + # SciPy's overwrite_data not working for type='constant' :/ + dat = sci.detrend(dat, type='constant', axis=0, overwrite_data=True) + elif polyremoval == 1: + dat = sci.detrend(dat, type='linear', axis=0, overwrite_data=True) + + # max order is signal length + if order is None: + order = dat.shape[0] + + # construct the filter + fkernel = design_wsinc(window, order, freq / samplerate, filter_type) + + # filtering by convolution + if direction == 'onepass': + filtered = apply_fir(dat, fkernel) + + # for symmetric filters actual + # filter direction does NOT matter + elif direction == 'twopass': + filtered = apply_fir(dat, fkernel) + filtered = apply_fir(filtered, fkernel) + + elif direction == 'onepass-zerophase': + # 0-phase transform + fkernel = minphaserceps(fkernel) + filtered = apply_fir(dat, fkernel) + + return filtered + + +class Sinc_Filtering(ComputationalRoutine): + + """ + Compute class that performs filtering with windowed sinc filters + of :class:`~syncopy.AnalogData` objects + + Sub-class of :class:`~syncopy.shared.computational_routine.ComputationalRoutine`, + see :doc:`/developer/compute_kernels` for technical details on Syncopy's compute + classes and metafunctions. + + See also + -------- + syncopy.preprocessing : parent metafunction + """ + + computeFunction = staticmethod(sinc_filtering_cF) + + # 1st argument,the data, gets omitted + valid_kws = list(signature(sinc_filtering_cF).parameters.keys())[1:] + + def process_metadata(self, data, out): + + # Some index gymnastics to get trial begin/end "samples" + if data._selection is not None: + chanSec = data._selection.channel + trl = data._selection.trialdefinition + for row in range(trl.shape[0]): + trl[row, :2] = [row, row + 1] + else: + chanSec = slice(None) + trl = data.trialdefinition + + out.trialdefinition = trl + + out.samplerate = data.samplerate + out.channel = np.array(data.channel[chanSec]) + + +@unwrap_io +def but_filtering_cF(dat, + samplerate=1, + filter_type='lp', + freq=None, + order=6, + direction='twopass', + polyremoval=None, + timeAxis=0, + noCompute=False, + chunkShape=None + ): + """ + Provides basic filtering of signals with IIR (Butterworth) + filters. Supported are low-pass, high-pass, + band-pass and band-stop (Notch) filtering. + + dat : (N, K) :class:`numpy.ndarray` + Uniformly sampled multi-channel time-series data + The 1st dimension is interpreted as the time axis, + columns represent individual channels. + Dimensions can be transposed to `(K, N)` with the `timeAxis` parameter + filter_type : {'lp', 'hp', 'bp, 'bs'}, optional + Select type of filter, either low-pass `'lp'`, + high-pass `'hp'`, band-pass `'bp'` or band-stop (Notch) `'bs'`. + freq : float or array_like + Cut-off frequency for low- and high-pass filters or sequence + of two frequencies for band-stop and band-pass filter. + order : int, optional + Order of the filter, default is 6. + Higher orders yield a sharper transition width + or less 'roll off' of the filter, but are more computationally expensive. + direction : {'twopass', 'onepass'} + Filter direction: + `'twopass'` - zero-phase forward and reverse filter + `'onepass'` - forward filter, introduces group delays + polyremoval : int or None + Order of polynomial used for de-trending data in the time domain prior + to filtering. A value of 0 corresponds to subtracting the mean + ("de-meaning"), ``polyremoval = 1`` removes linear trends (subtracting the + least squares fit of a linear polynomial). + timeAxis : int, optional + Index of running time axis in `dat` (0 or 1) + noCompute : bool + Preprocessing flag. If `True`, do not perform actual calculation but + instead return expected shape and :class:`numpy.dtype` of output + array. + + Returns + ------- + filtered : (N, K) :class:`~numpy.ndarray` + The filtered signals + + Notes + ----- + This method is intended to be used as + :meth:`~syncopy.shared.computational_routine.ComputationalRoutine.computeFunction` + inside a :class:`~syncopy.shared.computational_routine.ComputationalRoutine`. + Thus, input parameters are presumed to be forwarded from a parent metafunction. + Consequently, this function does **not** perform any error checking and operates + under the assumption that all inputs have been externally validated and cross-checked. + + See also + -------- + `Scipy butterworth documentation `_ + + """ + + # Re-arrange array if necessary and get dimensional information + if timeAxis != 0: + dat = dat.T # does not copy but creates view of `dat` + else: + dat = dat + + # filtering does not change the shape + outShape = dat.shape + if noCompute: + return outShape, np.float32 + + # detrend + if polyremoval == 0: + # SciPy's overwrite_data not working for type='constant' :/ + dat = sci.detrend(dat, type='constant', axis=0, overwrite_data=True) + elif polyremoval == 1: + dat = sci.detrend(dat, type='linear', axis=0, overwrite_data=True) + + # design the butterworth filter with "second-order-sections" output + sos = sci.butter(order, freq, filter_type, fs=samplerate, output='sos') + + # do the filtering + if direction == 'twopass': + filtered = sci.sosfiltfilt(sos, dat, axis=0) + return filtered + + elif direction == 'onepass': + filtered = sci.sosfilt(sos, dat, axis=0) + return filtered + + +class But_Filtering(ComputationalRoutine): + + """ + Compute class that performs filtering with butterworth filters + of :class:`~syncopy.AnalogData` objects + + Sub-class of :class:`~syncopy.shared.computational_routine.ComputationalRoutine`, + see :doc:`/developer/compute_kernels` for technical details on Syncopy's compute + classes and metafunctions. + + See also + -------- + syncopy.preprocessing : parent metafunction + """ + + computeFunction = staticmethod(but_filtering_cF) + + # 1st argument,the data, gets omitted + valid_kws = list(signature(but_filtering_cF).parameters.keys())[1:] + + def process_metadata(self, data, out): + + # Some index gymnastics to get trial begin/end "samples" + if data._selection is not None: + chanSec = data._selection.channel + trl = data._selection.trialdefinition + for row in range(trl.shape[0]): + trl[row, :2] = [row, row + 1] + else: + chanSec = slice(None) + trl = data.trialdefinition + + out.trialdefinition = trl + + out.samplerate = data.samplerate + out.channel = np.array(data.channel[chanSec]) diff --git a/syncopy/preproc/firws.py b/syncopy/preproc/firws.py new file mode 100644 index 000000000..fc02a74b5 --- /dev/null +++ b/syncopy/preproc/firws.py @@ -0,0 +1,235 @@ +# -*- coding: utf-8 -*- +# +# Routines for designing and applying +# FIR windowed sinc filters +# + +# Builtin/3rd party package imports +import numpy as np +import scipy.signal.windows as sci_win +from scipy.signal import fftconvolve + + +def apply_fir(data, fkernel): + + """ + Convolution in the time domain of the input `data` + with an FIR filter. The filter's impulse response + is given by `fkernel`. + + Parameters + ---------- + data : (N, K) :class:`numpy.ndarray` + Uniformly sampled multi-channel time-series data + The 1st dimension is interpreted as the time axis, + columns represent individual channels. + fkernel : (N,) :class:`numpy.ndarray` + The time domain representation of the FIR filter + + Returns + ------- + filtered : (N, K) :class:`~numpy.ndarray` + The filtered signals + + """ + + slices = [None for _ in data.shape] + slices[0] = slice(None) + slices = tuple(slices) + + filtered = fftconvolve(data, fkernel[slices], mode='same') + return filtered + + +def design_wsinc(window, order, f_c, filter_type='lp'): + + """ + Construct the filter kernel in the time domain + + Parameters + ---------- + window : str + One of `scipy.signal.windows` + order : int + The order of the filter, if not even gets incremented by one + f_c : float or array_like + Cut-off frequenc(ies) in sampling units, + maximum is Nyquist `f_c=0.5`. For band-pass + and band-stop filters they have to be ordered low to high. + filter_type : {'lp', 'hp', 'bp, 'bs'}, optional + Select type of filter, either low-pass `'lp'`, + high-pass `'hp'`, band-pass `'bp'` or band-stop (Notch) `'bs'`. + """ + + # order has to be even + if order % 2 != 0: + order += 1 + + if filter_type == 'lp': + kernel = windowed_sinc(window, order, f_c) + return kernel + + elif filter_type == 'hp': + lp_kernel = windowed_sinc(window, order, f_c) + kernel = invert_sinc(lp_kernel) + return kernel + + if filter_type == 'bp': + # high-pass freq is lower than low-pass freq + # for band-pass filters + f_hp, f_lp = f_c + elif filter_type == 'bs': + # high-pass freq is higher than low-pass freq + # for band-stop filters + f_lp, f_hp = f_c + + # construct band filters + lp_kernel = windowed_sinc(window, order, f_lp) + kernel = windowed_sinc(window, order, f_hp) + hp_kernel = invert_sinc(kernel) + kernel = lp_kernel + hp_kernel + + # subtract dc component from filter addition + # for band-pass filters in the time-domain + if filter_type == 'bp': + kernel[len(kernel) // 2] -= 1 + + return kernel + + +def windowed_sinc(window, order, f_c): + + """ + Construct the symmetric windowed sinc filter + with a cut-off frequency `f_c` + + Parameters + ---------- + window : str + One of `scipy.signal.windows` + order : int + The order of the filter, has to be strictly even + f_c : float + Cut-off frequency in sampling units, + maximum is Nyquist `f_c=0.5` + + Returns + ------- + kernel : :class:`numpy.ndarray` + The windowed filter with length `order + 1` + + """ + + # angular cut-off frequency + omega_c = 2 * np.pi * f_c + + win_func = getattr(sci_win, window) + win = win_func(order + 1) + + # one-sided support + m_half = np.arange(1, order / 2 + 1) + kernel = np.sin(omega_c * m_half) / m_half + kernel = np.hstack([kernel[::-1], omega_c, kernel]) * win + # normalize to unity gain + kernel = kernel / kernel.sum() + + return kernel + + +def invert_sinc(kernel): + + """ + In frequency space the high-pass filter + is just 1 - low-pass. Hence, formally we can + calculate the high-pass version + in the time domain via the correspinding inverse Fourier: + + Fourier^-1 {1 - Fourier(kernel)} = Fourier^-1(1) - kernel + + This gives a delta-peak at the 0-lag midpoint + minus the original low-pass impulse response. + In the DSP world the delta distribution corresponds to a mere `1`. + """ + kernel = -kernel + # kernel size is always odd + kernel[len(kernel) // 2] += 1 + return kernel + + +def minphaserceps(fkernel): + + """ + Tranform FIR filter to zero-phase filter + + The original Matlab function was written for FieldTrip in 2013 by + Andreas Widmann, University of Leipzig, widmann@uni-leipzig.de + + Notes + ----- + .. [1] Smith III, O. J. (2007). Introduction to Digital Filters with Audio + Applications. W3K Publishing. Retrieved Nov 11 2013, from + https://ccrma.stanford.edu/~jos/fp/Matlab_listing_mps_m.html + .. [2] Vetter, K. (2013, Nov 11). Long FIR filters with low latency. + Retrieved Nov 11 2013, from + http://www.katjaas.nl/minimumphase/minimumphase.html + """ + + nSamples = len(fkernel) + upsamplingFactor = 1e3 # Impulse response upsampling/zero padding to reduce time-aliasing + nFFT = int(2**np.ceil(np.log2(nSamples * upsamplingFactor))) # Power of 2 + clipThresh = 1e-8 # -160 dB + + # Spectrum + specC = np.abs(np.fft.fft(fkernel, nFFT)) + specC[specC < clipThresh] = clipThresh # Clip spectrum to reduce time-aliasing + + # Real cepstrum + specR = np.real(np.fft.ifft(np.log(specC))) + + # Convolve + ires = np.hstack([specR[1:nFFT // 2], 0]) + np.conj(specR[nFFT // 2:nFFT + 1][::-1]) + specR = np.hstack([ + specR[0], ires, np.zeros(nFFT // 2 - 2)]) + + # Minimum phase + MinPhase = np.real(np.fft.ifft(np.exp(np.fft.fft(specR)))) + + # Remove zero-padding + return MinPhase[:nSamples] + + +def _fir_df(cutoffArray, fs): + + """ + Computes default and maximum possible transition band width from + FIR filter cutoff frequency(ies) according to the following heuristic: + + Transition band width is 25% of the lower cutoff + frequency, but not lower than 2 Hz, where possible (for bandpass, + highpass, and bandstop) and distance from passband edge to critical + frequency (DC, Nyquist) otherwise. + + This is almost a 1:1 copy of the FT routine `fir_df`, only difference + is the bandwidths are already normalized with the sampling rate `fs`. + + See also + -------- + `FieldTrip implementation `_ + """ + + # still WIP + raise NotImplementedError + + TRANSWIDTHRATIO = 0.25 + Fn = fs / 2 + cutoffArray = np.array(cutoffArray) / fs + + # Max possible transition band width + cutoffArray = np.sort(cutoffArray) + maxTBWArray = cutoffArray * 2 * (Fn - cutoffArray) * 2 * np.diff(cutoffArray) + maxDf = np.min(maxTBWArray) + + # Default filter order heuristic + df = np.min(np.max(cutoffArray[0] * TRANSWIDTHRATIO * 2) * maxDf) + + return df, maxDf diff --git a/syncopy/preproc/preprocessing.py b/syncopy/preproc/preprocessing.py index d05c625d4..67fc85075 100644 --- a/syncopy/preproc/preprocessing.py +++ b/syncopy/preproc/preprocessing.py @@ -13,35 +13,35 @@ from syncopy.shared.errors import SPYValueError, SPYTypeError, SPYWarning, SPYInfo from syncopy.shared.kwarg_decorators import (unwrap_cfg, unwrap_select, detect_parallel_client) - from syncopy.shared.input_processors import ( check_effective_parameters, - check_passed_kwargs, - process_padding + check_passed_kwargs ) -from .butterworthCR import But_Filtering +from .compRoutines import But_Filtering, Sinc_Filtering availableFilters = ('but', 'firws') availableFilterTypes = ('lp', 'hp', 'bp', 'bs') -availableDirections = ('twopass', 'onepass') +availableDirections = ('twopass', 'onepass', 'onepass-zerophase') +availableWindows = ("hamming", "hann", "blackmann") @unwrap_cfg @unwrap_select @detect_parallel_client def preprocessing(data, - filter_class = 'but', + filter_class='but', filter_type='lp', freq=None, - order=6, - direction='twopass', + order=None, + direction=None, + window="hamming", polyremoval=None, **kwargs ): """ Filtering of time continuous raw data with IIR and FIR filters - + data : `~syncopy.AnalogData` A non-empty Syncopy :class:`~syncopy.AnalogData` object filter_class : {'but', 'firws'} @@ -60,6 +60,9 @@ def preprocessing(data, Filter direction: `'twopass'` - zero-phase forward and reverse filter `'onepass'` - forward filter, introduces group delays + `'onepass-zerophase' - forward filter with zerophase correction, default for 'firws' + window : {"hamming", "hann", "blackmann"}, optional + The type of window to use for the FIR filter polyremoval : int or None, optional Order of polynomial used for de-trending data in the time domain prior to filtering. A value of 0 corresponds to subtracting the mean @@ -73,21 +76,20 @@ def preprocessing(data, """ # -- Basic input parsing -- - + # Make sure our one mandatory input object can be processed try: data_parser(data, varname="data", dataclass="AnalogData", writable=None, empty=False) except Exception as exc: raise exc - timeAxis = data.dimord.index("time") + timeAxis = data.dimord.index("time") # Get everything of interest in local namespace defaults = get_defaults(preprocessing) lcls = locals() # check for ineffective additional kwargs check_passed_kwargs(lcls, defaults, frontend_name="preprocessing") - # Ensure a valid computational method was selected if filter_class not in availableFilters: lgl = "'" + "or '".join(opt + "' " for opt in availableFilters) @@ -104,20 +106,20 @@ def preprocessing(data, elif filter_type in ('bp', 'bs'): array_parser(freq, varname='freq', hasinf=False, hasnan=False, lims=[0, data.samplerate / 2], dims=(2,)) - # filter order - scalar_parser(order, varname='order', lims=[0, 100], ntype='int_like') + freq = np.sort(freq) - # filter direction - if not isinstance(direction, str) or direction not in availableDirections: - lgl = "'" + "or '".join(opt + "' " for opt in availableDirections) - raise SPYValueError(legal=lgl, varname="direction", actual=direction) + # -- here the defaults are filter specific and get set later -- + + # filter order + if order is not None: + scalar_parser(order, varname='order', lims=[0, np.inf], ntype='int_like') # check polyremoval if polyremoval is not None: scalar_parser(polyremoval, varname="polyremoval", ntype="int_like", lims=[0, 1]) - # -- get trial info - + # -- get trial info + # if a subset selection is present # get sampleinfo and check for equidistancy if data._selection is not None: @@ -132,7 +134,7 @@ def preprocessing(data, trialList = list(range(len(data.trials))) sinfo = data.sampleinfo lenTrials = np.diff(sinfo).squeeze() - + # check for equidistant sampling as needed for filtering if not all([np.allclose(np.diff(time), 1 / data.samplerate) for time in data.time]): lgl = "equidistant sampling in time" @@ -146,12 +148,33 @@ def preprocessing(data, log_dict = {"filter_class": filter_class, "filter_type": filter_type, "freq": freq, - "order": order, - "direction": direction, "polyremoval": polyremoval, } if filter_class == 'but': + + if window != defaults['window'] and window is not None: + lgl = "no `window` setting for IIR filtering" + act = window + raise SPYValueError(lgl, 'window', act) + + # set filter specific defaults here + if direction is None: + direction = 'twopass' + msg = f"Setting default direction for IIR filter to '{direction}'" + SPYInfo(msg) + elif not isinstance(direction, str) or direction not in ('onepass', 'twopass'): + lgl = "'" + "or '".join(opt + "' " for opt in ('onepass', 'twopass')) + raise SPYValueError(legal=lgl, varname="direction", actual=direction) + + if order is None: + order = 4 + msg = f"Setting default order for IIR filter to {order}" + SPYInfo(msg) + + log_dict["order"] = order + log_dict["direction"] = direction + check_effective_parameters(But_Filtering, defaults, lcls) filterMethod = But_Filtering(samplerate=data.samplerate, @@ -161,16 +184,48 @@ def preprocessing(data, direction=direction, polyremoval=polyremoval, timeAxis=timeAxis) - + if filter_class == 'firws': - raise NotImplementedError('FIR coming soon..') + + if window not in availableWindows: + lgl = "'" + "or '".join(opt + "' " for opt in availableWindows) + raise SPYValueError(legal=lgl, varname="window", actual=window) + + # set filter specific defaults here + if direction is None: + direction = 'onepass-zerophase' + msg = f"Setting default direction for FIR filter to '{direction}'" + SPYInfo(msg) + elif not isinstance(direction, str) or direction not in availableDirections: + lgl = "'" + "or '".join(opt + "' " for opt in availableDirections) + raise SPYValueError(legal=lgl, varname="direction", actual=direction) + + if order is None: + order = int(lenTrials.min()) + msg = f"Setting order for FIR filter to {order}" + SPYInfo(msg) + + log_dict["order"] = order + log_dict["direction"] = direction + + check_effective_parameters(Sinc_Filtering, defaults, lcls, + besides=['filter_class']) + + filterMethod = Sinc_Filtering(samplerate=data.samplerate, + filter_type=filter_type, + freq=freq, + order=order, + window=window, + direction=direction, + polyremoval=polyremoval, + timeAxis=timeAxis) # ------------------------------------ # Call the chosen ComputationalRoutine # ------------------------------------ out = AnalogData(dimord=data.dimord) - # Perform actual computation + # Perform actual computation filterMethod.initialize(data, out._stackingDim, chan_per_worker=kwargs.get("chan_per_worker"), diff --git a/syncopy/shared/input_processors.py b/syncopy/shared/input_processors.py index a457424e0..c9325379a 100644 --- a/syncopy/shared/input_processors.py +++ b/syncopy/shared/input_processors.py @@ -330,7 +330,7 @@ def process_taper(taper, return 'dpss', dpss_opt -def check_effective_parameters(CR, defaults, lcls): +def check_effective_parameters(CR, defaults, lcls, besides=None): """ For a given ComputationalRoutine, compare set parameters @@ -346,10 +346,16 @@ def check_effective_parameters(CR, defaults, lcls): parameter names plus values with default values lcls : dict Result of `locals()`, all names and values of the local (frontend-)name space + besides : list or None + List of kws which don't get checked """ # list of possible parameter names of the CR expected = CR.valid_kws + ["parallel", "select"] + if besides is not None: + expected += besides + relevant = [name for name in defaults if name not in generalParameters] + for name in relevant: if name not in expected and (lcls[name] != defaults[name]): msg = f"option `{name}` has no effect in method `{CR.__name__}`!" diff --git a/syncopy/tests/local_spy.py b/syncopy/tests/local_spy.py index 6209c7e91..c9d2cdbe5 100644 --- a/syncopy/tests/local_spy.py +++ b/syncopy/tests/local_spy.py @@ -21,7 +21,6 @@ from syncopy.tests import synth_data - # Prepare code to be executed using, e.g., iPython's `%run` magic command if __name__ == "__main__": From 5f27a6333189206a6b1a799fbc933ce84f2d9387 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 7 Mar 2022 10:30:46 +0100 Subject: [PATCH 081/166] CHG: tiny doc string amendment Changes to be committed: modified: syncopy/preproc/firws.py --- syncopy/preproc/firws.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/syncopy/preproc/firws.py b/syncopy/preproc/firws.py index fc02a74b5..acd874d35 100644 --- a/syncopy/preproc/firws.py +++ b/syncopy/preproc/firws.py @@ -44,14 +44,16 @@ def apply_fir(data, fkernel): def design_wsinc(window, order, f_c, filter_type='lp'): """ - Construct the filter kernel in the time domain + Construct the windowed sinc filter kernel in the time domain Parameters ---------- window : str - One of `scipy.signal.windows` + One of `scipy.signal.windows`, good choices are + "blackmann", "hamming" and "hann" order : int - The order of the filter, if not even gets incremented by one + The order, or simply length, of the filter + If not even gets incremented by one f_c : float or array_like Cut-off frequenc(ies) in sampling units, maximum is Nyquist `f_c=0.5`. For band-pass From 42d7997cd23d3e2a18af8f24e64aca3347205593 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 7 Mar 2022 11:01:11 +0100 Subject: [PATCH 082/166] CHG: Corrected mix-up of zero-phase and minphase nomenclature - `onepasse-zerophase` for FIR is not a real different operation than just `onepass` it probably just emphasizes that standard FIR is a zero-phase filter - `onepass-minphase` however transforms the standard FIR into a causal filter befoe the convolition Changes to be committed: modified: syncopy/preproc/compRoutines.py modified: syncopy/preproc/firws.py modified: syncopy/preproc/preprocessing.py --- syncopy/preproc/compRoutines.py | 14 +++++++------- syncopy/preproc/firws.py | 2 +- syncopy/preproc/preprocessing.py | 12 ++++++------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/syncopy/preproc/compRoutines.py b/syncopy/preproc/compRoutines.py index 6d68b3ca9..0f8bb156b 100644 --- a/syncopy/preproc/compRoutines.py +++ b/syncopy/preproc/compRoutines.py @@ -24,7 +24,7 @@ def sinc_filtering_cF(dat, freq=None, order=None, window="hamming", - direction='onepass-zerophase', + direction='onepass', polyremoval=None, timeAxis=0, noCompute=False, @@ -56,11 +56,11 @@ def sinc_filtering_cF(dat, or less 'roll off' of the filter, but are more computationally expensive. window : {"hamming", "hann", "blackmann", "kaiser"} The type of taper to use for the sinc function - direction : {'twopass', 'onepass', 'onepass-zerophase'} - Filter direction: - `'twopass'` - zero-phase forward and reverse filter, default for 'but' - `'onepass'` - forward filter, introduces group delays - `'onepass-zerophase' - forward filter with zerophase correction, default for 'firws' + direction : {'twopass', 'onepass', 'onepass-minphase'} + Filter direction: + `'twopass'` - zero-phase forward and reverse filter, IIR and FIR + `'onepass'` - forward filter, introduces group delays for IIR, zerophase for FIR + `'onepass-minphase' - forward causal/minumum phase filter, FIR only polyremoval : int or None Order of polynomial used for de-trending data in the time domain prior to filtering. A value of 0 corresponds to subtracting the mean @@ -124,7 +124,7 @@ def sinc_filtering_cF(dat, filtered = apply_fir(dat, fkernel) filtered = apply_fir(filtered, fkernel) - elif direction == 'onepass-zerophase': + elif direction == 'onepass-minphase': # 0-phase transform fkernel = minphaserceps(fkernel) filtered = apply_fir(dat, fkernel) diff --git a/syncopy/preproc/firws.py b/syncopy/preproc/firws.py index acd874d35..978b607c8 100644 --- a/syncopy/preproc/firws.py +++ b/syncopy/preproc/firws.py @@ -161,7 +161,7 @@ def invert_sinc(kernel): def minphaserceps(fkernel): """ - Tranform FIR filter to zero-phase filter + Tranform FIR filter to minmum phase (causal) filter The original Matlab function was written for FieldTrip in 2013 by Andreas Widmann, University of Leipzig, widmann@uni-leipzig.de diff --git a/syncopy/preproc/preprocessing.py b/syncopy/preproc/preprocessing.py index 67fc85075..d3c69c286 100644 --- a/syncopy/preproc/preprocessing.py +++ b/syncopy/preproc/preprocessing.py @@ -22,7 +22,7 @@ availableFilters = ('but', 'firws') availableFilterTypes = ('lp', 'hp', 'bp', 'bs') -availableDirections = ('twopass', 'onepass', 'onepass-zerophase') +availableDirections = ('twopass', 'onepass', 'onepass-minphase') availableWindows = ("hamming", "hann", "blackmann") @@ -56,11 +56,11 @@ def preprocessing(data, Order of the filter, default is 6. Higher orders yield a sharper transition width or less 'roll off' of the filter, but are more computationally expensive. - direction : {'twopass', 'onepass'} + direction : {'twopass', 'onepass', 'onepasse-minphase'} Filter direction: - `'twopass'` - zero-phase forward and reverse filter - `'onepass'` - forward filter, introduces group delays - `'onepass-zerophase' - forward filter with zerophase correction, default for 'firws' + `'twopass'` - zero-phase forward and reverse filter, IIR and FIR + `'onepass'` - forward filter, introduces group delays for IIR, zerophase for FIR + `'onepass-minphase' - forward causal/minumum phase filter, FIR only window : {"hamming", "hann", "blackmann"}, optional The type of window to use for the FIR filter polyremoval : int or None, optional @@ -193,7 +193,7 @@ def preprocessing(data, # set filter specific defaults here if direction is None: - direction = 'onepass-zerophase' + direction = 'onepass' msg = f"Setting default direction for FIR filter to '{direction}'" SPYInfo(msg) elif not isinstance(direction, str) or direction not in availableDirections: From aeea84dcbc50edc6bcf0f8718f88a31d9857b750 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 7 Mar 2022 16:22:06 +0100 Subject: [PATCH 083/166] CHG: Re-normalization of mtmfft - now the integrated power yields the squared amplitude of a harmonic - actual power hence gets inversely scaled by frequency-bin width - None taper now possible, closes #229 - stft normalization works totally different.. --- syncopy/shared/const_def.py | 1 + syncopy/specest/compRoutines.py | 12 +++++++++--- syncopy/specest/mtmconvol.py | 19 ++++++++++++++----- syncopy/specest/mtmfft.py | 22 +++++++++++++++------- 4 files changed, 39 insertions(+), 15 deletions(-) diff --git a/syncopy/shared/const_def.py b/syncopy/shared/const_def.py index c703304b3..9fe92a8f0 100644 --- a/syncopy/shared/const_def.py +++ b/syncopy/shared/const_def.py @@ -20,6 +20,7 @@ #: available tapers of :func:`~syncopy.freqanalysis` and :func:`~syncopy.connectivity` all_windows = windows.__all__ +all_windows.remove("get_window") # aux. function all_windows.remove("exponential") # not symmetric all_windows.remove("hanning") # deprecated all_windows.remove("dpss") # activated via `tapsmofrq` diff --git a/syncopy/specest/compRoutines.py b/syncopy/specest/compRoutines.py index 3271155e4..dcdc943db 100644 --- a/syncopy/specest/compRoutines.py +++ b/syncopy/specest/compRoutines.py @@ -212,7 +212,10 @@ def process_metadata(self, data, out): # Attach remaining meta-data out.samplerate = data.samplerate out.channel = np.array(data.channel[chanSec]) - out.taper = np.array([self.cfg["method_kwargs"]["taper"]] * self.outputShape[out.dimord.index("taper")]) + if self.cfg["method_kwargs"]["taper"] is None: + out.taper = np.array(['None']) + else: + out.taper = np.array([self.cfg["method_kwargs"]["taper"]] * self.outputShape[out.dimord.index("taper")]) out.freq = self.cfg["foi"] @@ -341,7 +344,7 @@ def mtmconvol_cF( nFreq = foi.size taper_opt = method_kwargs['taper_opt'] if taper_opt: - nTaper = taper_opt["Kmax"] + nTaper = taper_opt.get("Kmax", 1) outShape = (nTime, max(1, nTaper * keeptapers), nFreq, nChannels) if noCompute: return outShape, spectralDTypes[output_fmt] @@ -435,7 +438,10 @@ def process_metadata(self, data, out): out.trialdefinition = trl out.samplerate = srate out.channel = np.array(data.channel[chanSec]) - out.taper = np.array([self.cfg["method_kwargs"]["taper"]] * self.outputShape[out.dimord.index("taper")]) + if self.cfg["method_kwargs"]["taper"] is None: + out.taper = np.array(['None']) + else: + out.taper = np.array([self.cfg["method_kwargs"]["taper"]] * self.outputShape[out.dimord.index("taper")]) out.freq = self.cfg["foi"] diff --git a/syncopy/specest/mtmconvol.py b/syncopy/specest/mtmconvol.py index fe273b2e0..d039d07c1 100644 --- a/syncopy/specest/mtmconvol.py +++ b/syncopy/specest/mtmconvol.py @@ -76,21 +76,25 @@ def mtmconvol(data_arr, samplerate, nperseg, noverlap=None, taper="hann", freqs = np.fft.rfftfreq(nperseg, 1 / samplerate) nFreq = freqs.size - taper_func = getattr(signal.windows, taper) + if taper is None: + taper = 'boxcar' + + taper_func = getattr(signal.windows, taper) # this parameter mitigates the sum-to-zero problem for the odd slepians # as signal.stft has hardcoded scaling='spectrum' # -> normalizes with win.sum() :/ # see also https://github.com/scipy/scipy/issues/14740 if taper == 'dpss': - taper_opt['sym'] = False + taper_opt['sym'] = False # only truly 2d for multi-taper "dpss" windows = np.atleast_2d(taper_func(nperseg, **taper_opt)) # Slepian normalization if taper == 'dpss': - windows = windows * np.sqrt(taper_opt.get('Kmax', 1)) / np.sqrt(nperseg) + # windows = windows * np.sqrt(taper_opt.get('Kmax', 1) / 2) / np.sqrt(nperseg) + windows = windows / np.sqrt(nperseg) # number of time points in the output if boundary is None: @@ -106,15 +110,20 @@ def mtmconvol(data_arr, samplerate, nperseg, noverlap=None, taper="hann", for taperIdx, win in enumerate(windows): # pxx has shape (nFreq, nChannels, nTime) + # stft has inbuild normalization of tapered signal _, _, pxx = signal.stft(data_arr, samplerate, win, nperseg, noverlap, boundary=boundary, padded=padded, axis=0, detrend=detrend) if taper == 'dpss': # reverse scipy window normalization - pxx = win.sum() * pxx + pxx = win.sum() * pxx * np.sqrt(2) + + # only tapers which have len(signal) norm + elif taper == 'boxcar': + pxx = np.sqrt(2) * pxx # normalization for half the spectrum/power - ftr[:, taperIdx, ...] = 2 * pxx.transpose(2, 0, 1)[:nTime, ...] + ftr[:, taperIdx, ...] = np.sqrt(2) * pxx.transpose(2, 0, 1)[:nTime, ...] return ftr, freqs diff --git a/syncopy/specest/mtmfft.py b/syncopy/specest/mtmfft.py index da1c2b347..c9ce609f5 100644 --- a/syncopy/specest/mtmfft.py +++ b/syncopy/specest/mtmfft.py @@ -71,6 +71,8 @@ def mtmfft(data_arr, freqs = np.fft.rfftfreq(nSamples, 1 / samplerate) nFreq = freqs.size + # frequency bins + dFreq = freqs[1] - freqs[0] # no taper is boxcar if taper is None: @@ -85,13 +87,19 @@ def mtmfft(data_arr, windows = np.atleast_2d(taper_func(signal_length, **taper_opt)) # only(!!) slepian windows are already normalized - # still have to normalize by number of tapers - # such that taper-averaging yields correct amplitudes - if taper == 'dpss': - windows = windows * np.sqrt(taper_opt.get('Kmax', 1)) # per pedes L2 normalisation for all other tapers + + if taper == 'dpss': + windows = np.sqrt(2 / dFreq) * windows + # weird 3 point normalization, + # scipy's hann is NOT [.25, .5, .25] in Fourier space + elif taper in ('hann', 'hamming'): + windows = np.sqrt(8 / 3) * windows / np.sqrt(windows.sum() * dFreq) + # boxcar has full length integral + elif taper == 'boxcar': + windows = windows / np.sqrt(signal_length / 2 * dFreq) else: - windows = windows * np.sqrt(signal_length) / np.sum(windows) + windows = np.sqrt(2) * windows / np.sqrt(windows.sum() * dFreq) # Fourier transforms (nTapers x nFreq x nChannels) ftr = np.zeros((windows.shape[0], nFreq, nChannels), dtype='complex64') @@ -103,8 +111,8 @@ def mtmfft(data_arr, if demean_taper: win -= win.mean(axis=0) # real fft takes only 'half the energy'/positive frequencies, - # multiply by 2 to correct for this - ftr[taperIdx] = 2 * np.fft.rfft(win, n=nSamples, axis=0) + # multiply by sqrt of 2 to correct for this + ftr[taperIdx] = np.sqrt(2) * np.fft.rfft(win, n=nSamples, axis=0) # normalization ftr[taperIdx] /= np.sqrt(nSamples) From 66fc42c220e3697913aa8f3c4c62bb2883e2f227 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Tue, 8 Mar 2022 17:59:01 +0100 Subject: [PATCH 084/166] NEW: Unified spectral normalization for mtmfft and mtmconvol - plundered SciPy's helper zoo to implement our own stft routine with the np.fft backend, now with full control over normalization - new helper module `_norm_spec.py` to uniformly define normalization for the whole specest module - still To Do: wavelet based methods On branch spectral-normalization Changes to be committed: new file: syncopy/specest/_norm_spec.py modified: syncopy/specest/mtmconvol.py modified: syncopy/specest/mtmfft.py new file: syncopy/specest/stft.py --- syncopy/specest/_norm_spec.py | 38 +++++++++++++++++ syncopy/specest/mtmconvol.py | 43 +++++++++---------- syncopy/specest/mtmfft.py | 31 ++++---------- syncopy/specest/stft.py | 80 +++++++++++++++++++++++++++++++++++ 4 files changed, 146 insertions(+), 46 deletions(-) create mode 100644 syncopy/specest/_norm_spec.py create mode 100644 syncopy/specest/stft.py diff --git a/syncopy/specest/_norm_spec.py b/syncopy/specest/_norm_spec.py new file mode 100644 index 000000000..c8e5b9b57 --- /dev/null +++ b/syncopy/specest/_norm_spec.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# +# Helper routines to normalize Fourier spectra +# + +import numpy as np + + +def _norm_spec(ftr, nSamples, freqs): + + """ + Normalizes the complex Fourier transform to + power spectral density units. + """ + + # frequency bins + delta_f = freqs[1] - freqs[0] + ftr = ftr / (nSamples / 2 * np.sqrt(delta_f)) + + return ftr + + +def _norm_taper(taper, windows, nSamples): + + """ + Helper function to normalize tapers such + that the resulting spectra are normalized + to power density units. + """ + + if taper == 'dpss': + windows = np.sqrt(nSamples) * windows + # weird 3 point normalization, + # checks out exactly for 'hann' though + elif taper != 'boxcar': + windows = np.sqrt(4 / 3) * windows * np.sqrt(nSamples / windows.sum()) + + return windows diff --git a/syncopy/specest/mtmconvol.py b/syncopy/specest/mtmconvol.py index d039d07c1..d86815ad5 100644 --- a/syncopy/specest/mtmconvol.py +++ b/syncopy/specest/mtmconvol.py @@ -7,6 +7,10 @@ import numpy as np from scipy import signal +# local imports +from .stft import stft +from ._norm_spec import _norm_taper + def mtmconvol(data_arr, samplerate, nperseg, noverlap=None, taper="hann", taper_opt={}, boundary='zeros', padded=True, detrend=False): @@ -37,10 +41,10 @@ def mtmconvol(data_arr, samplerate, nperseg, noverlap=None, taper="hann", `'Kmax'` and `'NW'`. For further details, please refer to the `SciPy docs `_ - boundary : bool + boundary : str or None Wether or not to auto-pad the signal such that a window is centered on each - sample. If set to `False` half the window size (`nperseg`) will be lost - on each side of the signal. + sample. If set to `None` half the window size (`nperseg`) will be lost + on each side of the signal. Defaults `'zeros'`, for zero padding extension. padded : bool Additional padding in case ``noverlap != nperseg - 1`` to fit an integer number of windows. @@ -75,6 +79,8 @@ def mtmconvol(data_arr, samplerate, nperseg, noverlap=None, taper="hann", # FFT frequencies from the window size freqs = np.fft.rfftfreq(nperseg, 1 / samplerate) nFreq = freqs.size + # frequency bins + dFreq = freqs[1] - freqs[0] if taper is None: taper = 'boxcar' @@ -91,12 +97,10 @@ def mtmconvol(data_arr, samplerate, nperseg, noverlap=None, taper="hann", # only truly 2d for multi-taper "dpss" windows = np.atleast_2d(taper_func(nperseg, **taper_opt)) - # Slepian normalization - if taper == 'dpss': - # windows = windows * np.sqrt(taper_opt.get('Kmax', 1) / 2) / np.sqrt(nperseg) - windows = windows / np.sqrt(nperseg) + # normalize + windows = _norm_taper(taper, windows, nperseg) - # number of time points in the output + # NUMBER of time points in the output if boundary is None: # no padding: we loose half the window on each side nTime = int(np.ceil(nSamples / (nperseg - noverlap))) - nperseg @@ -109,21 +113,12 @@ def mtmconvol(data_arr, samplerate, nperseg, noverlap=None, taper="hann", ftr = np.zeros((nTime, windows.shape[0], nFreq, nChannels), dtype='complex64') for taperIdx, win in enumerate(windows): - # pxx has shape (nFreq, nChannels, nTime) - # stft has inbuild normalization of tapered signal - _, _, pxx = signal.stft(data_arr, samplerate, win, - nperseg, noverlap, boundary=boundary, - padded=padded, axis=0, detrend=detrend) - - if taper == 'dpss': - # reverse scipy window normalization - pxx = win.sum() * pxx * np.sqrt(2) - - # only tapers which have len(signal) norm - elif taper == 'boxcar': - pxx = np.sqrt(2) * pxx - - # normalization for half the spectrum/power - ftr[:, taperIdx, ...] = np.sqrt(2) * pxx.transpose(2, 0, 1)[:nTime, ...] + # ftr has shape (nFreq, nChannels, nTime) + pxx, _ = stft(data_arr, samplerate, window=win, + nperseg=nperseg, noverlap=noverlap, + boundary=boundary, padded=padded, + axis=0, detrend=detrend) + + ftr[:, taperIdx, ...] = pxx.transpose(2, 0, 1)[:nTime, ...] return ftr, freqs diff --git a/syncopy/specest/mtmfft.py b/syncopy/specest/mtmfft.py index c9ce609f5..acf4fdd01 100644 --- a/syncopy/specest/mtmfft.py +++ b/syncopy/specest/mtmfft.py @@ -7,6 +7,9 @@ import numpy as np from scipy import signal +# local imports +from ._norm_spec import _norm_spec, _norm_taper + def mtmfft(data_arr, samplerate, @@ -71,8 +74,6 @@ def mtmfft(data_arr, freqs = np.fft.rfftfreq(nSamples, 1 / samplerate) nFreq = freqs.size - # frequency bins - dFreq = freqs[1] - freqs[0] # no taper is boxcar if taper is None: @@ -86,21 +87,8 @@ def mtmfft(data_arr, # here we take the actual signal lengths! windows = np.atleast_2d(taper_func(signal_length, **taper_opt)) - # only(!!) slepian windows are already normalized - # per pedes L2 normalisation for all other tapers - - if taper == 'dpss': - windows = np.sqrt(2 / dFreq) * windows - # weird 3 point normalization, - # scipy's hann is NOT [.25, .5, .25] in Fourier space - elif taper in ('hann', 'hamming'): - windows = np.sqrt(8 / 3) * windows / np.sqrt(windows.sum() * dFreq) - # boxcar has full length integral - elif taper == 'boxcar': - windows = windows / np.sqrt(signal_length / 2 * dFreq) - else: - windows = np.sqrt(2) * windows / np.sqrt(windows.sum() * dFreq) - + windows = _norm_taper(taper, windows, nSamples) + # Fourier transforms (nTapers x nFreq x nChannels) ftr = np.zeros((windows.shape[0], nFreq, nChannels), dtype='complex64') @@ -110,10 +98,9 @@ def mtmfft(data_arr, # de-mean again after tapering - needed for Granger! if demean_taper: win -= win.mean(axis=0) - # real fft takes only 'half the energy'/positive frequencies, - # multiply by sqrt of 2 to correct for this - ftr[taperIdx] = np.sqrt(2) * np.fft.rfft(win, n=nSamples, axis=0) - # normalization - ftr[taperIdx] /= np.sqrt(nSamples) + ftr[taperIdx] = np.fft.rfft(win, n=nSamples, axis=0) + # normalization for half the spectrum/power + # ftr[taperIdx] = ftr[taperIdx] / (nSamples / 2 * np.sqrt(dFreq)) + ftr[taperIdx] = _norm_spec(ftr[taperIdx], nSamples, freqs) return ftr, freqs diff --git a/syncopy/specest/stft.py b/syncopy/specest/stft.py new file mode 100644 index 000000000..534266182 --- /dev/null +++ b/syncopy/specest/stft.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# +# Short-time Fourier transform, uses np.fft as backend +# + +# Builtin/3rd party package imports +import numpy as np +import scipy.signal as sci_sig + +# local imports +from ._norm_spec import _norm_spec + + +def stft(dat, + fs=1., + window=None, + nperseg=200, + noverlap=None, + boundary='zeros', + detrend=False, + padded=True, + axis=0): + + # needed for stride tricks + # from here on axis=-1 is the data axis! + if dat.ndim > 1: + if axis != -1: + dat = np.moveaxis(dat, axis, -1) + + # extend along time axis to fit in + # sliding windows at the edges + if boundary is not None: + zeros_shape = list(dat.shape) + zeros_shape[-1] = nperseg // 2 + zeros = np.zeros(zeros_shape, dtype=dat.dtype) + dat = np.concatenate((zeros, dat, zeros), axis=-1) + + # defaults to half window overlap + if noverlap is None: + noverlap = nperseg // 2 + nstep = nperseg - noverlap + + if padded: + # Pad to integer number of windowed segments + # I.e make x.shape[-1] = nperseg + (nseg-1)*nstep, with integer nseg + nadd = (-(dat.shape[-1]-nperseg) % nstep) % nperseg + zeros_shape = list(dat.shape[:-1]) + [nadd] + dat = np.concatenate((dat, np.zeros(zeros_shape)), axis=-1) + + # Create strided array of data segments + if nperseg == 1 and noverlap == 0: + dat = dat[..., np.newaxis] + else: + # https://stackoverflow.com/a/5568169 + step = nperseg - noverlap + shape = dat.shape[:-1] + ((dat.shape[-1] - noverlap) // step, nperseg) + strides = dat.strides[:-1] + (step * dat.strides[-1], dat.strides[-1]) + dat = np.lib.stride_tricks.as_strided(dat, shape=shape, + strides=strides) + + # detrend each window separately + if detrend: + dat = sci_sig.detrend(dat, type=detrend, overwrite_data=True) + + if window is not None: + # Apply window by multiplication + dat = window * dat + + freqs = np.fft.rfftfreq(nperseg, 1 / fs) + + # the complex transforms + ftr = np.fft.rfft(dat, axis=-1) + + # normalization to squared amplitude density + ftr = _norm_spec(ftr, nperseg, freqs) + + # Roll frequency axis back to axis where the data came from + ftr = np.moveaxis(ftr, -1, 0) + + return ftr, freqs From ef41f6773ef84dc93efefb45a41cbbd92a14a4bd Mon Sep 17 00:00:00 2001 From: tensionhead Date: Wed, 9 Mar 2022 10:09:17 +0100 Subject: [PATCH 085/166] CHG: Remove experimental warning and update local_spy.py Changes to be committed: modified: syncopy/datatype/methods/selectdata.py modified: syncopy/tests/local_spy.py --- syncopy/datatype/methods/selectdata.py | 4 ---- syncopy/tests/local_spy.py | 7 ++++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/syncopy/datatype/methods/selectdata.py b/syncopy/datatype/methods/selectdata.py index 78c3fc24d..c3ee4b2eb 100644 --- a/syncopy/datatype/methods/selectdata.py +++ b/syncopy/datatype/methods/selectdata.py @@ -313,10 +313,6 @@ def selectdata(data, "'".join(key + "', " for key in kwargs.keys())[:-2] raise SPYValueError(legal=lgl, varname="kwargs", actual=act) - # FIXME: remove once tests are in place (cf #165) - if channel_i is not None or channel_j is not None: - SPYWarning("CrossSpectralData channel selection currently experimental!") - # First simplest case: determine whether we just need to clear an existing selection if clear: if any(value is not None for value in selectDict.values()): diff --git a/syncopy/tests/local_spy.py b/syncopy/tests/local_spy.py index 0e2c21658..a0227f48c 100644 --- a/syncopy/tests/local_spy.py +++ b/syncopy/tests/local_spy.py @@ -21,7 +21,6 @@ from syncopy.tests import synth_data - # Prepare code to be executed using, e.g., iPython's `%run` magic command if __name__ == "__main__": @@ -35,5 +34,7 @@ # defaults AR(2) parameters yield 40Hz peak trls.append(synth_data.AR2_network(None, nSamples=nSamples)) ad1 = spy.AnalogData(trls, samplerate=200) - gr = spy.connectivityanalysis(ad1, method='granger', taper='dpss', tapsmofrq=3, - foilim=[0, 100]) + + spec = spy.freqanalysis(ad1, tapsmofrq=2) + + gr = spy.connectivityanalysis(ad1, method='granger', tapsmofrq=5) From 6bfde050c39fb6fd41059a442bbdda8005909795 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Wed, 9 Mar 2022 11:30:10 +0100 Subject: [PATCH 086/166] CHG: Keep supported selectors in sync w/selectdata keywords - based on the discussion in #227, the `supported` list is now constructed based on `selectdata`'s keyword args. On branch fix_selectcsdata Changes to be committed: modified: syncopy/datatype/base_data.py --- syncopy/datatype/base_data.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/syncopy/datatype/base_data.py b/syncopy/datatype/base_data.py index 1d471bcdb..042fd6f0f 100644 --- a/syncopy/datatype/base_data.py +++ b/syncopy/datatype/base_data.py @@ -16,6 +16,7 @@ from hashlib import blake2b from itertools import islice from functools import reduce +from inspect import signature import shutil import numpy as np from numpy.lib.format import open_memmap, read_magic @@ -1469,8 +1470,13 @@ def __init__(self, data, select): varname="select", actual=select) if not isinstance(select, dict): raise SPYTypeError(select, "select", expected="dict") - supported = ["trials", "channel", "channel_i", "channel_j", "toi", - "toilim", "foi", "foilim", "taper", "unit", "eventid"] + + # Keep list of supported selectors in sync w/supported keywords of `selectdata` + supported = list(signature(selectdata).parameters.keys()) + for key in ["data", "out", "inplace", "clear", "parallel", "kwargs"]: + supported.remove(key) + # supported = ["trials", "channel", "channel_i", "channel_j", "toi", + # "toilim", "foi", "foilim", "taper", "unit", "eventid"] if not set(select.keys()).issubset(supported): lgl = "dict with one or all of the following keys: '" +\ "'".join(opt + "', " for opt in supported)[:-2] From d2d134d6e3f1d479d84123256678ca77241c5056 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Wed, 9 Mar 2022 11:36:31 +0100 Subject: [PATCH 087/166] CHG: Doc-string for homebrewed stft and cosmetics - included segment times output for our stft (even though we don't need it atm) Changes to be committed: modified: syncopy/specest/mtmconvol.py modified: syncopy/specest/mtmfft.py modified: syncopy/specest/stft.py --- syncopy/specest/mtmconvol.py | 12 +++---- syncopy/specest/mtmfft.py | 6 ++-- syncopy/specest/stft.py | 65 ++++++++++++++++++++++++++++++++++-- 3 files changed, 70 insertions(+), 13 deletions(-) diff --git a/syncopy/specest/mtmconvol.py b/syncopy/specest/mtmconvol.py index d86815ad5..833fc2695 100644 --- a/syncopy/specest/mtmconvol.py +++ b/syncopy/specest/mtmconvol.py @@ -97,10 +97,10 @@ def mtmconvol(data_arr, samplerate, nperseg, noverlap=None, taper="hann", # only truly 2d for multi-taper "dpss" windows = np.atleast_2d(taper_func(nperseg, **taper_opt)) - # normalize + # normalize window(s) windows = _norm_taper(taper, windows, nperseg) - # NUMBER of time points in the output + # number of time points in the output if boundary is None: # no padding: we loose half the window on each side nTime = int(np.ceil(nSamples / (nperseg - noverlap))) - nperseg @@ -114,10 +114,10 @@ def mtmconvol(data_arr, samplerate, nperseg, noverlap=None, taper="hann", for taperIdx, win in enumerate(windows): # ftr has shape (nFreq, nChannels, nTime) - pxx, _ = stft(data_arr, samplerate, window=win, - nperseg=nperseg, noverlap=noverlap, - boundary=boundary, padded=padded, - axis=0, detrend=detrend) + pxx, _, _ = stft(data_arr, samplerate, window=win, + nperseg=nperseg, noverlap=noverlap, + boundary=boundary, padded=padded, + axis=0, detrend=detrend) ftr[:, taperIdx, ...] = pxx.transpose(2, 0, 1)[:nTime, ...] diff --git a/syncopy/specest/mtmfft.py b/syncopy/specest/mtmfft.py index acf4fdd01..c8a0e44ef 100644 --- a/syncopy/specest/mtmfft.py +++ b/syncopy/specest/mtmfft.py @@ -86,9 +86,9 @@ def mtmfft(data_arr, # only really 2d if taper='dpss' with Kmax > 1 # here we take the actual signal lengths! windows = np.atleast_2d(taper_func(signal_length, **taper_opt)) - + # normalize window windows = _norm_taper(taper, windows, nSamples) - + # Fourier transforms (nTapers x nFreq x nChannels) ftr = np.zeros((windows.shape[0], nFreq, nChannels), dtype='complex64') @@ -99,8 +99,6 @@ def mtmfft(data_arr, if demean_taper: win -= win.mean(axis=0) ftr[taperIdx] = np.fft.rfft(win, n=nSamples, axis=0) - # normalization for half the spectrum/power - # ftr[taperIdx] = ftr[taperIdx] / (nSamples / 2 * np.sqrt(dFreq)) ftr[taperIdx] = _norm_spec(ftr[taperIdx], nSamples, freqs) return ftr, freqs diff --git a/syncopy/specest/stft.py b/syncopy/specest/stft.py index 534266182..eb020ecea 100644 --- a/syncopy/specest/stft.py +++ b/syncopy/specest/stft.py @@ -14,13 +14,66 @@ def stft(dat, fs=1., window=None, - nperseg=200, + nperseg=256, noverlap=None, boundary='zeros', detrend=False, padded=True, axis=0): + """ + Implements the short-time (or windowed) Fourier transform + + The interface is designed to be close to SciPy's implementation: :func: `~scipy.signal.stft` + + Parameters + ---------- + dat : (N, K) :class:`numpy.ndarray` + Uniformly sampled multi-channel time-series data + The 1st dimension is interpreted as the time axis + per default + fs : float + Samplerate in Hz + window : (M,) :class:`numpy.ndarray` or None, optional + Taper to be multiplied with the + signal segments, has to be of length `nperseg` + nperseg : int, optional + Length of each segment. Defaults to 256. + noverlap : int, optional + Number of points to overlap between segments. If `None`, + `noverlap = nperseg // 2`. Set to `nperseg - 1` to have an output + with `N` time points. Defaults to `None`. + boundary : 'zeros' or None + Specifies whether the input signal is extended at both ends with + `nperseg // 2` zeros in order to center the first windowed segment on + the first input point. If set to `None` half the segment size is + lost on each side of the input signal. Defaults to `'zeros'` + detrend : str or `False`, optional + Optional detrending of the individual segments. + Sets `type` argument of :func: `~scipy.signal.detrend`, + acceptable are either `'constant'` or `'linear'`. + Defaults to `False` such that no detrending is done. + padded : bool, optional + Specifies whether the input signal is zero-padded at the end to + make the signal fit exactly into an integer number of window + segments, so that all of the signal is included in the output. + Defaults to `True`. Padding occurs after boundary extension, if + `boundary` is not `None`, and `padded` is `True`, as is the + default. + axis : int, optional + Axis along which the STFT is computed; the default is over the + first axis (i.e. `axis=0`) + + Returns + ------- + ftr : :class:`numpy.ndarray` + Short-time fourier transform of the input `dat` + Per default the first axis corresponds to the segment times + freqs : :class:`numpy.ndarray` + Array of sampling frequencies + times : :class:`numpy.ndarray` + Array of segment times + """ # needed for stride tricks # from here on axis=-1 is the data axis! if dat.ndim > 1: @@ -57,8 +110,9 @@ def stft(dat, strides = dat.strides[:-1] + (step * dat.strides[-1], dat.strides[-1]) dat = np.lib.stride_tricks.as_strided(dat, shape=shape, strides=strides) + # dat now has shape (nChannels, nSamples, nperseg) - # detrend each window separately + # detrend each segment separately if detrend: dat = sci_sig.detrend(dat, type=detrend, overwrite_data=True) @@ -66,6 +120,11 @@ def stft(dat, # Apply window by multiplication dat = window * dat + times = np.arange(nperseg / 2, dat.shape[-1] - nperseg / 2 + 1, + nperseg - noverlap) / fs + if boundary is not None: + times -= (nperseg / 2) / fs + freqs = np.fft.rfftfreq(nperseg, 1 / fs) # the complex transforms @@ -77,4 +136,4 @@ def stft(dat, # Roll frequency axis back to axis where the data came from ftr = np.moveaxis(ftr, -1, 0) - return ftr, freqs + return ftr, freqs, times From 188b62bd0fc776a56ff2bbfc4c0e7e8e845cd300 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Wed, 9 Mar 2022 14:17:17 +0100 Subject: [PATCH 088/166] NEW: Amended CHANGELOG - updated CHANGELOG in preparation for release On branch dev Changes to be committed: modified: CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccd96e009..6767f1037 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,9 @@ Bugfix release packages/cache - Inverted `selectdata` messaging policy: only actual on-disk copy operations trigger a `SPYInfo` message (closes #197) +- Matched selector keywords and class attribute names, i.e., selecting channels + is now done by using a `select` dictionary with key `'channel'` (not `'channels'` + as before). See the documentation of `selectdata` for details. ### FIXED - The `trialdefinition` arrays constructed by the `Selector` class were incorrect From c40379d2d63a73e3e31f2ec9a281dc220b49950a Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Wed, 9 Mar 2022 14:25:57 +0100 Subject: [PATCH 089/166] CHG: Removed TravisCI in favor of GitHub actions - prepared switch from Travis to GH Actions On branch dev Changes to be committed: new file: .github/workflows/cov_test_workflow.yml deleted: .travis.yml modified: MANIFEST.in --- .github/workflows/cov_test_workflow.yml | 40 +++++++++++++++++++++++++ .travis.yml | 23 -------------- MANIFEST.in | 1 - 3 files changed, 40 insertions(+), 24 deletions(-) create mode 100644 .github/workflows/cov_test_workflow.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/cov_test_workflow.yml b/.github/workflows/cov_test_workflow.yml new file mode 100644 index 000000000..f10302f18 --- /dev/null +++ b/.github/workflows/cov_test_workflow.yml @@ -0,0 +1,40 @@ +name: Run tests and determine coverage + +on: + # Triggers the workflow on push or pull request events + push: + branches: [ dev ] + pull_request: + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + build-linux: + runs-on: ubuntu-latest + strategy: + max-parallel: 5 + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Install SyNCoPy + run: | + pip install -e .[dev] + - name: Lint with flake8 + run: | + pip install flake8 + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest and get coverage + run: | + pytest --cov=./ --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v2 + with: + name: syncopy-codecov + verbose: true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 89addbbc7..000000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -language: python -python: - - "3.8" - -cache: pip - -branches: - only: - - master - - dev - -install: - - pip install "ruamel.yaml >=0.16,<0.17" setuptools_scm - - python -m conda2pip - - pip install -r requirements.txt - - pip install -r requirements-test.txt - - python setup.py -q install - -script: - - pytest -v --cov=./ - -after_success: - - bash <(curl -s https://codecov.io/bash) || echo "Codecov did not collect coverage reports" diff --git a/MANIFEST.in b/MANIFEST.in index fa31d82c3..fd545305c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,6 +2,5 @@ exclude .gitignore exclude .gitlab-ci.yml exclude MANIFEST.in exclude .readthedocs.yml -exclude .travis.yml recursive-exclude .github * recursive-exclude doc * From 2ad0440178258a7d54c5051bd1dff1d14a0d9730 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Wed, 9 Mar 2022 14:50:22 +0100 Subject: [PATCH 090/166] CHG: Removed unused Travis badges from README - include GH actions + codecov badges in README On branch dev Changes to be committed: modified: CHANGELOG.md modified: README.rst --- CHANGELOG.md | 2 ++ README.rst | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6767f1037..1af9ff2e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,8 @@ Bugfix release - Matched selector keywords and class attribute names, i.e., selecting channels is now done by using a `select` dictionary with key `'channel'` (not `'channels'` as before). See the documentation of `selectdata` for details. +- Retired travis CI tests since free test runs are exhausted. Migrated to GitHub + actions (and re-included codecov) ### FIXED - The `trialdefinition` arrays constructed by the `Selector` class were incorrect diff --git a/README.rst b/README.rst index 4e0d846ba..3d946507f 100644 --- a/README.rst +++ b/README.rst @@ -8,11 +8,25 @@ Systems Neuroscience Computing in Python |Conda Version| |PyPi Version| |License| .. |Conda Version| image:: https://img.shields.io/conda/vn/conda-forge/esi-syncopy.svg - :target: https://anaconda.org/conda-forge/esi-syncopy -.. |PyPI version| image:: https://badge.fury.io/py/esi-syncopy.svg + :target: https://anaconda.org/conda-forge/esi-syncopy +.. |PyPI version| image:: https://badge.fury.io/py/esi-syncopy.svg :target: https://badge.fury.io/py/esi-syncopy .. |License| image:: https://img.shields.io/github/license/esi-neuroscience/syncopy +master branch status: |Master Tests| |Master Coverage| + +.. |Master Tests| image:: https://github.com/esi-neuroscience/syncopy/actions/workflows/cov_test_workflow.yml/badge.svg?branch=master + :target: https://github.com/esi-neuroscience/syncopy/actions/workflows/cov_test_workflow.yml +.. |Master Coverage| image:: https://codecov.io/gh/esi-neuroscience/syncopy/branch/master/graph/badge.svg?token=JEI3QQGNBQ + :target: https://codecov.io/gh/esi-neuroscience/syncopy + +dev branch status: |Dev Tests| |Dev Coverage| + +.. |Dev Tests| image:: https://github.com/esi-neuroscience/syncopy/actions/workflows/cov_test_workflow.yml/badge.svg?branch=dev + :target: https://github.com/esi-neuroscience/syncopy/actions/workflows/cov_test_workflow.yml +.. |Dev Coverage| image:: https://codecov.io/gh/esi-neuroscience/syncopy/branch/dev/graph/badge.svg?token=JEI3QQGNBQ + :target: https://codecov.io/gh/esi-neuroscience/syncopy + Syncopy aims to be a user-friendly toolkit for *large-scale* electrophysiology data-analysis in Python. We strive to achieve the following goals: From 270de71fb6581f90366102b2896866153ff2c282 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Wed, 9 Mar 2022 15:05:54 +0100 Subject: [PATCH 091/166] FIX: Remove open_memmap from wrapper_io - removed (dysfunctional) memmap call On branch dev Changes to be committed: modified: syncopy/shared/kwarg_decorators.py --- syncopy/shared/kwarg_decorators.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/syncopy/shared/kwarg_decorators.py b/syncopy/shared/kwarg_decorators.py index 87e8ab988..00c0c0acb 100644 --- a/syncopy/shared/kwarg_decorators.py +++ b/syncopy/shared/kwarg_decorators.py @@ -599,14 +599,6 @@ def wrapper_io(trl_dat, *wrkargs, **kwargs): arr = np.array(h5fin[indset][ingrid])[np.ix_(*sigrid)] else: arr = np.array(h5fin[indset][ingrid]) - except OSError: - try: - if fancy: - arr = open_memmap(infilename, mode="c")[np.ix_(*ingrid)] - else: - arr = np.array(open_memmap(infilename, mode="c")[ingrid]) - except: - raise SPYIOError(infilename) except Exception as exc: raise exc From e0ea6d8a6195f1c07331557b48414b22a336c1c5 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Wed, 9 Mar 2022 15:21:56 +0100 Subject: [PATCH 092/166] CHG: Specest-backend tests test for power-density instead for amplitude - check that integrated power-density along freq-axis yields squared amplitudes Changes to be committed: modified: syncopy/shared/input_processors.py modified: syncopy/specest/mtmconvol.py modified: syncopy/specest/mtmfft.py modified: syncopy/tests/backend/test_timefreq.py --- syncopy/shared/input_processors.py | 4 +- syncopy/specest/mtmconvol.py | 6 +- syncopy/specest/mtmfft.py | 5 +- syncopy/tests/backend/test_timefreq.py | 124 ++++++++++++++----------- 4 files changed, 81 insertions(+), 58 deletions(-) diff --git a/syncopy/shared/input_processors.py b/syncopy/shared/input_processors.py index a457424e0..caf58b191 100644 --- a/syncopy/shared/input_processors.py +++ b/syncopy/shared/input_processors.py @@ -212,7 +212,7 @@ def process_taper(taper, # no tapering at all if taper is None and tapsmofrq is None: return None, {} - + # See if taper choice is supported if taper not in availableTapers: lgl = "'" + "or '".join(opt + "' " for opt in availableTapers) @@ -286,7 +286,7 @@ def process_taper(taper, try: scalar_parser(tapsmofrq, varname="tapsmofrq", lims=[0, np.inf]) - except Exception as exc: + except Exception: lgl = "smoothing bandwidth in Hz, typical values are in the range 1-10Hz" raise SPYValueError(legal=lgl, varname="tapsmofrq", actual=tapsmofrq) diff --git a/syncopy/specest/mtmconvol.py b/syncopy/specest/mtmconvol.py index 833fc2695..9d7644491 100644 --- a/syncopy/specest/mtmconvol.py +++ b/syncopy/specest/mtmconvol.py @@ -64,8 +64,10 @@ def mtmconvol(data_arr, samplerate, nperseg, noverlap=None, taper="hann", ``Sxx = np.real(ftr * ftr.conj()).mean(axis=0)`` - The short time FFT result is normalized such that - this yields the squared harmonic amplitudes. + The STFT result is normalized such that this yields the power + spectral density. For a clean harmonic and a frequency bin + width of `dF` this will give a peak power of `A**2 * dF`, + with `A` as harmonic ampltiude. """ # attach dummy channel axis in case only a diff --git a/syncopy/specest/mtmfft.py b/syncopy/specest/mtmfft.py index c8a0e44ef..a92547820 100644 --- a/syncopy/specest/mtmfft.py +++ b/syncopy/specest/mtmfft.py @@ -58,7 +58,10 @@ def mtmfft(data_arr, ``Sxx = np.real(ftr * ftr.conj()).mean(axis=0)`` - The FFT result is normalized such that this yields the squared amplitudes. + The FFT result is normalized such that this yields the power + spectral density. For a clean harmonic and a Fourier frequency bin + width of `dF` this will give a peak power of `A**2 * dF`, + with `A` as harmonic ampltiude. """ # attach dummy channel axis in case only a diff --git a/syncopy/tests/backend/test_timefreq.py b/syncopy/tests/backend/test_timefreq.py index 929f293a6..edd41eeee 100644 --- a/syncopy/tests/backend/test_timefreq.py +++ b/syncopy/tests/backend/test_timefreq.py @@ -51,15 +51,16 @@ def gen_testdata(freqs=[20, 40, 60], return signal -fs = 1000 # sampling frequency +# -- test signal for time-freq methods -- -# generate 3 packets at 20, 40 and 60Hz with 10 cycles each +fs = 1000 # sampling frequency + +# generate 3 packets at 20, 40 and 60Hz with 12 cycles each # Noise variance is given by eps signal_freqs = np.array([20, 50, 80]) -# signal_freqs = np.array([20, 70]) cycles = 12 -A = 5 # signal amplitude +A = 5 # signal amplitude signal = A * gen_testdata(freqs=signal_freqs, cycles=cycles, fs=fs, eps=0.) # define frequencies of interest for wavelet methods @@ -73,8 +74,8 @@ def gen_testdata(freqs=[20, 40, 60], def test_mtmconvol(): - # 10 cycles of 40Hz are 250 samples - window_size = 750 + # 2Hz wide freq bins. + window_size = 500 # default - stft pads with 0's to make windows fit # we choose N-1 overlap to retrieve a time-freq estimate @@ -87,9 +88,7 @@ def test_mtmconvol(): noverlap=window_size - 1) # absolute squared for power spectrum and taper averaging - spec = np.real(ftr * ftr.conj()).mean(axis=1)[:, :, 0] # 1st Channel - # amplitude spectrum - ampls = np.sqrt(spec) + spec = np.real(ftr * ftr.conj()).mean(axis=1)[:, :, 0] # 1st Channel fig, (ax1, ax2) = ppl.subplots(2, 1, sharex=True, @@ -108,20 +107,20 @@ def test_mtmconvol(): extent = [0, len(signal) / fs, freqs[0] - df / 2, freqs[-1] - df / 2] # test also the plotting # scale with amplitude - ax2.imshow(ampls.T, + ax2.imshow(spec.T, cmap='magma', aspect='auto', origin='lower', extent=extent, vmin=0, - vmax=1.2 * A) + vmax=1. * A**2 / df) # zoom into foi region ax2.set_ylim((foi[0], foi[-1])) # get the 'mappable' for the colorbar im = ax2.images[0] - fig.colorbar(im, ax = ax2, orientation='horizontal', + fig.colorbar(im, ax=ax2, orientation='horizontal', shrink=0.7, pad=0.2, label='amplitude (a.u.)') # closest spectral indices to validate time-freq results @@ -137,10 +136,9 @@ def test_mtmconvol(): '--', c='0.5') - # number of cycles with relevant - # amplitude at the respective frequency - cycle_num = (ampls[:, idx] > A / np.e).sum() / fs * frequency - print(f'{cycle_num} cycles for the {frequency} band') + # integrated power at the respective frquency + cycle_num = (spec[:, idx] * df > A**2 / np.e**2).sum() / fs * frequency + print(f'{cycle_num} cycles for the {frequency} Hz band') # we have 2 times the cycles for each frequency (temporal neighbor) assert cycle_num > 2 * cycles # power should decay fast, so we don't detect more cycles @@ -153,7 +151,13 @@ def test_mtmconvol(): # ------------------------- taper = 'dpss' - taper_opt = {'Kmax' : 10, 'NW' : 2} + tapsmofrq = 10 # Hz + # set parameters for scipy.signal.windows.dpss + NW = tapsmofrq * window_size / (2 * fs) + # from the minBw setting NW always is at least 1 + Kmax = int(2 * NW - 1) # optimal number of tapers + + taper_opt = {'Kmax': Kmax, 'NW': NW} # the transforms have shape (nTime, nTaper, nFreq, nChannel) ftr2, freqs2 = mtmconvol.mtmconvol(signal, samplerate=fs, taper=taper, taper_opt=taper_opt, @@ -161,8 +165,6 @@ def test_mtmconvol(): noverlap=window_size - 1) spec2 = np.real((ftr2 * ftr2.conj()).mean(axis=1)[..., 0]) - # amplitude spectrum - ampls2 = np.sqrt(spec2) fig, (ax1, ax2) = ppl.subplots(2, 1, sharex=True, @@ -178,20 +180,20 @@ def test_mtmconvol(): # test also the plotting # scale with amplitude - ax2.imshow(ampls2.T, + ax2.imshow(spec2.T, cmap='magma', aspect='auto', origin='lower', extent=extent, vmin=0, - vmax=1.2 * A) + vmax=1. * A**2 / df) # zoom into foi region ax2.set_ylim((foi[0], foi[-1])) # get the 'mappable' for the colorbar im = ax2.images[0] - fig.colorbar(im, ax = ax2, orientation='horizontal', + fig.colorbar(im, ax=ax2, orientation='horizontal', shrink=0.7, pad=0.2, label='amplitude (a.u.)') fig.tight_layout() @@ -207,9 +209,9 @@ def test_mtmconvol(): # check for the whole time domain # due to too much spectral broadening/smearing # so we just check that the maximum estimated - # amplitude is within 10% boundsof the real amplitude - - assert 0.9 * A < ampls2.max() < 1.1 * A + # power within one bin is within 15% bounds of the real power + nBins = tapsmofrq / df + assert 0.85 * A**2 / df < spec2.max() * nBins < 1.15 * A**2 / df def test_superlet(): @@ -224,8 +226,8 @@ def test_superlet(): order_min=2, c_1=1, adaptive=False) - # amplitude spectrum - ampls = np.abs(spec) + # power spectrum + power_spec = np.real(spec * spec.conj()) fig, (ax1, ax2) = ppl.subplots(2, 1, sharex=True, @@ -241,13 +243,13 @@ def test_superlet(): extent = [0, len(signal) / fs, foi[0], foi[-1]] # test also the plotting # scale with amplitude - ax2.imshow(ampls, + ax2.imshow(power_spec, cmap='magma', aspect='auto', extent=extent, origin='lower', vmin=0, - vmax=1.2 * A) + vmax=1.2 * A**2) # get the 'mappable' im = ax2.images[0] @@ -263,7 +265,7 @@ def test_superlet(): # number of cycles with relevant # amplitude at the respective frequency - cycle_num = (ampls[idx, :] > A / np.e).sum() / fs * frequency + cycle_num = (power_spec[idx, :] > (A / np.e)**2).sum() / fs * frequency print(f'{cycle_num} cycles for the {frequency} band') # we have 2 times the cycles for each frequency (temporal neighbor) assert cycle_num > 2 * cycles @@ -285,7 +287,7 @@ def test_wavelet(): scales=scales, wavelet=wfun) # amplitude spectrum - ampls = np.abs(spec) + power_spec = np.real(spec * spec.conj()) fig, (ax1, ax2) = ppl.subplots(2, 1, sharex=True, @@ -301,17 +303,17 @@ def test_wavelet(): # test also the plotting # scale with amplitude - ax2.imshow(ampls, + ax2.imshow(power_spec, cmap='magma', aspect='auto', extent=extent, origin='lower', vmin=0, - vmax=1.2 * A) + vmax=1.2 * A**2) # get the 'mappable' im = ax2.images[0] - fig.colorbar(im, ax = ax2, orientation='horizontal', + fig.colorbar(im, ax=ax2, orientation='horizontal', shrink=0.7, pad=0.2, label='amplitude (a.u.)') for idx, frequency in zip(freq_idx, signal_freqs): @@ -323,7 +325,7 @@ def test_wavelet(): # number of cycles with relevant # amplitude at the respective frequency - cycle_num = (ampls[idx, :] > A / np.e).sum() / fs * frequency + cycle_num = (power_spec[idx, :] > (A / np.e)**2).sum() / fs * frequency print(f'{cycle_num} cycles for the {frequency} band') # we have at least 2 times the cycles for each frequency (temporal neighbor) assert cycle_num > 2 * cycles @@ -338,7 +340,8 @@ def test_mtmfft(): # superposition 40Hz and 100Hz oscillations A1:A2 for 1s f1, f2 = 40, 100 A1, A2 = 5, 3 - tvec = np.arange(0, 1, 1 / 1000) + nSamples = 1000 + tvec = np.arange(0, 1, 1 / nSamples) signal = A1 * np.cos(2 * np.pi * f1 * tvec) signal += A2 * np.cos(2 * np.pi * f2 * tvec) @@ -357,42 +360,53 @@ def test_mtmfft(): # average over potential tapers (only 1 here) spec = np.real(ftr * ftr.conj()).mean(axis=0) - amplitudes = np.sqrt(spec)[:, 0] # only 1 channel - # our FFT normalisation recovers the signal amplitudes: - assert np.allclose([A1, A2], amplitudes[[f1, f2]]) + powers = spec[:, 0] # only 1 channel + # our FFT normalisation recovers the integrated squared signal amplitudes + # as frequency bin width is 1Hz, one bin 'integral' is enough + assert np.allclose([A1**2, A2**2], powers[[f1, f2]]) fig, ax = ppl.subplots() ax.set_title(f"Amplitude spectrum {A1} x 40Hz + {A2} x 100Hz") - ax.plot(freqs[:150], amplitudes[:150], label="No taper", lw=2) + ax.plot(freqs[:150], powers[:150], label="No taper", lw=2) ax.set_xlabel('frequency (Hz)') - ax.set_ylabel('amplitude (a.u.)') + ax.set_ylabel('power-density') # ------------------------- # test multi-taper analysis # ------------------------- + tapsmofrq = 10 # Hz + # set parameters for scipy.signal.windows.dpss + NW = tapsmofrq * nSamples / (2 * fs) + # from the minBw setting NW always is at least 1 + Kmax = int(2 * NW - 1) # optimal number of tapers - taper_opt = {'Kmax' : 8, 'NW' : 1} + taper_opt = {'Kmax': Kmax, 'NW': NW} ftr, freqs = mtmfft.mtmfft(signal, fs, taper="dpss", taper_opt=taper_opt) # average over tapers dpss_spec = np.real(ftr * ftr.conj()).mean(axis=0) - dpss_amplitudes = np.sqrt(dpss_spec)[:, 0] # only 1 channel - # check for amplitudes (and taper normalisation) - assert np.allclose(dpss_amplitudes[[f1, f2]], [A1, A2], atol=1e-1) + dpss_powers = dpss_spec[:, 0] # only 1 channel + # check for integrated power (and taper normalisation) + # summing up all dpss powers should give total power of the + # test signal which is A1**2 + A2**2 + assert np.allclose(np.sum(dpss_powers), A1**2 + A2**2, atol=1e-2) - ax.plot(freqs[:150], dpss_amplitudes[:150], label="Slepian", lw=2) + ax.plot(freqs[:150], dpss_powers[:150], label="Slepian", lw=2) ax.legend() # ----------------- # test kaiser taper (is boxcar for beta -> inf) # ----------------- - taper_opt = {'beta' : 2} + taper_opt = {'beta': 3} ftr, freqs = mtmfft.mtmfft(signal, fs, taper="kaiser", taper_opt=taper_opt) # average over tapers (only 1 here) kaiser_spec = np.real(ftr * ftr.conj()).mean(axis=0) - kaiser_amplitudes = np.sqrt(kaiser_spec)[:, 0] # only 1 channel + kaiser_powers = kaiser_spec[:, 0] # only 1 channel # check for amplitudes (and taper normalisation) - assert np.allclose(kaiser_amplitudes[[f1, f2]], [A1, A2], atol=1e-2) + # normalization less exact for arbitraty windows + assert np.allclose(np.sum(kaiser_powers), A1**2 + A2**2, atol=1.5) + ax.plot(freqs[:150], kaiser_powers[:150], label="Kaiser", lw=2) + ax.legend() # ------------------------------- # test all other window functions (which don't need a parameter) @@ -410,10 +424,14 @@ def test_mtmfft(): ftr, freqs = mtmfft.mtmfft(signal, fs, taper=win, taper_opt=taper_opt) # average over tapers (only 1 here) spec = np.real(ftr * ftr.conj()).mean(axis=0) - amplitudes = np.sqrt(spec)[:, 0] # only 1 channel - # print(win, amplitudes[[f1, f2]]) - assert np.allclose(amplitudes[[f1, f2]], [A1, A2], atol=1e-3) + powers = spec[:, 0] # only 1 channel + print(np.sum(powers), win) + if win != 'tukey': + assert np.allclose(np.sum(powers), A1**2 + A2**2, atol=4) + # not sure why tukey and triang are so off.. + else: + assert np.allclose(np.sum(powers), A1**2 + A2**2, atol=8) + except TypeError: # we didn't provide default parameters.. pass - From 3b19fdac2ec4a7f344ddb0e10bd4a8a5b096c9a8 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Thu, 10 Mar 2022 17:00:30 +0100 Subject: [PATCH 093/166] FIX: Corrected docstring indentation - fixed indent in docstring of `stft` On branch spectral-normalization Changes to be committed: modified: syncopy/specest/stft.py --- syncopy/specest/stft.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/syncopy/specest/stft.py b/syncopy/specest/stft.py index eb020ecea..8e9ab8b31 100644 --- a/syncopy/specest/stft.py +++ b/syncopy/specest/stft.py @@ -54,21 +54,21 @@ def stft(dat, acceptable are either `'constant'` or `'linear'`. Defaults to `False` such that no detrending is done. padded : bool, optional - Specifies whether the input signal is zero-padded at the end to - make the signal fit exactly into an integer number of window - segments, so that all of the signal is included in the output. - Defaults to `True`. Padding occurs after boundary extension, if - `boundary` is not `None`, and `padded` is `True`, as is the - default. + Specifies whether the input signal is zero-padded at the end to + make the signal fit exactly into an integer number of window + segments, so that all of the signal is included in the output. + Defaults to `True`. Padding occurs after boundary extension, if + `boundary` is not `None`, and `padded` is `True`, as is the + default. axis : int, optional - Axis along which the STFT is computed; the default is over the - first axis (i.e. `axis=0`) + Axis along which the STFT is computed; the default is over the + first axis (i.e. `axis=0`) Returns ------- ftr : :class:`numpy.ndarray` - Short-time fourier transform of the input `dat` - Per default the first axis corresponds to the segment times + Short-time fourier transform of the input `dat` + Per default the first axis corresponds to the segment times freqs : :class:`numpy.ndarray` Array of sampling frequencies times : :class:`numpy.ndarray` From ee9adb506206a8c5baa13e26034c109632371b5a Mon Sep 17 00:00:00 2001 From: tensionhead Date: Thu, 10 Mar 2022 18:03:36 +0100 Subject: [PATCH 094/166] CHG: Turn on taper demeaning for Granger.. does not really help though :/ - started writing connectivity quickstart section Changes to be committed: modified: doc/source/quickstart/damped_harm.py modified: doc/source/quickstart/quickstart.rst modified: syncopy/nwanalysis/ST_compRoutines.py modified: syncopy/tests/local_spy.py modified: syncopy/tests/synth_data.py --- doc/source/quickstart/damped_harm.py | 4 ++-- doc/source/quickstart/quickstart.rst | 19 +++++++++++++++++-- syncopy/nwanalysis/ST_compRoutines.py | 7 ++++++- syncopy/tests/local_spy.py | 26 +++++++++++++++++++------- syncopy/tests/synth_data.py | 15 ++++++++++++++- 5 files changed, 58 insertions(+), 13 deletions(-) diff --git a/doc/source/quickstart/damped_harm.py b/doc/source/quickstart/damped_harm.py index 44e94ed70..241f1016c 100644 --- a/doc/source/quickstart/damped_harm.py +++ b/doc/source/quickstart/damped_harm.py @@ -3,7 +3,7 @@ nTrials = 50 -nSamples = 1000 +nSamples = 1000 nChannels = 2 samplerate = 500 # in Hz @@ -11,7 +11,7 @@ tvec = np.arange(nSamples) * 1 / samplerate # the 30Hz harmonic harm = np.cos(2 * np.pi * 30 * tvec) -# the damped amplitudes +# dampening down to 10% of the original amplitude dampening = np.linspace(1, 0.1, nSamples) signal = dampening * harm diff --git a/doc/source/quickstart/quickstart.rst b/doc/source/quickstart/quickstart.rst index 9e67cb4ac..aff4e12e0 100644 --- a/doc/source/quickstart/quickstart.rst +++ b/doc/source/quickstart/quickstart.rst @@ -58,7 +58,7 @@ which gives nicely formatted output: Use `.log` to see object history -So we see that we indeed got 50 trials with 2 channels and 1000 samples each. Note that Syncopy per default **stores and writes all data on disk**, as this allows for seamless processing of larger than RAM datasets. The exact location and filename of a dataset in question is listed at the ``filename`` field. The standard location is the ``.spy`` directory created automatically in the user's home directory. To change this and for more details please see :ref:`setup_env`. +So we see that we indeed got 50 trials with 2 channels and 1000 samples each. Note that Syncopy per default **stores and writes all data on disk**, as this allows for seamless processing of **larger than memory** datasets. The exact location and filename of a dataset in question is listed at the ``filename`` field. The standard location is the ``.spy`` directory created automatically in the user's home directory. To change this and for more details please see :ref:`setup_env`. .. hint:: You can access each of the shown meta-information fields separately using standard Python attribute access, e.g. ``data.filename`` or ``data.samplerate``. @@ -78,7 +78,7 @@ Multitapered Fourier Analysis .. code-block:: - fft_spectra = spy.freqanalsysis(data, method='mtmfft', foilim=[0, 50], taper='dpss', tapsmofrq=3) + fft_spectra = spy.freqanalsysis(data, method='mtmfft', foilim=[0, 50], tapsmofrq=3) The parameter ``foilim`` controls the *frequencies of interest limits*, so in this case we are interested in the range 0-50Hz. Starting the computation interactively will show additional information:: @@ -137,3 +137,18 @@ To quickly inspect the results for each channel we can use:: Again, we see a strong 30Hz signal in the 1st channel, and channel 2 is devoid of any rhythms. However, in contrast to the ``method=mtmfft`` call, now we also get information along the time axis. The dampening of the harmonic over time in channel 1 is clearly visible. An improved method, the superlet transform, providing super-resolution time-frequency representations can be computed via ``method='superlet'``, see :func:`~syncopy.freqanalysis` for more details. + +Connectivity Analysis +===================== + +Having time-frequency results for individual channels is useful, however we hardly learn anything about functional relationships between these different units. Even if two channels have a spectral peak at say 30Hz, we don't know if these signals are actually connected. Syncopy offers various distinct methods to elucidate such putative connections: coherence, cross-correlation and Granger-Geweke causality. + +Setup +----- + +To have a synthetic albeit meaningful dataset to illustrate the different methodologies + +Coherence +--------- + + diff --git a/syncopy/nwanalysis/ST_compRoutines.py b/syncopy/nwanalysis/ST_compRoutines.py index 78bc06766..1c4c84acb 100644 --- a/syncopy/nwanalysis/ST_compRoutines.py +++ b/syncopy/nwanalysis/ST_compRoutines.py @@ -155,7 +155,12 @@ def cross_spectra_cF(trl_dat, elif polyremoval == 1: dat = detrend(dat, type='linear', axis=0, overwrite_data=True) - CS_ij = csd(dat, samplerate, nSamples, taper=taper, taper_opt=taper_opt) + CS_ij = csd(dat, + samplerate, + nSamples, + taper=taper, + taper_opt=taper_opt, + demean_taper=demean_taper) # where does freqs go/come from - # we will eventually solve this issue.. diff --git a/syncopy/tests/local_spy.py b/syncopy/tests/local_spy.py index a0227f48c..f11a23954 100644 --- a/syncopy/tests/local_spy.py +++ b/syncopy/tests/local_spy.py @@ -28,13 +28,25 @@ ad1 = spy.AnalogData([mock_up] * 5) nTrials = 50 - nSamples = 200 + nSamples = 2000 trls = [] + AdjMat = np.zeros((2, 2)) + AdjMat[0, 1] = .25 + for _ in range(nTrials): # defaults AR(2) parameters yield 40Hz peak - trls.append(synth_data.AR2_network(None, nSamples=nSamples)) - ad1 = spy.AnalogData(trls, samplerate=200) - - spec = spy.freqanalysis(ad1, tapsmofrq=2) - - gr = spy.connectivityanalysis(ad1, method='granger', tapsmofrq=5) + alphas = [.74, -.46] # broad peak at 60Hz + alphas = [0.24, -.46] + alphas = [.55, -.8] + trl = synth_data.AR2_network(AdjMat, nSamples=nSamples, + alphas=alphas) + #trl = synth_data.AR2_network(None, nSamples=nSamples, + # alphas=alphas) + + trls.append(trl) + print(trl.mean()) + ad1 = spy.AnalogData(trls, samplerate=2000) + + spec = spy.freqanalysis(ad1, tapsmofrq=5, keeptrials=False) + coh = spy.connectivityanalysis(ad1, method='coh', tapsmofrq=5) + gr = spy.connectivityanalysis(ad1, method='granger', tapsmofrq=10, polyremoval=0) diff --git a/syncopy/tests/synth_data.py b/syncopy/tests/synth_data.py index b53650a7c..4deca7bb6 100644 --- a/syncopy/tests/synth_data.py +++ b/syncopy/tests/synth_data.py @@ -5,6 +5,8 @@ import numpy as np +_2pi = np.pi * 2 + # noisy phase evolution <-> phase diffusion def phase_diffusion(freq, eps=.1, fs=1000, nChannels=2, nSamples=1000): @@ -103,11 +105,22 @@ def AR2_network(AdjMat=None, nSamples=2500, alphas=[0.55, -0.8]): sol[i, :] = (DiagMat + AdjMat.T) @ sol[i - 1, :] + alpha2 * sol[i - 2, :] sol[i, :] += np.random.randn(nChannels) - # X2 drives X1 return sol +def AR2_peak_freq(a1, a2, fs=1): + + """ + Helper function to tune spectral peak of AR(2) process + """ + + if np.any((a1**2 + 4 * a2) > 0): + raise ValueError("No complex roots!") + + return np.arccos(a1 * (a2 - 1) / (4 * a2)) * 1 / _2pi * fs + + def mk_RandomAdjMat(nChannels=3, conn_thresh=0.25, max_coupling=0.25): """ Create a random adjacency matrix From 31e133181e5bd5312c39b56b1453e60571173d4d Mon Sep 17 00:00:00 2001 From: tensionhead Date: Thu, 10 Mar 2022 19:32:07 +0100 Subject: [PATCH 095/166] CHG: make use of in-place operators --- syncopy/specest/_norm_spec.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/syncopy/specest/_norm_spec.py b/syncopy/specest/_norm_spec.py index c8e5b9b57..d522ae119 100644 --- a/syncopy/specest/_norm_spec.py +++ b/syncopy/specest/_norm_spec.py @@ -15,7 +15,7 @@ def _norm_spec(ftr, nSamples, freqs): # frequency bins delta_f = freqs[1] - freqs[0] - ftr = ftr / (nSamples / 2 * np.sqrt(delta_f)) + ftr /= (nSamples / 2 * np.sqrt(delta_f)) return ftr @@ -33,6 +33,6 @@ def _norm_taper(taper, windows, nSamples): # weird 3 point normalization, # checks out exactly for 'hann' though elif taper != 'boxcar': - windows = np.sqrt(4 / 3) * windows * np.sqrt(nSamples / windows.sum()) + windows *= np.sqrt(4 / 3) * np.sqrt(nSamples / windows.sum()) return windows From 34639e3cb9f79ee3ae0467a22c1b9e6134611909 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Thu, 10 Mar 2022 19:32:50 +0100 Subject: [PATCH 096/166] Update stft.py --- syncopy/specest/stft.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncopy/specest/stft.py b/syncopy/specest/stft.py index 8e9ab8b31..f4ac20759 100644 --- a/syncopy/specest/stft.py +++ b/syncopy/specest/stft.py @@ -118,7 +118,7 @@ def stft(dat, if window is not None: # Apply window by multiplication - dat = window * dat + dat *= window times = np.arange(nperseg / 2, dat.shape[-1] - nperseg / 2 + 1, nperseg - noverlap) / fs From ccf4e68578307224d98de7d9c8e44fa24f199bc5 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Thu, 10 Mar 2022 19:37:26 +0100 Subject: [PATCH 097/166] Update _norm_spec.py --- syncopy/specest/_norm_spec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncopy/specest/_norm_spec.py b/syncopy/specest/_norm_spec.py index d522ae119..58c00612e 100644 --- a/syncopy/specest/_norm_spec.py +++ b/syncopy/specest/_norm_spec.py @@ -29,7 +29,7 @@ def _norm_taper(taper, windows, nSamples): """ if taper == 'dpss': - windows = np.sqrt(nSamples) * windows + windows *= np.sqrt(nSamples) # weird 3 point normalization, # checks out exactly for 'hann' though elif taper != 'boxcar': From e29101a7235a17c2851745f29b1078b28c22b843 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 11 Mar 2022 12:16:32 +0100 Subject: [PATCH 098/166] CHG: Added option to solve spectral factorization via least-squares - addresses #172 On branch improve-granger Changes to be committed: modified: syncopy/nwanalysis/wilson_sf.py modified: syncopy/tests/backend/test_conn.py --- syncopy/nwanalysis/wilson_sf.py | 22 +++++++++++++++++----- syncopy/tests/backend/test_conn.py | 20 ++++++++------------ 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/syncopy/nwanalysis/wilson_sf.py b/syncopy/nwanalysis/wilson_sf.py index a4a2fea11..bd8eec7e8 100644 --- a/syncopy/nwanalysis/wilson_sf.py +++ b/syncopy/nwanalysis/wilson_sf.py @@ -13,7 +13,7 @@ import numpy as np -def wilson_sf(CSD, nIter=100, rtol=1e-9): +def wilson_sf(CSD, nIter=100, rtol=1e-9, direct_inversion=True): """ Wilsons spectral matrix factorization ("analytic method") @@ -35,6 +35,10 @@ def wilson_sf(CSD, nIter=100, rtol=1e-9): rtol : float Tolerance of the relative maximal error of the factorization. + direct_inversion : bool + With `True` a direct matrix inversion is + performed, `False` solves the associated + least-square problems. Returns ------- @@ -59,17 +63,25 @@ def wilson_sf(CSD, nIter=100, rtol=1e-9): psi = np.tile(psi0, (nFreq, 1, 1)) assert psi.shape == CSD.shape + g = np.zeros(CSD.shape, dtype=np.complex64) converged = False for _ in range(nIter): - psi_inv = np.linalg.inv(psi) - # the bracket of equation 3.1 - g = psi_inv @ CSD @ psi_inv.conj().transpose(0, 2, 1) + if direct_inversion: + psi_inv = np.linalg.inv(psi) + # the bracket of equation 3.1 + g = psi_inv @ CSD @ psi_inv.conj().transpose(0, 2, 1) + else: + for i in range(nFreq): + C = np.linalg.lstsq(psi[i], CSD[i], rcond=None)[0] + g[i] = np.linalg.lstsq( + psi[i], C.conj().T, rcond=None)[0].conj().T + gplus, gplus_0 = _plusOperator(g + Ident) # the 'any' matrix S = np.triu(gplus_0) - S = S - S.conj().T # S + S* = 0 + S = S - S.conj().T # S + S* = 0 # the next step psi_{tau+1} psi = psi @ (gplus + S) diff --git a/syncopy/tests/backend/test_conn.py b/syncopy/tests/backend/test_conn.py index 1056168f9..1f654bdce 100644 --- a/syncopy/tests/backend/test_conn.py +++ b/syncopy/tests/backend/test_conn.py @@ -237,8 +237,8 @@ def test_wilson(): ax.set_xlim((f1 - 5, f2 + 5)) ax.legend() - -def test_granger(): + +def test_granger(direct_inversion=True): """ Test the granger causality measure @@ -250,7 +250,7 @@ def test_granger(): of time series data." Physical review letters 100.1 (2008): 018701. """ - fs = 200 # Hz + fs = 200 # Hz nSamples = 2500 nTrials = 25 @@ -263,18 +263,19 @@ def test_granger(): # --- get CSD --- bw = 2 NW = bw * nSamples / (2 * fs) - Kmax = int(2 * NW - 1) # optimal number of tapers + Kmax = int(2 * NW - 1) # optimal number of tapers CSD, freqs = csd.csd(sol, fs, taper='dpss', - taper_opt={'Kmax' : Kmax, 'NW' : NW}, - fullOutput=True) + taper_opt={'Kmax': Kmax, 'NW': NW}, + fullOutput=True, + demean_taper=True) CSDav += CSD CSDav /= nTrials # with only 2 channels this CSD is well conditioned assert np.linalg.cond(CSDav).max() < 1e2 - H, Sigma, conv = wilson_sf(CSDav) + H, Sigma, conv = wilson_sf(CSDav, direct_inversion=direct_inversion) G = granger(CSDav, H, Sigma) assert G.shape == CSDav.shape @@ -294,8 +295,3 @@ def test_granger(): assert G[freq_idx, 0, 1] < 0.1 # check high causality for 2->1 assert G[freq_idx, 1, 0] > 0.8 - - -# --- Helper routines --- - - From f4330d5656d4d435744bea4ef4a2f12cb16242f9 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 11 Mar 2022 12:28:21 +0100 Subject: [PATCH 099/166] NEW: Test both versions of spectral-factorization in the backend - once with direct inversion and once with the least-squares solution On branch improve-granger Changes to be committed: modified: syncopy/tests/backend/test_conn.py --- syncopy/tests/backend/test_conn.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/syncopy/tests/backend/test_conn.py b/syncopy/tests/backend/test_conn.py index 1f654bdce..eeb0c05e5 100644 --- a/syncopy/tests/backend/test_conn.py +++ b/syncopy/tests/backend/test_conn.py @@ -238,7 +238,7 @@ def test_wilson(): ax.legend() -def test_granger(direct_inversion=True): +def test_granger(): """ Test the granger causality measure @@ -275,7 +275,7 @@ def test_granger(direct_inversion=True): CSDav /= nTrials # with only 2 channels this CSD is well conditioned assert np.linalg.cond(CSDav).max() < 1e2 - H, Sigma, conv = wilson_sf(CSDav, direct_inversion=direct_inversion) + H, Sigma, conv = wilson_sf(CSDav, direct_inversion=True) G = granger(CSDav, H, Sigma) assert G.shape == CSDav.shape @@ -286,7 +286,7 @@ def test_granger(direct_inversion=True): ax.plot(freqs, G[:, 0, 1], label=r'Granger $1\rightarrow2$') ax.plot(freqs, G[:, 1, 0], label=r'Granger $2\rightarrow1$') ax.legend() - + # check for directional causality at 40Hz freq_idx = np.argmin(freqs < 40) assert 39 < freqs[freq_idx] < 41 @@ -295,3 +295,18 @@ def test_granger(direct_inversion=True): assert G[freq_idx, 0, 1] < 0.1 # check high causality for 2->1 assert G[freq_idx, 1, 0] > 0.8 + + # repeat test with least-square solution + H, Sigma, conv = wilson_sf(CSDav, direct_inversion=False) + G2 = granger(CSDav, H, Sigma) + + # check low to no causality for 1->2 + assert G2[freq_idx, 0, 1] < 0.1 + # check high causality for 2->1 + assert G2[freq_idx, 1, 0] > 0.8 + + ax.plot(freqs, G2[:, 0, 1], label=r'Granger (LS) $1\rightarrow2$') + ax.plot(freqs, G2[:, 1, 0], label=r'Granger (LS) $2\rightarrow1$') + ax.legend() + + return G, G2 From 6cfbdbda74492e1d06b97e46534952e83cabfbf4 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 11 Mar 2022 12:28:21 +0100 Subject: [PATCH 100/166] NEW: Test both versions of spectral-factorization in the backend - once with direct inversion and once with the least-squares solution On branch improve-granger Changes to be committed: modified: syncopy/tests/backend/test_conn.py --- syncopy/tests/backend/test_conn.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/syncopy/tests/backend/test_conn.py b/syncopy/tests/backend/test_conn.py index eeb0c05e5..17f208d91 100644 --- a/syncopy/tests/backend/test_conn.py +++ b/syncopy/tests/backend/test_conn.py @@ -308,5 +308,3 @@ def test_granger(): ax.plot(freqs, G2[:, 0, 1], label=r'Granger (LS) $1\rightarrow2$') ax.plot(freqs, G2[:, 1, 0], label=r'Granger (LS) $2\rightarrow1$') ax.legend() - - return G, G2 From c693d13561df4fad2fab688c29f78037dca99397 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 11 Mar 2022 16:24:57 +0100 Subject: [PATCH 101/166] WIP: Coherence measure quickstart - it get's quite voluminous.. Changes to be committed: new file: doc/source/quickstart/ar2_coh1.png new file: doc/source/quickstart/ar2_nw.py new file: doc/source/quickstart/ar2_signals.png new file: doc/source/quickstart/ar2_specs.png modified: doc/source/quickstart/quickstart.rst --- doc/source/quickstart/ar2_coh1.png | Bin 0 -> 19912 bytes doc/source/quickstart/ar2_nw.py | 17 +++++++ doc/source/quickstart/ar2_signals.png | Bin 0 -> 70280 bytes doc/source/quickstart/ar2_specs.png | Bin 0 -> 22892 bytes doc/source/quickstart/quickstart.rst | 68 +++++++++++++++++++++++++- 5 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 doc/source/quickstart/ar2_coh1.png create mode 100644 doc/source/quickstart/ar2_nw.py create mode 100644 doc/source/quickstart/ar2_signals.png create mode 100644 doc/source/quickstart/ar2_specs.png diff --git a/doc/source/quickstart/ar2_coh1.png b/doc/source/quickstart/ar2_coh1.png new file mode 100644 index 0000000000000000000000000000000000000000..248ef60dcf9bd76612c0bb58c2ccffdc97c09530 GIT binary patch literal 19912 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 literal 0 HcmV?d00001 diff --git a/doc/source/quickstart/ar2_nw.py b/doc/source/quickstart/ar2_nw.py new file mode 100644 index 000000000..034882305 --- /dev/null +++ b/doc/source/quickstart/ar2_nw.py @@ -0,0 +1,17 @@ +import numpy as np +import syncopy as spy +from syncopy.tests import synth_data + +nTrials = 50 +nSamples = 1500 +trls = [] +# empty adjacency matrix - no coupling +AdjMat = np.zeros((2, 2)) + +for _ in range(nTrials): + + trl = synth_data.AR2_network(AdjMat, nSamples=nSamples) + trls.append(trl) + +data_uc = spy.AnalogData(trls, samplerate=500) +spec_uc = spy.freqanalysis(data_uc, tapsmofrq=3, keeptrials=False) diff --git a/doc/source/quickstart/ar2_signals.png b/doc/source/quickstart/ar2_signals.png new file mode 100644 index 0000000000000000000000000000000000000000..7ff78d4b9e5972c5b62d49f037075437f4c125a0 GIT binary patch literal 70280 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

2%0fjE-<}u^UJz${#EUMeiZG0 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:: + + import matplotlib.pyplot as ppl + fig, ax = ppl.subplots() + ax.plot(chan1, label='channel 1') + ax.plot(chan2 + 8, label='channel 2') + ax.set_xlabel('samples') + ax.set_title('Uncoupled AR(2) signals') + ax.legend(ncol=2) + +We shifted the 2nd channel up by 8 units on the y-axis for better visibility. Both channels show visible oscillations: + +.. image:: ar2_signals.png + :height: 340px + + +as is confirmed by looking at the power spectra:: + + spec_uc.singlepanelplot() + +.. image:: ar2_specs.png + :height: 300px + +As expected for the stochastic AR(2) model, we have a fairly broad spectral peak at around 100Hz. + +.. note:: + Careful when using :func:`~syncopy.show` on large datasets, as the output is loaded directly into memory. It is advisable to make sufficiently small selections (e.g. 1 channel, 1 trial) to avoid out-of-memory problems on your machine! Coherence --------- +One way to check for relationships between different oscillating channels is to calculate the pairwise `coherence `_ measure. It can be roughly understood as a frequency dependent correlation. Let's do this for our uncoupled AR(2) signals:: + + coherence = spy.connectivityanalysis(data_uc, method='coh', tapsmofrq=3) + +The result is of type :class:`spy.CrossSpectralData`, the standard datatype for all connectivity measures. It contains the results for all ``nChannels x nChannels`` possible combinations. Let's pick the two available channel combinations and plot the results:: + coh12 = coherence.show(channel_i='channel1', channel_j='channel2') + coh21 = coherence.show(channel_i='channel2', channel_j='channel1') + + # plotting + fig, ax = ppl.subplots() + ax.plot(coherence.freq, coh12, label='1-2') + ax.plot(coherence.freq, coh21, label='2-1') + ax.set_xlabel('frequency (Hz)') + ax.set_ylabel('coherence') + ax.set_ylim((0,1.2)) + ax.legend(ncol=2) + +.. image:: ar2_coh1.png + :height: 300px + +This shows us the following properties of the coherence: + +* **symmetry**: the coherence of channel pair 1-2 is the same as 2-1 +* **range**: like correlations, coherence measure lives on the [0,1] interval +* **sensitivity**: even though both channels have a lot of power around 100Hz, coherence is as low as for all other frequencies as there is no coupling + +.. hint:: + To inspect the available dimensions of any Syncopy dataset, access the ``.dimord`` property. From 84bb6f047d550f64f59893da28e955db3f77685c Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Fri, 11 Mar 2022 21:56:52 +0100 Subject: [PATCH 102/166] NEW: New pytest option `--full` for more granular testing - added support for new `'--full'` pytest command line arg; without providing `--full` not all selection permutations etc. are tested so that tests can be run quickly without sacrificing coverage. - amended new `full` option to test run script - modified runner config: run full test suite only with SLURM On branch dev Changes to be committed: modified: .gitlab-ci.yml modified: CHANGELOG.md modified: syncopy/tests/conftest.py modified: syncopy/tests/run_tests.sh modified: syncopy/tests/test_continuousdata.py modified: syncopy/tests/test_discretedata.py modified: syncopy/tests/test_specest.py --- .gitlab-ci.yml | 6 +- CHANGELOG.md | 4 + syncopy/tests/conftest.py | 13 ++ syncopy/tests/run_tests.sh | 24 ++- syncopy/tests/test_continuousdata.py | 227 +++++++++++++++++---------- syncopy/tests/test_discretedata.py | 49 ++++-- syncopy/tests/test_specest.py | 224 ++++++++++++++++---------- 7 files changed, 365 insertions(+), 182 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 468f8b0d8..2b2915a65 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -99,9 +99,9 @@ slurmtest: - conda env update -f syncopy.yml --prune - conda activate syncopy - export PYTHONPATH=$CI_PROJECT_DIR - - srun -p DEV --mem=8000m -c 4 pytest -v $TEST_DIR/test_specest.py - - srun -p DEV --mem=8000m -c 4 pytest -v $TEST_DIR/test_connectivity.py - - srun -p DEV --mem=8000m -c 4 pytest --ignore=$TEST_DIR/test_specest.py --ignore=$TEST_DIR/test_connectivity.py + - srun -p DEV --mem=8000m -c 4 pytest --full $TEST_DIR/test_specest.py + - srun -p DEV --mem=8000m -c 4 pytest --full $TEST_DIR/test_connectivity.py + - srun -p DEV --mem=8000m -c 4 pytest --full --ignore=$TEST_DIR/test_specest.py --ignore=$TEST_DIR/test_connectivity.py pypitest: stage: upload diff --git a/CHANGELOG.md b/CHANGELOG.md index 1af9ff2e2..192b85e59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ Bugfix release - Added experimental loading functionality for Matlab mat files - Added support for "scalar" selections, i.e., things like `selectdata(trials=0)` or `data.selectdata(channels='mychannel')` +- Added command line argument "--full" for more granular testing: the new default + for running the testing pipeline is to execute a trimmed-down testing suite that + does not probe all possible input permutations but focuses on the core functionality + without sacrificing coverage. ### CHANGED - Renamed `_selection` class property to `selection` diff --git a/syncopy/tests/conftest.py b/syncopy/tests/conftest.py index 3025ab821..cde8fc8b0 100644 --- a/syncopy/tests/conftest.py +++ b/syncopy/tests/conftest.py @@ -56,3 +56,16 @@ def pytest_collection_modifyitems(items): # Save potentially re-ordered test sequence items[:] = [items[idx] for idx in newOrder] + +# Define custom command-line argument `--full` +def pytest_addoption(parser): + parser.addoption( + "--full", + action="store_true", + help="run exhaustive test suite", + ) + +# Build corresponding fixture for `--full` +@pytest.fixture +def fulltests(request): + return request.config.getoption("--full") diff --git a/syncopy/tests/run_tests.sh b/syncopy/tests/run_tests.sh index d041c1506..bba8d2a0f 100755 --- a/syncopy/tests/run_tests.sh +++ b/syncopy/tests/run_tests.sh @@ -21,11 +21,14 @@ Arguments: COMMAND pytest perform testing using pytest in current user environment (if SLURM is available, tests are executed via `srun`) + full (OPTIONAL) if provided, an exhaustive test-run is conducted + including, e.g., all selection permutations etc. Default: off tox use tox to set up a new virtual environment (as defined in tox.ini) and run tests within this newly created env -h or --help show this help message and exit Example: $_selfie pytest + $_selfie pytest full " } @@ -41,23 +44,36 @@ export PYTEST_ADDOPTS="--color=yes --tb=short --verbose" while [ "$1" != "" ]; do case "$1" in pytest) + if [ "$2" == "full" ]; then + fulltests="--full" + else + fulltests="" + fi shift export PYTHONPATH=$(cd ../../ && pwd) if [ $_useSLURM ]; then - srun -p DEV --mem=8000m -c 4 pytest + CMD="srun -p DEV --mem=8000m -c 4 pytest $fulltests" else PYTEST_ADDOPTS="$PYTEST_ADDOPTS --cov=../../syncopy --cov-config=../../.coveragerc" export PYTEST_ADDOPTS - pytest + CMD="pytest $fulltests" fi + echo ">>>" + echo ">>> Running $CMD $PYTEST_ADDOPTS" + echo ">>>" + ${CMD} ;; tox) shift if [ $_useSLURM ]; then - srun -p DEV --mem=8000m -c 4 tox + CMD="srun -p DEV --mem=8000m -c 4 tox" else - tox + CMD="tox" fi + echo ">>>" + echo ">>> Running $CMD " + echo ">>>" + ${CMD} ;; -h | --help) shift diff --git a/syncopy/tests/test_continuousdata.py b/syncopy/tests/test_continuousdata.py index 526af3d8b..c48b811a7 100644 --- a/syncopy/tests/test_continuousdata.py +++ b/syncopy/tests/test_continuousdata.py @@ -8,6 +8,8 @@ import tempfile import time import pytest +import random +import numbers import numpy as np from numpy.lib.format import open_memmap @@ -80,7 +82,6 @@ freqSelections = list(zip(["foi"] * len(foiSelections), foiSelections)) \ + list(zip(["foilim"] * len(foilimSelections), foilimSelections)) - # Local helper function for performing basic arithmetic tests def _base_op_tests(dummy, ymmud, dummy2, ymmud2, dummyC, operation): @@ -590,7 +591,7 @@ def test_object_padding(self): create_new=False) # test data-selection via class method - def test_dataselection(self): + def test_dataselection(self, fulltests): # Create testing objects (regular and swapped dimords) dummy = AnalogData(data=self.data, @@ -601,13 +602,23 @@ def test_dataselection(self): samplerate=self.samplerate, dimord=AnalogData._defaultDimord[::-1]) + # Randomly pick one selection unless tests are run with `--full` + if fulltests: + trialSels = trialSelections + chanSels = chanSelections + timeSels = timeSelections + else: + trialSels = [random.choice(trialSelections)] + chanSels = [random.choice(chanSelections)] + timeSels = [random.choice(timeSelections)] + for obj in [dummy, ymmud]: idx = [slice(None)] * len(obj.dimord) timeIdx = obj.dimord.index("time") chanIdx = obj.dimord.index("channel") - for trialSel in trialSelections: - for chanSel in chanSelections: - for timeSel in timeSelections: + for trialSel in trialSels: + for chanSel in chanSels: + for timeSel in timeSels: kwdict = {} kwdict["trials"] = trialSel kwdict["channel"] = chanSel @@ -615,7 +626,7 @@ def test_dataselection(self): cfg = StructDict(kwdict) # data selection via class-method + `Selector` instance for indexing selected = obj.selectdata(**kwdict) - time.sleep(0.05) + time.sleep(0.001) selector = Selector(obj, kwdict) idx[chanIdx] = selector.channel for tk, trialno in enumerate(selector.trials): @@ -628,10 +639,10 @@ def test_dataselection(self): selectdata(cfg) assert np.array_equal(cfg.out.channel, selected.channel) assert np.array_equal(cfg.out.data, selected.data) - time.sleep(0.05) + time.sleep(0.001) # test arithmetic operations - def test_ang_arithmetic(self): + def test_ang_arithmetic(self, fulltests): # Create testing objects and corresponding arrays to perform arithmetics with dummy = AnalogData(data=self.data, @@ -659,22 +670,25 @@ def test_ang_arithmetic(self): _base_op_tests(dummy, ymmud, dummy2, ymmud2, None, operation) - # Now the most complicated case: user-defined subset selections are present - kwdict = {} - kwdict["trials"] = trialSelections[1] - kwdict["channel"] = chanSelections[3] - kwdict[timeSelections[4][0]] = timeSelections[4][1] - _selection_op_tests(dummy, ymmud, dummy2, ymmud2, kwdict, operation) - - # # Go through full selection stack - WARNING: this takes > 15 minutes - # for trialSel in trialSelections: - # for chanSel in chanSelections: - # for timeSel in timeSelections: - # kwdict = {} - # kwdict["trials"] = trialSel - # kwdict["channels"] = chanSel - # kwdict[timeSel[0]] = timeSel[1] - # _selection_op_tests(dummy, ymmud, dummy2, ymmud2, kwdict, operation) + # Go through full selection stack - WARNING: this takes > 15 minutes + if fulltests: + for trialSel in trialSelections: + for chanSel in chanSelections: + for timeSel in timeSelections: + kwdict = {} + kwdict["trials"] = trialSel + kwdict["channel"] = chanSel + kwdict[timeSel[0]] = timeSel[1] + ScalarSelectors = [isinstance(val, (numbers.Number, str)) for val in kwdict.values()] + if sum(ScalarSelectors) >= 2: + continue + _selection_op_tests(dummy, ymmud, dummy2, ymmud2, kwdict, operation) + else: + kwdict = {} + kwdict["trials"] = trialSelections[1] + kwdict["channel"] = chanSelections[3] + kwdict[timeSelections[4][0]] = timeSelections[4][1] + _selection_op_tests(dummy, ymmud, dummy2, ymmud2, kwdict, operation) # Finally, perform a representative chained operation to ensure chaining works result = (dummy + dummy2) / dummy ** 3 @@ -683,17 +697,20 @@ def test_ang_arithmetic(self): (dummy.trials[tk] + dummy2.trials[tk]) / dummy.trials[tk] ** 3) @skip_without_acme - def test_parallel(self, testcluster): + def test_parallel(self, testcluster, fulltests): # repeat selected test w/parallel processing engine client = dd.Client(testcluster) - par_tests = ["test_relative_array_padding", - "test_absolute_nextpow2_array_padding", - "test_object_padding", - "test_dataselection", - "test_ang_arithmetic"] - for test in par_tests: + quick_tests = ["test_relative_array_padding", + "test_absolute_nextpow2_array_padding", + "test_object_padding"] + slow_tests = ["test_dataselection", + "test_ang_arithmetic"] + for test in quick_tests: getattr(self, test)() flush_local_cluster(testcluster) + for test in slow_tests: + getattr(self, test)(fulltests) + flush_local_cluster(testcluster) client.close() @@ -799,7 +816,7 @@ def test_sd_saveload(self): del dummy, dummy2 # test data-selection via class method - def test_sd_dataselection(self): + def test_sd_dataselection(self, fulltests): # Create testing objects (regular and swapped dimords) dummy = SpectralData(data=self.data, @@ -812,17 +829,31 @@ def test_sd_dataselection(self): taper=["TestTaper_0{}".format(k) for k in range(1, self.nt + 1)], dimord=SpectralData._defaultDimord[::-1]) + # Randomly pick one selection unless tests are run with `--full` + if fulltests: + trialSels = trialSelections + chanSels = chanSelections + timeSels = timeSelections + freqSels = freqSelections + taperSels = taperSelections + else: + trialSels = [random.choice(trialSelections)] + chanSels = [random.choice(chanSelections)] + timeSels = [random.choice(timeSelections)] + freqSels = [random.choice(freqSelections)] + taperSels = [random.choice(taperSelections)] + for obj in [dummy, ymmud]: idx = [slice(None)] * len(obj.dimord) timeIdx = obj.dimord.index("time") chanIdx = obj.dimord.index("channel") freqIdx = obj.dimord.index("freq") taperIdx = obj.dimord.index("taper") - for trialSel in trialSelections: - for chanSel in chanSelections: - for timeSel in timeSelections: - for freqSel in freqSelections: - for taperSel in taperSelections: + for trialSel in trialSels: + for chanSel in chanSels: + for timeSel in timeSels: + for freqSel in freqSels: + for taperSel in taperSels: kwdict = {} kwdict["trials"] = trialSel kwdict["channel"] = chanSel @@ -832,7 +863,7 @@ def test_sd_dataselection(self): cfg = StructDict(kwdict) # data selection via class-method + `Selector` instance for indexing selected = obj.selectdata(**kwdict) - time.sleep(0.05) + time.sleep(0.001) selector = Selector(obj, kwdict) idx[chanIdx] = selector.channel idx[freqIdx] = selector.freq @@ -850,10 +881,10 @@ def test_sd_dataselection(self): assert np.array_equal(cfg.out.freq, selected.freq) assert np.array_equal(cfg.out.taper, selected.taper) assert np.array_equal(cfg.out.data, selected.data) - time.sleep(0.05) + time.sleep(0.001) # test arithmetic operations - def test_sd_arithmetic(self): + def test_sd_arithmetic(self, fulltests): # Create testing objects and corresponding arrays to perform arithmetics with dummy = SpectralData(data=self.data, @@ -889,28 +920,32 @@ def test_sd_arithmetic(self): _base_op_tests(dummy, ymmud, dummy2, ymmud2, dummyC, operation) - # Now the most complicated case: user-defined subset selections are present - kwdict = {} - kwdict["trials"] = trialSelections[1] - kwdict["channel"] = chanSelections[3] - kwdict[timeSelections[4][0]] = timeSelections[4][1] - kwdict[freqSelections[4][0]] = freqSelections[4][1] - kwdict["taper"] = taperSelections[2] - _selection_op_tests(dummy, ymmud, dummy2, ymmud2, kwdict, operation) - - # # Go through full selection stack - WARNING: this takes > 1 hour - # for trialSel in trialSelections: - # for chanSel in chanSelections: - # for timeSel in timeSelections: - # for freqSel in freqSelections: - # for taperSel in taperSelections: - # kwdict = {} - # kwdict["trials"] = trialSel - # kwdict["channels"] = chanSel - # kwdict[timeSel[0]] = timeSel[1] - # kwdict[freqSel[0]] = freqSel[1] - # kwdict["tapers"] = taperSel - # _selection_op_tests(dummy, ymmud, dummy2, ymmud2, kwdict, operation) + # Go through full selection stack - WARNING: this takes > 1 hour + if fulltests: + for trialSel in trialSelections: + for chanSel in chanSelections: + for timeSel in timeSelections: + for freqSel in freqSelections: + for taperSel in taperSelections: + kwdict = {} + kwdict["trials"] = trialSel + kwdict["channel"] = chanSel + kwdict[timeSel[0]] = timeSel[1] + kwdict[freqSel[0]] = freqSel[1] + kwdict["taper"] = taperSel + ScalarSelectors = [isinstance(val, (numbers.Number, str)) for val in kwdict.values()] + if sum(ScalarSelectors) >= 2: + continue + _selection_op_tests(dummy, ymmud, dummy2, ymmud2, kwdict, operation) + else: + kwdict = {} + kwdict["trials"] = trialSelections[1] + kwdict["channel"] = chanSelections[3] + kwdict[timeSelections[4][0]] = timeSelections[4][1] + kwdict[freqSelections[4][0]] = freqSelections[4][1] + kwdict["taper"] = taperSelections[2] + _selection_op_tests(dummy, ymmud, dummy2, ymmud2, kwdict, operation) + # Finally, perform a representative chained operation to ensure chaining works result = (dummy + dummy2) / dummy ** 3 @@ -919,12 +954,12 @@ def test_sd_arithmetic(self): (dummy.trials[tk] + dummy2.trials[tk]) / dummy.trials[tk] ** 3) @skip_without_acme - def test_sd_parallel(self, testcluster): + def test_sd_parallel(self, testcluster, fulltests): # repeat selected test w/parallel processing engine client = dd.Client(testcluster) par_tests = ["test_sd_dataselection", "test_sd_arithmetic"] for test in par_tests: - getattr(self, test)() + getattr(self, test)(fulltests) flush_local_cluster(testcluster) client.close() @@ -1033,7 +1068,7 @@ def test_csd_saveload(self): del dummy, dummy2 # test data-selection via class method - def test_csd_dataselection(self): + def test_csd_dataselection(self, fulltests): # Create testing objects (regular and swapped dimords) dummy = CrossSpectralData(data=self.data, @@ -1044,17 +1079,29 @@ def test_csd_dataselection(self): dimord=CrossSpectralData._defaultDimord[::-1]) ymmud.trialdefinition = self.trl + # Randomly pick one selection unless tests are run with `--full` + if fulltests: + trialSels = trialSelections + chanSels = chanSelections[2:] + timeSels = timeSelections + freqSels = freqSelections + else: + trialSels = [random.choice(trialSelections)] + chanSels = [random.choice(chanSelections[2:])] + timeSels = [random.choice(timeSelections)] + freqSels = [random.choice(freqSelections)] + for obj in [dummy, ymmud]: idx = [slice(None)] * len(obj.dimord) timeIdx = obj.dimord.index("time") chanIdx = obj.dimord.index("channel_i") chanJdx = obj.dimord.index("channel_j") freqIdx = obj.dimord.index("freq") - for trialSel in trialSelections: - for chaniSel in chanSelections[2:]: - for chanjSel in chanSelections[2:]: - for timeSel in timeSelections: - for freqSel in freqSelections: + for trialSel in trialSels: + for chaniSel in chanSels: + for chanjSel in chanSels: + for timeSel in timeSels: + for freqSel in freqSels: kwdict = {} kwdict["trials"] = trialSel kwdict["channel_i"] = chaniSel @@ -1064,7 +1111,7 @@ def test_csd_dataselection(self): cfg = StructDict(kwdict) # data selection via class-method + `Selector` instance for indexing selected = obj.selectdata(**kwdict) - time.sleep(0.05) + time.sleep(0.001) selector = Selector(obj, kwdict) idx[chanIdx] = selector.channel_i idx[chanJdx] = selector.channel_j @@ -1084,10 +1131,10 @@ def test_csd_dataselection(self): assert np.array_equal(cfg.out.channel_j, selected.channel_j) assert np.array_equal(cfg.out.freq, selected.freq) assert np.array_equal(cfg.out.data, selected.data) - time.sleep(0.05) + time.sleep(0.001) # test arithmetic operations - def test_csd_arithmetic(self): + def test_csd_arithmetic(self, fulltests): # Create testing objects and corresponding arrays to perform arithmetics with dummy = CrossSpectralData(data=self.data, @@ -1118,14 +1165,28 @@ def test_csd_arithmetic(self): _base_op_tests(dummy, ymmud, dummy2, ymmud2, dummyC, operation) - # Now the most complicated case: user-defined subset selections are present - kwdict = {} - kwdict["trials"] = trialSelections[1] - kwdict["channel_i"] = chanSelections[3] - kwdict["channel_j"] = chanSelections[4] - kwdict[timeSelections[4][0]] = timeSelections[4][1] - kwdict[freqSelections[4][0]] = freqSelections[4][1] - _selection_op_tests(dummy, ymmud, dummy2, ymmud2, kwdict, operation) + # Go through full selection stack - WARNING: this takes > 1 hour + if fulltests: + for trialSel in trialSelections: + for chaniSel in chanSelections[2:]: + for chanjSel in chanSelections[2:]: + for timeSel in timeSelections: + for freqSel in freqSelections: + kwdict = {} + kwdict["trials"] = trialSel + kwdict["channel_i"] = chaniSel + kwdict["channel_j"] = chanjSel + kwdict[timeSel[0]] = timeSel[1] + kwdict[freqSel[0]] = freqSel[1] + _selection_op_tests(dummy, ymmud, dummy2, ymmud2, kwdict, operation) + else: + kwdict = {} + kwdict["trials"] = trialSelections[1] + kwdict["channel_i"] = chanSelections[3] + kwdict["channel_j"] = chanSelections[4] + kwdict[timeSelections[4][0]] = timeSelections[4][1] + kwdict[freqSelections[4][0]] = freqSelections[4][1] + _selection_op_tests(dummy, ymmud, dummy2, ymmud2, kwdict, operation) # Finally, perform a representative chained operation to ensure chaining works result = (dummy + dummy2) / dummy ** 3 @@ -1134,11 +1195,11 @@ def test_csd_arithmetic(self): (dummy.trials[tk] + dummy2.trials[tk]) / dummy.trials[tk] ** 3) @skip_without_acme - def test_csd_parallel(self, testcluster): + def test_csd_parallel(self, testcluster, fulltests): # repeat selected test w/parallel processing engine client = dd.Client(testcluster) par_tests = ["test_csd_dataselection", "test_csd_arithmetic"] for test in par_tests: - getattr(self, test)() + getattr(self, test)(fulltests) flush_local_cluster(testcluster) client.close() diff --git a/syncopy/tests/test_discretedata.py b/syncopy/tests/test_discretedata.py index 8aeff25b3..1cadec4c3 100644 --- a/syncopy/tests/test_discretedata.py +++ b/syncopy/tests/test_discretedata.py @@ -7,6 +7,7 @@ import os import tempfile import time +import random import pytest import numpy as np @@ -148,7 +149,7 @@ def test_saveload(self): time.sleep(0.1) # test data-selection via class method - def test_dataselection(self): + def test_dataselection(self, fulltests): # Create testing objects (regular and swapped dimords) dummy = SpikeData(data=self.data, @@ -187,14 +188,26 @@ def test_dataselection(self): timeSelections = list(zip(["toi"] * len(toiSelections), toiSelections)) \ + list(zip(["toilim"] * len(toilimSelections), toilimSelections)) + # Randomly pick one selection unless tests are run with `--full` + if fulltests: + trialSels = trialSelections + chanSels = chanSelections + unitSels = unitSelections + timeSels = timeSelections + else: + trialSels = [random.choice(trialSelections)] + chanSels = [random.choice(chanSelections)] + unitSels = [random.choice(unitSelections)] + timeSels = [random.choice(timeSelections)] + for obj in [dummy, ymmud]: chanIdx = obj.dimord.index("channel") unitIdx = obj.dimord.index("unit") chanArr = np.arange(obj.channel.size) - for trialSel in trialSelections: - for chanSel in chanSelections: - for unitSel in unitSelections: - for timeSel in timeSelections: + for trialSel in trialSels: + for chanSel in chanSels: + for unitSel in unitSels: + for timeSel in timeSels: kwdict = {} kwdict["trials"] = trialSel kwdict["channel"] = chanSel @@ -223,12 +236,12 @@ def test_dataselection(self): assert np.array_equal(cfg.out.data, selected.data) @skip_without_acme - def test_parallel(self, testcluster): + def test_parallel(self, testcluster, fulltests): # repeat selected test w/parallel processing engine client = dd.Client(testcluster) par_tests = ["test_dataselection"] for test in par_tests: - getattr(self, test)() + getattr(self, test)(fulltests) flush_local_cluster(testcluster) client.close() @@ -479,7 +492,7 @@ def test_ed_trialsetting(self): ang_dummy.definetrial(evt_dummy, pre=pre, post=post, trigger=1) # test data-selection via class method - def test_ed_dataselection(self): + def test_ed_dataselection(self, fulltests): # Create testing objects (regular and swapped dimords) dummy = EventData(data=self.data, @@ -511,11 +524,21 @@ def test_ed_dataselection(self): timeSelections = list(zip(["toi"] * len(toiSelections), toiSelections)) \ + list(zip(["toilim"] * len(toilimSelections), toilimSelections)) + # Randomly pick one selection unless tests are run with `--full` + if fulltests: + trialSels = trialSelections + eventidSels = eventidSelections + timeSels = timeSelections + else: + trialSels = [random.choice(trialSelections)] + eventidSels = [random.choice(eventidSelections)] + timeSels = [random.choice(timeSelections)] + for obj in [dummy, ymmud]: eventidIdx = obj.dimord.index("eventid") - for trialSel in trialSelections: - for eventidSel in eventidSelections: - for timeSel in timeSelections: + for trialSel in trialSels: + for eventidSel in eventidSels: + for timeSel in timeSels: kwdict = {} kwdict["trials"] = trialSel kwdict["eventid"] = eventidSel @@ -540,11 +563,11 @@ def test_ed_dataselection(self): assert np.array_equal(cfg.out.data, selected.data) @skip_without_acme - def test_ed_parallel(self, testcluster): + def test_ed_parallel(self, testcluster, fulltests): # repeat selected test w/parallel processing engine client = dd.Client(testcluster) par_tests = ["test_ed_dataselection"] for test in par_tests: - getattr(self, test)() + getattr(self, test)(fulltests) flush_local_cluster(testcluster) client.close() diff --git a/syncopy/tests/test_specest.py b/syncopy/tests/test_specest.py index 2004988e7..ac8bcde26 100644 --- a/syncopy/tests/test_specest.py +++ b/syncopy/tests/test_specest.py @@ -7,6 +7,7 @@ import os import tempfile import inspect +import random import psutil import gc import pytest @@ -148,6 +149,12 @@ class TestMTMFFT(): # Error tolerance for frequency-matching ftol = 0.25 + # Helper function that reduces dataselections (keep `None` selection no matter what) + def test_cut_selections(self, fulltests): + if not fulltests: + self.sigdataSelections.pop(random.choice([-1, 1])) + self.artdataSelections.pop(random.choice([-1, 1])) + def test_output(self): # ensure that output type specification is respected for select in self.sigdataSelections: @@ -386,7 +393,7 @@ def test_parallel(self, testcluster): # (skip VirtualData tests since ``wrapper_io`` expects valid headers) client = dd.Client(testcluster) all_tests = [attr for attr in self.__dir__() - if (inspect.ismethod(getattr(self, attr)) and attr != "test_parallel")] + if (inspect.ismethod(getattr(self, attr)) and attr not in ["test_parallel", "test_cut_selections"])] all_tests.remove("test_vdata") for test in all_tests: getattr(self, test)() @@ -481,26 +488,32 @@ class TestMTMConvol(): "channel": range(0, nChan2), "toilim": [-20, 60.8]}] - def test_tf_output(self): + # Helper function that reduces dataselections (keep `None` selection no matter what) + def test_tf_cut_selections(self, fulltests): + if not fulltests: + self.dataSelections.pop(random.choice([-1, 1])) + + def test_tf_output(self, fulltests): # Set up basic TF analysis parameters to not slow down things too much cfg = get_defaults(freqanalysis) cfg.method = "mtmconvol" cfg.taper = "hann" cfg.toi = np.linspace(-20, 60, 10) cfg.t_ftimwin = 1.0 + outputDict = {"fourier" : "complex", "abs" : "float", "pow" : "float"} for select in self.dataSelections: select = self.dataSelections[-1] cfg.select = select - cfg.output = "fourier" - tfSpec = freqanalysis(cfg, self.tfData) - assert "complex" in tfSpec.data.dtype.name - cfg.output = "abs" - tfSpec = freqanalysis(cfg, self.tfData) - assert "float" in tfSpec.data.dtype.name - cfg.output = "pow" - tfSpec = freqanalysis(cfg, self.tfData) - assert "float" in tfSpec.data.dtype.name + if fulltests: + for key, value in outputDict.items(): + cfg.output = key + tfSpec = freqanalysis(cfg, self.tfData) + assert value in tfSpec.data.dtype.name + else: # randomly pick from 'fourier', 'abs' and 'pow' + cfg.output = random.choice(list(outputDict.keys())) + tfSpec = freqanalysis(cfg, self.tfData) + assert outputDict[cfg.output] in tfSpec.data.dtype.name def test_tf_allocout(self): # use `mtmconvol` w/pre-allocated output object @@ -726,7 +739,7 @@ def test_tf_toi(self): freqanalysis(cfg, self.tfData) assert "Invalid value of `toi`: 'unsorted list/array'" in str(spyval.value) - def test_tf_irregular_trials(self): + def test_tf_irregular_trials(self, fulltests): # Settings for computing "full" non-overlapping TF-spectrum with DPSS tapers: # ensure non-equidistant/overlapping trials are processed (padded) correctly # also make sure ``toi = "all"`` works under any circumstance @@ -737,9 +750,17 @@ def test_tf_irregular_trials(self): cfg.output = "pow" cfg.keeptapers = True + # Reduce test-data size for quick test runs + if fulltests: + nTrials = 5 + nChannels = 8 + else: + nTrials = 2 + nChannels = 2 + # start harmless: equidistant trials w/multiple tapers cfg.toi = 0.0 - artdata = generate_artificial_data(nTrials=5, nChannels=16, + artdata = generate_artificial_data(nTrials=nTrials, nChannels=nChannels, equidistant=True, inmemory=False) tfSpec = freqanalysis(artdata, **cfg) assert tfSpec.taper.size >= 1 @@ -748,7 +769,7 @@ def test_tf_irregular_trials(self): # to process all time-points via `stft`, reduce dataset size (avoid oom kills) cfg.toi = "all" - artdata = generate_artificial_data(nTrials=5, nChannels=4, + artdata = generate_artificial_data(nTrials=nTrials, nChannels=nChannels, equidistant=True, inmemory=False) tfSpec = freqanalysis(artdata, **cfg) for tk, origTime in enumerate(artdata.time): @@ -756,7 +777,7 @@ def test_tf_irregular_trials(self): # non-equidistant trials w/multiple tapers cfg.toi = 0.0 - artdata = generate_artificial_data(nTrials=5, nChannels=8, + artdata = generate_artificial_data(nTrials=nTrials, nChannels=nChannels, equidistant=False, inmemory=False) tfSpec = freqanalysis(artdata, **cfg) assert tfSpec.taper.size >= 1 @@ -769,7 +790,7 @@ def test_tf_irregular_trials(self): # same + reversed dimensional order in input object cfg.toi = 0.0 - cfg.data = generate_artificial_data(nTrials=5, nChannels=8, + cfg.data = generate_artificial_data(nTrials=nTrials, nChannels=nChannels, equidistant=False, inmemory=False, dimord=AnalogData._defaultDimord[::-1]) tfSpec = freqanalysis(cfg) @@ -783,7 +804,7 @@ def test_tf_irregular_trials(self): # same + overlapping trials cfg.toi = 0.0 - cfg.data = generate_artificial_data(nTrials=5, nChannels=4, + cfg.data = generate_artificial_data(nTrials=nTrials, nChannels=nChannels, equidistant=False, inmemory=False, dimord=AnalogData._defaultDimord[::-1], overlapping=True) @@ -798,14 +819,20 @@ def test_tf_irregular_trials(self): @skip_without_acme @skip_low_mem - def test_tf_parallel(self, testcluster): + def test_tf_parallel(self, testcluster, fulltests): # collect all tests of current class and repeat them running concurrently client = dd.Client(testcluster) - all_tests = [attr for attr in self.__dir__() - if (inspect.ismethod(getattr(self, attr)) and attr != "test_tf_parallel")] - for test in all_tests: + quick_tests = [attr for attr in self.__dir__() + if (inspect.ismethod(getattr(self, attr)) and attr not in ["test_tf_parallel", "test_tf_cut_selections"])] + slow_tests = [] + slow_tests.append(quick_tests.pop(quick_tests.index("test_tf_output"))) + slow_tests.append(quick_tests.pop(quick_tests.index("test_tf_irregular_trials"))) + for test in quick_tests: getattr(self, test)() flush_local_cluster(testcluster) + for test in slow_tests: + getattr(self, test)(fulltests) + flush_local_cluster(testcluster) # now create uniform `cfg` for remaining SLURM tests cfg = StructDict() @@ -815,14 +842,22 @@ def test_tf_parallel(self, testcluster): cfg.toi = 0 cfg.output = "pow" + # reduce test dataset size unless we're in `--full` mode + if fulltests: + nChannels = self.nChannels + nTrials = self.nTrials + else: + nChannels = 3 + nTrials = 2 + # no. of HDF5 files that will make up virtual data-set in case of channel-chunking chanPerWrkr = 2 - nFiles = self.nTrials * (int(self.nChannels/chanPerWrkr) - + int(self.nChannels % chanPerWrkr > 0)) + nFiles = nTrials * (int(nChannels/chanPerWrkr) + + int(nChannels % chanPerWrkr > 0)) # simplest case: equidistant trial spacing, all in memory - fileCount = [self.nTrials, nFiles] - artdata = generate_artificial_data(nTrials=self.nTrials, nChannels=self.nChannels, + fileCount = [nTrials, nFiles] + artdata = generate_artificial_data(nTrials=nTrials, nChannels=nChannels, inmemory=True) for k, chan_per_worker in enumerate([None, chanPerWrkr]): cfg.chan_per_worker = chan_per_worker @@ -832,7 +867,7 @@ def test_tf_parallel(self, testcluster): # non-equidistant trial spacing cfg.keeptapers = False - artdata = generate_artificial_data(nTrials=self.nTrials, nChannels=self.nChannels, + artdata = generate_artificial_data(nTrials=nTrials, nChannels=nChannels, inmemory=True, equidistant=False) expectedFreqs = np.arange(artdata.samplerate / 2 + 1) for chan_per_worker in enumerate([None, chanPerWrkr]): @@ -844,7 +879,7 @@ def test_tf_parallel(self, testcluster): cfg.output = "abs" cfg.tapsmofrq = 10 cfg.keeptapers = True - artdata = generate_artificial_data(nTrials=self.nTrials, nChannels=self.nChannels, + artdata = generate_artificial_data(nTrials=nTrials, nChannels=nChannels, inmemory=False) for chan_per_worker in enumerate([None, chanPerWrkr]): tfSpec = freqanalysis(artdata, cfg) @@ -854,7 +889,7 @@ def test_tf_parallel(self, testcluster): cfg.keeptapers = False cfg.keeptrials = "no" cfg.output = "pow" - artdata = generate_artificial_data(nTrials=self.nTrials, nChannels=self.nChannels, + artdata = generate_artificial_data(nTrials=nTrials, nChannels=nChannels, inmemory=False, equidistant=True, overlapping=True) expectedFreqs = np.arange(artdata.samplerate / 2 + 1) @@ -862,7 +897,7 @@ def test_tf_parallel(self, testcluster): assert np.array_equal(tfSpec.freq, expectedFreqs) assert tfSpec.taper.size == 1 assert np.array_equal(np.unique(np.floor(artdata.time[0])), tfSpec.time[0]) - assert tfSpec.data.shape == (tfSpec.time[0].size, 1, expectedFreqs.size, self.nChannels) + assert tfSpec.data.shape == (tfSpec.time[0].size, 1, expectedFreqs.size, nChannels) client.close() @@ -887,6 +922,11 @@ class TestWavelet(): "channel": range(0, int(nChannels / 2)), "toilim": [-20, 60.8]}] + # Helper function that reduces dataselections (keep `None` selection no matter what) + def test_wav_cut_selections(self, fulltests): + if not fulltests: + self.dataSelections.pop(random.choice([-1, 1])) + @skip_low_mem def test_wav_solution(self): @@ -1088,11 +1128,11 @@ def test_wav_irregular_trials(self): @skip_without_acme @skip_low_mem - def test_wav_parallel(self, testcluster): + def test_wav_parallel(self, testcluster, fulltests): # collect all tests of current class and repeat them running concurrently client = dd.Client(testcluster) all_tests = [attr for attr in self.__dir__() - if (inspect.ismethod(getattr(self, attr)) and attr != "test_wav_parallel")] + if (inspect.ismethod(getattr(self, attr)) and attr not in ["test_wav_parallel", "test_wav_cut_selections"])] for test in all_tests: getattr(self, test)() flush_local_cluster(testcluster) @@ -1104,14 +1144,21 @@ def test_wav_parallel(self, testcluster): cfg.output = "pow" cfg.toi = "all" + # reduce test dataset size unless we're in `--full` mode + if fulltests: + nChannels = self.nChannels + nTrials = self.nTrials + else: + nChannels = 3 + nTrials = 2 + # no. of HDF5 files that will make up virtual data-set in case of channel-chunking chanPerWrkr = 2 - nFiles = self.nTrials * (int(self.nChannels/chanPerWrkr) - + int(self.nChannels % chanPerWrkr > 0)) + nFiles = nTrials * (int(nChannels/chanPerWrkr) + int(nChannels % chanPerWrkr > 0)) # simplest case: equidistant trial spacing, all in memory - fileCount = [self.nTrials, nFiles] - artdata = generate_artificial_data(nTrials=self.nTrials, nChannels=self.nChannels, + fileCount = [nTrials, nFiles] + artdata = generate_artificial_data(nTrials=nTrials, nChannels=nChannels, inmemory=True) for k, chan_per_worker in enumerate([None, chanPerWrkr]): cfg.chan_per_worker = chan_per_worker @@ -1121,7 +1168,7 @@ def test_wav_parallel(self, testcluster): # non-equidistant trial spacing cfg.keeptapers = False - artdata = generate_artificial_data(nTrials=self.nTrials, nChannels=self.nChannels, + artdata = generate_artificial_data(nTrials=nTrials, nChannels=nChannels, inmemory=True, equidistant=False) for chan_per_worker in enumerate([None, chanPerWrkr]): tfSpec = freqanalysis(artdata, cfg) @@ -1130,7 +1177,7 @@ def test_wav_parallel(self, testcluster): # equidistant trial spacing cfg.output = "abs" - artdata = generate_artificial_data(nTrials=self.nTrials, nChannels=self.nChannels, + artdata = generate_artificial_data(nTrials=nTrials, nChannels=nChannels, inmemory=False) for chan_per_worker in enumerate([None, chanPerWrkr]): tfSpec = freqanalysis(artdata, cfg) @@ -1141,12 +1188,12 @@ def test_wav_parallel(self, testcluster): cfg.keeptrials = "no" cfg.foilim = [1, 250] expectedFreqs = np.arange(1, cfg.foilim[1] + 1) - artdata = generate_artificial_data(nTrials=self.nTrials, nChannels=self.nChannels, + artdata = generate_artificial_data(nTrials=nTrials, nChannels=nChannels, inmemory=False, equidistant=True, overlapping=True) tfSpec = freqanalysis(artdata, cfg) assert np.allclose(tfSpec.freq, expectedFreqs) - assert tfSpec.data.shape == (tfSpec.time[0].size, 1, expectedFreqs.size, self.nChannels) + assert tfSpec.data.shape == (tfSpec.time[0].size, 1, expectedFreqs.size, nChannels) client.close() @@ -1171,8 +1218,13 @@ class TestSuperlet(): "channel": range(0, int(nChannels / 2)), "toilim": [-20, 60.8]}] + # Helper function that reduces dataselections (keep `None` selection no matter what) + def test_slet_cut_selections(self, fulltests): + if not fulltests: + self.dataSelections.pop(random.choice([-1, 1])) + @skip_low_mem - def test_slet_solution(self): + def test_slet_solution(self, fulltests): # Compute TF specturm across entire time-interval (use integer-valued # time-points as wavelet centroids) @@ -1208,22 +1260,21 @@ def test_slet_solution(self): # Compute TF objects w\w/o`foi`/`foilim` cfg.select = select - tfSpec = freqanalysis(cfg, self.tfData) cfg.foi = maxFreqs tfSpecFoi = freqanalysis(cfg, self.tfData) cfg.foi = None + assert np.allclose(tfSpecFoi.freq, maxFreqs) cfg.foilim = [maxFreqs.min(), maxFreqs.max()] tfSpecFoiLim = freqanalysis(cfg, self.tfData) cfg.foilim = None - - # Ensure TF objects contain expected/requested frequencies - assert 0.02 > tfSpec.freq.min() > 0 - assert tfSpec.freq.max() == (self.tfData.samplerate / 2) - assert tfSpec.freq.size > 60 - assert np.allclose(tfSpecFoi.freq, maxFreqs) assert np.allclose(tfSpecFoiLim.freq, foilimFreqs) + if fulltests: + tfSpec = freqanalysis(cfg, self.tfData) + assert 0.02 > tfSpec.freq.min() > 0 + assert tfSpec.freq.max() == (self.tfData.samplerate / 2) + assert tfSpec.freq.size > 60 - for tk, trlArr in enumerate(tfSpec.trials): + for tk, trlArr in enumerate(tfSpecFoi.trials): # Get reference trial-number in input object trlNo = tk @@ -1231,11 +1282,13 @@ def test_slet_solution(self): trlNo = select["trials"][tk] # Ensure timing array was computed correctly and independent of `foi`/`foilim` - assert np.array_equal(timeArr, tfSpec.time[tk]) - assert np.array_equal(tfSpec.time[tk], tfSpecFoi.time[tk]) + assert np.array_equal(timeArr, tfSpecFoi.time[tk]) assert np.array_equal(tfSpecFoi.time[tk], tfSpecFoiLim.time[tk]) + if fulltests: + assert np.array_equal(timeArr, tfSpec.time[tk]) + assert np.array_equal(tfSpec.time[tk], tfSpecFoi.time[tk]) - for chan in range(tfSpec.channel.size): + for chan in range(tfSpecFoi.channel.size): # Get reference channel in input object to determine underlying modulator chanNo = chan @@ -1250,22 +1303,6 @@ def test_slet_solution(self): modulator = self.modulators[timeSelection, modIdx] modCounts = [sum(modulator == modulator.min()), sum(modulator == modulator.max())] - # Be more lenient w/`tfSpec`: don't scan for min/max freq, but all peaks at once - # (auto-scale resolution potentially too coarse to differentiate b/w min/max); - # consider peak-count equal up to 2 misses - Zxx = trlArr[tuple(tfIdx)].squeeze() - ZxxMax = Zxx.max() - ZxxThresh = 0.2 * ZxxMax - _, freqPeaks = np.where(Zxx >= (ZxxMax - ZxxThresh)) - peakVals, peakCounts = np.unique(freqPeaks, return_counts=True) - freqPeak = peakVals[peakCounts.argmax()] - modCount = np.ceil(sum(modCounts) / 2) - peakProfile = Zxx[:, freqPeak - 1 : freqPeak + 2].mean(axis=1) - peaks, _ = scisig.find_peaks(peakProfile, height=2*ZxxThresh, distance=5) - if np.abs(peaks.size - modCount) > 2: - modCount = sum(modCounts) - assert np.abs(peaks.size - modCount) <= 2 - # Now for `tfSpecFoi`/`tfSpecFoiLim` on the other side be more # stringent and really count maxima/minima (frequency values have # been explicitly queried, must not be too coarse); that said, @@ -1285,7 +1322,24 @@ def test_slet_solution(self): peaks, _ = scisig.find_peaks(peakProfile, prominence=0.75*height, height=height, distance=5) assert np.abs(peaks.size - modCounts[fk]) <= 2 - def test_slet_toi(self): + # Be more lenient w/`tfSpec`: don't scan for min/max freq, but all peaks at once + # (auto-scale resolution potentially too coarse to differentiate b/w min/max); + # consider peak-count equal up to 2 misses + if fulltests: + Zxx = trlArr[tuple(tfIdx)].squeeze() + ZxxMax = Zxx.max() + ZxxThresh = 0.2 * ZxxMax + _, freqPeaks = np.where(Zxx >= (ZxxMax - ZxxThresh)) + peakVals, peakCounts = np.unique(freqPeaks, return_counts=True) + freqPeak = peakVals[peakCounts.argmax()] + modCount = np.ceil(sum(modCounts) / 2) + peakProfile = Zxx[:, freqPeak - 1 : freqPeak + 2].mean(axis=1) + peaks, _ = scisig.find_peaks(peakProfile, height=2*ZxxThresh, distance=5) + if np.abs(peaks.size - modCount) > 2: + modCount = sum(modCounts) + assert np.abs(peaks.size - modCount) <= 2 + + def test_slet_toi(self, fulltests): # Don't keep trials to speed things up a bit cfg = get_defaults(freqanalysis) cfg.method = "superlet" @@ -1299,6 +1353,10 @@ def test_slet_toi(self): np.arange(-15, -10, 1/self.tfData.samplerate), np.arange(1, 20, 2)] + # Just pick one `toi` at random for quickly running tests + if not fulltests: + toiArrs = [random.choice(toiArrs)] + # Combine `toi`-testing w/in-place data-pre-selection for select in self.dataSelections: cfg.select = select @@ -1330,7 +1388,7 @@ def test_slet_toi(self): freqanalysis(cfg, self.tfData) assert "Invalid value of `toi`: 'unsorted list/array'" in str(spyval.value) - def test_slet_irregular_trials(self): + def test_slet_irregular_trials(self, fulltests): # Set up wavelet to compute "full" TF spectrum for all time-points cfg = get_defaults(freqanalysis) cfg.method = "superlet" @@ -1338,22 +1396,30 @@ def test_slet_irregular_trials(self): cfg.output = "pow" cfg.toi = "all" + # Reduce test-data size for quick test runs + if fulltests: + nTrials = 5 + nChannels = 16 + else: + nTrials = 2 + nChannels = 2 + # start harmless: equidistant trials w/multiple tapers - artdata = generate_artificial_data(nTrials=5, nChannels=16, + artdata = generate_artificial_data(nTrials=nTrials, nChannels=nChannels, equidistant=True, inmemory=False) tfSpec = freqanalysis(artdata, **cfg) for tk, origTime in enumerate(artdata.time): assert np.array_equal(origTime, tfSpec.time[tk]) # non-equidistant trials w/multiple tapers - artdata = generate_artificial_data(nTrials=5, nChannels=16, + artdata = generate_artificial_data(nTrials=nTrials, nChannels=nChannels, equidistant=False, inmemory=False) tfSpec = freqanalysis(artdata, **cfg) for tk, origTime in enumerate(artdata.time): assert np.array_equal(origTime, tfSpec.time[tk]) # same + reversed dimensional order in input object - cfg.data = generate_artificial_data(nTrials=5, nChannels=16, + cfg.data = generate_artificial_data(nTrials=nTrials, nChannels=nChannels, equidistant=False, inmemory=False, dimord=AnalogData._defaultDimord[::-1]) tfSpec = freqanalysis(cfg) @@ -1361,23 +1427,23 @@ def test_slet_irregular_trials(self): assert np.array_equal(origTime, tfSpec.time[tk]) # same + overlapping trials - cfg.data = generate_artificial_data(nTrials=5, nChannels=16, - equidistant=False, inmemory=False, - dimord=AnalogData._defaultDimord[::-1], - overlapping=True) + cfg.data = generate_artificial_data(nTrials=nTrials, nChannels=nChannels, + equidistant=False, inmemory=False, + dimord=AnalogData._defaultDimord[::-1], + overlapping=True) tfSpec = freqanalysis(cfg) for tk, origTime in enumerate(cfg.data.time): assert np.array_equal(origTime, tfSpec.time[tk]) @skip_without_acme @skip_low_mem - def test_slet_parallel(self, testcluster): + def test_slet_parallel(self, testcluster, fulltests): # collect all tests of current class and repeat them running concurrently client = dd.Client(testcluster) all_tests = [attr for attr in self.__dir__() - if (inspect.ismethod(getattr(self, attr)) and attr != "test_slet_parallel")] + if (inspect.ismethod(getattr(self, attr)) and attr not in ["test_slet_parallel", "test_cut_slet_selections"])] for test in all_tests: - getattr(self, test)() + getattr(self, test)(fulltests) flush_local_cluster(testcluster) # now create uniform `cfg` for remaining SLURM tests From 784aef64aebf0a520a3d71103db8a6f0ab2a0d31 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Fri, 11 Mar 2022 22:23:05 +0100 Subject: [PATCH 103/166] FIX: Repair pytest ailments - paths and pytest don't mix well... On branch dev Changes to be committed: new file: syncopy/tests/__main__.py --- syncopy/tests/__main__.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 syncopy/tests/__main__.py diff --git a/syncopy/tests/__main__.py b/syncopy/tests/__main__.py new file mode 100644 index 000000000..03a3e6117 --- /dev/null +++ b/syncopy/tests/__main__.py @@ -0,0 +1,9 @@ +import os +import sys + +HERE = os.path.dirname(__file__) + +if __name__ == "__main__": + import pytest + errcode = pytest.main([HERE] + sys.argv[1:]) + sys.exit(errcode) From b4d8b7548ee83a3e6f2a443e6f801999e8492eca Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Fri, 11 Mar 2022 22:59:32 +0100 Subject: [PATCH 104/166] FIX: GH Action fix - hopefully... On branch dev Changes to be committed: modified: .github/workflows/cov_test_workflow.yml --- .github/workflows/cov_test_workflow.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cov_test_workflow.yml b/.github/workflows/cov_test_workflow.yml index f10302f18..e4307057c 100644 --- a/.github/workflows/cov_test_workflow.yml +++ b/.github/workflows/cov_test_workflow.yml @@ -32,7 +32,9 @@ jobs: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest and get coverage run: | - pytest --cov=./ --cov-report=xml + cd syncopy/tests + pytest --color=yes --tb=short --verbose --cov=../../syncopy --cov-config=../../.coveragerc --cov-report=xml + # pytest --cov=./ --cov-report=xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v2 with: From d3fa9bcaced759a0baacb60af4ab584911420eaa Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Fri, 11 Mar 2022 23:27:46 +0100 Subject: [PATCH 105/166] FIX: Hopefully final polish for GH actions - do not test external instance's cleanup in GH VM - do not attempt to run tests in parallel on *free* GH Actions VM On branch dev Changes to be committed: modified: .github/workflows/cov_test_workflow.yml deleted: syncopy/tests/__main__.py modified: syncopy/tests/test_packagesetup.py --- .github/workflows/cov_test_workflow.yml | 7 ++++--- syncopy/tests/__main__.py | 9 --------- syncopy/tests/test_packagesetup.py | 6 ++++++ 3 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 syncopy/tests/__main__.py diff --git a/.github/workflows/cov_test_workflow.yml b/.github/workflows/cov_test_workflow.yml index e4307057c..060f45890 100644 --- a/.github/workflows/cov_test_workflow.yml +++ b/.github/workflows/cov_test_workflow.yml @@ -3,7 +3,9 @@ name: Run tests and determine coverage on: # Triggers the workflow on push or pull request events push: - branches: [ dev ] + branches: + - master + - dev pull_request: # Allows you to run this workflow manually from the Actions tab @@ -13,7 +15,7 @@ jobs: build-linux: runs-on: ubuntu-latest strategy: - max-parallel: 5 + max-parallel: 1 steps: - uses: actions/checkout@v2 - name: Set up Python 3.8 @@ -34,7 +36,6 @@ jobs: run: | cd syncopy/tests pytest --color=yes --tb=short --verbose --cov=../../syncopy --cov-config=../../.coveragerc --cov-report=xml - # pytest --cov=./ --cov-report=xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v2 with: diff --git a/syncopy/tests/__main__.py b/syncopy/tests/__main__.py deleted file mode 100644 index 03a3e6117..000000000 --- a/syncopy/tests/__main__.py +++ /dev/null @@ -1,9 +0,0 @@ -import os -import sys - -HERE = os.path.dirname(__file__) - -if __name__ == "__main__": - import pytest - errcode = pytest.main([HERE] + sys.argv[1:]) - sys.exit(errcode) diff --git a/syncopy/tests/test_packagesetup.py b/syncopy/tests/test_packagesetup.py index a334b0149..18e9eb187 100644 --- a/syncopy/tests/test_packagesetup.py +++ b/syncopy/tests/test_packagesetup.py @@ -11,11 +11,16 @@ import tempfile import importlib import subprocess +import pytest from glob import glob # Local imports import syncopy +# Decorator to decide whether or not to run dask-related tests +skip_in_ghactions = pytest.mark.skipif( + "GITHUB_ACTIONS" in os.environ.keys(), reason="Do not execute by GitHub Actions") + # check if folder creation in `__storage__` works as expected def test_storage_access(): @@ -38,6 +43,7 @@ def test_spytmpdir(): # check if `cleanup` does what it's supposed to do +@skip_in_ghactions def test_cleanup(): # spawn new Python instance, which creates and saves an `AnalogData` object # in custom $SPYTMPDIR; force-kill the process after a few seconds preventing From 89d144bb2dae20d1bfb176d251c7d25d7aeb90a8 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 14 Mar 2022 10:49:37 +0100 Subject: [PATCH 106/166] PR commit - default order for FIRWS adjusted to be 1000 max - fixed some typos On branch preprocessing Your branch is up to date with 'origin/preprocessing'. Changes to be committed: modified: syncopy/preproc/compRoutines.py modified: syncopy/preproc/firws.py modified: syncopy/preproc/preprocessing.py --- syncopy/preproc/compRoutines.py | 6 ++---- syncopy/preproc/firws.py | 2 +- syncopy/preproc/preprocessing.py | 8 ++++---- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/syncopy/preproc/compRoutines.py b/syncopy/preproc/compRoutines.py index 0f8bb156b..836bdadf3 100644 --- a/syncopy/preproc/compRoutines.py +++ b/syncopy/preproc/compRoutines.py @@ -54,13 +54,13 @@ def sinc_filtering_cF(dat, samples in the trial. Higher orders yield a sharper transition width or less 'roll off' of the filter, but are more computationally expensive. - window : {"hamming", "hann", "blackmann", "kaiser"} + window : {"hamming", "hann", "blackman", "kaiser"} The type of taper to use for the sinc function direction : {'twopass', 'onepass', 'onepass-minphase'} Filter direction: `'twopass'` - zero-phase forward and reverse filter, IIR and FIR `'onepass'` - forward filter, introduces group delays for IIR, zerophase for FIR - `'onepass-minphase' - forward causal/minumum phase filter, FIR only + `'onepass-minphase' - forward causal/minimum phase filter, FIR only polyremoval : int or None Order of polynomial used for de-trending data in the time domain prior to filtering. A value of 0 corresponds to subtracting the mean @@ -102,7 +102,6 @@ def sinc_filtering_cF(dat, # detrend if polyremoval == 0: - # SciPy's overwrite_data not working for type='constant' :/ dat = sci.detrend(dat, type='constant', axis=0, overwrite_data=True) elif polyremoval == 1: dat = sci.detrend(dat, type='linear', axis=0, overwrite_data=True) @@ -251,7 +250,6 @@ def but_filtering_cF(dat, # detrend if polyremoval == 0: - # SciPy's overwrite_data not working for type='constant' :/ dat = sci.detrend(dat, type='constant', axis=0, overwrite_data=True) elif polyremoval == 1: dat = sci.detrend(dat, type='linear', axis=0, overwrite_data=True) diff --git a/syncopy/preproc/firws.py b/syncopy/preproc/firws.py index 978b607c8..da3681c89 100644 --- a/syncopy/preproc/firws.py +++ b/syncopy/preproc/firws.py @@ -50,7 +50,7 @@ def design_wsinc(window, order, f_c, filter_type='lp'): ---------- window : str One of `scipy.signal.windows`, good choices are - "blackmann", "hamming" and "hann" + "blackman", "hamming" and "hann" order : int The order, or simply length, of the filter If not even gets incremented by one diff --git a/syncopy/preproc/preprocessing.py b/syncopy/preproc/preprocessing.py index d3c69c286..9258ca9c6 100644 --- a/syncopy/preproc/preprocessing.py +++ b/syncopy/preproc/preprocessing.py @@ -23,7 +23,7 @@ availableFilters = ('but', 'firws') availableFilterTypes = ('lp', 'hp', 'bp', 'bs') availableDirections = ('twopass', 'onepass', 'onepass-minphase') -availableWindows = ("hamming", "hann", "blackmann") +availableWindows = ("hamming", "hann", "blackman") @unwrap_cfg @@ -56,12 +56,12 @@ def preprocessing(data, Order of the filter, default is 6. Higher orders yield a sharper transition width or less 'roll off' of the filter, but are more computationally expensive. - direction : {'twopass', 'onepass', 'onepasse-minphase'} + direction : {'twopass', 'onepass', 'onepass-minphase'} Filter direction: `'twopass'` - zero-phase forward and reverse filter, IIR and FIR `'onepass'` - forward filter, introduces group delays for IIR, zerophase for FIR `'onepass-minphase' - forward causal/minumum phase filter, FIR only - window : {"hamming", "hann", "blackmann"}, optional + window : {"hamming", "hann", "blackman"}, optional The type of window to use for the FIR filter polyremoval : int or None, optional Order of polynomial used for de-trending data in the time domain prior @@ -201,7 +201,7 @@ def preprocessing(data, raise SPYValueError(legal=lgl, varname="direction", actual=direction) if order is None: - order = int(lenTrials.min()) + order = int(lenTrials.min()) if lenTrials.min() < 1000 else 1000 msg = f"Setting order for FIR filter to {order}" SPYInfo(msg) From 500bf4a49b9a7de4a92e95f849a334cc9f860af7 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 14 Mar 2022 10:55:50 +0100 Subject: [PATCH 107/166] Fix local_spy Changes to be committed: modified: syncopy/tests/local_spy.py --- syncopy/tests/local_spy.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/syncopy/tests/local_spy.py b/syncopy/tests/local_spy.py index 426382b3d..83eb16a4d 100644 --- a/syncopy/tests/local_spy.py +++ b/syncopy/tests/local_spy.py @@ -37,9 +37,6 @@ nSamples = 2000 trls = [] - AdjMat = np.zeros((2, 2)) - AdjMat[0, 1] = .25 - for _ in range(nTrials): sig1 = A1 * np.cos(f1 * 2 * np.pi * np.arange(nSamples) / fs) @@ -49,6 +46,11 @@ ad1 = spy.AnalogData(trls, samplerate=500) #spy.preprocessing(ad1, filter_class='d') + trls = [] + AdjMat = np.zeros((2, 2)) + AdjMat[0, 1] = .25 + for _ in range(nTrials): + # defaults AR(2) parameters yield 40Hz peak alphas = [.74, -.46] # broad peak at 60Hz alphas = [0.24, -.46] @@ -60,9 +62,8 @@ trls.append(trl) print(trl.mean()) - ad1 = spy.AnalogData(trls, samplerate=2000) - - spec = spy.freqanalysis(ad1, tapsmofrq=5, keeptrials=False) - coh = spy.connectivityanalysis(ad1, method='coh', tapsmofrq=5) - gr = spy.connectivityanalysis(ad1, method='granger', tapsmofrq=10, polyremoval=0) + ad2 = spy.AnalogData(trls, samplerate=2000) + spec = spy.freqanalysis(ad2, tapsmofrq=5, keeptrials=False) + coh = spy.connectivityanalysis(ad2, method='coh', tapsmofrq=5) + gr = spy.connectivityanalysis(ad2, method='granger', tapsmofrq=10, polyremoval=0) From d1d556a1cb991eeff35dc926449a94c8ca1cf49c Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 14 Mar 2022 12:14:16 +0100 Subject: [PATCH 108/166] CHG: Stft window multiplication - dat *= windows is not (always) the same as dat = dat * window it seems.. On branch preprocessing Your branch is up to date with 'origin/preprocessing'. Changes to be committed: modified: syncopy/specest/stft.py --- syncopy/specest/stft.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncopy/specest/stft.py b/syncopy/specest/stft.py index f4ac20759..6ca4e21f1 100644 --- a/syncopy/specest/stft.py +++ b/syncopy/specest/stft.py @@ -118,7 +118,7 @@ def stft(dat, if window is not None: # Apply window by multiplication - dat *= window + dat = dat * window times = np.arange(nperseg / 2, dat.shape[-1] - nperseg / 2 + 1, nperseg - noverlap) / fs From 823c9d949b63c613060a3a19a30bd79fd7de930c Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Mon, 14 Mar 2022 15:43:49 +0100 Subject: [PATCH 109/166] FIX: Further reduce mtmconvol test-data size for GH actions - run `test_tf_output` using an even smaller dataset On branch preprocessing Changes to be committed: modified: test_specest.py --- syncopy/tests/test_specest.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/syncopy/tests/test_specest.py b/syncopy/tests/test_specest.py index ac8bcde26..2f92c6eb9 100644 --- a/syncopy/tests/test_specest.py +++ b/syncopy/tests/test_specest.py @@ -503,16 +503,17 @@ def test_tf_output(self, fulltests): outputDict = {"fourier" : "complex", "abs" : "float", "pow" : "float"} for select in self.dataSelections: - select = self.dataSelections[-1] - cfg.select = select if fulltests: + cfg.select = select for key, value in outputDict.items(): cfg.output = key tfSpec = freqanalysis(cfg, self.tfData) assert value in tfSpec.data.dtype.name - else: # randomly pick from 'fourier', 'abs' and 'pow' + else: # randomly pick from 'fourier', 'abs' and 'pow' and work w/smaller signal + cfg.select = {"trials" : 0, "channel" : 1} cfg.output = random.choice(list(outputDict.keys())) - tfSpec = freqanalysis(cfg, self.tfData) + cfg.toi = np.linspace(-20, 60, 5) + tfSpec = freqanalysis(cfg, _make_tf_signal(2, 2, self.seed, fadeIn=self.fadeIn, fadeOut=self.fadeOut)[0]) assert outputDict[cfg.output] in tfSpec.data.dtype.name def test_tf_allocout(self): From 28d3682e3c8e52d775462d3773462178341385d4 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Wed, 16 Mar 2022 18:15:38 +0100 Subject: [PATCH 110/166] WIP: First Prototype - first prototype for the new plotting interface and code structure - basically we make internal show() calls, parse/digest the output and send the data to the actual plotting functions - got a working AnalogData.singlepanelplot, with the limitations of #239 and #240 On branch rework-plotting Changes to be committed: modified: syncopy/datatype/continuous_data.py modified: syncopy/plotting/__init__.py new file: syncopy/plotting/_helpers.py deleted: syncopy/plotting/_plot_analog.py deleted: syncopy/plotting/_plot_spectral.py new file: syncopy/plotting/_singlepanelplot.py new file: syncopy/plotting/config.py deleted: syncopy/plotting/spy_plotting.py modified: syncopy/tests/local_spy.py --- syncopy/datatype/continuous_data.py | 46 +- syncopy/plotting/__init__.py | 5 +- syncopy/plotting/_helpers.py | 88 +++ syncopy/plotting/_plot_analog.py | 675 -------------------- syncopy/plotting/_plot_spectral.py | 703 -------------------- syncopy/plotting/_singlepanelplot.py | 39 ++ syncopy/plotting/config.py | 34 + syncopy/plotting/spy_plotting.py | 916 --------------------------- syncopy/tests/local_spy.py | 11 +- 9 files changed, 202 insertions(+), 2315 deletions(-) create mode 100644 syncopy/plotting/_helpers.py delete mode 100644 syncopy/plotting/_plot_analog.py delete mode 100644 syncopy/plotting/_plot_spectral.py create mode 100644 syncopy/plotting/_singlepanelplot.py create mode 100644 syncopy/plotting/config.py delete mode 100644 syncopy/plotting/spy_plotting.py diff --git a/syncopy/datatype/continuous_data.py b/syncopy/datatype/continuous_data.py index 79ad6a521..208fa1386 100644 --- a/syncopy/datatype/continuous_data.py +++ b/syncopy/datatype/continuous_data.py @@ -20,8 +20,8 @@ from syncopy.shared.parsers import scalar_parser, array_parser from syncopy.shared.errors import SPYValueError from syncopy.shared.tools import best_match -from syncopy.plotting import _plot_analog -from syncopy.plotting import _plot_spectral +from syncopy.plotting import _singlepanelplot as sp_plot +from syncopy.plotting import _helpers as plot_helpers __all__ = ["AnalogData", "SpectralData", "CrossSpectralData"] @@ -396,6 +396,10 @@ def __init__(self, data=None, channel=None, samplerate=None, **kwargs): # First, fill in dimensional info definetrial(self, kwargs.get("trialdefinition")) + # plotting, only virtual in the abc + def singlepanelplot(self): + raise NotImplementedError + class AnalogData(ContinuousData): """Multi-channel, uniformly-sampled, analog (real float) data @@ -415,10 +419,6 @@ class AnalogData(ContinuousData): _defaultDimord = ["time", "channel"] _stackingDimLabel = "time" - # Attach plotting routines to not clutter the core module code - singlepanelplot = _plot_analog.singlepanelplot - multipanelplot = _plot_analog.multipanelplot - @property def hdr(self): """dict with information about raw data @@ -476,6 +476,34 @@ def __init__(self, channel=channel, dimord=dimord) + # implement plotting + def singlepanelplot(self, shifted=True, **show_kwargs): + + """ + The probably simplest plot, a 2d-line + plot of selected channels + + Parameters + ---------- + show_kwargs : :func:`~syncopy.datatype.methods.show.show` arguments + """ + + # get the data to plot + data_x = plot_helpers.parse_toi(self, show_kwargs) + data_y = self.show(**show_kwargs) + + # multiple channels? + labels = plot_helpers.parse_channel(self, show_kwargs) + + # plot multiple channels with offsets for + # better visibility + if shifted: + data_y = plot_helpers.shift_multichan(data_y) + + # create the axes and figure + fig, ax = sp_plot.mk_line_figax() + sp_plot.plot_lines(ax, data_x, data_y, label=labels) + class SpectralData(ContinuousData): """ @@ -489,10 +517,6 @@ class SpectralData(ContinuousData): _defaultDimord = ["time", "taper", "freq", "channel"] _stackingDimLabel = "time" - # Attach plotting routines to not clutter the core module code - singlepanelplot = _plot_spectral.singlepanelplot - multipanelplot = _plot_spectral.multipanelplot - @property def taper(self): """ :class:`numpy.ndarray` : list of window functions used """ @@ -618,7 +642,7 @@ def __init__(self, self.freq = [1] if taper is not None: self.taper = ['taper'] - + class CrossSpectralData(ContinuousData): """ diff --git a/syncopy/plotting/__init__.py b/syncopy/plotting/__init__.py index a985f891d..1b48fd820 100644 --- a/syncopy/plotting/__init__.py +++ b/syncopy/plotting/__init__.py @@ -4,9 +4,8 @@ # # Importlocal modules, but only import routines from spy_plotting.py -from . import (spy_plotting, _plot_analog) -from .spy_plotting import * +from . import (_singlepanelplot,) # Populate local __all__ namespace __all__ = [] -__all__.extend(spy_plotting.__all__) +# __all__.extend(spy_plotting.__all__) diff --git a/syncopy/plotting/_helpers.py b/syncopy/plotting/_helpers.py new file mode 100644 index 000000000..70ab0f353 --- /dev/null +++ b/syncopy/plotting/_helpers.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +# +# Helpers to parse show keyword outputs +# to generate correct data, labels etc. for the plots +# + +import numpy as np +from syncopy.shared.tools import best_match +from syncopy.shared.errors import SPYWarning + + +def parse_toi(dataobject, show_kwargs): + + """ + Create the (multiple) time axis belonging to a toi/toilim + selection + + Parameters + ---------- + dataobject : one derived from :class:`~syncopy.datatype.base_data` + Syncopy datatype instance, needs to have a `time` property + show_kwargs : dict + The keywords provided to the `show` method + """ + + # right now we have to enforce + # single trial selection only + trl = show_kwargs.get('trials', None) + if not isinstance(trl, int): + SPYWarning("Please select a single trial for plotting!") + return + + time = dataobject.time[trl] + # cut to time selection + toilim = show_kwargs.get('toilim', None) + if toilim is not None: + time, _ = best_match(time, toilim, span=True) + # here show is broken atm, issue #240 + toi = show_kwargs.get('toi', None) + if toi is not None: + time, _ = best_match(time, toi, span=False) + + return time + + +def parse_channel(dataobject, show_kwargs): + + """ + Create the labels from a channel + selection + + Parameters + ---------- + dataobject : one derived from :class:`~syncopy.datatype.base_data` + Syncopy datatype instance, needs to have a `channel` property + show_kwargs : dict + The keywords provided to the `show` method + """ + + chs = show_kwargs.get('channel', None) + + # channel selections only allow for arrays and lists + if hasattr(chs, '__len__'): + # either str or int for index + if isinstance(chs[0], str): + labels = chs + else: + labels = ['channel' + str(i + 1) for i in chs] + # single channel + elif isinstance(chs, int): + labels = dataobject.channel[chs] + elif isinstance(chs, str): + labels = chs + # all channels + else: + labels = dataobject.channel + + return labels + + +def shift_multichan(data_y): + + if data_y.ndim > 1: + offsets = data_y.max(axis=0) + 1 + offsets = np.r_[0, offsets[1:]] + data_y += offsets + + return data_y diff --git a/syncopy/plotting/_plot_analog.py b/syncopy/plotting/_plot_analog.py deleted file mode 100644 index 459cc6903..000000000 --- a/syncopy/plotting/_plot_analog.py +++ /dev/null @@ -1,675 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Outsourced plotting class methods from respective parent classes -# - -# Builtin/3rd party package imports -import numpy as np -import os - -# Local imports -from syncopy.shared.errors import SPYValueError, SPYTypeError, SPYWarning -from syncopy.plotting.spy_plotting import (pltConfig, _layout_subplot_panels, - _prep_toilim_avg, _setup_figure, _prep_plots) - -# Conditional matplotlib import -from syncopy import __plt__ -if __plt__: - import matplotlib.pyplot as plt - -__all__ = [] - - -def singlepanelplot(self, trials="all", channels="all", toilim=None, avg_channels=True, - title=None, grid=None, fig=None, **kwargs): - """ - Plot contents of :class:`~syncopy.AnalogData` objects using single-panel figure(s) - - Please refer to :func:`syncopy.singlepanelplot` for detailed usage information. - - Examples - -------- - Use :func:`~syncopy.tests.misc.generate_artificial_data` to create two synthetic - :class:`~syncopy.AnalogData` objects. - - >>> from syncopy.tests.misc import generate_artificial_data - >>> adata = generate_artificial_data(nTrials=10, nChannels=32) - >>> bdata = generate_artificial_data(nTrials=5, nChannels=16) - - Plot an average of the first 16 channels, averaged across trials 2, 4, and 6: - - >>> fig = spy.singlepanelplot(adata, channels=range(16), trials=[2, 4, 6]) - - Overlay average of latter half of channels, averaged across trials 1, 3, 5: - - >>> fig = spy.singlepanelplot(adata, channels=range(16,32), trials=[1, 3, 5], fig=fig) - - Do not average channels: - - >>> fig = spy.singlepanelplot(adata, channels=range(16,32), trials=[1, 3, 5], avg_channels=False) - - Plot `adata` and `bdata` simultaneously in two separate figures: - - >>> fig1, fig2 = spy.singlepanelplot(adata, bdata, overlay=False) - - Overlay `adata` and `bdata`; use channel and trial selections that are valid - for both datasets: - - >>> fig3 = spy.singlepanelplot(adata, bdata, channels=range(16), trials=[1, 2, 3]) - - See also - -------- - syncopy.singlepanelplot : visualize Syncopy data objects using single-panel plots - """ - - # Collect input arguments in dict `inputArgs` and process them - inputArgs = locals() - inputArgs.pop("self") - dimArrs, dimCounts, idx, timeIdx, chanIdx = _prep_analog_plots(self, "singlepanelplot", **inputArgs) - (nTrials, nChan) = dimCounts - (trList, chArr) = dimArrs - - # If we're overlaying a multi-channel plot, ensure settings match up; also, - # do not try to overlay on top of multi-panel plots - if hasattr(fig, "multipanelplot"): - lgl = "single-panel figure generated by `singleplot`" - act = "multi-panel figure generated by `multipanelplot`" - raise SPYValueError(legal=lgl, varname="fig", actual=act) - if hasattr(fig, "chanOffsets"): - if avg_channels: - lgl = "multi-channel plot" - act = "channel averaging was requested for multi-channel plot overlay" - raise SPYValueError(legal=lgl, varname="channels/avg_channels", actual=act) - if nChan != len(fig.chanOffsets): - lgl = "channel-count matching existing multi-channel panels in figure" - act = "{} channels per panel but {} channels for plotting".format(len(fig.chanOffsets), - nChan) - raise SPYValueError(legal=lgl, varname="channels/channels per panel", actual=act) - - # Ensure provided timing selection can actually be averaged (leverage - # the fact that `toilim` selections exclusively generate slices) - if nTrials > 0: - tLengths = _prep_toilim_avg(self) - - # Generic titles for figures - overlayTitle = "Overlay of {} datasets" - - # Either create new figure or fetch existing - if fig is None: - if nTrials > 0: - xLabel = "Time (s)" - else: - xLabel = "Samples" - fig, ax = _setup_figure(1, xLabel=xLabel, grid=grid) - fig.analogPlot = True - else: - ax, = fig.get_axes() - - # Single-channel panel - if avg_channels: - - # Set up pieces of generic figure titles - if nChan > 1: - chanTitle = "Average of {} channels".format(nChan) - else: - chanTitle = chArr[0] - - # Plot entire timecourse - if nTrials == 0: - - # Do not fetch entire dataset at once, but channel by channel - chanSec = np.arange(self.channel.size)[self.selection.channel] - pltArr = np.zeros((self.data.shape[timeIdx],), dtype=self.data.dtype) - for chan in chanSec: - idx[chanIdx] = chan - pltArr += self.data[tuple(idx)].squeeze() - pltArr /= nChan - - # The actual plotting command... - ax.plot(pltArr) - - # Set plot title depending on dataset overlay - if fig.objCount == 0: - if title is None: - title = chanTitle - # ax.set_title(title, size=pltConfig["singleTitleSize"]) - else: - handles, labels = ax.get_legend_handles_labels() - ax.legend(handles, labels) - if title is None: - title = overlayTitle.format(len(handles)) - # ax.set_title(title, size=pltConfig["singleTitleSize"]) - - # Average across trials - else: - - # Compute channel-/trial-average time-course: 2D array with slice/list - # selection does not require fancy indexing - no need to check this here - pltArr = np.zeros((tLengths[0],), dtype=self.data.dtype) - for k, trlno in enumerate(trList): - idx[timeIdx] = self.selection.time[k] - pltArr += self._get_trial(trlno)[tuple(idx)].mean(axis=chanIdx).squeeze() - pltArr /= nTrials - - # The actual plotting command is literally one line... - time = self.time[trList[0]][self.selection.time[0]] - ax.plot(time, pltArr, label=os.path.basename(self.filename)) - ax.set_xlim([time[0], time[-1]]) - - # Set plot title depending on dataset overlay - if fig.objCount == 0: - if title is None: - if nTrials > 1: - trTitle = "{0}across {1} trials".format("averaged " if nChan == 1 else "", - nTrials) - else: - trTitle = "Trial #{}".format(trList[0]) - title = "{}, {}".format(chanTitle, trTitle) - # ax.set_title(title, size=pltConfig["singleTitleSize"]) - else: - handles, labels = ax.get_legend_handles_labels() - ax.legend(handles, labels) - if title is None: - title = overlayTitle.format(len(handles)) - # ax.set_title(title, size=pltConfig["singleTitleSize"]) - - # Multi-channel panel - else: - - # "Raw" data, do not respect any trials - if nTrials == 0: - - # If required, compute max amplitude across provided channels - if not hasattr(fig, "chanOffsets"): - maxAmps = np.zeros((nChan,), dtype=self.data.dtype) - tickOffsets = maxAmps.copy() - chanSec = np.arange(self.channel.size)[self.selection.channel] - for k, chan in enumerate(chanSec): - idx[chanIdx] = chan - pltArr = np.abs(self.data[tuple(idx)].squeeze()) - maxAmps[k] = pltArr.max() - tickOffsets[k] = pltArr.mean() - fig.chanOffsets = np.cumsum([0] + [maxAmps.max()] * (nChan - 1)) - fig.tickOffsets = fig.chanOffsets + tickOffsets.mean() - - # Do not plot all at once but cycle through channels to not overflow memory - for k, chan in enumerate(chanSec): - idx[chanIdx] = chan - ax.plot(self.data[tuple(idx)].squeeze() + fig.chanOffsets[k], - color=plt.rcParams["axes.prop_cycle"].by_key()["color"][fig.objCount], - label=os.path.basename(self.filename)) - if grid is not None: - ax.grid(grid) - - # Set plot title depending on dataset overlay - if fig.objCount == 0: - if title is None: - if nChan > 1: - title = "Entire Data Timecourse of {} channels".format(nChan) - else: - title = "Entire Data Timecourse of {}".format(chArr[0]) - ax.set_yticks(fig.tickOffsets) - ax.set_yticklabels(chArr) - # ax.set_title(title, size=pltConfig["singleTitleSize"]) - else: - handles, labels = ax.get_legend_handles_labels() - ax.legend(handles[ : : (nChan + 1)], - labels[ : : (nChan + 1)]) - if title is None: - title = overlayTitle.format(len(handles)) - # ax.set_title(title, size=pltConfig["singleTitleSize"]) - - # Average across trial(s) - else: - - # Compute trial-average - pltArr = np.zeros((tLengths[0], nChan), dtype=self.data.dtype) - for k, trlno in enumerate(trList): - idx[timeIdx] = self.selection.time[k] - pltArr += np.swapaxes(self._get_trial(trlno)[tuple(idx)], timeIdx, 0) - pltArr /= nTrials - - # If required, compute offsets for multi-channel plot - if not hasattr(fig, "chanOffsets"): - fig.chanOffsets = np.cumsum([0] + [np.abs(pltArr).max()] * (nChan - 1)) - fig.tickOffsets = fig.chanOffsets + np.abs(pltArr).mean() - - # Plot the entire trial-averaged array at once - time = self.time[trList[0]][self.selection.time[0]] - ax.plot(time, - (pltArr + fig.chanOffsets.reshape(1, nChan)).reshape(time.size, nChan), - color=plt.rcParams["axes.prop_cycle"].by_key()["color"][fig.objCount], - label=os.path.basename(self.filename)) - if grid is not None: - ax.grid(grid) - - # Set plot title depending on dataset overlay - if fig.objCount == 0: - if title is None: - title = "{0} channels {1}across {2} trials".format(nChan, - "averaged " if nTrials > 1 else "", - nTrials) - # ax.set_title(title, size=pltConfig["singleTitleSize"]) - else: - handles, labels = ax.get_legend_handles_labels() - ax.legend(handles[ : : (nChan + 1)], - labels[ : : (nChan + 1)]) - if title is None: - title = overlayTitle.format(len(handles)) - # ax.set_title(title, size=pltConfig["singleTitleSize"]) - - # Increment overlay-counter and draw figure - fig.objCount += 1 - plt.draw() - self.selection = None - return fig - - -def multipanelplot(self, trials="all", channels="all", toilim=None, avg_channels=False, - avg_trials=True, title=None, grid=None, fig=None, **kwargs): - """ - Plot contents of :class:`~syncopy.AnalogData` objects using multi-panel figure(s) - - Please refer to :func:`syncopy.multipanelplot` for detailed usage information. - - Examples - -------- - Use :func:`~syncopy.tests.misc.generate_artificial_data` to create two synthetic - :class:`~syncopy.AnalogData` objects. - - >>> from syncopy.tests.misc import generate_artificial_data - >>> adata = generate_artificial_data(nTrials=10, nChannels=32) - >>> bdata = generate_artificial_data(nTrials=5, nChannels=16) - - Show overview of first 5 channels, averaged across trials 2, 4, and 6: - - >>> fig = spy.multipanelplot(adata, channels=range(5), trials=[2, 4, 6]) - - Overlay last 5 channels, averaged across trials 1, 3, 5: - - >>> fig = spy.multipanelplot(adata, channels=range(27, 32), trials=[1, 3, 5], fig=fig) - - Do not average trials: - - >>> fig = spy.multipanelplot(adata, channels=range(27, 32), trials=[1, 3, 5], avg_trials=False) - - Plot `adata` and `bdata` simultaneously in two separate figures: - - >>> fig1, fig2 = spy.multipanelplot(adata, bdata, channels=range(5), overlay=False) - - Overlay `adata` and `bdata`; use channel and trial selections that are valid - for both datasets: - - >>> fig3 = spy.multipanelplot(adata, bdata, channels=range(5), trials=[1, 2, 3], avg_trials=False) - - See also - -------- - syncopy.multipanelplot : visualize Syncopy data objects using multi-panel plots - """ - - # Collect input arguments in dict `inputArgs` and process them - inputArgs = locals() - inputArgs.pop("self") - dimArrs, dimCounts, idx, timeIdx, chanIdx = _prep_analog_plots(self, "singlepanelplot", **inputArgs) - (nTrials, nChan) = dimCounts - (trList, chArr) = dimArrs - - # Get trial/channel count ("raw" plotting constitutes a special case) - if trials is None: - nTrials = 0 - if avg_trials: - msg = "`trials` is `None` but `avg_trials` is `True`. " +\ - "Cannot perform trial averaging without trial specification - " +\ - "setting ``avg_trials = False``. " - SPYWarning(msg) - avg_trials = False - if avg_channels: - msg = "Averaging across channels w/o trial specifications results in " +\ - "single-panel plot. Please use `singlepanelplot` instead" - SPYWarning(msg) - return - - # If we're overlaying, ensure settings match up - if hasattr(fig, "singlepanelplot"): - lgl = "overlay-figure generated by `multipanelplot`" - act = "figure generated by `singlepanelplot`" - raise SPYValueError(legal=lgl, varname="fig/singlepanelplot", actual=act) - if hasattr(fig, "nTrialPanels"): - if nTrials != fig.nTrialPanels: - lgl = "number of trials to plot matching existing panels in figure" - act = "{} panels but {} trials for plotting".format(fig.nTrialPanels, - nTrials) - raise SPYValueError(legal=lgl, varname="trials/figure panels", actual=act) - if avg_trials: - lgl = "overlay of multi-trial plot" - act = "trial averaging was requested for multi-trial plot overlay" - raise SPYValueError(legal=lgl, varname="trials/avg_trials", actual=act) - if trials is None: - lgl = "`trials` to be not `None` to append to multi-trial plot" - act = "multi-trial plot overlay was requested but `trials` is `None`" - raise SPYValueError(legal=lgl, varname="trials/overlay", actual=act) - if not avg_channels and not hasattr(fig, "chanOffsets"): - lgl = "single-channel or channel-averages for appending to multi-trial plot" - act = "multi-trial multi-channel plot overlay was requested" - raise SPYValueError(legal=lgl, varname="avg_channels/overlay", actual=act) - if hasattr(fig, "nChanPanels"): - if nChan != fig.nChanPanels: - lgl = "number of channels to plot matching existing panels in figure" - act = "{} panels but {} channels for plotting".format(fig.nChanPanels, - nChan) - raise SPYValueError(legal=lgl, varname="channels/figure panels", actual=act) - if avg_channels: - lgl = "overlay of multi-channel plot" - act = "channel averaging was requested for multi-channel plot overlay" - raise SPYValueError(legal=lgl, varname="channels/avg_channels", actual=act) - if not avg_trials: - lgl = "overlay of multi-channel plot" - act = "mulit-trial plot was requested for multi-channel plot overlay" - raise SPYValueError(legal=lgl, varname="channels/avg_trials", actual=act) - if hasattr(fig, "chanOffsets"): - if avg_channels: - lgl = "multi-channel plot" - act = "channel averaging was requested for multi-channel plot overlay" - raise SPYValueError(legal=lgl, varname="channels/avg_channels", actual=act) - if nChan != len(fig.chanOffsets): - lgl = "channel-count matching existing multi-channel panels in figure" - act = "{} channels per panel but {} channels for plotting".format(len(fig.chanOffsets), - nChan) - raise SPYValueError(legal=lgl, varname="channels/channels per panel", actual=act) - - # Generic title for overlay figures - overlayTitle = "Overlay of {} datasets" - - # Either construct subplot panel layout/vet provided layout or fetch existing - if fig is None: - - # Determine no. of required panels - if avg_trials and not avg_channels: - npanels = nChan - elif not avg_trials and avg_channels: - npanels = nTrials - elif not avg_trials and not avg_channels: - npanels = int(nTrials == 0) * nChan + nTrials - else: - msg = "Averaging across both trials and channels results in " +\ - "single-panel plot. Please use `singlepanelplot` instead" - SPYWarning(msg) - return - - # Although, `_setup_figure` can call `_layout_subplot_panels` for us, we - # need `nrow` and `ncol` below, so do it here - if nTrials > 0: - xLabel = "Time (s)" - else: - xLabel = "Samples" - nrow = kwargs.get("nrow", None) - ncol = kwargs.get("ncol", None) - nrow, ncol = _layout_subplot_panels(npanels, nrow=nrow, ncol=ncol) - fig, ax_arr = _setup_figure(npanels, nrow=nrow, ncol=ncol, xLabel=xLabel, grid=grid) - fig.analogPlot = True - - # Get existing layout - else: - ax_arr = fig.get_axes() - nrow, ncol = ax_arr[0].numRows, ax_arr[0].numCols - - # Panels correspond to channels - if avg_trials and not avg_channels: - - # Ensure provided timing selection can actually be averaged (leverage - # the fact that `toilim` selections exclusively generate slices) - tLengths = _prep_toilim_avg(self) - - # Compute trial-averaged time-courses: 2D array with slice/list - # selection does not require fancy indexing - no need to check this here - pltArr = np.zeros((tLengths[0], nChan), dtype=self.data.dtype) - for k, trlno in enumerate(trList): - idx[timeIdx] = self.selection.time[k] - pltArr += np.swapaxes(self._get_trial(trlno)[tuple(idx)], timeIdx, 0) - pltArr /= nTrials - - # Cycle through channels and plot trial-averaged time-courses (time- - # axis must be identical for all channels, set up `idx` just once) - idx[timeIdx] = self.selection.time[0] - time = self.time[trList[k]][self.selection.time[0]] - for k, chan in enumerate(chArr): - ax_arr[k].plot(time, pltArr[:, k], label=os.path.basename(self.filename)) - - # If we're overlaying datasets, adjust panel- and sup-titles: include - # legend in top-right axis (note: `ax_arr` is row-major flattened) - if fig.objCount == 0: - for k, chan in enumerate(chArr): - ax_arr[k].set_title(chan, size=pltConfig["multiTitleSize"]) - fig.nChanPanels = nChan - if title is None: - if nTrials > 1: - title = "Average of {} trials".format(nTrials) - else: - title = "Trial #{}".format(trList[0]) - fig.suptitle(title, size=pltConfig["singleTitleSize"]) - else: - for k, chan in enumerate(chArr): - ax_arr[k].set_title("{0}/{1}".format(ax_arr[k].get_title(), chan)) - ax = ax_arr[ncol - 1] - handles, labels = ax.get_legend_handles_labels() - ax.legend(handles, labels) - if title is None: - title = overlayTitle.format(len(handles)) - fig.suptitle(title, size=pltConfig["singleTitleSize"]) - - # Panels correspond to trials - elif not avg_trials and avg_channels: - - # Cycle through panels to plot by-trial channel-averages - for k, trlno in enumerate(trList): - idx[timeIdx] = self.selection.time[k] - time = self.time[trList[k]][self.selection.time[k]] - ax_arr[k].plot(time, - self._get_trial(trlno)[tuple(idx)].mean(axis=chanIdx).squeeze(), - label=os.path.basename(self.filename)) - - # If we're overlaying datasets, adjust panel- and sup-titles: include - # legend in top-right axis (note: `ax_arr` is row-major flattened) - if fig.objCount == 0: - for k, trlno in enumerate(trList): - ax_arr[k].set_title("Trial #{}".format(trlno), size=pltConfig["multiTitleSize"]) - fig.nTrialPanels = nTrials - if title is None: - if nChan > 1: - title = "Average of {} channels".format(nChan) - else: - title = chArr[0] - fig.suptitle(title, size=pltConfig["singleTitleSize"]) - else: - for k, trlno in enumerate(trList): - ax_arr[k].set_title("{0}/#{1}".format(ax_arr[k].get_title(), trlno)) - ax = ax_arr[ncol - 1] - handles, labels = ax.get_legend_handles_labels() - ax.legend(handles, labels) - if title is None: - title = overlayTitle.format(len(handles)) - fig.suptitle(title, size=pltConfig["singleTitleSize"]) - - # Panels correspond to channels (if `trials` is `None`) otherwise trials - elif not avg_trials and not avg_channels: - - # Plot each channel in separate panel - if nTrials == 0: - chanSec = np.arange(self.channel.size)[self.selection.channel] - for k, chan in enumerate(chanSec): - idx[chanIdx] = chan - ax_arr[k].plot(self.data[tuple(idx)].squeeze(), - label=os.path.basename(self.filename)) - - # If we're overlaying datasets, adjust panel- and sup-titles: include - # legend in top-right axis (note: `ax_arr` is row-major flattened) - if fig.objCount == 0: - for k, chan in enumerate(chArr): - ax_arr[k].set_title(chan, size=pltConfig["multiTitleSize"]) - fig.nChanPanels = nChan - if title is None: - title = "Entire Data Timecourse" - fig.suptitle(title, size=pltConfig["singleTitleSize"]) - else: - for k, chan in enumerate(chArr): - ax_arr[k].set_title("{0}/{1}".format(ax_arr[k].get_title(), chan)) - ax = ax_arr[ncol - 1] - handles, labels = ax.get_legend_handles_labels() - ax.legend(handles, labels) - if title is None: - title = overlayTitle.format(len(handles)) - fig.suptitle(title, size=pltConfig["singleTitleSize"]) - - # Each trial gets its own panel w/multiple channels per panel - else: - - # If required, compute max amplitude across provided trials + channels - if not hasattr(fig, "chanOffsets"): - maxAmps = np.zeros((nTrials,), dtype=self.data.dtype) - tickOffsets = maxAmps.copy() - for k, trlno in enumerate(trList): - idx[timeIdx] = self.selection.time[k] - pltArr = np.abs(self._get_trial(trlno)[tuple(idx)]) - maxAmps[k] = pltArr.max() - tickOffsets[k] = pltArr.mean() - fig.chanOffsets = np.cumsum([0] + [maxAmps.max()] * (nChan - 1)) - fig.tickOffsets = fig.chanOffsets + tickOffsets.mean() - - # Cycle through panels to plot by-trial multi-channel time-courses - for k, trlno in enumerate(trList): - idx[timeIdx] = self.selection.time[k] - time = self.time[trList[k]][self.selection.time[k]] - pltArr = np.swapaxes(self._get_trial(trlno)[tuple(idx)], timeIdx, 0) - ax_arr[k].plot(time, - (pltArr + fig.chanOffsets.reshape(1, nChan)).reshape(time.size, nChan), - color=plt.rcParams["axes.prop_cycle"].by_key()["color"][fig.objCount], - label=os.path.basename(self.filename)) - - # If we're overlaying datasets, adjust panel- and sup-titles: include - # legend in top-right axis (note: `ax_arr` is row-major flattened) - # Note: y-axis is shared across panels, so `yticks` need only be set once - if fig.objCount == 0: - for k, trlno in enumerate(trList): - ax_arr[k].set_title("Trial #{}".format(trlno), size=pltConfig["multiTitleSize"]) - ax_arr[0].set_yticks(fig.tickOffsets) - ax_arr[0].set_yticklabels(chArr) - fig.nTrialPanels = nTrials - if title is None: - if nChan > 1: - title = "{} channels".format(nChan) - else: - title = chArr[0] - fig.suptitle(title, size=pltConfig["singleTitleSize"]) - else: - for k, trlno in enumerate(trList): - ax_arr[k].set_title("{0}/#{1}".format(ax_arr[k].get_title(), trlno)) - ax_arr[0].set_yticklabels([" "] * chArr.size) - ax = ax_arr[ncol - 1] - handles, labels = ax.get_legend_handles_labels() - ax.legend(handles[ : : (nChan + 1)], - labels[ : : (nChan + 1)]) - if title is None: - title = overlayTitle.format(len(handles)) - fig.suptitle(title, size=pltConfig["singleTitleSize"]) - - # Increment overlay-counter, draw figure and wipe data-selection slot - fig.objCount += 1 - plt.draw() - self.selection = None - return fig - - -def _prep_analog_plots(self, name, **inputArgs): - """ - Local helper that performs sanity checks and sets up data selection - - Parameters - ---------- - self : :class:`~syncopy.AnalogData` object - Syncopy :class:`~syncopy.AnalogData` object that is being processed by - the respective :meth:`.singlepanelplot` or :meth:`.multipanelplot` class methods - defined in this module. - name : str - Name of caller (i.e., "singlepanelplot" or "multipanelplot") - inputArgs : dict - Input arguments of caller (i.e., :meth:`.singlepanelplot` or :meth:`.multipanelplot`) - collected in dictionary - - Returns - ------- - dimArrs : tuple - Tuple containing (in this order) `trList`, list of (selected) - trials to visualize and `chArr`, 1D :class:`numpy.ndarray` of channel specifiers - based on provided user selection. Note that `"all"` and `None` selections - are converted to arrays ready for indexing. - dimCounts : tuple - Tuple holding sizes of corresponding selection arrays comprised - in `dimArrs`. Elements are `nTrials`, number of (selected) trials and `nChan`, - number of (selected) channels. - idx : list - Three element indexing list (respecting non-default `dimord`s) intended - for use with trial-array data. - timeIdx : int - Position of time-axis within indexing list `idx` (either 0 or 1). - chanIdx : int - Position of channel-axis within indexing list `idx` (either 0 or 1). - - Notes - ----- - This is an auxiliary method that is intended purely for internal use. Please - refer to the user-exposed methods :func:`~syncopy.singlepanelplot` and/or - :func:`~syncopy.multipanelplot` to actually generate plots of Syncopy data objects. - - See also - -------- - :meth:`syncopy.plotting.spy_plotting._prep_plots` : General basic input parsing for all Syncopy plotting routines - """ - - # Basic sanity checks for all plotting routines w/any Syncopy object - _prep_plots(self, name, **inputArgs) - - # Ensure our binary flags are actually binary - if not isinstance(inputArgs["avg_channels"], bool): - raise SPYTypeError(inputArgs["avg_channels"], varname="avg_channels", expected="bool") - if not isinstance(inputArgs.get("avg_trials", True), bool): - raise SPYTypeError(inputArgs["avg_trials"], varname="avg_trials", expected="bool") - - # Pass provided selections on to `Selector` class which performs error - # checking and generates required indexing arrays - self.selection = {"trials": inputArgs["trials"], - "channels": inputArgs["channels"], - "toilim": inputArgs["toilim"]} - - # Ensure any optional keywords controlling plotting appearance make sense - if inputArgs["title"] is not None: - if not isinstance(inputArgs["title"], str): - raise SPYTypeError(inputArgs["title"], varname="title", expected="str") - if inputArgs["grid"] is not None: - if not isinstance(inputArgs["grid"], bool): - raise SPYTypeError(inputArgs["grid"], varname="grid", expected="bool") - - # Get trial and channel counts - if inputArgs["trials"] is None: - trList = [] - nTrials = 0 - if inputArgs["toilim"] is not None: - lgl = "`trials` to be not `None` to perform timing selection" - act = "`toilim` was provided but `trials` is `None`" - raise SPYValueError(legal=lgl, varname="trials/toilim", actual=act) - else: - trList = self.selection.trials - nTrials = len(trList) - chArr = self.channel[self.selection.channel] - nChan = chArr.size - - # Collect arrays and counts in tuples - dimCounts = (nTrials, nChan) - dimArrs = (trList, chArr) - - # Prepare indexing list respecting potential non-default `dimord`s - idx = [slice(None), slice(None)] - chanIdx = self.dimord.index("channel") - timeIdx = self.dimord.index("time") - idx[chanIdx] = self.selection.channel - - return dimArrs, dimCounts, idx, timeIdx, chanIdx diff --git a/syncopy/plotting/_plot_spectral.py b/syncopy/plotting/_plot_spectral.py deleted file mode 100644 index 6cd6184aa..000000000 --- a/syncopy/plotting/_plot_spectral.py +++ /dev/null @@ -1,703 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Outsourced plotting class methods from respective parent classes -# - -# Builtin/3rd party package imports -import os -import numpy as np - -# Local imports -from syncopy.shared.errors import SPYValueError, SPYError, SPYTypeError, SPYWarning -from syncopy.shared.parsers import scalar_parser -from syncopy.plotting.spy_plotting import (pltErrMsg, pltConfig, _prep_toilim_avg, - _setup_figure, _setup_colorbar, _prep_plots) - -# Conditional matplotlib import -from syncopy import __plt__ -if __plt__: - import matplotlib.pyplot as plt - -#: available panel settings for :func:`~syncopy.multipanelplot` -availablePanels = tuple(["channels", "trials", "tapers"]) - -__all__ = [] - - -def singlepanelplot(self, trials="all", channels="all", tapers="all", - toilim=None, foilim=None, avg_channels=False, avg_tapers=True, - interp="spline36", cmap="plasma", vmin=None, vmax=None, - title=None, grid=None, fig=None, **kwargs): - """ - Plot contents of :class:`~syncopy.SpectralData` objects using single-panel figure(s) - - Please refer to :func:`syncopy.singlepanelplot` for detailed usage information. - - Examples - -------- - Show frequency range 30-80 Hz of channel `'ecog_mua2'` averaged across - trials 2, 4, and 6: - - >>> fig = spy.singlepanelplot(freqData, trials=[2, 4, 6], channels=["ecog_mua2"], - foilim=[30, 80]) - - Overlay channel `'ecog_mua3'` with same settings: - - >>> fig2 = spy.singlepanelplot(freqData, trials=[2, 4, 6], channels=['ecog_mua3'], - foilim=[30, 80], fig=fig) - - Plot time-frequency contents of channel `'ecog_mua1'` present in both objects - `tfData1` and `tfData2` using the 'viridis' colormap, a plot grid, manually - defined lower and upper color value limits and no interpolation - - >>> fig1, fig2 = spy.singlepanelplot(tfData1, tfData2, channels=['ecog_mua1'], - cmap="viridis", vmin=0.25, vmax=0.95, - interp=None, grid=True, overlay=False) - - Note that overlay plotting is **not** supported for time-frequency objects. - - See also - -------- - syncopy.singlepanelplot : visualize Syncopy data objects using single-panel plots - """ - - # Collect input arguments in dict `inputArgs` and process them - inputArgs = locals() - inputArgs.pop("self") - (dimArrs, - dimCounts, - isTimeFrequency, - complexConversion, - pltDtype, - dataLbl) = _prep_spectral_plots(self, "singlepanelplot", **inputArgs) - (nTrials, nChan, nFreq, nTap) = dimCounts - (trList, chArr, freqArr, tpArr) = dimArrs - - # If we're overlaying, ensure data and plot type match up - if hasattr(fig, "objCount"): - if isTimeFrequency: - msg = "Overlay plotting not supported for time-frequency data" - raise SPYError(msg) - if not hasattr(fig, "spectralPlot"): - lgl = "figure visualizing data from a Syncopy `SpectralData` object" - act = "visualization of other Syncopy data" - raise SPYValueError(legal=lgl, varname="fig", actual=act) - if hasattr(fig, "multipanelplot"): - lgl = "single-panel figure generated by `singleplot`" - act = "multi-panel figure generated by `multipanelplot`" - raise SPYValueError(legal=lgl, varname="fig", actual=act) - - # No time-frequency shenanigans: this is a simple power-spectrum (line-plot) - if not isTimeFrequency: - - # Generic titles for figures - overlayTitle = "Overlay of {} datasets" - - # Either create new figure or fetch existing - if fig is None: - fig, ax = _setup_figure(1, xLabel="Frequency (Hz)", yLabel=dataLbl, grid=grid) - fig.spectralPlot = True - else: - ax, = fig.get_axes() - - # Average across channels, tapers or both using local helper func - nTime = 1 - if not avg_channels and not avg_tapers and nTap > 1: - msg = "Either channels or trials need to be averaged for single-panel plot" - SPYWarning(msg) - return - if avg_channels and not avg_tapers: - panelTitle = "{} tapers averaged across {} channels and {} trials".format(nTap, nChan, nTrials) - pltArr = _compute_pltArr(self, nFreq, nTap, nTime, complexConversion, pltDtype, - avg1="channel") - if avg_tapers and not avg_channels: - panelTitle = "{} channels averaged across {} tapers and {} trials".format(nChan, nTap, nTrials) - pltArr = _compute_pltArr(self, nFreq, nChan, nTime, complexConversion, pltDtype, - avg1="taper") - if avg_tapers and avg_channels: - panelTitle = "Average of {} channels, {} tapers and {} trials".format(nChan, nTap, nTrials) - pltArr = _compute_pltArr(self, nFreq, 1, nTime, complexConversion, pltDtype, - avg1="taper", avg2="channel") - - # Perform the actual plotting - ax.plot(freqArr, np.log10(pltArr), label=os.path.basename(self.filename)) - ax.set_xlim([freqArr[0], freqArr[-1]]) - - # Set plot title depending on dataset overlay - if fig.objCount == 0: - if title is None: - title = panelTitle - # ax.set_title(title, size=pltConfig["singleTitleSize"]) - else: - handles, labels = ax.get_legend_handles_labels() - ax.legend(handles, labels) - if title is None: - title = overlayTitle.format(len(handles)) - # ax.set_title(title, size=pltConfig["singleTitleSize"]) - - else: - - # For a single-panel TF visualization, we need to average across both tapers + channels - if not avg_channels and (not avg_tapers and nTap > 1): - msg = "Single-panel time-frequency visualization requires averaging " +\ - "across both tapers and channels" - SPYWarning(msg) - return - - # Compute (and verify) length of selected time intervals and assemble array for plotting - panelTitle = "Average of {} channels, {} tapers and {} trials".format(nChan, nTap, nTrials) - tLengths = _prep_toilim_avg(self) - nTime = tLengths[0] - pltArr = _compute_pltArr(self, nFreq, 1, nTime, complexConversion, pltDtype, - avg1="taper", avg2="channel") - - # Prepare figure - fig, ax, cax = _setup_figure(1, xLabel="Time (s)", yLabel="Frequency (Hz)", - include_colorbar=True, grid=grid) - fig.spectralPlot = True - - # Use `imshow` to render array as image - time = self.time[trList[0]][self.selection.time[0]] - ax.imshow(pltArr, origin="lower", interpolation=interp, - cmap=cmap, vmin=vmin, vmax=vmax, - extent=(time[0], time[-1], freqArr[0], freqArr[-1]), aspect="auto") - cbar = _setup_colorbar(fig, ax, cax, label=dataLbl.replace(" (dB)", "")) - if title is None: - title = panelTitle - # ax.set_title(title, size=pltConfig["singleTitleSize"]) - - # Increment overlay-counter and draw figure - fig.objCount += 1 - plt.draw() - self.selection = None - return fig - - -def multipanelplot(self, trials="all", channels="all", tapers="all", toilim=None, foilim=None, - avg_channels=False, avg_tapers=True, avg_trials=True, panels="channels", - interp="spline36", cmap="plasma", vmin=None, vmax=None, - title=None, grid=None, fig=None, **kwargs): - """ - Plot contents of :class:`~syncopy.SpectralData` objects using multi-panel figure(s) - - Please refer to :func:`syncopy.multipanelplot` for detailed usage information. - - Examples - -------- - Use 16 panels to show frequency range 30-80 Hz of first 16 channels in `freqData` - averaged across trials 2, 4, and 6: - - >>> fig = spy.multipanelplot(freqData, trials=[2, 4, 6], channels=range(16), - foilim=[30, 80], panels="channels") - - Same settings, but each panel represents a trial: - - >>> fig = spy.multipanelplot(freqData, trials=[2, 4, 6], channels=range(16), - foilim=[30, 80], panels="trials", avg_trials=False, - avg_channels=True) - - Plot time-frequency contents of channels `'ecog_mua1'` and `'ecog_mua2'` of - `tfData` - - >>> fig = spy.multipanelplot(tfData, channels=['ecog_mua1', 'ecog_mua2']) - - Note that multi-panel overlay plotting is **not** supported for - :class:`~syncopy.SpectralData` objects. - - See also - -------- - syncopy.multipanelplot : visualize Syncopy data objects using multi-panel plots - """ - - # Collect input arguments in dict `inputArgs` and process them - inputArgs = locals() - inputArgs.pop("self") - (dimArrs, - dimCounts, - isTimeFrequency, - complexConversion, - pltDtype, - dataLbl) = _prep_spectral_plots(self, "multipanelplot", **inputArgs) - (nTrials, nChan, nFreq, nTap) = dimCounts - (trList, chArr, freqArr, tpArr) = dimArrs - - # No overlaying here... - if hasattr(fig, "objCount"): - msg = "Overlays of multi-panel `SpectralData` plots not supported" - raise SPYError(msg) - - # Ensure panel-specification makes sense and is compatible w/averaging selection - if not isinstance(panels, str): - raise SPYTypeError(panels, varname="panels", expected="str") - if panels not in availablePanels: - lgl = "'" + "or '".join(opt + "' " for opt in availablePanels) - raise SPYValueError(legal=lgl, varname="panels", actual=panels) - if (panels == "channels" and avg_channels) or (panels == "trials" and avg_trials) \ - or (panels == "tapers" and avg_tapers): - msg = "Cannot use `panels = {}` and average across {} at the same time. " - SPYWarning(msg.format(panels, panels)) - return - - # Ensure the proper amount of averaging was specified - avgFlags = [avg_channels, avg_trials, avg_tapers] - if sum(avgFlags) == 0 and nTap * nTrials > 1: - msg = "Need to average across at least one of tapers, channels or trials " +\ - "for visualization. " - SPYWarning(msg) - return - if sum(avgFlags) == 3: - msg = "Averaging across trials, channels and tapers results in " +\ - "single-panel plot. Please use `singlepanelplot` instead" - SPYWarning(msg) - return - if isTimeFrequency: - if sum(avgFlags) != 2: - msg = "Multi-panel time-frequency visualization requires averaging across " +\ - "two out of three dimensions (tapers, channels trials)" - SPYWarning(msg) - return - - # Prepare figure (same for all cases) - if panels == "channels": - npanels = nChan - elif panels == "trials": - npanels = nTrials - else: # ``panels == "tapers"`` - npanels = nTap - - # Construct subplot panel layout or vet provided layout - nrow = kwargs.get("nrow", None) - ncol = kwargs.get("ncol", None) - if not isTimeFrequency: - fig, ax_arr = _setup_figure(npanels, nrow=nrow, ncol=ncol, - xLabel="Frequency (Hz)", - yLabel=dataLbl, grid=grid, - include_colorbar=False, - sharex=True, sharey=True) - else: - fig, ax_arr, cax = _setup_figure(npanels, nrow=nrow, ncol=ncol, - xLabel="Time (s)", - yLabel="Frequency (Hz)", grid=grid, - include_colorbar=True, - sharex=True, sharey=True) - - # Monkey-patch object-counter to newly created figure - fig.spectralPlot = True - - # Start with the "simple" case: "regular" spectra, no time involved - if not isTimeFrequency: - - # We're not dealing w/TF data here - nTime = 1 - N = 1 - - # For each panel stratification, set corresponding positional and - # keyword args for iteratively calling `_compute_pltArr` - if panels == "channels": - - panelVar = "channel" - panelValues = chArr - panelTitles = chArr - - if not avg_trials and avg_tapers: - avgDim1 = "taper" - avgDim2 = None - innerVar = "trial" - innerValues = trList - majorTitle = "{} trials averaged across {} tapers".format(nTrials, nTap) - showLegend = True - elif avg_trials and not avg_tapers: - avgDim1 = None - avgDim2 = None - innerVar = "taper" - innerValues = tpArr - majorTitle = "{} tapers averaged across {} trials".format(nTap, nTrials) - showLegend = True - else: # `avg_trials` and `avg_tapers` - avgDim1 = "taper" - avgDim2 = None - innerVar = "trial" - innerValues = ["all"] - majorTitle = " Average of {} tapers and {} trials".format(nTap, nTrials) - showLegend = False - - elif panels == "trials": - - panelVar = "trial" - panelValues = trList - panelTitles = ["Trial #{}".format(trlno) for trlno in trList] - - if not avg_channels and avg_tapers: - avgDim1 = "taper" - avgDim2 = None - innerVar = "channel" - innerValues = chArr - majorTitle = "{} channels averaged across {} tapers".format(nChan, nTap) - showLegend = True - elif avg_channels and not avg_tapers: - avgDim1 = "channel" - avgDim2 = None - innerVar = "taper" - innerValues = tpArr - majorTitle = "{} tapers averaged across {} channels".format(nTap, nChan) - showLegend = True - else: # `avg_channels` and `avg_tapers` - avgDim1 = "taper" - avgDim2 = "channel" - innerVar = "trial" - innerValues = ["all"] - majorTitle = " Average of {} channels and {} tapers".format(nChan, nTap) - showLegend = False - - else: # panels = "tapers" - - panelVar = "taper" - panelValues = tpArr - panelTitles = ["Taper #{}".format(tpno) for tpno in tpArr] - - if not avg_trials and avg_channels: - avgDim1 = "channel" - avgDim2 = None - innerVar = "trial" - innerValues = trList - majorTitle = "{} trials averaged across {} channels".format(nTrials, nChan) - showLegend = True - elif avg_trials and not avg_channels: - avgDim1 = None - avgDim2 = None - innerVar = "channel" - innerValues = chArr - majorTitle = "{} channels averaged across {} trials".format(nChan, nTrials) - showLegend = True - else: # `avg_trials` and `avg_channels` - avgDim1 = "channel" - avgDim2 = None - innerVar = "trial" - innerValues = ["all"] - majorTitle = " Average of {} channels and {} trials".format(nChan, nTrials) - showLegend = False - - # Loop over panels, within each panel, loop over `innerValues` to (potentially) - # plot multiple spectra per panel - kwargs = {"avg1": avgDim1, "avg2": avgDim2} - for panelCount, panelVal in enumerate(panelValues): - kwargs[panelVar] = panelVal - for innerVal in innerValues: - kwargs[innerVar] = innerVal - pltArr = _compute_pltArr(self, nFreq, N, nTime, complexConversion, pltDtype, **kwargs) - ax_arr[panelCount].plot(freqArr, np.log10(pltArr), - label=innerVar.capitalize() + " " + str(innerVal)) - ax_arr[panelCount].set_title(panelTitles[panelCount], size=pltConfig["multiTitleSize"]) - if showLegend: - handles, labels = ax_arr[0].get_legend_handles_labels() - ax_arr[0].legend(handles, labels) - if title is None: - fig.suptitle(majorTitle, size=pltConfig["singleTitleSize"]) - - # Now, multi-panel time-frequency visualizations - else: - - # Compute (and verify) length of selected time intervals - tLengths = _prep_toilim_avg(self) - nTime = tLengths[0] - time = self.time[trList[0]][self.selection.time[0]] - N = 1 - - if panels == "channels": - panelVar = "channel" - panelValues = chArr - panelTitles = chArr - majorTitle = " Average of {} tapers and {} trials".format(nTap, nTrials) - avgDim1 = "taper" - avgDim2 = None - - elif panels == "trials": - panelVar = "trial" - panelValues = trList - panelTitles = ["Trial #{}".format(trlno) for trlno in trList] - majorTitle = " Average of {} channels and {} tapers".format(nChan, nTap) - avgDim1 = "taper" - avgDim2 = "channel" - - else: # panels = "tapers" - panelVar = "taper" - panelValues = tpArr - panelTitles = ["Taper #{}".format(tpno) for tpno in tpArr] - majorTitle = " Average of {} channels and {} trials".format(nChan, nTrials) - avgDim1 = "channel" - avgDim2 = None - - # Loop over panels, within each panel, loop over `innerValues` to (potentially) - # plot multiple spectra per panel - kwargs = {"avg1": avgDim1, "avg2": avgDim2} - vmins = [] - vmaxs = [] - for panelCount, panelVal in enumerate(panelValues): - kwargs[panelVar] = panelVal - pltArr = _compute_pltArr(self, nFreq, N, nTime, complexConversion, pltDtype, **kwargs) - vmins.append(pltArr.min()) - vmaxs.append(pltArr.max()) - ax_arr[panelCount].imshow(pltArr, origin="lower", interpolation=interp, cmap=cmap, - extent=(time[0], time[-1], freqArr[0], freqArr[-1]), - aspect="auto") - ax_arr[panelCount].set_title(panelTitles[panelCount], size=pltConfig["multiTitleSize"]) - - # Render colorbar - if vmin is None: - vmin = min(vmins) - if vmax is None: - vmax = max(vmaxs) - cbar = _setup_colorbar(fig, ax_arr, cax, label=dataLbl.replace(" (dB)", ""), - outline=False, vmin=vmin, vmax=vmax) - if title is None: - fig.suptitle(majorTitle, size=pltConfig["singleTitleSize"]) - - # Increment overlay-counter and draw figure - fig.objCount += 1 - plt.draw() - self.selection = None - return fig - -def _compute_pltArr(self, nFreq, N, nTime, complexConversion, pltDtype, - avg1="channel", avg2=None, trial="all", channel="all", - freq="all", taper="all"): - """ - Local helper that extracts/averages data from :class:`~syncopy.SpectralData` object - - Parameters - ---------- - self : :class:`~syncopy.SpectralData` object - Syncopy :class:`~syncopy.SpectralData` object that is being processed by - the respective :meth:`.singlepanelplot` or :meth:`.multipanelplot` class methods - defined in this module. - nFreq : int - Number of frequencies of interest - N : int - Size of free dimension post averaging. Depending on `avg1` and `avg2` - can be either `nChan`, `nTap` or 1 - nTime : int - Number of time-points of interest. If object does not contain time-frequency - data, `nTime` has to be 1 - complexConversion : callable - Automatically set by :meth:`~syncopy.plotting._plot_spectral._prep_spectral_plots` - to (potentially) convert complex Fourier coefficients to float. - pltDtype : str or :class:`numpy.dtype` - Automatically set by :meth:`~syncopy.plotting._plot_spectral._prep_spectral_plots`: - numeric type of (potentially converted) complex Fourier coefficients. - avg1 : str or None - First dimension for averaging. If `None`, no mean-value is computed. Otherwise, - `avg1` can be either `"channel"` or `"taper"`. - avg2 : str or None - Second dimension for averaging. If `None`, no mean-value is computed. Otherwise, - `avg2` can be either `"channel"` or `"taper"`. - trial : str or list - Either list of trial indices or `"all"`; set by - :meth:`~syncopy.plotting._plot_spectral._prep_spectral_plots` - channel : str or :class:`numpy.ndarray` - Either array of channel specifiers or `"all"`; set by - :meth:`~syncopy.plotting._plot_spectral._prep_spectral_plots` - freq : str or :class:`numpy.ndarray` - Either array of frequency specifiers or `"all"`; set by - :meth:`~syncopy.plotting._plot_spectral._prep_spectral_plots` - taper : str or :class:`numpy.ndarray` - Either array of taper specifiers or `"all"`; set by - :meth:`~syncopy.plotting._plot_spectral._prep_spectral_plots` - - Returns - ------- - pltArr : 1D, 2D or 3D :class:`numpy.ndarray` - Extracted/averaged data ready for plotting; if the :class:`~syncopy.SpectralData` - input object contains time-frequency data, `pltArr` is a three-dimensional - array of shape ``(nFreq, nTime, N)``, otherwise `pltArr` is two-dimensional - with shape ``(nFreq, N)`` for ``N > 1``, or a one-dimensional ``(nFreq,)`` - array if ``N = 1``. - - Notes - ----- - This is an auxiliary method that is intended purely for internal use. Please - refer to the user-exposed methods :func:`~syncopy.singlepanelplot` and/or - :func:`~syncopy.multipanelplot` to actually generate plots of Syncopy data objects. - """ - - # Prepare indexing list respecting potential non-default `dimord`s - idx = [slice(None), slice(None), slice(None), slice(None)] - timeIdx = self.dimord.index("time") - chanIdx = self.dimord.index("channel") - freqIdx = self.dimord.index("freq") - taperIdx = self.dimord.index("taper") - - if trial == "all": - trList = self.selection.trials - else: - trList = [trial] - nTrls = len(trList) - useFancy = self.selection._useFancy - if channel == "all": - idx[chanIdx] = self.selection.channel - else: - idx[chanIdx] = np.where(self.channel == channel)[0] - useFancy = True - if freq == "all": - idx[freqIdx] = self.selection.freq - else: - idx[freqIdx] = np.where(self.freq == freq)[0] - useFancy = True - if taper == "all": - idx[taperIdx] = self.selection.taper - else: - idx[taperIdx] = [taper] - useFancy = True - - if nTime == 1: - pltArr = np.zeros((nFreq, N), dtype=pltDtype).squeeze() # `squeeze` in case `N = 1` - else: - pltArr = np.zeros((nFreq, nTime, N), dtype=pltDtype).squeeze() # `squeeze` for `singlepanelplot` - - for tk, trlno in enumerate(trList): - trlArr = complexConversion(self._get_trial(trlno)) - idx[timeIdx] = self.selection.time[tk] - if not useFancy: - trlArr = trlArr[tuple(idx)] - else: - trlArr = trlArr[idx[0], ...][:, idx[1], ...][:, :, idx[2], :][..., idx[3]] - if avg1: - trlArr = trlArr.mean(axis=self.dimord.index(avg1), keepdims=True) - if avg2: - trlArr = trlArr.mean(axis=self.dimord.index(avg2), keepdims=True) - pltArr += np.swapaxes(trlArr, freqIdx, 0).squeeze() - return pltArr / len(trList) - - -def _prep_spectral_plots(self, name, **inputArgs): - """ - Local helper that performs sanity checks and sets up data selection - - Parameters - ---------- - self : :class:`~syncopy.SpectralData` object - Syncopy :class:`~syncopy.SpectralData` object that is being processed by - the respective :meth:`.singlepanelplot` or :meth:`.multipanelplot` class methods - defined in this module. - name : str - Name of caller (i.e., "singlepanelplot" or "multipanelplot") - inputArgs : dict - Input arguments of caller (i.e., :meth:`.singlepanelplot` or :meth:`.multipanelplot`) - collected in dictionary - - Returns - ------- - dimArrs : tuple - Four-element tuple containing (in this order): `trList`, list of (selected) - trials to visualize, `chArr`, 1D :class:`numpy.ndarray` of channel specifiers - based on provided user selection, `freqArr`, 1D :class:`numpy.ndarray` of - frequency specifiers based on provided user selection, `tpArr`, - 1D :class:`numpy.ndarray` of taper specifiers based on provided user selection. - Note that `"all"` and `None` selections are converted to arrays ready for - indexing. - dimCounts : tuple - Four-element tuple holding sizes of corresponding selection arrays comprised - in `dimArrs`. Elements are (in this order): number of (selected) trials - `nTrials`, number of (selected) channels `nChan`, number of (selected) - frequencies `nFreq`, number of (selected) tapers `nTap`. - isTimeFrequency : bool - If `True`, input object contains time-frequency data, `False` otherwise - complexConversion : callable - Lambda function that performs complex-to-float conversion of Fourier - coefficients (if necessary). - pltDtype : str or :class:`numpy.dtype` - Numeric type of (potentially converted) complex Fourier coefficients. - dataLbl : str - Caption for y-axis or colorbar (depending on value of `isTimeFrequency`). - - Notes - ----- - This is an auxiliary method that is intended purely for internal use. Please - refer to the user-exposed methods :func:`~syncopy.singlepanelplot` and/or - :func:`~syncopy.multipanelplot` to actually generate plots of Syncopy data objects. - - See also - -------- - :meth:`syncopy.plotting.spy_plotting._prep_plots` : General basic input parsing for all Syncopy plotting routines - """ - - # Basic sanity checks for all plotting routines w/any Syncopy object - _prep_plots(self, name, **inputArgs) - - # Ensure our binary flags are actually binary - if not isinstance(inputArgs["avg_channels"], bool): - raise SPYTypeError(inputArgs["avg_channels"], varname="avg_channels", expected="bool") - if not isinstance(inputArgs["avg_tapers"], bool): - raise SPYTypeError(inputArgs["avg_tapers"], varname="avg_tapers", expected="bool") - if not isinstance(inputArgs.get("avg_trials", True), bool): - raise SPYTypeError(inputArgs["avg_trials"], varname="avg_trials", expected="bool") - - # Pass provided selections on to `Selector` class which performs error - # checking and generates required indexing arrays - self.selection = {"trials": inputArgs["trials"], - "channels": inputArgs["channels"], - "tapers": inputArgs["tapers"], - "toilim": inputArgs["toilim"], - "foilim": inputArgs["foilim"]} - - # Ensure any optional keywords controlling plotting appearance make sense - if inputArgs["title"] is not None: - if not isinstance(inputArgs["title"], str): - raise SPYTypeError(inputArgs["title"], varname="title", expected="str") - if inputArgs["grid"] is not None: - if not isinstance(inputArgs["grid"], bool): - raise SPYTypeError(inputArgs["grid"], varname="grid", expected="bool") - - # Get trial/channel/taper count and collect quantities in tuple - trList = self.selection.trials - nTrials = len(trList) - chArr = self.channel[self.selection.channel] - nChan = chArr.size - freqArr = self.freq[self.selection.freq] - nFreq = freqArr.size - tpArr = np.arange(self.taper.size)[self.selection.taper] - nTap = tpArr.size - dimCounts = (nTrials, nChan, nFreq, nTap) - dimArrs = (trList, chArr, freqArr, tpArr) - - # Determine whether we're dealing w/tf data - isTimeFrequency = False - if any([t.size > 1 for t in self.time]): - isTimeFrequency = True - - # Ensure provided min/max range for plotting TF data makes sense - vminmax = False - if inputArgs.get("vmin", None) is not None: - try: - scalar_parser(inputArgs["vmin"], varname="vmin") - except Exception as exc: - raise exc - vminmax = True - if inputArgs.get("vmax", None) is not None: - try: - scalar_parser(inputArgs["vmax"], varname="vmax") - except Exception as exc: - raise exc - vminmax = True - if inputArgs.get("vmin", None) and inputArgs.get("vmax", None): - if inputArgs["vmin"] >= inputArgs["vmax"]: - lgl = "minimal data range bound to be less than provided maximum " - act = "vmax < vmin" - raise SPYValueError(legal=lgl, varname="vmin/vamx", actual=act) - if vminmax and not isTimeFrequency: - msg = "`vmin` and `vmax` is only used for time-frequency visualizations" - SPYWarning(msg) - - # Check for complex entries in data and set datatype for plotting arrays - # constructed below (always use floats w/same precision as data) - if "complex" in self.data.dtype.name: - msg = "Found complex Fourier coefficients - visualization will use absolute values." - SPYWarning(msg) - complexConversion = lambda x: np.absolute(x).real - pltDtype = "f{}".format(self.data.dtype.itemsize) - dataLbl = "Absolute Frequency (dB)" - else: - complexConversion = lambda x: x - pltDtype = self.data.dtype - dataLbl = "Power (dB)" - - return dimArrs, dimCounts, isTimeFrequency, complexConversion, pltDtype, dataLbl diff --git a/syncopy/plotting/_singlepanelplot.py b/syncopy/plotting/_singlepanelplot.py new file mode 100644 index 000000000..fb0e22df8 --- /dev/null +++ b/syncopy/plotting/_singlepanelplot.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# +# Syncopy singlepanel plot backend +# + +from syncopy.plotting.config import pltConfig, pltErrMsg +from syncopy import __plt__ + +if __plt__: + import matplotlib.pyplot as ppl +else: + print(pltErrMsg.format("singlepanelplot")) + + +def mk_line_figax(xlabel='time (s)', ylabel='signal (a.u.)'): + + """ + Create the figure and axes for a + standard 2d-line plot + """ + + fig, ax = ppl.subplots(figsize=pltConfig['sFigSize']) + # Hide the right and top spines + ax.spines['right'].set_visible(False) + ax.spines['top'].set_visible(False) + ax.set_xlabel(xlabel) + ax.set_ylabel(ylabel) + + return fig, ax + + +def plot_lines(ax, data_x, data_y, **pkwargs): + + if 'alpha' not in pkwargs: + ax.plot(data_x, data_y, alpha=0.9, **pkwargs) + else: + ax.plot(data_x, data_y, **pkwargs) + if 'label' in pkwargs: + ax.legend(ncol=2, loc='upper right') diff --git a/syncopy/plotting/config.py b/syncopy/plotting/config.py new file mode 100644 index 000000000..b6f53a5a2 --- /dev/null +++ b/syncopy/plotting/config.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# +# Syncopy plotting setup +# + +from syncopy import __plt__ + +if __plt__: + import matplotlib as mpl + mpl.style.use('seaborn-colorblind') + # a hint of gray + mpl.rcParams['figure.facecolor'] = '#f5faf6' + mpl.rcParams['figure.edgecolor'] = '#f5faf6' + mpl.rcParams['axes.facecolor'] = '#f5faf6' + +# Global style settings for single-/multi-plots +pltConfig = {"sTitleSize": 12, + "sLabelSize": 14, + "sTickSize": 12, + "sLegendSize": 12, + "sFigSize": (6.4, 4.8), + "mTitleSize": 14, + "mLabelSize": 14, + "mTickSize": 10, + "mLegendSize": 12, + "mFigSize": (10, 6.8)} + +# Global consistent error message if matplotlib is missing +pltErrMsg = "\nSyncopy WARNING: Could not import 'matplotlib'. \n" +\ + "{} requires a working matplotlib installation. \n" +\ + "Please consider installing 'matplotlib', e.g., via conda: \n" +\ + "\tconda install matplotlib\n" +\ + "or using pip:\n" +\ + "\tpip install matplotlib" diff --git a/syncopy/plotting/spy_plotting.py b/syncopy/plotting/spy_plotting.py deleted file mode 100644 index 105c8fc76..000000000 --- a/syncopy/plotting/spy_plotting.py +++ /dev/null @@ -1,916 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Syncopy plotting routines -# - -# Builtin/3rd party package imports -import warnings -import numpy as np - -# Local imports -from syncopy.shared.kwarg_decorators import unwrap_cfg -from syncopy.shared.errors import SPYError, SPYTypeError, SPYValueError, SPYWarning -from syncopy.shared.parsers import data_parser, scalar_parser -from syncopy.shared.tools import get_defaults -from syncopy import __plt__ - -# Conditional imports and mpl customizations (provided mpl defaults have not been -# changed by user) -if __plt__: - import matplotlib.pyplot as plt - import matplotlib.style as mplstyle - import matplotlib as mpl - from matplotlib import colors - - # Syncopy default plotting settings - spyMplRc = {"figure.dpi": 100} - - # Check if we're running w/mpl's default settings: if user either changed - # existing setting or appended new ones (e.g., color definitions), abort - changeMplConf = True - rcDefaults = mpl.rc_params() - rcKeys = rcDefaults.keys() - with warnings.catch_warnings(): # examples.directory was deprecated in Matplotlib 3.0, silence the warning - warnings.simplefilter("ignore") - rcParams = dict(mpl.rcParams) - rcParams.pop("examples.directory", None) - rcParams.pop("backend") - rcParams.pop("interactive") - for key, value in rcParams.items(): - if key not in rcKeys: - changeMplConf = False - break - if rcDefaults[key] != value: - changeMplConf = False - break - - # If matplotlib's global config has not been changed, incorporate modifications - if changeMplConf: - mplstyle.use("fast") - for key, value in spyMplRc.items(): - mpl.rcParams[key] = value - -# Global style settings for single-/multi-plots -pltConfig = {"singleTitleSize": 12, - "singleLabelSize": 14, - "singleTickSize": 12, - "singleLegendSize": 12, - "singleFigSize": (6.4, 4.8), - "multiTitleSize": 14, - "multiLabelSize": 14, - "multiTickSize": 10, - "multiLegendSize": 12, - "multiFigSize": (10, 6.8)} - -# Global consistent error message if matplotlib is missing -pltErrMsg = "\nSyncopy WARNING: Could not import 'matplotlib'. \n" +\ - "{} requires a working matplotlib installation. \n" +\ - "Please consider installing 'matplotlib', e.g., via conda: \n" +\ - "\tconda install matplotlib\n" +\ - "or using pip:\n" +\ - "\tpip install matplotlib" - -__all__ = ["singlepanelplot", "multipanelplot"] - - -@unwrap_cfg -def singlepanelplot(*data, - trials="all", channels="all", tapers="all", - toilim=None, foilim=None, avg_channels=False, avg_tapers=True, - interp="spline36", cmap="plasma", vmin=None, vmax=None, - title=None, grid=None, overlay=True, fig=None, **kwargs): - """ - Plot contents of Syncopy data object(s) using single-panel figure(s) - - **Usage Summary** - - List of Syncopy data objects and respective valid plotting commands/selectors: - - :class:`~syncopy.AnalogData` : trials, channels, toi/toilim - Examples - - >>> fig1, fig2 = spy.singlepanelplot(data1, data2, channels=["channel1", "channel2"], overlay=False) - >>> cfg = spy.StructDict() - >>> cfg.trials = [5, 3, 0]; cfg.toilim = [0.25, 0.5] - >>> fig = spy.singlepanelplot(cfg, data1, data2, overlay=True) - - :class:`~syncopy.SpectralData` : trials, channels, tapers, toi/toilim, foi/foilim - Examples - - >>> fig1, fig2 = spy.singlepanelplot(data1, data2, channels=["channel1", "channel2"], - tapers=[3, 0], foilim=[30, 80], avg_channels=False, - avg_tapers=True, grid=True, overlay=False) - >>> cfg = spy.StructDict() - >>> cfg.trials = [1, 0, 3]; cfg.toilim = [-0.25, 0.5]; cfg.vmin=0.2; cfg.vmax=1.0 - >>> fig = spy.singlepanelplot(cfg, tfData1) - - Parameters - ---------- - data : Syncopy data object(s) - One or more non-empty Syncopy data object(s). **Note**: if multiple - datasets are provided, they must be all of the same type (e.g., - :class:`~syncopy.AnalogData`) and should contain the same or at - least comparable channels, trials etc. Consequently, some keywords are - only valid for certain types of Syncopy objects, e.g., `foilim` is not a - valid plotting-selector for an :class:`~syncopy.AnalogData` object. - trials : list (integers) or None or "all" - Trials to average across. Either list of integers representing trial numbers - (can include repetitions and need not be sorted), "all" or `None`. If `data` - is a (series of) :class:`~syncopy.AnalogData` object(s), `trials` may be - `None`, so that no trial information is used and the raw contents of - provided input dataset(s) is plotted (**Warning**: depending on the size - of the supplied dataset(s), this might be very memory-intensive). For all - other Syncopy data objects, `trials` must not be `None`. - channels : list (integers or strings), slice, range or "all" - Channel-selection; can be a list of channel names (``['channel3', 'channel1']``), - a list of channel indices (``[3, 5]``), a slice (``slice(3, 10)``) or - range (``range(3, 10)``). Selections can be unsorted and may include - repetitions. If multiple input objects are provided, `channels` needs to be a - valid selector for all supplied datasets. - tapers : list (integers or strings), slice, range or "all" - Taper-selection; can be a list of taper names (``['dpss-win-1', 'dpss-win-3']``), - a list of taper indices (``[3, 5]``), a slice (``slice(3, 10)``) or range - (``range(3, 10)``). Selections can be unsorted and may include repetitions - but must match exactly, be finite and not NaN. If multiple input objects - are provided, `tapers` needs to be a valid selector for all supplied datasets. - toilim : list (floats [tmin, tmax]) or None - Time-window ``[tmin, tmax]`` (in seconds) to be extracted from each trial. - Window specifications must be sorted and not NaN but may be unbounded. Boundaries - `tmin` and `tmax` are included in the selection. If `toilim` is `None`, - the entire time-span in each trial is selected. If multiple input objects - are provided, `toilim` needs to be a valid selector for all supplied datasets. - **Note** `toilim` is only a valid selector if `trials` is not `None`. - foilim : list (floats [fmin, fmax]) or "all" - Frequency-window ``[fmin, fmax]`` (in Hz) to be extracted from each trial; - Window specifications must be sorted and not NaN but may be unbounded. - Boundaries `fmin` and `fmax` are included in the selection. If `foilim` - is `None` or all frequencies are selected for plotting. If multiple input - objects are provided, `foilim` needs to be a valid selector for all supplied - datasets. - avg_channels : bool - If `True`, plot input dataset(s) averaged across channels specified by - `channels`. If `False`, no averaging is performed resulting in multiple - plots, each representing a single channel. - avg_tapers : bool - If `True`, plot :class:`~syncopy.SpectralData` objects averaged across - tapers specified by `tapers`. If `False`, no averaging is performed - resulting in multiple plots, each representing a single taper. - interp : str or None - Interpolation method used for plotting two-dimensional contour maps - such as time-frequency power spectra. To see a list of available - interpolation methods use the command ``list(mpl.image._interpd_.keys())``. - Please consult the matplotlib documentation for more details. - Has no effect on line-plots. - cmap : str - Colormap used for plotting two-dimensional contour maps - such as time-frequency power spectra. To see a list of available - color-maps use the command ``list(mpl.cm._cmap_registry.keys())``. - Pleasee consult the matplotlib documentation for more details. - Has no effect on line-plots. - vmin : float or None - Lower bound of data-range covered by colormap when plotting two-dimensional - contour maps such as time-frequency power spectra. If `vmin` is `None` - the minimal (absolute) value of the shown dataset is used. When comparing - multiple contour maps, all visualizations should use the same `vmin` to - ensure quantitative similarity of peak values. - vmax : float or None - Upper bound of data-range covered by colormap when plotting two-dimensional - contour maps such as time-frequency power spectra. If `vmax` is `None` - the maximal (absolute) value of the shown dataset is used. When comparing - multiple contour maps, all visualizations should use the same `vmin` to - ensure quantitative similarity of peak values. - title : str or None - If `str`, `title` specifies as axis panel-title, if `None`, an auto-generated - title is used. - grid : bool or None - If `True`, grid-lines are drawn, if `None` or `False` no grid-lines are - rendered. - overlay : bool - If `True`, and multiple input objects were provided, supplied datasets are - plotted on top of each other (in the order of submission). If a single object - was provided, ``overlay = True`` and `fig` is a :class:`~matplotlib.figure.Figure`, - the supplied dataset is overlaid on top of any existing plot(s) in `fig`. - **Note 1**: using an existing figure to overlay dataset(s) is only - supported for figures created with this routine. - **Note 2**: overlay-plotting is *not* supported for time-frequency - :class:`~syncopy.SpectralData` objects. - fig : matplotlib.figure.Figure or None - If `None`, new :class:`~matplotlib.figure.Figure` instance(s) are created - for provided input dataset(s). If `fig` is a :class:`~matplotlib.figure.Figure`, - the code attempts to overlay provided input dataset(s) on top of existing - plots in `fig`. **Note**: overlay-plots are only supported for figures - generated with this routine. Only a single figure can be provided. Thus, - in case of multiple input datasets with ``overlay = False``, any supplied - `fig` is ignored. - - Returns - ------- - fig : (list of) matplotlib.figure.Figure instance(s) - Either single figure (single input dataset or multiple input datasets - with ``overlay = True``) or list of figures (multiple input datasets - and ``overlay = False``). - - Notes - ----- - This function uses `matplotlib `_ to render data - visualizations. Thus, usage of Syncopy's plotting capabilities requires - a working matplotlib installation. - - The actual rendering is performed by class methods specific to the provided - input object types (e.g., :class:`~syncopy.AnalogData`). Thus, - :func:`~syncopy.singlepanelplot` is mainly a convenience function and management - routine that invokes the appropriate drawing code. - - Data subset selection for plotting is performed using :func:`~syncopy.selectdata`, - thus additional in-place data-selection via a `select` keyword is **not** supported. - - Examples - -------- - Please refer to the respective `singlepanelplot` class methods for detailed usage - examples specific to the respective Syncopy data object type. - - See also - -------- - :func:`~syncopy.multipanelplot` : visualize Syncopy objects using multi-panel figure(s) - :meth:`syncopy.AnalogData.singlepanelplot` : `singlepanelplot` for :class:`~syncopy.AnalogData` objects - :meth:`syncopy.SpectralData.singlepanelplot` : `singlepanelplot` for :class:`~syncopy.SpectralData` objects - """ - - # Abort if matplotlib is not available: FIXME -> `_prep_plots`? - if not __plt__: - raise SPYError(pltErrMsg.format("singlepanelplot")) - - # Collect all keywords of corresponding class-method (w/possibly user-provided - # values) in dictionary - defaults = get_defaults(data[0].singlepanelplot) - lcls = locals() - kwords = {} - for kword in defaults: - kwords[kword] = lcls[kword] - - # Call plotting manager - return _anyplot(*data, overlay=overlay, method="singlepanelplot", **kwords, **kwargs) - - -@unwrap_cfg -def multipanelplot(*data, - trials="all", channels="all", tapers="all", - toilim=None, foilim=None, avg_channels=False, avg_tapers=True, avg_trials=True, - panels="channels", interp="spline36", cmap="plasma", vmin=None, vmax=None, - title=None, grid=None, overlay=True, fig=None, **kwargs): - """ - Plot contents of Syncopy data object(s) using multi-panel figure(s) - - **Usage Summary** - - List of Syncopy data objects and respective valid plotting commands/selectors: - - :class:`~syncopy.AnalogData` : trials, channels, toi/toilim - Examples - - >>> fig = spy.multipanelplot(data, channels=["channel1", "channel2"]) - >>> cfg = spy.StructDict() - >>> cfg.trials = [5, 3, 0]; cfg.toilim = [0.25, 0.5] - >>> fig = spy.multipanelplot(cfg, data1, data2, overlay=True) - - :class:`~syncopy.SpectralData` : trials, channels, tapers, toi/toilim, foi/foilim - Examples - - >>> fig1, fig2 = spy.multipanelplot(data1, data2, channels=["channel1", "channel2"]) - >>> cfg = spy.StructDict() - >>> cfg.toilim = [0.25, 0.5]; cfg.foilim=[30, 80]; cfg.avg_trials = False - >>> cfg.avg_channels = True; cfg.panels = "trials" - >>> fig = spy.multipanelplot(cfg, tfData) - - Parameters - ---------- - data : Syncopy data object(s) - One or more non-empty Syncopy data object(s). **Note**: if multiple - datasets are provided, they must be all of the same type (e.g., - :class:`~syncopy.AnalogData`) and should contain the same or at - least comparable channels, trials etc. Consequently, some keywords are - only valid for certain types of Syncopy objects, e.g., "freqs" is not a - valid plotting-selector for an :class:`~syncopy.AnalogData` object. - trials : list (integers) or None or "all" - Either list of integers representing trial numbers - (can include repetitions and need not be sorted), "all" or `None`. If - `trials` is `None`, no trial information is used and the raw contents of - provided input dataset(s) is plotted (**Warning**: depending on the size - of the supplied dataset(s), this might be very memory-intensive). - channels : list (integers or strings), slice, range or "all" - Channel-selection; can be a list of channel names (``['channel3', 'channel1']``), - a list of channel indices (``[3, 5]``), a slice (``slice(3, 10)``) or - range (``range(3, 10)``). Selections can be unsorted and may include - repetitions. If multiple input objects are provided, `channels` needs to be a - valid selector for all supplied datasets. - tapers : list (integers or strings), slice, range or "all" - Taper-selection; can be a list of taper names (``['dpss-win-1', 'dpss-win-3']``), - a list of taper indices (``[3, 5]``), a slice (``slice(3, 10)``) or range - (``range(3, 10)``). Selections can be unsorted and may include repetitions - but must match exactly, be finite and not NaN. If multiple input objects - are provided, `tapers` needs to be a valid selector for all supplied datasets. - toilim : list (floats [tmin, tmax]) or None - Time-window ``[tmin, tmax]`` (in seconds) to be extracted from each trial. - Window specifications must be sorted and not NaN but may be unbounded. Edges - `tmin` and `tmax` are included in the selection. If `toilim` is `None`, - the entire time-span in each trial is selected. If multiple input objects - are provided, `toilim` needs to be a valid selector for all supplied datasets. - **Note** `toilim` is only a valid selector if `trials` is not `None`. - foilim : list (floats [fmin, fmax]) or "all" - Frequency-window ``[fmin, fmax]`` (in Hz) to be extracted from each trial; - Window specifications must be sorted and not NaN but may be unbounded. - Boundaries `fmin` and `fmax` are included in the selection. If `foilim` - is `None` or all frequencies are selected for plotting. If multiple input - objects are provided, `foilim` needs to be a valid selector for all supplied - datasets. - avg_channels : bool - If `True`, plot input dataset(s) averaged across channels specified by - `channels`. If `False` no channel-averaging is performed. - avg_tapers : bool - If `True`, plot :class:`~syncopy.SpectralData` objects averaged across - tapers specified by `tapers`. If `False`, no averaging is performed. - avg_trials : bool - If `True`, plot input dataset(s) averaged across trials specified by `trials`. - Specific panel allocation depends on value of `avg_channels` and `avg_tapers` - (if applicable). For :class:`~syncopy.AnalogData` objects setting `avg_trials` - to `True` but `trials` to `None` triggers a :class:`~syncopy.shared.errors.SPYValueError`. - panels : str - Panel specification. Only valid for :class:`~syncopy.SpectralData` objects. - Can be `"channels"`, `"trials"`, or `"tapers"`. - Panel specification and averaging flags have to align, i.e., if `panels` - is `trials` then `avg_trials` must be `False`, otherwise the code issues - a :class:`~syncopy.shared.errors.SPYWarning` and exits. Note that a - multi-panel visualization of time-frequency datasets requires averaging - across two out of three data dimensions (i.e., two of the flags `avg_channels`, - `avg_tapers` and `avg_trials` must be `True`). - interp : str or None - Interpolation method used for plotting two-dimensional contour maps - such as time-frequency power spectra. To see a list of available - interpolation methods use the command ``list(mpl.image._interpd_.keys())``. - Please consult the matplotlib documentation for more details. - Has no effect on line-plots. - cmap : str - Colormap used for plotting two-dimensional contour maps - such as time-frequency power spectra. To see a list of available - color-maps use the command ``list(mpl.cm._cmap_registry.keys())``. - Pleasee consult the matplotlib documentation for more details. - Has no effect on line-plots. - vmin : float or None - Lower bound of data-range covered by colormap when plotting two-dimensional - contour maps such as time-frequency power spectra. If `vmin` is `None` - the minimal (absolute) value across all shown panels is used. When comparing - multiple objects, all visualizations should use the same `vmin` to - ensure quantitative similarity of peak values. - vmax : float or None - Upper bound of data-range covered by colormap when plotting two-dimensional - contour maps such as time-frequency power spectra. If `vmax` is `None` - the maximal (absolute) value of all shown panels is used. When comparing - multiple contour maps, all visualizations should use the same `vmin` to - ensure quantitative similarity of peak values. - title : str or None - If `str`, `title` specifies figure title, if `None`, an auto-generated - title is used. - grid : bool or None - If `True`, grid-lines are drawn, if `None` or `False` no grid-lines are - rendered. - overlay : bool - If `True`, and multiple input objects were provided, supplied datasets are - plotted on top of each other (in the order of submission). If a single object - was provided, ``overlay = True`` and `fig` is a :class:`~matplotlib.figure.Figure`, - the supplied dataset is overlaid on top of any existing plot(s) in `fig`. - **Note 1**: using an existing figure to overlay dataset(s) is only - supported for figures created with this routine. - **Note 2**: overlay-plotting is *not* supported for time-frequency - :class:`~syncopy.SpectralData` objects. - fig : matplotlib.figure.Figure or None - If `None`, new :class:`~matplotlib.figure.Figure` instance(s) are created - for provided input dataset(s). If `fig` is a :class:`~matplotlib.figure.Figure`, - the code attempts to overlay provided input dataset(s) on top of existing - plots in `fig`. **Note**: overlay-plots are only supported for figures - generated with this routine. Only a single figure can be provided. Thus, - in case of multiple input datasets with ``overlay = False``, any supplied - `fig` is ignored. - - Returns - ------- - fig : (list of) matplotlib.figure.Figure instance(s) - Either single figure (single input dataset or multiple input datasets - with ``overlay = True``) or list of figures (multiple input datasets - and ``overlay = False``). - - Notes - ----- - This function uses `matplotlib `_ to render data - visualizations. Thus, usage of Syncopy's plotting capabilities requires - a working matplotlib installation. - - The actual rendering is performed by class methods specific to the provided - input object types (e.g., :class:`~syncopy.AnalogData`). Thus, - :func:`~syncopy.multipanelplot` is mainly a convenience function and management routine - that invokes the appropriate drawing code. - - Data subset selection for plotting is performed using :func:`~syncopy.selectdata`, - thus additional in-place data-selection via a `select` keyword is **not** supported. - - Examples - -------- - Please refer to the respective `multipanelplot` class methods for detailed usage - examples specific to the respective Syncopy data object type. - - See also - -------- - :func:`~syncopy.singlepanelplot` : visualize Syncopy objects using single-panel figure(s) - :meth:`syncopy.AnalogData.multipanelplot` : `multipanelplot` for :class:`~syncopy.AnalogData` objects - :meth:`syncopy.SpectralData.multipanelplot` : `multipanelplot` for :class:`~syncopy.SpectralData` objects - """ - - # Abort if matplotlib is not available FIXME -> `_prep_plots`? - if not __plt__: - raise SPYError(pltErrMsg.format("multipanelplot")) - - # Collect all keywords of corresponding class-method (w/possibly user-provided - # values) in dictionary - defaults = get_defaults(data[0].multipanelplot) - lcls = locals() - kwords = {} - for kword in defaults: - kwords[kword] = lcls[kword] - - # Call plotting manager - return _anyplot(*data, overlay=overlay, method="multipanelplot", **kwords, **kwargs) - - -def _anyplot(*data, overlay=None, method=None, **kwargs): - """ - Local management routine that invokes respective class methods based on - caller (`obj.singlepanelplot` or `obj.multipanelplot`) - - This is an auxiliary method that is intended purely for internal use. Please - refer to the user-exposed methods :func:`~syncopy.singlepanelplot` and/or - :func:`~syncopy.multipanelplot` to actually generate plots of Syncopy data objects. - """ - - # The only error-checking done in here: ensure `overlay` is Boolean and assert - # `data` contains only non-empty Syncopy objects - if not isinstance(overlay, bool): - raise SPYTypeError(overlay, varname="overlay", expected="bool") - for obj in data: - try: - data_parser(obj, varname="data", empty=False) - except Exception as exc: - raise exc - # FIXME: while plotting is still WIP - if obj.__class__.__name__ not in ["AnalogData", "SpectralData"]: - errmsg = "Plotting currently only supported for `AnalogData` and `SpectralData` objects" - raise NotImplementedError(errmsg) - - # See if figure was provided - start = 0 - nData = len(data) - fig = kwargs.pop("fig", None) - if not overlay and fig is not None and nData > 1: - msg = "User-provided figures not supported for non-overlay visualization " +\ - "of {} datasets. Supplied figure will not be used. " - SPYWarning(msg.format(nData)) - fig = None - - # If we're overlaying, preserve initial figure object to plot over iteratively - if overlay: - fig = getattr(data[0], method)(fig=fig, **kwargs) - start = 1 - figList = [] - for n in range(start, nData): - figList.append(getattr(data[n], method)(fig=fig, **kwargs)) - - # Return single figure object (if `overlay` is `True`) or list of mulitple figs - if overlay: - return fig - return figList - - -def _prep_plots(self, name, **inputs): - """ - Helper performing most basal error checking for all plotting sub-routines - - Parameters - ---------- - self : Syncopy data object - Input object that is being processed by the respective :func:`~syncopy.singlepanelplot` - or :func:`~syncopy.multipanelplot` function/class method. - name : str - Name of caller (i.e., "singlepanelplot" or "multipanelplot") - inputArgs : dict - Input arguments of caller (i.e., :func:`~syncopy.singlepanelplot` or - :func:`~syncopy.multipanelplot`) collected in dictionary - - Returns - ------- - Nothing : None - - Notes - ----- - This is an auxiliary method that is intended purely for internal use. Please - refer to the user-exposed methods :func:`~syncopy.singlepanelplot` and/or - :func:`~syncopy.multipanelplot` to actually generate plots of Syncopy data objects. - - See also - -------- - :meth:`syncopy.plotting._plot_spectral._prep_spectral_plots` : sanity checks and data selection for plotting :class:`~syncopy.SpectralData` objects - :meth:`syncopy.plotting._plot_analog._prep_analog_plots` : sanity checks and data selection for plotting :class:`~syncopy.AnalogData` objects - """ - - # Abort if matplotlib is not available - if not __plt__: - raise SPYError(pltErrMsg.format(name)) - - # Abort if in-place selection is attempted - if inputs.get("kwargs", {}).get("select") is not None: - msg = "In-place data-selection not supported in plotting routines. " + \ - "Please use method-specific keywords (`trials`, `channels`, etc.) instead. " - raise SPYError(msg) - - -def _prep_toilim_avg(self): - """ - Set up averaging data across trials given `toilim` selection - - Parameters - ---------- - self : Syncopy data object - Input object that is being processed by the respective :func:`~syncopy.singlepanelplot` - or :func:`~syncopy.multipanelplot` function/class method. - - Returns - ------- - tLengths : 1D :class:`numpy.ndarray` - Array of length `nSelectedTrials` with each element encoding the number of - samples contained in the provided `toilim` selection. - - Notes - ----- - If `tLengths` contains more than one unique element, a - :class:`~syncopy.shared.errors.SPYValueError` is raised. - - Note further, that this is an auxiliary method that is intended purely for - internal use. Please refer to the user-exposed methods :func:`~syncopy.singlepanelplot` - and/or :func:`~syncopy.multipanelplot` to actually generate plots of Syncopy data objects. - - See also - -------- - :func:`~syncopy.singlepanelplot` : visualize Syncopy objects using single-panel figure(s) - :func:`~syncopy.multipanelplot` : visualize Syncopy objects using multi-panel figure(s) - """ - - tLengths = np.zeros((len(self.selection.trials),), dtype=np.intp) - for k, tsel in enumerate(self.selection.time): - if not isinstance(tsel, slice): - msg = "Cannot average `toilim` selection. Please check `.time` property for consistency. " - raise SPYError(msg) - start, stop = tsel.start, tsel.stop - if start is None: - start = 0 - if stop is None: - stop = self._get_time([self.selection.trials[k]], - toilim=[-np.inf, np.inf])[0].stop - tLengths[k] = stop - start - - if np.unique(tLengths).size > 1: - lgl = "time-selections of equal length for averaging across trials" - act = "time-selections of varying length" - raise SPYValueError(legal=lgl, varname="toilim", actual=act) - - if tLengths[0] < 2: - lgl = "time-selections containing at least two samples" - act = "time-selections containing fewer than two samples" - raise SPYValueError(legal=lgl, varname="toilim", actual=act) - - return tLengths - - -def _setup_figure(npanels, nrow=None, ncol=None, xLabel=None, yLabel=None, - include_colorbar=False, sharex=None, sharey=None, grid=None): - """ - Create and set up a :class:`~matplotlib.figure.Figure` object for Syncopy visualizations - - Parameters - ---------- - npanels, nrow, ncol : int or None - Subplot-panel parameters. Please refer to :func:`._layout_subplot_panels` - for details. - xLabel : str or None - If not `None`, x-axis caption. - yLabel : str or None - If not `None`, y-axis caption. - include_colorbar : bool - If `True`, axis panel(s) are set up to leave enough space for a colorbar - sharex : bool or None - If `True`, axis panels have common x-axis ticks and limits. If `None` - or `False`, x-ticks and -limits are not shared across axis panels. - sharey : bool or None - If `True`, axis panels have common y-axis ticks and limits. If `None` - or `False`, y-ticks and -limits are not shared across axis panels. - grid : bool or None - If `True`, axis panels are set up to include grid-lines, if `None` or - `False` no grid-lines will be rendered. - - Returns - ------- - fig : :class:`~matplotlib.figure.Figure` object - Matplotlib figure formatted for Syncopy plotting routines - ax : (list of) :class:`~matplotlib.axis.Axis` instance(s) - Either single :class:`~matplotlib.axis.Axis` object (if ``npanels = 1``) - or list of multiple :class:`~matplotlib.axis.Axis` objects (if ``npanels > 1``) - cax : None or :class:`~matplotlib.axis.Axis` - If `include_colorbar` is `True`, all axis panels are laid out to leave - space for an additional axis `cax` reserved for a colorbar. - - Notes - ----- - If `npanels` is greater than one, the local helper function :func:`._layout_subplot_panels` - is invoked to create a space-optimal panel grid (adjusted for an additional - axis reserved for a colorbar, if `include_colorbar` is `True`). - - To ease internal processing, this routine attaches a number of additional - attributes to the generated :class:`~matplotlib.figure.Figure` object `fig`, namely: - - * **fig.singlepanelplot** (bool): if ``npanels = 1`` the caller was :func:`~syncopy.singlepanelplot` - and the identically named attribute is created - * **fig.multipanelplot** (bool): conversely, if ``npanels > 1`` the caller was - :func:`~syncopy.multipanelplot` and the identically named attribute is created - * **fig.objCount** (int): internal counter used for overlay-plotting. Initially, - `fig.objCount` is set to 0, every overly increments `fig.objCount` by one - * **fig.npanels** (int): number of panels as given by the input argument `npanels` - - Note that this is an auxiliary method that is intended purely for internal use. - Please refer to the user-exposed methods :func:`~syncopy.singlepanelplot` - and/or :func:`~syncopy.multipanelplot` to actually generate plots of Syncopy data objects. - - See also - -------- - :func:`._layout_subplot_panels` : Create space-optimal subplot grid for Syncopy visualizations - :func:`._setup_colorbar` : format colorbar for Syncopy visualizations - """ - - # If `grid` was not provided, do not render grid-lines in plots - if grid is None: - grid = False - - # Note: if `xLabel` and/or `yLabel` is `None`, setting the corresponding axis - # label simply uses an empty string '' and does not alter the axis - no need - # for any ``if is None``` gymnastics below - if npanels == 1: - - # Simplest case: single panel, no colorbar - if not include_colorbar: - fig, ax = plt.subplots(1, tight_layout=True, squeeze=True, - figsize=pltConfig["singleFigSize"]) - - # Single panel w/colorbar - else: - fig, (ax, cax) = plt.subplots(1, 2, tight_layout=True, squeeze=True, - gridspec_kw={"wspace": 0.05, "width_ratios": [1, 0.025]}, - figsize=pltConfig["singleFigSize"]) - cax.tick_params(axis="both", labelsize=pltConfig["singleTickSize"]) - - # Axes formatting gymnastics done for all single-panel plots - ax.set_xlabel(xLabel, size=pltConfig["singleLabelSize"]) - ax.set_ylabel(yLabel, size=pltConfig["singleLabelSize"]) - ax.tick_params(axis="both", labelsize=pltConfig["singleTickSize"]) - ax.autoscale(enable=True, axis="x", tight=True) - ax.grid(grid) - - # Designate figure object as single-panel plotting target - fig.singlepanelplot = True - - else: - - # Either use provided row/col settings or compute best fit - nrow, ncol = _layout_subplot_panels(npanels, nrow, ncol) - - # If no explicit axis sharing settings were provided, make an executive decision - if sharex is None: - sharex = True - if sharey is None: - sharey = True - - # Multiple panels, no colorbar - if not include_colorbar: - (fig, ax_arr) = plt.subplots(nrow, ncol, constrained_layout=False, - gridspec_kw={"wspace": 0, "hspace": 0.35, - "left": 0.05, "right": 0.97}, - figsize=pltConfig["multiFigSize"], - sharex=sharex, sharey=sharey, squeeze=False) - - # Multiple panels, append colorbar via gridspec - else: - (fig, ax_arr) = plt.subplots(nrow, ncol, constrained_layout=False, - gridspec_kw={"wspace": 0, "hspace": 0.35, - "left": 0.05, "right": 0.94}, - figsize=pltConfig["multiFigSize"], - sharex=sharex, sharey=sharey, squeeze=False) - gs = fig.add_gridspec(nrows=nrow, ncols=1, left=0.945, right=0.955) - cax = fig.add_subplot(gs[:, 0]) - cax.tick_params(axis="both", labelsize=pltConfig["multiTickSize"]) - - # Show xlabel only on bottom row of panels - for col in range(ncol): - ax_arr[-1, col].set_xlabel(xLabel, size=pltConfig["multiLabelSize"]) - - # Omit first x-tick in all panels except first panel-row, show ylabel only - # on left border of first panel column - for row in range(nrow): - for col in range(1, ncol): - ax_arr[row, col].xaxis.get_major_locator().set_params(prune="lower") - ax_arr[row, 0].set_ylabel(yLabel, size=pltConfig["multiLabelSize"]) - - # Flatten axis array (to ease panel counting) and remove surplus panels - ax_arr = ax_arr.flatten(order="C") - for ax in ax_arr: - ax.tick_params(axis="both", labelsize=pltConfig["multiTickSize"]) - ax.autoscale(enable=True, axis="x", tight=True) - ax.grid(grid) - for k in range(npanels, nrow * ncol): - ax_arr[k].remove() - ax = ax_arr - - # Designate figure object as multi-panel plotting target - fig.multipanelplot = True - - # Attach custom Syncopy plotting attributes to newly created figure - fig.objCount = 0 - fig.npanels = npanels - - # All done, return figure object, axis (array) and potentially color-bar axis - if not include_colorbar: - return fig, ax - return fig, ax, cax - - -def _setup_colorbar(fig, ax, cax, label=None, outline=False, vmin=None, vmax=None): - """ - Create and format a :class:`~matplotlib.colorbar.Colorbar` object for Syncopy visualizations - - Parameters - ---------- - fig : :class:`~matplotlib.figure.Figure` - Matplotlib figure object created by :func:`._setup_figure` - ax : (list of) :class:`~matplotlib.axis.Axis` - Either single :class:`~matplotlib.axis.Axis` object or list of multiple - :class:`~matplotlib.axis.Axis` objects created by :func:`._setup_figure` - cax : :class:`~matplotlib.axis.Axis` - Matplotlib :class:`~matplotlib.axis.Axis` object created by :func:`._setup_figure` - reserved for a colorbar - label : None or str - Caption for colorbar (if not `None`) - outline : bool - If `True`, draw border-lines around colorbar. - vmin : float or None - If not `None`, lower bound of data-range covered by colorbar. If `vmin` - is `None`, the colorbar uses the lowest data-value found in the last - invoked axis. - vmax : float or None - If not `None`, upper bound of data-range covered by colorbar. If `vmax` - is `None`, the colorbar uses the highest data-value found in the last - invoked axis. - - Returns - ------- - cbar : :class:`~matplotlib.colorbar.Colorbar` - Color-bar attached to provided :class:`~matplotlib.axis.Axis` `cax` - - Notes - ----- - This is an auxiliary method that is intended purely for internal use. Please - refer to the user-exposed methods :func:`~syncopy.singlepanelplot` and/or - :func:`~syncopy.multipanelplot` to actually generate plots of Syncopy data objects. - - See also - -------- - :func:`._layout_subplot_panels` : Create space-optimal subplot grid for Syncopy visualizations - :func:`._setup_figure` : create figures for Syncopy visualizations - """ - - if fig.npanels == 1: - axes = [ax] - else: - axes = ax - - if vmin is not None or vmax is not None: - norm = colors.Normalize(vmin=vmin, vmax=vmax) - for k in range(fig.npanels): - axes[k].images[0].set_norm(norm) - cbar = fig.colorbar(axes[0].images[0], cax=cax) - cbar.set_label(label, size=pltConfig["singleLabelSize"]) - cbar.outline.set_visible(outline) - return cbar - - -def _layout_subplot_panels(npanels, nrow=None, ncol=None, ndefault=5, maxpanels=50): - """ - Create space-optimal subplot grid given required number of panels - - Parameters - ---------- - npanels : int - Number of required subplot panels in figure - nrow : int or None - Required number of panel rows. Note, if both `nrow` and `ncol` are not `None`, - then ``nrow * ncol >= npanels`` has to be satisfied, otherwise a - :class:`~syncopy.shared.errors.SPYValueError` is raised. - ncol : int or None - Required number of panel columns. Note, if both `nrow` and `ncol` are not `None`, - then ``nrow * ncol >= npanels`` has to be satisfied, otherwise a - :class:`~syncopy.shared.errors.SPYValueError` is raised. - ndefault: int - Default number of panel columns for grid construction (only relevant if - both `nrow` and `ncol` are `None`). - maxpanels : int - Maximally allowed number of subplot panels for which a grid is constructed - - Returns - ------- - nrow : int - Number of rows of constructed subplot panel grid - nrow : int - Number of columns of constructed subplot panel grid - - Notes - ----- - If both `nrow` and `ncol` are `None`, the constructed grid will have the - dimension `N` x `ndefault`, where `N` is chosen "optimally", i.e., the smallest - integer that satisfies ``ndefault * N >= npanels``. - Note further, that this is an auxiliary method that is intended purely for - internal use. Thus, error-checking is only performed on potentially user-provided - inputs (`nrow` and `ncol`). - - See also - -------- - :func:`._setup_figure` : create and prepare figures for Syncopy visualizations - - Examples - -------- - Create grid of default dimensions to hold eight panels - - >>> _layout_subplot_panels(8, ndefault=5) - (2, 5) - - Create a grid that must have 4 rows - - >>> _layout_subplot_panels(8, nrow=4) - (4, 2) - - Create a grid that must have 8 columns - - >>> _layout_subplot_panels(8, ncol=8) - (1, 8) - """ - - # Abort if requested panel count is less than one or exceeds provided maximum - try: - scalar_parser(npanels, varname="npanels", ntype="int_like", lims=[1, np.inf]) - except Exception as exc: - raise exc - if npanels > maxpanels: - lgl = "a maximum of {} panels in total".format(maxpanels) - raise SPYValueError(legal=lgl, actual=str(npanels), varname="npanels") - - # Row specifcation was provided, cols may or may not - if nrow is not None: - try: - scalar_parser(nrow, varname="nrow", ntype="int_like", lims=[1, np.inf]) - except Exception as exc: - raise exc - if ncol is None: - ncol = np.ceil(npanels / nrow).astype(np.intp) - - # Column specifcation was provided, rows may or may not - if ncol is not None: - try: - scalar_parser(ncol, varname="ncol", ntype="int_like", lims=[1, np.inf]) - except Exception as exc: - raise exc - if nrow is None: - nrow = np.ceil(npanels / ncol).astype(np.intp) - - # After the preparations above, this condition is *only* satisfied if both - # `nrow` = `ncol` = `None` -> then use generic grid-layout - if nrow is None: - ncol = ndefault - nrow = np.ceil(npanels / ncol).astype(np.intp) - ncol = min(ncol, npanels) - - # Complain appropriately if requested no. of panels does not fit inside grid - if nrow * ncol < npanels: - lgl = "row- and column-specification of grid to fit all panels" - act = "grid with {0} rows and {1} columns but {2} panels" - raise SPYValueError(legal=lgl, actual=act.format(nrow, ncol, npanels), - varname="nrow/ncol") - - # In case a grid was provided too big for the requested no. of panels (e.g., - # 8 panels in an 4 x 3 grid -> would fit in 3 x 3), just warn, don't crash - if nrow * ncol - npanels >= ncol: - msg = "Grid dimension ({0} rows x {1} columns) larger than necessary " +\ - "for {2} panels. " - SPYWarning(msg.format(nrow, ncol, npanels)) - - return nrow, ncol diff --git a/syncopy/tests/local_spy.py b/syncopy/tests/local_spy.py index 83eb16a4d..77de254b4 100644 --- a/syncopy/tests/local_spy.py +++ b/syncopy/tests/local_spy.py @@ -24,15 +24,12 @@ # Prepare code to be executed using, e.g., iPython's `%run` magic command if __name__ == "__main__": - mock_up = np.arange(24).reshape((8, 3)) - ad1 = spy.AnalogData([mock_up] * 5) - nTrials = 50 nSamples = 500 fs = 500 f1, f2 = 20, 40 - A1, A2 = 2, 3 + A1, A2 = 0.3, 1 nSamples = 2000 @@ -64,6 +61,6 @@ print(trl.mean()) ad2 = spy.AnalogData(trls, samplerate=2000) - spec = spy.freqanalysis(ad2, tapsmofrq=5, keeptrials=False) - coh = spy.connectivityanalysis(ad2, method='coh', tapsmofrq=5) - gr = spy.connectivityanalysis(ad2, method='granger', tapsmofrq=10, polyremoval=0) + #spec = spy.freqanalysis(ad2, tapsmofrq=5, keeptrials=False) + #coh = spy.connectivityanalysis(ad2, method='coh', tapsmofrq=5) + #gr = spy.connectivityanalysis(ad2, method='granger', tapsmofrq=10, polyremoval=0) From ff6d6f416e063c35ba67918cf9e6bf410a534af1 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Thu, 17 Mar 2022 15:31:45 +0100 Subject: [PATCH 111/166] NEW: Singlepanel plot for SpectralData - both 2d line plots and image plots for time-frequency --- syncopy/datatype/continuous_data.py | 67 ++++++++++++++++++++++-- syncopy/plotting/_helpers.py | 77 +++++++++++++++++++++++----- syncopy/plotting/_singlepanelplot.py | 51 ++++++++++++++++-- syncopy/plotting/config.py | 7 +-- syncopy/tests/local_spy.py | 16 +++--- 5 files changed, 189 insertions(+), 29 deletions(-) diff --git a/syncopy/datatype/continuous_data.py b/syncopy/datatype/continuous_data.py index 208fa1386..a777e5fcb 100644 --- a/syncopy/datatype/continuous_data.py +++ b/syncopy/datatype/continuous_data.py @@ -18,7 +18,7 @@ from .base_data import BaseData, FauxTrial from .methods.definetrial import definetrial from syncopy.shared.parsers import scalar_parser, array_parser -from syncopy.shared.errors import SPYValueError +from syncopy.shared.errors import SPYValueError, SPYWarning from syncopy.shared.tools import best_match from syncopy.plotting import _singlepanelplot as sp_plot from syncopy.plotting import _helpers as plot_helpers @@ -488,8 +488,18 @@ def singlepanelplot(self, shifted=True, **show_kwargs): show_kwargs : :func:`~syncopy.datatype.methods.show.show` arguments """ + # right now we have to enforce + # single trial selection only + trl = show_kwargs.get('trials', None) + if not isinstance(trl, int) and len(self.trials) > 1: + SPYWarning("Please select a single trial for plotting!") + return + # only 1 trial so no explicit selection needed + elif len(self.trials) == 1: + trl = 0 + # get the data to plot - data_x = plot_helpers.parse_toi(self, show_kwargs) + data_x = plot_helpers.parse_toi(self, trl, show_kwargs) data_y = self.show(**show_kwargs) # multiple channels? @@ -642,7 +652,58 @@ def __init__(self, self.freq = [1] if taper is not None: self.taper = ['taper'] - + + # implement plotting + def singlepanelplot(self, **show_kwargs): + + """ + Plot either a 2d-line plot in case of + singleton time axis or an image plot + for time-frequency spectra. + + Parameters + ---------- + show_kwargs : :func:`~syncopy.datatype.methods.show.show` arguments + """ + + # right now we have to enforce + # single trial selection only + trl = show_kwargs.get('trials', None) + if not isinstance(trl, int) and len(self.trials) > 1: + SPYWarning("Please select a single trial for plotting!") + return + elif len(self.trials) == 1: + trl = 0 + + # how got the spectrum computed + method = plot_helpers.get_method(self) + if method in ('wavelet', 'superlet', 'mtmconvol'): + # multiple channels? + label = plot_helpers.parse_channel(self, show_kwargs) + if not isinstance(label, str): + SPYWarning("Please select a single channel for plotting!") + return + fig, ax = sp_plot.mk_img_figax(title=f'{label}') + + time = plot_helpers.parse_toi(self, trl, show_kwargs) + # dimord is time x taper x freq x channel + # need freq x time for plotting + data_yx = self.show(**show_kwargs).T + sp_plot.plot_tfreq(ax, data_yx, time, self.freq) + # just a line plot + else: + # get the data to plot + data_x = plot_helpers.parse_foi(self, show_kwargs) + data_y = np.log10(self.show(**show_kwargs)) + + # multiple channels? + labels = plot_helpers.parse_channel(self, show_kwargs) + + # create the axes and figure + fig, ax = sp_plot.mk_line_figax(xlabel='frequency (Hz)', + ylabel='power (dB)') + sp_plot.plot_lines(ax, data_x, data_y, label=labels) + class CrossSpectralData(ContinuousData): """ diff --git a/syncopy/plotting/_helpers.py b/syncopy/plotting/_helpers.py index 70ab0f353..adcd30b42 100644 --- a/syncopy/plotting/_helpers.py +++ b/syncopy/plotting/_helpers.py @@ -1,15 +1,46 @@ # -*- coding: utf-8 -*- # -# Helpers to parse show keyword outputs -# to generate correct data, labels etc. for the plots +# Helpers to generate correct data, labels etc. for the plots +# from Syncopy dataypes # +# Builtin/3rd party package imports import numpy as np +import re + +# Syncopy imports from syncopy.shared.tools import best_match from syncopy.shared.errors import SPYWarning -def parse_toi(dataobject, show_kwargs): +def parse_foi(dataobject, show_kwargs): + + """ + Create the frequency axis belonging to a foi/foilim + selection + + Parameters + ---------- + dataobject : one derived from :class:`~syncopy.datatype.base_data` + Syncopy datatype instance, needs to have a `freq` property + show_kwargs : dict + The keywords provided to the `show` method + """ + + freq = dataobject.freq + # cut to foi selection + foilim = show_kwargs.get('foilim', None) + if foilim is not None: + freq, _ = best_match(freq, foilim, span=True) + # here show is broken atm, issue #240 + foi = show_kwargs.get('foi', None) + if foi is not None: + freq, _ = best_match(freq, foi, span=False) + + return freq + + +def parse_toi(dataobject, trl, show_kwargs): """ Create the (multiple) time axis belonging to a toi/toilim @@ -19,17 +50,12 @@ def parse_toi(dataobject, show_kwargs): ---------- dataobject : one derived from :class:`~syncopy.datatype.base_data` Syncopy datatype instance, needs to have a `time` property + trl : int + The index of the selected trial to plot show_kwargs : dict The keywords provided to the `show` method """ - # right now we have to enforce - # single trial selection only - trl = show_kwargs.get('trials', None) - if not isinstance(trl, int): - SPYWarning("Please select a single trial for plotting!") - return - time = dataobject.time[trl] # cut to time selection toilim = show_kwargs.get('toilim', None) @@ -55,6 +81,13 @@ def parse_channel(dataobject, show_kwargs): Syncopy datatype instance, needs to have a `channel` property show_kwargs : dict The keywords provided to the `show` method + + Returns + ------- + labels : str or list + Depending on the channel selection returns + a list of str for multiple channels or a single + str for a single channel selection. """ chs = show_kwargs.get('channel', None) @@ -81,8 +114,28 @@ def parse_channel(dataobject, show_kwargs): def shift_multichan(data_y): if data_y.ndim > 1: - offsets = data_y.max(axis=0) + 1 - offsets = np.r_[0, offsets[1:]] + # shift 0-line for next channel + # above max of prev. + offsets = data_y.max(axis=0)[:-1] + # shift even further if next channel + # dips below 0 + offsets += np.abs(data_y.min(axis=0)[1:]) + offsets = np.r_[0, offsets] * 1.1 data_y += offsets return data_y + + +def get_method(dataobject): + + """ + Returns the method string from + the log of a Syncopy data object + """ + + # get the method string in a capture group + pattern = re.compile('[\s\w\D]+method = (\w+)') + match = pattern.match(dataobject._log) + if match: + meth_str = match.group(1) + return meth_str diff --git a/syncopy/plotting/_singlepanelplot.py b/syncopy/plotting/_singlepanelplot.py index fb0e22df8..250e32db0 100644 --- a/syncopy/plotting/_singlepanelplot.py +++ b/syncopy/plotting/_singlepanelplot.py @@ -11,6 +11,8 @@ else: print(pltErrMsg.format("singlepanelplot")) +# -- 2d-line plots -- + def mk_line_figax(xlabel='time (s)', ylabel='signal (a.u.)'): @@ -23,8 +25,11 @@ def mk_line_figax(xlabel='time (s)', ylabel='signal (a.u.)'): # Hide the right and top spines ax.spines['right'].set_visible(False) ax.spines['top'].set_visible(False) - ax.set_xlabel(xlabel) - ax.set_ylabel(ylabel) + ax.tick_params(axis='both', which='major', + labelsize=pltConfig['sTickSize']) + + ax.set_xlabel(xlabel, fontsize=pltConfig['sLabelSize']) + ax.set_ylabel(ylabel, fontsize=pltConfig['sLabelSize']) return fig, ax @@ -36,4 +41,44 @@ def plot_lines(ax, data_x, data_y, **pkwargs): else: ax.plot(data_x, data_y, **pkwargs) if 'label' in pkwargs: - ax.legend(ncol=2, loc='upper right') + ax.legend(ncol=2, loc='upper right', + fontsize=pltConfig['sLegendSize']) + + +# -- image plots -- + +def mk_img_figax(xlabel='time (s)', ylabel='frequency (Hz)', title=''): + + """ + Create the figure and axes for an + image plot with `imshow` + """ + + fig, ax = ppl.subplots(figsize=pltConfig['sFigSize']) + + ax.tick_params(axis='both', which='major', + labelsize=pltConfig['sTickSize']) + ax.set_xlabel(xlabel, fontsize=pltConfig['sLabelSize']) + ax.set_ylabel(ylabel, fontsize=pltConfig['sLabelSize']) + ax.set_title(title, fontsize=pltConfig['sTitleSize']) + + return fig, ax + + +def plot_tfreq(ax, data_yx, times, freqs, **pkwargs): + + """ + Plot time frequency data on a 2d grid, expects standard + row-column (freq-time) axis ordering. + + Needs frequencies (`freqs`) and sampling rate (`fs`) + for correct units. + """ + + # extent is defined in xy order + df = freqs[1] - freqs[0] + extent = [times[0], times[-1], + freqs[0] - df / 2, freqs[-1] - df / 2] + + ax.imshow(data_yx, aspect='auto', cmap=pltConfig['cmap'], + extent=extent) diff --git a/syncopy/plotting/config.py b/syncopy/plotting/config.py index b6f53a5a2..38277e558 100644 --- a/syncopy/plotting/config.py +++ b/syncopy/plotting/config.py @@ -14,8 +14,8 @@ mpl.rcParams['axes.facecolor'] = '#f5faf6' # Global style settings for single-/multi-plots -pltConfig = {"sTitleSize": 12, - "sLabelSize": 14, +pltConfig = {"sTitleSize": 15, + "sLabelSize": 16, "sTickSize": 12, "sLegendSize": 12, "sFigSize": (6.4, 4.8), @@ -23,7 +23,8 @@ "mLabelSize": 14, "mTickSize": 10, "mLegendSize": 12, - "mFigSize": (10, 6.8)} + "mFigSize": (10, 6.8), + "cmap": "magma"} # Global consistent error message if matplotlib is missing pltErrMsg = "\nSyncopy WARNING: Could not import 'matplotlib'. \n" +\ diff --git a/syncopy/tests/local_spy.py b/syncopy/tests/local_spy.py index 77de254b4..44089ac1d 100644 --- a/syncopy/tests/local_spy.py +++ b/syncopy/tests/local_spy.py @@ -24,14 +24,12 @@ # Prepare code to be executed using, e.g., iPython's `%run` magic command if __name__ == "__main__": - nTrials = 50 + nTrials = 20 - nSamples = 500 + nSamples = 1000 fs = 500 f1, f2 = 20, 40 - A1, A2 = 0.3, 1 - - nSamples = 2000 + A1, A2 = 2.3, 1 trls = [] for _ in range(nTrials): @@ -61,6 +59,8 @@ print(trl.mean()) ad2 = spy.AnalogData(trls, samplerate=2000) - #spec = spy.freqanalysis(ad2, tapsmofrq=5, keeptrials=False) - #coh = spy.connectivityanalysis(ad2, method='coh', tapsmofrq=5) - #gr = spy.connectivityanalysis(ad2, method='granger', tapsmofrq=10, polyremoval=0) + spec = spy.freqanalysis(ad1, tapsmofrq=2, keeptrials=False) + foi = np.linspace(10, 60, 25) + spec2 = spy.freqanalysis(ad1, method='wavelet', keeptrials=False, foi=foi) + # coh = spy.connectivityanalysis(ad2, method='coh', tapsmofrq=5) + # gr = spy.connectivityanalysis(ad2, method='granger', tapsmofrq=10, polyremoval=0) From ab6346552b600a6ca1e7d7a8f1170e85c61f6d6a Mon Sep 17 00:00:00 2001 From: tensionhead Date: Thu, 17 Mar 2022 16:10:36 +0100 Subject: [PATCH 112/166] NEW: Single panel plots for CrossSpectralData - a bit more work as we have 3 quite different possible datasets depending on the method On branch rework-plotting Changes to be committed: modified: syncopy/datatype/continuous_data.py modified: syncopy/plotting/_singlepanelplot.py modified: syncopy/tests/local_spy.py --- syncopy/datatype/continuous_data.py | 63 +++++++++++++++++++++++++++- syncopy/plotting/_singlepanelplot.py | 3 ++ syncopy/tests/local_spy.py | 47 ++++++++++----------- 3 files changed, 86 insertions(+), 27 deletions(-) diff --git a/syncopy/datatype/continuous_data.py b/syncopy/datatype/continuous_data.py index a777e5fcb..c2e8da837 100644 --- a/syncopy/datatype/continuous_data.py +++ b/syncopy/datatype/continuous_data.py @@ -510,8 +510,8 @@ def singlepanelplot(self, shifted=True, **show_kwargs): if shifted: data_y = plot_helpers.shift_multichan(data_y) - # create the axes and figure fig, ax = sp_plot.mk_line_figax() + sp_plot.plot_lines(ax, data_x, data_y, label=labels) @@ -683,6 +683,7 @@ def singlepanelplot(self, **show_kwargs): if not isinstance(label, str): SPYWarning("Please select a single channel for plotting!") return + # here we always need a new axes fig, ax = sp_plot.mk_img_figax(title=f'{label}') time = plot_helpers.parse_toi(self, trl, show_kwargs) @@ -699,9 +700,9 @@ def singlepanelplot(self, **show_kwargs): # multiple channels? labels = plot_helpers.parse_channel(self, show_kwargs) - # create the axes and figure fig, ax = sp_plot.mk_line_figax(xlabel='frequency (Hz)', ylabel='power (dB)') + sp_plot.plot_lines(ax, data_x, data_y, label=labels) @@ -859,3 +860,61 @@ def __init__(self, freq=freq, dimord=dimord) + # implement plotting + def singlepanelplot(self, **show_kwargs): + + """ + Plot either a 2d-line plot in case of + singleton time axis or an image plot + for time-frequency spectra. + + Parameters + ---------- + show_kwargs : :func:`~syncopy.datatype.methods.show.show` arguments + """ + + # right now we have to enforce + # single trial selection only + trl = show_kwargs.get('trials', None) + if not isinstance(trl, int) and len(self.trials) > 1: + SPYWarning("Please select a single trial for plotting!") + return + elif len(self.trials) == 1: + trl = 0 + + # what channel combination + if 'channel_i' not in show_kwargs or 'channel_j' not in show_kwargs: + SPYWarning("Please select a channel combination for plotting!") + return + chi, chj = show_kwargs['channel_i'], show_kwargs['channel_j'] + + # what data do we have? + method = plot_helpers.get_method(self) + if method == 'granger': + xlabel = 'frequency (Hz)' + ylabel = 'Granger causality' + label = rf"channel{chi} $\rightarrow$ channel{chj}" + data_x = plot_helpers.parse_foi(self, show_kwargs) + elif method == 'coh': + xlabel = 'frequency (Hz)' + ylabel = 'coherence' + label = rf"channel{chi} - channel{chj}" + data_x = plot_helpers.parse_foi(self, show_kwargs) + elif method == 'corr': + xlabel = 'lag' + ylabel = 'correlation' + label = rf"channel{chi} - channel{chj}" + data_x = plot_helpers.parse_toi(self, show_kwargs) + # that's all the methods we got so far + else: + raise NotImplementedError + + # get the data to plot + data_y = self.show(**show_kwargs) + + # create the axes and figure if needed + # persisten axes allows for plotting different + # channel combinations into the same figure + if not hasattr(self, 'ax'): + fig, self.ax = sp_plot.mk_line_figax(xlabel, ylabel) + sp_plot.plot_lines(self.ax, data_x, data_y, label=label) diff --git a/syncopy/plotting/_singlepanelplot.py b/syncopy/plotting/_singlepanelplot.py index 250e32db0..de4a91aae 100644 --- a/syncopy/plotting/_singlepanelplot.py +++ b/syncopy/plotting/_singlepanelplot.py @@ -43,6 +43,9 @@ def plot_lines(ax, data_x, data_y, **pkwargs): if 'label' in pkwargs: ax.legend(ncol=2, loc='upper right', fontsize=pltConfig['sLegendSize']) + # make room for the legend + mn, mx = ax.get_ylim() + ax.set_ylim((mn, 1.1 * mx)) # -- image plots -- diff --git a/syncopy/tests/local_spy.py b/syncopy/tests/local_spy.py index 44089ac1d..5286f7e2c 100644 --- a/syncopy/tests/local_spy.py +++ b/syncopy/tests/local_spy.py @@ -28,39 +28,36 @@ nSamples = 1000 fs = 500 - f1, f2 = 20, 40 - A1, A2 = 2.3, 1 - - trls = [] - for _ in range(nTrials): - - sig1 = A1 * np.cos(f1 * 2 * np.pi * np.arange(nSamples) / fs) - sig1 += A2 * np.cos(f2 * 2 * np.pi * np.arange(nSamples) / fs) - sig2 = np.random.randn(nSamples) - trls.append(np.vstack([sig1, sig2]).T) - ad1 = spy.AnalogData(trls, samplerate=500) - #spy.preprocessing(ad1, filter_class='d') trls = [] AdjMat = np.zeros((2, 2)) - AdjMat[0, 1] = .25 + # coupling from 0 to 1 + AdjMat[0, 1] = .15 for _ in range(nTrials): # defaults AR(2) parameters yield 40Hz peak - alphas = [.74, -.46] # broad peak at 60Hz - alphas = [0.24, -.46] alphas = [.55, -.8] trl = synth_data.AR2_network(AdjMat, nSamples=nSamples, alphas=alphas) - #trl = synth_data.AR2_network(None, nSamples=nSamples, - # alphas=alphas) trls.append(trl) - print(trl.mean()) - ad2 = spy.AnalogData(trls, samplerate=2000) - - spec = spy.freqanalysis(ad1, tapsmofrq=2, keeptrials=False) - foi = np.linspace(10, 60, 25) - spec2 = spy.freqanalysis(ad1, method='wavelet', keeptrials=False, foi=foi) - # coh = spy.connectivityanalysis(ad2, method='coh', tapsmofrq=5) - # gr = spy.connectivityanalysis(ad2, method='granger', tapsmofrq=10, polyremoval=0) + + ad2 = spy.AnalogData(trls, samplerate=fs) + spec = spy.freqanalysis(ad2, tapsmofrq=2, keeptrials=False) + foi = np.linspace(40, 160, 25) + spec2 = spy.freqanalysis(ad2, method='wavelet', keeptrials=False, foi=foi) + coh = spy.connectivityanalysis(ad2, method='coh', tapsmofrq=5) + gr = spy.connectivityanalysis(ad2, method='granger', tapsmofrq=10, polyremoval=0) + + # show new plotting + ad2.singlepanelplot(trials=12, toilim=[0, 0.35]) + + # mtmfft spectrum + spec.singlepanelplot() + # time freq singlepanel needs single channel + spec2.singlepanelplot(channel=0, toilim=[0, 0.35]) + + coh.singlepanelplot(channel_i=0, channel_j=1) + + gr.singlepanelplot(channel_i=0, channel_j=1, foilim=[40, 160]) + gr.singlepanelplot(channel_i=1, channel_j=0, foilim=[40, 160]) From 2e2a689058e39df6c9b089b30fd647e786e1153b Mon Sep 17 00:00:00 2001 From: tensionhead Date: Thu, 17 Mar 2022 18:08:10 +0100 Subject: [PATCH 113/166] CHG: Move plotting methods to plotting module - the data types now just call their respective plot_*** functions from sp_plotting.py Changes to be committed: modified: syncopy/datatype/continuous_data.py modified: syncopy/plotting/__init__.py modified: syncopy/plotting/_helpers.py modified: syncopy/plotting/_singlepanelplot.py new file: syncopy/plotting/sp_plotting.py new file: syncopy/plotting/spy_plotting.py modified: syncopy/tests/local_spy.py --- syncopy/datatype/continuous_data.py | 145 +--------------------- syncopy/plotting/__init__.py | 4 +- syncopy/plotting/_helpers.py | 1 - syncopy/plotting/_singlepanelplot.py | 4 +- syncopy/plotting/sp_plotting.py | 176 +++++++++++++++++++++++++++ syncopy/plotting/spy_plotting.py | 29 +++++ syncopy/tests/local_spy.py | 3 + 7 files changed, 216 insertions(+), 146 deletions(-) create mode 100644 syncopy/plotting/sp_plotting.py create mode 100644 syncopy/plotting/spy_plotting.py diff --git a/syncopy/datatype/continuous_data.py b/syncopy/datatype/continuous_data.py index c2e8da837..71f621e37 100644 --- a/syncopy/datatype/continuous_data.py +++ b/syncopy/datatype/continuous_data.py @@ -20,8 +20,8 @@ from syncopy.shared.parsers import scalar_parser, array_parser from syncopy.shared.errors import SPYValueError, SPYWarning from syncopy.shared.tools import best_match -from syncopy.plotting import _singlepanelplot as sp_plot -from syncopy.plotting import _helpers as plot_helpers +from syncopy.plotting import sp_plotting + __all__ = ["AnalogData", "SpectralData", "CrossSpectralData"] @@ -479,40 +479,7 @@ def __init__(self, # implement plotting def singlepanelplot(self, shifted=True, **show_kwargs): - """ - The probably simplest plot, a 2d-line - plot of selected channels - - Parameters - ---------- - show_kwargs : :func:`~syncopy.datatype.methods.show.show` arguments - """ - - # right now we have to enforce - # single trial selection only - trl = show_kwargs.get('trials', None) - if not isinstance(trl, int) and len(self.trials) > 1: - SPYWarning("Please select a single trial for plotting!") - return - # only 1 trial so no explicit selection needed - elif len(self.trials) == 1: - trl = 0 - - # get the data to plot - data_x = plot_helpers.parse_toi(self, trl, show_kwargs) - data_y = self.show(**show_kwargs) - - # multiple channels? - labels = plot_helpers.parse_channel(self, show_kwargs) - - # plot multiple channels with offsets for - # better visibility - if shifted: - data_y = plot_helpers.shift_multichan(data_y) - - fig, ax = sp_plot.mk_line_figax() - - sp_plot.plot_lines(ax, data_x, data_y, label=labels) + sp_plotting.plot_AnalogData(self, shifted, **show_kwargs) class SpectralData(ContinuousData): @@ -656,54 +623,7 @@ def __init__(self, # implement plotting def singlepanelplot(self, **show_kwargs): - """ - Plot either a 2d-line plot in case of - singleton time axis or an image plot - for time-frequency spectra. - - Parameters - ---------- - show_kwargs : :func:`~syncopy.datatype.methods.show.show` arguments - """ - - # right now we have to enforce - # single trial selection only - trl = show_kwargs.get('trials', None) - if not isinstance(trl, int) and len(self.trials) > 1: - SPYWarning("Please select a single trial for plotting!") - return - elif len(self.trials) == 1: - trl = 0 - - # how got the spectrum computed - method = plot_helpers.get_method(self) - if method in ('wavelet', 'superlet', 'mtmconvol'): - # multiple channels? - label = plot_helpers.parse_channel(self, show_kwargs) - if not isinstance(label, str): - SPYWarning("Please select a single channel for plotting!") - return - # here we always need a new axes - fig, ax = sp_plot.mk_img_figax(title=f'{label}') - - time = plot_helpers.parse_toi(self, trl, show_kwargs) - # dimord is time x taper x freq x channel - # need freq x time for plotting - data_yx = self.show(**show_kwargs).T - sp_plot.plot_tfreq(ax, data_yx, time, self.freq) - # just a line plot - else: - # get the data to plot - data_x = plot_helpers.parse_foi(self, show_kwargs) - data_y = np.log10(self.show(**show_kwargs)) - - # multiple channels? - labels = plot_helpers.parse_channel(self, show_kwargs) - - fig, ax = sp_plot.mk_line_figax(xlabel='frequency (Hz)', - ylabel='power (dB)') - - sp_plot.plot_lines(ax, data_x, data_y, label=labels) + sp_plotting.plot_SpectralData(self, **show_kwargs) class CrossSpectralData(ContinuousData): @@ -860,61 +780,6 @@ def __init__(self, freq=freq, dimord=dimord) - # implement plotting def singlepanelplot(self, **show_kwargs): - """ - Plot either a 2d-line plot in case of - singleton time axis or an image plot - for time-frequency spectra. - - Parameters - ---------- - show_kwargs : :func:`~syncopy.datatype.methods.show.show` arguments - """ - - # right now we have to enforce - # single trial selection only - trl = show_kwargs.get('trials', None) - if not isinstance(trl, int) and len(self.trials) > 1: - SPYWarning("Please select a single trial for plotting!") - return - elif len(self.trials) == 1: - trl = 0 - - # what channel combination - if 'channel_i' not in show_kwargs or 'channel_j' not in show_kwargs: - SPYWarning("Please select a channel combination for plotting!") - return - chi, chj = show_kwargs['channel_i'], show_kwargs['channel_j'] - - # what data do we have? - method = plot_helpers.get_method(self) - if method == 'granger': - xlabel = 'frequency (Hz)' - ylabel = 'Granger causality' - label = rf"channel{chi} $\rightarrow$ channel{chj}" - data_x = plot_helpers.parse_foi(self, show_kwargs) - elif method == 'coh': - xlabel = 'frequency (Hz)' - ylabel = 'coherence' - label = rf"channel{chi} - channel{chj}" - data_x = plot_helpers.parse_foi(self, show_kwargs) - elif method == 'corr': - xlabel = 'lag' - ylabel = 'correlation' - label = rf"channel{chi} - channel{chj}" - data_x = plot_helpers.parse_toi(self, show_kwargs) - # that's all the methods we got so far - else: - raise NotImplementedError - - # get the data to plot - data_y = self.show(**show_kwargs) - - # create the axes and figure if needed - # persisten axes allows for plotting different - # channel combinations into the same figure - if not hasattr(self, 'ax'): - fig, self.ax = sp_plot.mk_line_figax(xlabel, ylabel) - sp_plot.plot_lines(self.ax, data_x, data_y, label=label) + sp_plotting.plot_CrossSpectralData(self, **show_kwargs) diff --git a/syncopy/plotting/__init__.py b/syncopy/plotting/__init__.py index 1b48fd820..b6986ae44 100644 --- a/syncopy/plotting/__init__.py +++ b/syncopy/plotting/__init__.py @@ -4,8 +4,8 @@ # # Importlocal modules, but only import routines from spy_plotting.py -from . import (_singlepanelplot,) +from .spy_plotting import * # Populate local __all__ namespace __all__ = [] -# __all__.extend(spy_plotting.__all__) +__all__.extend(spy_plotting.__all__) diff --git a/syncopy/plotting/_helpers.py b/syncopy/plotting/_helpers.py index adcd30b42..e3aab6852 100644 --- a/syncopy/plotting/_helpers.py +++ b/syncopy/plotting/_helpers.py @@ -10,7 +10,6 @@ # Syncopy imports from syncopy.shared.tools import best_match -from syncopy.shared.errors import SPYWarning def parse_foi(dataobject, show_kwargs): diff --git a/syncopy/plotting/_singlepanelplot.py b/syncopy/plotting/_singlepanelplot.py index de4a91aae..35817b6c5 100644 --- a/syncopy/plotting/_singlepanelplot.py +++ b/syncopy/plotting/_singlepanelplot.py @@ -8,11 +8,9 @@ if __plt__: import matplotlib.pyplot as ppl -else: - print(pltErrMsg.format("singlepanelplot")) -# -- 2d-line plots -- +# -- 2d-line plots -- def mk_line_figax(xlabel='time (s)', ylabel='signal (a.u.)'): diff --git a/syncopy/plotting/sp_plotting.py b/syncopy/plotting/sp_plotting.py new file mode 100644 index 000000000..81a2664bb --- /dev/null +++ b/syncopy/plotting/sp_plotting.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- +# +# The singlepanel plotting functions for Syncopy +# data types +# + +# Builtin/3rd party package imports +import numpy as np + +# Syncopy imports +from syncopy import __plt__ +from syncopy.shared.errors import SPYWarning +from syncopy.plotting import _singlepanelplot as sp_plot +from syncopy.plotting import _helpers as plot_helpers +from syncopy.plotting.config import pltErrMsg + + +def plot_AnalogData(data, shifted=True, **show_kwargs): + + """ + The probably simplest plot, a 2d-line + plot of selected channels + + Parameters + ---------- + data : :class:`~syncopy.datatype.AnalogData` + show_kwargs : :func:`~syncopy.datatype.methods.show.show` arguments + """ + + if not __plt__: + SPYWarning(pltErrMsg) + return + + # right now we have to enforce + # single trial selection only + trl = show_kwargs.get('trials', None) + if not isinstance(trl, int) and len(data.trials) > 1: + SPYWarning("Please select a single trial for plotting!") + return + # only 1 trial so no explicit selection needed + elif len(data.trials) == 1: + trl = 0 + + # get the data to plot + data_x = plot_helpers.parse_toi(data, trl, show_kwargs) + data_y = data.show(**show_kwargs) + + # multiple channels? + labels = plot_helpers.parse_channel(data, show_kwargs) + + # plot multiple channels with offsets for + # better visibility + if shifted: + data_y = plot_helpers.shift_multichan(data_y) + + fig, ax = sp_plot.mk_line_figax() + + sp_plot.plot_lines(ax, data_x, data_y, label=labels) + + +def plot_SpectralData(data, **show_kwargs): + + """ + Plot either a 2d-line plot in case of + singleton time axis or an image plot + for time-frequency spectra. + + Parameters + ---------- + data : :class:`~syncopy.datatype.SpectralData` + show_kwargs : :func:`~syncopy.datatype.methods.show.show` arguments + """ + + if not __plt__: + SPYWarning(pltErrMsg) + return + + # right now we have to enforce + # single trial selection only + trl = show_kwargs.get('trials', None) + if not isinstance(trl, int) and len(data.trials) > 1: + SPYWarning("Please select a single trial for plotting!") + return + elif len(data.trials) == 1: + trl = 0 + + # how got the spectrum computed + method = plot_helpers.get_method(data) + if method in ('wavelet', 'superlet', 'mtmconvol'): + # multiple channels? + label = plot_helpers.parse_channel(data, show_kwargs) + if not isinstance(label, str): + SPYWarning("Please select a single channel for plotting!") + return + # here we always need a new axes + fig, ax = sp_plot.mk_img_figax(title=f'{label}') + + time = plot_helpers.parse_toi(data, trl, show_kwargs) + # dimord is time x taper x freq x channel + # need freq x time for plotting + data_yx = data.show(**show_kwargs).T + sp_plot.plot_tfreq(ax, data_yx, time, data.freq) + # just a line plot + else: + # get the data to plot + data_x = plot_helpers.parse_foi(data, show_kwargs) + data_y = np.log10(data.show(**show_kwargs)) + + # multiple channels? + labels = plot_helpers.parse_channel(data, show_kwargs) + + fig, ax = sp_plot.mk_line_figax(xlabel='frequency (Hz)', + ylabel='power (dB)') + + sp_plot.plot_lines(ax, data_x, data_y, label=labels) + + +def plot_CrossSpectralData(data, **show_kwargs): + """ + Plot 2d-line plots for the different connectivity measures. + + Parameters + ---------- + data : :class:`~syncopy.datatype.CrossSpectralData` + show_kwargs : :func:`~syncopy.datatype.methods.show.show` arguments + """ + + if not __plt__: + SPYWarning(pltErrMsg) + return + + # right now we have to enforce + # single trial selection only + trl = show_kwargs.get('trials', None) + if not isinstance(trl, int) and len(data.trials) > 1: + SPYWarning("Please select a single trial for plotting!") + return + elif len(data.trials) == 1: + trl = 0 + + # what channel combination + if 'channel_i' not in show_kwargs or 'channel_j' not in show_kwargs: + SPYWarning("Please select a channel combination for plotting!") + return + chi, chj = show_kwargs['channel_i'], show_kwargs['channel_j'] + + # what data do we have? + method = plot_helpers.get_method(data) + if method == 'granger': + xlabel = 'frequency (Hz)' + ylabel = 'Granger causality' + label = rf"channel{chi} $\rightarrow$ channel{chj}" + data_x = plot_helpers.parse_foi(data, show_kwargs) + elif method == 'coh': + xlabel = 'frequency (Hz)' + ylabel = 'coherence' + label = rf"channel{chi} - channel{chj}" + data_x = plot_helpers.parse_foi(data, show_kwargs) + elif method == 'corr': + xlabel = 'lag' + ylabel = 'correlation' + label = rf"channel{chi} - channel{chj}" + data_x = plot_helpers.parse_toi(data, show_kwargs) + # that's all the methods we got so far + else: + raise NotImplementedError + + # get the data to plot + data_y = data.show(**show_kwargs) + + # create the axes and figure if needed + # persisten axes allows for plotting different + # channel combinations into the same figure + if not hasattr(data, 'ax'): + fig, data.ax = sp_plot.mk_line_figax(xlabel, ylabel) + sp_plot.plot_lines(data.ax, data_x, data_y, label=label) diff --git a/syncopy/plotting/spy_plotting.py b/syncopy/plotting/spy_plotting.py new file mode 100644 index 000000000..f7d2cdccc --- /dev/null +++ b/syncopy/plotting/spy_plotting.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# +# Top-level interfaces for the plotting functionality +# + +from syncopy import __plt__ +from syncopy.plotting.config import pltErrMsg +from syncopy.shared.errors import SPYWarning + +__all__ = ['singlepanelplot'] + + +def singlepanelplot(data, **show_kwargs): + + """ + This is just an adapter to call the + plotting methods of the respective datatype + + Parameters + ---------- + data : an instance derived from :class:`~syncopy.datatype.base_data` + show_kwargs : :func:`~syncopy.datatype.methods.show.show` arguments + """ + + if not __plt__: + SPYWarning(pltErrMsg) + return + + data.singlepanelplot(**show_kwargs) diff --git a/syncopy/tests/local_spy.py b/syncopy/tests/local_spy.py index 5286f7e2c..01e5688e0 100644 --- a/syncopy/tests/local_spy.py +++ b/syncopy/tests/local_spy.py @@ -61,3 +61,6 @@ gr.singlepanelplot(channel_i=0, channel_j=1, foilim=[40, 160]) gr.singlepanelplot(channel_i=1, channel_j=0, foilim=[40, 160]) + + # test top-level interface + spy.singlepanelplot(ad2, trials=2, toilim=[-.2, .2]) From 56c6c7ba350e8abbfe7b155ffdf0b8360eb3854d Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 18 Mar 2022 12:57:10 +0100 Subject: [PATCH 114/166] NEW: tests.helpers: higher-order functions for standard tests - as prototyped for the connectivity tests, make these helper routines available for the tests module - selection generatorion via itertools - higher order runners with frontend derived callables as argument On branch preproc-tests Changes to be committed: new file: syncopy/tests/helpers.py modified: syncopy/tests/test_connectivity.py --- syncopy/tests/helpers.py | 222 ++++++++++++++++++++++++++ syncopy/tests/test_connectivity.py | 247 +++-------------------------- 2 files changed, 243 insertions(+), 226 deletions(-) create mode 100644 syncopy/tests/helpers.py diff --git a/syncopy/tests/helpers.py b/syncopy/tests/helpers.py new file mode 100644 index 000000000..ae17ece37 --- /dev/null +++ b/syncopy/tests/helpers.py @@ -0,0 +1,222 @@ +# -*- coding: utf-8 -*- +# +# Helper functions for frontend test design +# +# The runner signatures take a callable, +# the `method_call`, as 1st argument +# + +# 3rd party imports +import itertools +import numpy as np + +from syncopy.shared.errors import SPYValueError, SPYTypeError + + +def run_padding_test(method_call, pad_length): + """ + The callable should test a solution and support + a single keyword argument `pad_to_length` + """ + + pad_options = [pad_length, 'nextpow2', None] + for pad in pad_options: + method_call(pad_to_length=pad) + + # test invalid pads + try: + method_call(pad_to_length=2) + except SPYValueError as err: + assert 'pad_to_length' in str(err) + assert 'expected value to be greater' in str(err) + + try: + method_call(pad_to_length='IamNoPad') + except SPYValueError as err: + assert 'Invalid value of `pad_to_length`' in str(err) + assert 'nextpow2' in str(err) + + try: + method_call(pad_to_length=np.array([1000])) + except SPYValueError as err: + assert 'Invalid value of `pad_to_length`' in str(err) + assert 'nextpow2' in str(err) + + +def run_polyremoval_test(method_call): + """ + The callable should test a solution and support + a single keyword argument `polyremoval` + """ + + poly_options = [0, 1] + for poly in poly_options: + method_call(polyremoval=poly) + + # test invalid polyremoval options + try: + method_call(polyremoval=2) + except SPYValueError as err: + assert 'polyremoval' in str(err) + assert 'expected value to be greater' in str(err) + + try: + method_call(polyremoval='IamNoPad') + except SPYTypeError as err: + assert 'Wrong type of `polyremoval`' in str(err) + + try: + method_call(polyremoval=np.array([1000])) + except SPYTypeError as err: + assert 'Wrong type of `polyremoval`' in str(err) + + +def run_cfg_test(method_call, method, cfg, positivity=True): + + cfg.method = method + if method != 'granger': + cfg.foilim = [0, 70] + # test general tapers with + # additional parameters + cfg.taper = 'kaiser' + cfg.taper_opt = {'beta': 2} + + cfg.output = 'abs' + + result = method_call(cfg) + + # check here just for finiteness and positivity + assert np.all(np.isfinite(result.data)) + if positivity: + assert np.all(result.data[0, ...] >= -1e-10) + + +def run_foi_test(method_call, foilim, positivity=True): + + # only positive frequencies + assert np.min(foilim) >= 0 + assert np.max(foilim) <= 500 + + # fois + foi1 = np.arange(foilim[0], foilim[1]) # 1Hz steps + foi2 = np.arange(foilim[0], foilim[1], 0.25) # 0.5Hz steps + foi3 = 'all' + fois = [foi1, foi2, foi3, None] + + for foi in fois: + result = method_call(foi=foi, foilim=None) + # check here just for finiteness and positivity + assert np.all(np.isfinite(result.data)) + if positivity: + assert np.all(result.data[0, ...] >= -1e-10) + + # 2 foilims + foilims = [[2, 60], [7.65, 45.1234], None] + for foil in foilims: + result = method_call(foilim=foil, foi=None) + # check here just for finiteness and positivity + assert np.all(np.isfinite(result.data)) + if positivity: + assert np.all(result.data[0, ...] >= -1e-10) + + # make sure specification of both foi and foilim triggers a + # Syncopy ValueError + try: + result = method_call(foi=foi, foilim=foil) + except SPYValueError as err: + assert 'foi/foilim' in str(err) + + # make sure out-of-range foi selections are detected + try: + result = method_call(foilim=[-1, 70], foi=None) + except SPYValueError as err: + assert 'foilim' in str(err) + assert 'bounded by' in str(err) + + try: + result = method_call(foi=np.arange(550, 700), foilim=None) + except SPYValueError as err: + assert 'foi' in str(err) + assert 'bounded by' in str(err) + + +def mk_selection_dicts(nTrials, nChannels, toi_min, toi_max): + + """ + Takes 4 numbers, the last two descibing a time-interval + + Returns + ------- + selections : list + The list of dicts holding the keys and values for + Syncopy selections. + """ + + # at least 10 trials + assert nTrials > 9 + # at least 2 channels + assert nChannels > 1 + # at least 250ms + assert (toi_max - toi_min) > 0.25 + + # create 3 random trial and channel selections + trials, channels = [], [] + for _ in range(3): + + sizeTr = np.random.randint(10, nTrials + 1) + trials.append(list(np.random.choice( + nTrials, size=sizeTr + ) + )) + + sizeCh = np.random.randint(2, nChannels + 1) + channels.append(['channel' + str(i + 1) + for i in + np.random.choice( + nChannels, size=sizeCh, replace=False)]) + + # create toi selections, signal length is toi_max + # with -1s as offset (from synthetic data instantiation) + # subsampling does NOT WORK due to precision issues :/ + # toi1 = np.linspace(-.4, 2, 100) + tois = [None, 'all'] + toi_combinations = itertools.product(trials, + channels, + tois) + + # 2 random toilims + toilims = [] + while len(toilims) < 2: + + toil = np.sort(np.random.rand(2)) * (toi_max - toi_min) + toi_min + # at least 250ms + if np.diff(toil) < 0.25: + continue + else: + toilims.append(toil) + + # combinatorics of all selection options + # order matters to assign the selection dict keys! + toilim_combinations = itertools.product(trials, + channels, + toilims) + + selections = [] + # digest generators to create all selection dictionaries + for comb in toi_combinations: + + sel_dct = {} + sel_dct['trials'] = comb[0] + sel_dct['channel'] = comb[1] + sel_dct['toi'] = comb[2] + selections.append(sel_dct) + + for comb in toilim_combinations: + + sel_dct = {} + sel_dct['trials'] = comb[0] + sel_dct['channel'] = comb[1] + sel_dct['toilim'] = comb[2] + selections.append(sel_dct) + + return selections diff --git a/syncopy/tests/test_connectivity.py b/syncopy/tests/test_connectivity.py index 24a8917e7..b699c69a0 100644 --- a/syncopy/tests/test_connectivity.py +++ b/syncopy/tests/test_connectivity.py @@ -7,11 +7,11 @@ import psutil import pytest import inspect -import itertools import numpy as np import matplotlib.pyplot as ppl # Local imports + from syncopy import __acme__ if __acme__: import dask.distributed as dd @@ -19,6 +19,7 @@ from syncopy import AnalogData from syncopy import connectivityanalysis as ca import syncopy.tests.synth_data as synth_data +import syncopy.tests.helpers as helpers from syncopy.shared.errors import SPYValueError, SPYTypeError from syncopy.shared.tools import get_defaults @@ -93,9 +94,9 @@ def test_gr_solution(self, **kwargs): def test_gr_selections(self): # trial, channel and toi selections - selections = mk_selection_dicts(self.nTrials, - self.nChannels, - *self.time_span) + selections = helpers.mk_selection_dicts(self.nTrials, + self.nChannels, + *self.time_span) for sel_dct in selections: @@ -126,7 +127,7 @@ def test_gr_foi(self): def test_gr_cfg(self): call = lambda cfg: ca(self.data, cfg) - run_cfg_test(call, method='granger') + helpers.run_cfg_test(call, method='granger', cfg=get_defaults(ca)) @skip_without_acme @skip_low_mem @@ -147,7 +148,7 @@ def test_gr_padding(self): pad_length = int(1.7 * self.nSamples) call = lambda pad_to_length: self.test_gr_solution(pad_to_length=pad_to_length) - run_padding_test(call, pad_length) + helpers.run_padding_test(call, pad_length) def test_gr_polyremoval(self): @@ -155,7 +156,7 @@ def test_gr_polyremoval(self): self.data = self.data + 10 call = lambda polyremoval: self.test_gr_solution(polyremoval=polyremoval) - run_polyremoval_test(call) + helpers.run_polyremoval_test(call) # remove the constant again self.data = self.data - 10 @@ -214,12 +215,12 @@ def test_coh_solution(self, **kwargs): assert np.all(res.data[0, null_idx, 0, 1] < 0.1) plot_coh(res, 0, 1, label="channel 0-1") - + def test_coh_selections(self): - selections = mk_selection_dicts(self.nTrials, - self.nChannels, - *self.time_span) + selections = helpers.mk_selection_dicts(self.nTrials, + self.nChannels, + *self.time_span) for sel_dct in selections: @@ -236,12 +237,12 @@ def test_coh_foi(self): foi=foi, foilim=foilim) - run_foi_test(call, foilim=[0, 70]) + helpers.run_foi_test(call, foilim=[0, 70]) def test_coh_cfg(self): call = lambda cfg: ca(self.data, cfg) - run_cfg_test(call, method='coh') + helpers.run_cfg_test(call, method='coh', cfg=get_defaults(ca)) @skip_without_acme @skip_low_mem @@ -262,12 +263,12 @@ def test_coh_padding(self): pad_length = int(1.2 * self.nSamples) call = lambda pad_to_length: self.test_coh_solution(pad_to_length=pad_to_length) - run_padding_test(call, pad_length) + helpers.run_padding_test(call, pad_length) def test_coh_polyremoval(self): call = lambda polyremoval: self.test_coh_solution(polyremoval=polyremoval) - run_polyremoval_test(call) + helpers.run_polyremoval_test(call) class TestCorrelation: @@ -374,9 +375,9 @@ def test_corr_padding(self): def test_corr_selections(self): - selections = mk_selection_dicts(self.nTrials, - self.nChannels, - *self.time_span) + selections = helpers.mk_selection_dicts(self.nTrials, + self.nChannels, + *self.time_span) for sel_dct in selections: @@ -388,7 +389,7 @@ def test_corr_selections(self): def test_corr_cfg(self): call = lambda cfg: ca(self.data, cfg) - run_cfg_test(call, method='corr', positivity=False) + helpers.run_cfg_test(call, method='corr', positivity=False, cfg=get_defaults(ca)) @skip_without_acme @skip_low_mem @@ -408,213 +409,7 @@ def test_corr_parallel(self, testcluster=None): def test_corr_polyremoval(self): call = lambda polyremoval: self.test_corr_solution(polyremoval=polyremoval) - run_polyremoval_test(call) - -# -- helper functions -- - - -def run_padding_test(call, pad_length): - """ - The callable should test a solution and support - a single keyword argument `pad_to_length` - """ - - pad_options = [pad_length, 'nextpow2', None] - for pad in pad_options: - call(pad_to_length=pad) - - # test invalid pads - try: - call(pad_to_length=2) - except SPYValueError as err: - assert 'pad_to_length' in str(err) - assert 'expected value to be greater' in str(err) - - try: - call(pad_to_length='IamNoPad') - except SPYValueError as err: - assert 'Invalid value of `pad_to_length`' in str(err) - assert 'nextpow2' in str(err) - - try: - call(pad_to_length=np.array([1000])) - except SPYValueError as err: - assert 'Invalid value of `pad_to_length`' in str(err) - assert 'nextpow2' in str(err) - - -def run_polyremoval_test(call): - """ - The callable should test a solution and support - a single keyword argument `polyremoval` - """ - - poly_options = [0, 1] - for poly in poly_options: - call(polyremoval=poly) - - # test invalid polyremoval options - try: - call(polyremoval=2) - except SPYValueError as err: - assert 'polyremoval' in str(err) - assert 'expected value to be greater' in str(err) - - try: - call(polyremoval='IamNoPad') - except SPYTypeError as err: - assert 'Wrong type of `polyremoval`' in str(err) - - try: - call(polyremoval=np.array([1000])) - except SPYTypeError as err: - assert 'Wrong type of `polyremoval`' in str(err) - - -def run_cfg_test(call, method, positivity=True): - - cfg = get_defaults(ca) - - cfg.method = method - if method != 'granger': - cfg.foilim = [0, 70] - # test general tapers with - # additional parameters - cfg.taper = 'kaiser' - cfg.taper_opt = {'beta': 2} - - cfg.output = 'abs' - - result = call(cfg) - - # check here just for finiteness and positivity - assert np.all(np.isfinite(result.data)) - if positivity: - assert np.all(result.data[0, ...] >= -1e-10) - - -def run_foi_test(call, foilim, positivity=True): - - # only positive frequencies - assert np.min(foilim) >= 0 - assert np.max(foilim) <= 500 - - # fois - foi1 = np.arange(foilim[0], foilim[1]) # 1Hz steps - foi2 = np.arange(foilim[0], foilim[1], 0.25) # 0.5Hz steps - foi3 = 'all' - fois = [foi1, foi2, foi3, None] - - for foi in fois: - # FIXME: this works for method='granger' but not method='coh' 0.0 - - result = call(foi=foi, foilim=None) - # check here just for finiteness and positivity - assert np.all(np.isfinite(result.data)) - if positivity: - assert np.all(result.data[0, ...] >= -1e-10) - - # 2 foilims - foilims = [[2, 60], [7.65, 45.1234], None] - for foil in foilims: - result = call(foilim=foil, foi=None) - # check here just for finiteness and positivity - assert np.all(np.isfinite(result.data)) - if positivity: - assert np.all(result.data[0, ...] >= -1e-10) - - # make sure specification of both foi and foilim triggers a - # Syncopy ValueError - try: - result = call(foi=foi, foilim=foil) - except SPYValueError as err: - assert 'foi/foilim' in str(err) - - # make sure out-of-range foi selections are detected - try: - result = call(foilim=[-1, 70], foi=None) - except SPYValueError as err: - assert 'foilim' in str(err) - assert 'bounded by' in str(err) - - try: - result = call(foi=np.arange(550, 700), foilim=None) - except SPYValueError as err: - assert 'foi' in str(err) - assert 'bounded by' in str(err) - - -def mk_selection_dicts(nTrials, nChannels, toi_min, toi_max): - - # at least 10 trials - assert nTrials > 9 - # at least 2 channels - assert nChannels > 1 - # at least 250ms - assert (toi_max - toi_min) > 0.25 - - # create 3 random trial and channel selections - trials, channels = [], [] - for _ in range(3): - - sizeTr = np.random.randint(10, nTrials + 1) - trials.append(list(np.random.choice( - nTrials, size=sizeTr - ) - )) - - sizeCh = np.random.randint(2, nChannels + 1) - channels.append(['channel' + str(i + 1) - for i in - np.random.choice( - nChannels, size=sizeCh, replace=False - )]) - - # create toi selections, signal length is toi_max - # with -1s as offset (from synthetic data instantiation) - # subsampling does NOT WORK due to precision issues :/ - # toi1 = np.linspace(-.4, 2, 100) - tois = [None, 'all'] - toi_combinations = itertools.product(trials, - channels, - tois) - - # 2 random toilims - toilims = [] - while len(toilims) < 2: - - toil = np.sort(np.random.rand(2)) * (toi_max - toi_min) + toi_min - # at least 250ms - if np.diff(toil) < 0.25: - continue - else: - toilims.append(toil) - - # combinatorics of all selection options - # order matters to assign the selection dict keys! - toilim_combinations = itertools.product(trials, - channels, - toilims) - - selections = [] - # digest generators to create all selection dictionaries - for comb in toi_combinations: - - sel_dct = {} - sel_dct['trials'] = comb[0] - sel_dct['channel'] = comb[1] - sel_dct['toi'] = comb[2] - selections.append(sel_dct) - - for comb in toilim_combinations: - - sel_dct = {} - sel_dct['trials'] = comb[0] - sel_dct['channel'] = comb[1] - sel_dct['toilim'] = comb[2] - selections.append(sel_dct) - - return selections + helpers.run_polyremoval_test(call) def plot_Granger(G, i, j): From a52d424627864a973312fc000bbc7e90d5ec6ad7 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 18 Mar 2022 14:33:09 +0100 Subject: [PATCH 115/166] WIP: Butterworth tests - fixed ._selection to .selection change - basic butterworth test layout On branch preproc-tests Changes to be committed: modified: syncopy/preproc/__init__.py modified: syncopy/preproc/compRoutines.py modified: syncopy/preproc/preprocessing.py new file: syncopy/tests/test_preproc.py --- syncopy/preproc/__init__.py | 8 +- syncopy/preproc/compRoutines.py | 12 +-- syncopy/preproc/preprocessing.py | 8 +- syncopy/tests/test_preproc.py | 151 +++++++++++++++++++++++++++++++ 4 files changed, 165 insertions(+), 14 deletions(-) create mode 100644 syncopy/tests/test_preproc.py diff --git a/syncopy/preproc/__init__.py b/syncopy/preproc/__init__.py index 2e05ec49c..cb245615b 100644 --- a/syncopy/preproc/__init__.py +++ b/syncopy/preproc/__init__.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -# +# # Populate namespace with preprocessing frontend -# +# -# Import __all__ routines from local modules -from .preprocessing import preprocessing +# Import everything from fontend submodule +from .preprocessing import * # Populate local __all__ namespace # with the user-exposed frontend diff --git a/syncopy/preproc/compRoutines.py b/syncopy/preproc/compRoutines.py index 836bdadf3..e6befda94 100644 --- a/syncopy/preproc/compRoutines.py +++ b/syncopy/preproc/compRoutines.py @@ -154,9 +154,9 @@ class Sinc_Filtering(ComputationalRoutine): def process_metadata(self, data, out): # Some index gymnastics to get trial begin/end "samples" - if data._selection is not None: - chanSec = data._selection.channel - trl = data._selection.trialdefinition + if data.selection is not None: + chanSec = data.selection.channel + trl = data.selection.trialdefinition for row in range(trl.shape[0]): trl[row, :2] = [row, row + 1] else: @@ -290,9 +290,9 @@ class But_Filtering(ComputationalRoutine): def process_metadata(self, data, out): # Some index gymnastics to get trial begin/end "samples" - if data._selection is not None: - chanSec = data._selection.channel - trl = data._selection.trialdefinition + if data.selection is not None: + chanSec = data.selection.channel + trl = data.selection.trialdefinition for row in range(trl.shape[0]): trl[row, :2] = [row, row + 1] else: diff --git a/syncopy/preproc/preprocessing.py b/syncopy/preproc/preprocessing.py index 9258ca9c6..40339bd30 100644 --- a/syncopy/preproc/preprocessing.py +++ b/syncopy/preproc/preprocessing.py @@ -122,11 +122,11 @@ def preprocessing(data, # if a subset selection is present # get sampleinfo and check for equidistancy - if data._selection is not None: - sinfo = data._selection.trialdefinition[:, :2] - trialList = data._selection.trials + if data.selection is not None: + sinfo = data.selection.trialdefinition[:, :2] + trialList = data.selection.trials # user picked discrete set of time points - if isinstance(data._selection.time[0], list): + if isinstance(data.selection.time[0], list): lgl = "equidistant time points (toi) or time slice (toilim)" actual = "non-equidistant set of time points" raise SPYValueError(legal=lgl, varname="select", actual=actual) diff --git a/syncopy/tests/test_preproc.py b/syncopy/tests/test_preproc.py new file mode 100644 index 000000000..c42d3798b --- /dev/null +++ b/syncopy/tests/test_preproc.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- +# +# Test preprocessing +# + +# 3rd party imports +import psutil +import pytest +import inspect +import itertools +import numpy as np +import matplotlib.pyplot as ppl + +# Local imports +from syncopy import __acme__ +if __acme__: + import dask.distributed as dd + +from syncopy import preprocessing as pp +from syncopy import AnalogData, freqanalysis +import syncopy.preproc as preproc # submodule +import syncopy.tests.synth_data as synth_data +from syncopy.shared.errors import SPYValueError, SPYTypeError +from syncopy.shared.tools import get_defaults + +# Decorator to decide whether or not to run dask-related tests +skip_without_acme = pytest.mark.skipif(not __acme__, reason="acme not available") +# Decorator to decide whether or not to run memory-intensive tests +availMem = psutil.virtual_memory().total +minRAM = 5 +skip_low_mem = pytest.mark.skipif(availMem < minRAM * 1024**3, reason=f"less than {minRAM}GB RAM available") + +#availableFilterTypes = ('lp', 'hp', 'bp', 'bs') +#availableDirections = ('twopass', 'onepass', 'onepass-minphase') +#availableWindows = ("hamming", "hann", "blackman") + + +class TestButterworth: + + nSamples = 1000 + nChannels = 2 + nTrials = 100 + fs = 200 + fNy = fs / 2 + + # -- use flat white noise as test data -- + + trls = [] + for _ in range(nTrials): + trl = np.random.randn(nSamples, nChannels) + trls.append(trl) + + data = AnalogData(trls, samplerate=fs) + # for toi tests, -1s offset + time_span = [-.5, .1] + flow, fhigh = 0.3 * fNy, 0.4 * fNy + freq_kw = {'lp': fhigh, 'hp': flow, + 'bp': [flow, fhigh], 'bs': [flow, fhigh]} + + def test_filter(self): + + fig, ax = mk_spec_ax() + for ftype in preproc.availableFilterTypes: + filtered = pp(self.data, + filter_class='but', + filter_type=ftype, + freq=self.freq_kw[ftype], + direction='twopass') + # check in frequency space + spec = freqanalysis(filtered, tapsmofrq=3, keeptrials=False) + if ftype == 'lp': + foilim = [0, self.freq_kw[ftype]] + elif ftype == 'hp': + foilim = [self.freq_kw[ftype], self.fNy] + else: + foilim = self.freq_kw[ftype] + + plot_spec(ax, spec, label=ftype, lw=1.5) + + # finally the unfiltered data + spec = freqanalysis(self.data, tapsmofrq=3, keeptrials=False) + print('unfi', spec.show(channel=1).sum()) + # plotting + plot_spec(ax, spec, c='0.3', label='unfiltered') + annotate_foilims(ax, *self.freq_kw['bp']) + ax.set_title("Twopass Butterworth, order = 4") + + print(spec.show(channel=1, foilim=foilim).sum(), ftype) + + def test_filter_comb(self): + + call = lambda ftype, direction, order: pp(self.data, + filter_class='but', + filter_type=ftype, + freq=self.freq_kw[ftype], + direction=direction, + order=order) + fig, ax = mk_spec_ax() + for ftype in preproc.availableFilterTypes: + for direction in preproc.availableDirections: + for order in [2, 20]: + # only for firws + if 'minphase' in direction: + try: + call(ftype, direction, order) + except SPYValueError as err: + assert "expected 'onepass'" in str(err) + continue + + filtered = call(ftype, direction, order) + # check in frequency space + spec = freqanalysis(filtered, tapsmofrq=3, keeptrials=False) + if ftype == 'lp': + foilim = [0, self.freq_kw[ftype]] + elif ftype == 'hp': + foilim = [self.freq_kw[ftype], self.fNy] + else: + foilim = self.freq_kw[ftype] + if direction == 'twopass' and ftype == 'bs': + plot_spec(ax, spec, label=f"order {order}", lw=1.5) + ax.set_title("Twopass Butterworth bandstop") + + print(spec.show(channel=1, foilim=foilim).sum(), ftype) + + +def mk_spec_ax(): + + fig, ax = ppl.subplots() + ax.set_xlabel('frequency (Hz)') + ax.set_ylabel('power (dB)') + return fig, ax + + +def plot_spec(ax, spec, **pkwargs): + + ax.plot(spec.freq, spec.show(channel=1), alpha=0.8, **pkwargs) + ax.legend() + + +def annotate_foilims(ax, flow, fhigh): + + ylim = ax.get_ylim() + ax.plot([flow, flow], [0, 1], 'k--') + ax.plot([fhigh, fhigh], [0, 1], 'k--') + ax.set_ylim(ylim) + + +if __name__ == '__main__': + T1 = TestButterworth() + #T2 = TestCoherence() + #T3 = TestCorrelation() From 363f8eb9b7be79a884ae9e41d06d8395a9dcbede Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 18 Mar 2022 15:17:31 +0100 Subject: [PATCH 116/166] CHG: Power spectral normalization as FT - there was still a factor 2 missing, apparently FT normalizes to A**2 /2 Changes to be committed: modified: syncopy/specest/_norm_spec.py modified: syncopy/specest/mtmconvol.py modified: syncopy/specest/mtmfft.py modified: syncopy/specest/stft.py modified: syncopy/tests/backend/test_timefreq.py --- syncopy/specest/_norm_spec.py | 6 +++--- syncopy/specest/mtmconvol.py | 2 +- syncopy/specest/mtmfft.py | 4 ++-- syncopy/specest/stft.py | 14 +++++++++++++- syncopy/tests/backend/test_timefreq.py | 24 ++++++++++++------------ 5 files changed, 31 insertions(+), 19 deletions(-) diff --git a/syncopy/specest/_norm_spec.py b/syncopy/specest/_norm_spec.py index 58c00612e..036bea17c 100644 --- a/syncopy/specest/_norm_spec.py +++ b/syncopy/specest/_norm_spec.py @@ -6,7 +6,7 @@ import numpy as np -def _norm_spec(ftr, nSamples, freqs): +def _norm_spec(ftr, nSamples, fs): """ Normalizes the complex Fourier transform to @@ -14,8 +14,8 @@ def _norm_spec(ftr, nSamples, freqs): """ # frequency bins - delta_f = freqs[1] - freqs[0] - ftr /= (nSamples / 2 * np.sqrt(delta_f)) + delta_f = fs / nSamples + ftr *= np.sqrt(2) / (nSamples * np.sqrt(delta_f)) return ftr diff --git a/syncopy/specest/mtmconvol.py b/syncopy/specest/mtmconvol.py index 9d7644491..923666782 100644 --- a/syncopy/specest/mtmconvol.py +++ b/syncopy/specest/mtmconvol.py @@ -66,7 +66,7 @@ def mtmconvol(data_arr, samplerate, nperseg, noverlap=None, taper="hann", The STFT result is normalized such that this yields the power spectral density. For a clean harmonic and a frequency bin - width of `dF` this will give a peak power of `A**2 * dF`, + width of `dF` this will give a peak power of `A**2 / 2 * dF`, with `A` as harmonic ampltiude. """ diff --git a/syncopy/specest/mtmfft.py b/syncopy/specest/mtmfft.py index a92547820..752835bf7 100644 --- a/syncopy/specest/mtmfft.py +++ b/syncopy/specest/mtmfft.py @@ -60,7 +60,7 @@ def mtmfft(data_arr, The FFT result is normalized such that this yields the power spectral density. For a clean harmonic and a Fourier frequency bin - width of `dF` this will give a peak power of `A**2 * dF`, + width of `dF` this will give a peak power of `A**2 / 2 * dF`, with `A` as harmonic ampltiude. """ @@ -102,6 +102,6 @@ def mtmfft(data_arr, if demean_taper: win -= win.mean(axis=0) ftr[taperIdx] = np.fft.rfft(win, n=nSamples, axis=0) - ftr[taperIdx] = _norm_spec(ftr[taperIdx], nSamples, freqs) + ftr[taperIdx] = _norm_spec(ftr[taperIdx], nSamples, samplerate) return ftr, freqs diff --git a/syncopy/specest/stft.py b/syncopy/specest/stft.py index 6ca4e21f1..7730ee7b1 100644 --- a/syncopy/specest/stft.py +++ b/syncopy/specest/stft.py @@ -73,6 +73,18 @@ def stft(dat, Array of sampling frequencies times : :class:`numpy.ndarray` Array of segment times + + Notes + ----- + For a power spectral estimate compute: + + ``Sxx = np.real(ftr * ftr.conj())`` + + The STFT result is normalized such that this yields the power + spectral density. For a clean harmonic and a frequency bin + width of `dF` this will give a peak power of `A**2 / 2 * dF`, + with `A` as harmonic ampltiude. + """ # needed for stride tricks # from here on axis=-1 is the data axis! @@ -131,7 +143,7 @@ def stft(dat, ftr = np.fft.rfft(dat, axis=-1) # normalization to squared amplitude density - ftr = _norm_spec(ftr, nperseg, freqs) + ftr = _norm_spec(ftr, nperseg, fs) # Roll frequency axis back to axis where the data came from ftr = np.moveaxis(ftr, -1, 0) diff --git a/syncopy/tests/backend/test_timefreq.py b/syncopy/tests/backend/test_timefreq.py index edd41eeee..8c26b5722 100644 --- a/syncopy/tests/backend/test_timefreq.py +++ b/syncopy/tests/backend/test_timefreq.py @@ -10,7 +10,7 @@ def gen_testdata(freqs=[20, 40, 60], cycles=11, fs=1000, - eps = 0): + eps=0): """ Harmonic superposition of multiple @@ -113,7 +113,7 @@ def test_mtmconvol(): origin='lower', extent=extent, vmin=0, - vmax=1. * A**2 / df) + vmax=.5 * A**2 / df) # zoom into foi region ax2.set_ylim((foi[0], foi[-1])) @@ -137,7 +137,7 @@ def test_mtmconvol(): c='0.5') # integrated power at the respective frquency - cycle_num = (spec[:, idx] * df > A**2 / np.e**2).sum() / fs * frequency + cycle_num = (spec[:, idx] * df > .5 * A**2 / np.e**2).sum() / fs * frequency print(f'{cycle_num} cycles for the {frequency} Hz band') # we have 2 times the cycles for each frequency (temporal neighbor) assert cycle_num > 2 * cycles @@ -186,7 +186,7 @@ def test_mtmconvol(): origin='lower', extent=extent, vmin=0, - vmax=1. * A**2 / df) + vmax=.5 * A**2 / df) # zoom into foi region ax2.set_ylim((foi[0], foi[-1])) @@ -211,7 +211,7 @@ def test_mtmconvol(): # so we just check that the maximum estimated # power within one bin is within 15% bounds of the real power nBins = tapsmofrq / df - assert 0.85 * A**2 / df < spec2.max() * nBins < 1.15 * A**2 / df + assert 0.4 * A**2 / df < spec2.max() * nBins < .65 * A**2 / df def test_superlet(): @@ -253,7 +253,7 @@ def test_superlet(): # get the 'mappable' im = ax2.images[0] - fig.colorbar(im, ax = ax2, orientation='horizontal', + fig.colorbar(im, ax=ax2, orientation='horizontal', shrink=0.7, pad=0.2, label='amplitude (a.u.)') for idx, frequency in zip(freq_idx, signal_freqs): @@ -363,10 +363,10 @@ def test_mtmfft(): powers = spec[:, 0] # only 1 channel # our FFT normalisation recovers the integrated squared signal amplitudes # as frequency bin width is 1Hz, one bin 'integral' is enough - assert np.allclose([A1**2, A2**2], powers[[f1, f2]]) + assert np.allclose([0.5 * A1**2, 0.5 * A2**2], powers[[f1, f2]]) fig, ax = ppl.subplots() - ax.set_title(f"Amplitude spectrum {A1} x 40Hz + {A2} x 100Hz") + ax.set_title(f"Power spectrum {A1} x 40Hz + {A2} x 100Hz") ax.plot(freqs[:150], powers[:150], label="No taper", lw=2) ax.set_xlabel('frequency (Hz)') ax.set_ylabel('power-density') @@ -388,7 +388,7 @@ def test_mtmfft(): # check for integrated power (and taper normalisation) # summing up all dpss powers should give total power of the # test signal which is A1**2 + A2**2 - assert np.allclose(np.sum(dpss_powers), A1**2 + A2**2, atol=1e-2) + assert np.allclose(np.sum(dpss_powers) * 2, A1**2 + A2**2, atol=1e-2) ax.plot(freqs[:150], dpss_powers[:150], label="Slepian", lw=2) ax.legend() @@ -404,7 +404,7 @@ def test_mtmfft(): kaiser_powers = kaiser_spec[:, 0] # only 1 channel # check for amplitudes (and taper normalisation) # normalization less exact for arbitraty windows - assert np.allclose(np.sum(kaiser_powers), A1**2 + A2**2, atol=1.5) + assert np.allclose(np.sum(kaiser_powers) * 2, A1**2 + A2**2, atol=1.5) ax.plot(freqs[:150], kaiser_powers[:150], label="Kaiser", lw=2) ax.legend() @@ -427,10 +427,10 @@ def test_mtmfft(): powers = spec[:, 0] # only 1 channel print(np.sum(powers), win) if win != 'tukey': - assert np.allclose(np.sum(powers), A1**2 + A2**2, atol=4) + assert np.allclose(np.sum(powers) * 2, A1**2 + A2**2, atol=4) # not sure why tukey and triang are so off.. else: - assert np.allclose(np.sum(powers), A1**2 + A2**2, atol=8) + assert np.allclose(np.sum(powers) * 2, A1**2 + A2**2, atol=8) except TypeError: # we didn't provide default parameters.. From ad09e13a2a3d9d84fe81fd23f4e15516ca62c5c8 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 21 Mar 2022 09:44:30 +0100 Subject: [PATCH 117/166] CHG: Remove old testing routines --- syncopy/tests/test_plotting.py | 345 --------------------------------- 1 file changed, 345 deletions(-) delete mode 100644 syncopy/tests/test_plotting.py diff --git a/syncopy/tests/test_plotting.py b/syncopy/tests/test_plotting.py deleted file mode 100644 index e1076193a..000000000 --- a/syncopy/tests/test_plotting.py +++ /dev/null @@ -1,345 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Test Syncopy's plotting functionality -# - -# Builtin/3rd party package imports -import pytest -import numpy as np -from copy import copy - -# Local imports -from syncopy.tests.misc import generate_artificial_data, figs_equal -from syncopy.shared.errors import SPYValueError -from syncopy.plotting import singlepanelplot, multipanelplot -from syncopy import __plt__, __acme__ -if __plt__: - import matplotlib.pyplot as plt - import matplotlib as mpl - -# If matplotlib is not available, this whole testing module is pointless; also, -# if tests are run in parallel, skip `singlepanelplot` tests due to parallel -# writing errors -skip_without_plt = pytest.mark.skipif(not __plt__, reason="matplotlib not available") -skip_with_acme = pytest.mark.skipif(__acme__, reason="do not run with acme") - - -@skip_without_plt -@skip_with_acme -@pytest.mark.xfail(reason="figure comparison breaks sometimes", strict=False) -class TestAnalogDataPlotting(): - - nChannels = 16 - nTrials = 8 - seed = 130810 - - # To use `selectdata` w/``trials = None``-raw plotting, the trials must not - # overlap - construct separate set of `raw*` AnalogData-objects for testing! - dataReg = generate_artificial_data(nTrials=nTrials, - nChannels=nChannels, - seed=seed, - equidistant=True, - overlapping=True) - dataInv = generate_artificial_data(nTrials=nTrials, - nChannels=nChannels, - seed=seed, - equidistant=True, - overlapping=True, - dimord=dataReg.dimord[::-1]) - rawReg = generate_artificial_data(nTrials=nTrials, - nChannels=nChannels, - seed=seed, - equidistant=True, - overlapping=False) - rawInv = generate_artificial_data(nTrials=nTrials, - nChannels=nChannels, - seed=seed, - equidistant=True, - overlapping=False, - dimord=rawReg.dimord[::-1]) - - trials = ["all", [4, 3, 2, 2, 7]] - channels = ["all", [14, 13, 12, 12, 15]] - toilim = [None, [1.9, 2.5], [2.1, np.inf]] - - - def test_singlepanelplot(self): - - # Lowest possible dpi setting permitting valid png comparisons in `figs_equal` - mpl.rcParams["figure.dpi"] = 150 - - # Test everything except "raw" plotting - for trials in self.trials: - for channels in self.channels: - for toilim in self.toilim: - for avg_channels in [True, False]: - - # Render figure using singlepanelplot mechanics, recreate w/`selectdata`, - # results must be identical - fig1 = self.dataReg.singlepanelplot(trials=trials, - channels=channels, - toilim=toilim, - avg_channels=avg_channels) - selected = self.dataReg.selectdata(trials=trials, - channels=channels, - toilim=toilim) - fig2 = selected.singlepanelplot(trials="all", - avg_channels=avg_channels) - - # Recreate `fig1` and `fig2` in a single sweep by using - # `spy.singlepanelplot` w/multiple input objects - fig1a, fig2a = singlepanelplot(self.dataReg, self.dataInv, - trials=trials, - channels=channels, - toilim=toilim, - avg_channels=avg_channels, - overlay=False) - - # `fig2a` is based on `dataInv` - be more lenient there - tol = None - if avg_channels: - tol = 1e-2 - assert figs_equal(fig1, fig2) - assert figs_equal(fig1, fig1a) - assert figs_equal(fig2, fig2a, tol=tol) - assert figs_equal(fig1, fig2a, tol=tol) - - # Create overlay figures: `fig3` combines `dataReg` and - # `dataInv` - must be identical to overlaying `fig1` w/`dataInv` - fig3 = singlepanelplot(self.dataReg, self.dataInv, - trials=trials, - channels=channels, - toilim=toilim, - avg_channels=avg_channels, - overlay=True) - fig1 = singlepanelplot(self.dataInv, - trials=trials, - channels=channels, - toilim=toilim, - avg_channels=avg_channels, - fig=fig1) - assert figs_equal(fig1, fig3) - - # Close figures to avoid memory overflow - plt.close("all") - - # The `selectdata(trials="all")` part requires consecutive trials! - for channels in self.channels: - for avg_channels in [True, False]: - fig1 = self.rawReg.singlepanelplot(trials=None, - channels=channels, - avg_channels=avg_channels) - selected = self.rawReg.selectdata(trials="all", - channels=channels) - fig2 = selected.singlepanelplot(trials=None, avg_channels=avg_channels) - assert figs_equal(fig1, fig2) - plt.close("all") - - # Do not allow selecting time-intervals w/o trial-specification - with pytest.raises(SPYValueError): - self.dataReg.singlepanelplot(trials=None, toilim=self.toilim[1]) - - # Do not overlay multi-channel plot w/chan-average and unequal channel-count - multiChannelFig = self.dataReg.singlepanelplot(avg_channels=False, - channels=self.channels[1]) - with pytest.raises(SPYValueError): - self.dataReg.singlepanelplot(avg_channels=True, fig=multiChannelFig) - with pytest.raises(SPYValueError): - self.dataReg.singlepanelplot(channels="all", fig=multiChannelFig) - - # Do not overlay multi-panel plot w/figure produced by `singlepanelplot` - multiChannelFig.nTrialsPanels = 99 - with pytest.raises(SPYValueError): - self.dataReg.singlepanelplot(fig=multiChannelFig) - multiChannelFig.nChanPanels = 99 - with pytest.raises(SPYValueError): - self.dataReg.singlepanelplot(fig=multiChannelFig) - - # Ensure grid and title specifications are rendered correctly - theTitle = "A title" - gridFig = self.dataReg.singlepanelplot(grid=True) - assert gridFig.axes[0].get_xgridlines()[0].get_visible() == True - titleFig = self.dataReg.singlepanelplot(title=theTitle) - assert titleFig.axes[0].get_title() == theTitle - - plt.close("all") - - def test_multipanelplot(self): - - # Lowest possible dpi setting permitting valid png comparisons in `figs_equal` - mpl.rcParams["figure.dpi"] = 75 - - # Test everything except "raw" plotting - for trials in self.trials: - for channels in self.channels: - for toilim in self.toilim: - for avg_trials in [True, False]: - for avg_channels in [True, False]: - - # ``avg_trials == avg_channels == True`` yields `SPYWarning` to - # use `singlepanelplot` -> no figs to compare here - if avg_trials is avg_channels is True: - continue - - # Render figure using multipanelplot mechanics, recreate w/`selectdata`, - # results must be identical - fig1 = self.dataReg.multipanelplot(trials=trials, - channels=channels, - toilim=toilim, - avg_trials=avg_trials, - avg_channels=avg_channels) - selected = self.dataReg.selectdata(trials=trials, - channels=channels, - toilim=toilim) - fig2 = selected.multipanelplot(trials="all", - avg_trials=avg_trials, - avg_channels=avg_channels) - - # Recreate `fig1` and `fig2` in a single sweep by using - # `spy.multipanelplot` w/multiple input objects - fig1a, fig2a = multipanelplot(self.dataReg, self.dataInv, - trials=trials, - channels=channels, - toilim=toilim, - avg_trials=avg_trials, - avg_channels=avg_channels, - overlay=False) - - - # `selectdata` preserves trial order but not numbering: ensure - # plot titles are correct, but then remove them to allow - # comparison of `selected` and `dataReg` figures - figTitleLists = [] - if avg_trials is False: - if trials != "all": - for fig in [fig1, fig1a]: - titleList = [] - for ax in fig.axes: - titleList.append(copy(ax.title)) - ax.set_title("") - titles = [title.get_text() for title in titleList] - assert titles == ["Trial #{}".format(trlno) for trlno in trials] - figTitleLists.append(titleList) - for fig in [fig2, fig2a]: - for ax in fig.axes: - ax.set_title("") - - # After (potential) axes title removal, compare figures; - # `fig2a` is based on `dataInv` - be more lenient there - tol = None - if avg_channels: - tol = 1e-2 - assert figs_equal(fig1, fig2) - assert figs_equal(fig1, fig1a) - assert figs_equal(fig2, fig2a, tol=tol) - assert figs_equal(fig1, fig2a, tol=tol) - - # If necessary, restore axes title from `figTitleLists` - if figTitleLists: - for k, ax in enumerate(fig1.axes): - ax.title = figTitleLists[0][k] - - # Create overlay figures: `fig3` combines `dataReg` and - # `dataInv` - must be identical to overlaying `fig1` w/`dataInv` - fig3 = multipanelplot(self.dataReg, self.dataInv, - trials=trials, - channels=channels, - toilim=toilim, - avg_trials=avg_trials, - avg_channels=avg_channels) - fig4 = multipanelplot(self.dataInv, - trials=trials, - channels=channels, - toilim=toilim, - avg_trials=avg_trials, - avg_channels=avg_channels, - fig=fig1) - assert figs_equal(fig3, fig4) - - plt.close("all") - - # The `selectdata(trials="all")` part requires consecutive trials! Add'ly, - # `avg_channels` must be `False`, otherwise single-panel plot warning is triggered - for channels in self.channels: - fig1 = self.rawReg.multipanelplot(trials=None, - channels=channels, - avg_channels=False, - avg_trials=False) - selected = self.rawReg.selectdata(trials="all", - channels=channels) - fig2 = selected.multipanelplot(trials=None, - avg_channels=False) - assert figs_equal(fig1, fig2) - plt.close("all") - - # Do not allow selecting time-intervals w/o trial-specification - with pytest.raises(SPYValueError): - self.dataReg.multipanelplot(trials=None, toilim=self.toilim[1]) - - # Panels = trials, each panel shows single (averaged) channel - multiTrialSingleChanFig = self.dataReg.multipanelplot(trials=self.trials[1], - avg_trials=False, - avg_channels=True) - with pytest.raises(SPYValueError): # trial-count does not match up - self.dataReg.multipanelplot(trials="all", - avg_trials=False, - avg_channels=True, - fig=multiTrialSingleChanFig) - with pytest.raises(SPYValueError): # multi-channel overlay - self.dataReg.multipanelplot(trials=self.trials[1], - avg_trials=False, - avg_channels=False, - fig=multiTrialSingleChanFig) - - # Panels = trials, each panel shows multiple channels - multiTrialMultiChanFig = self.dataReg.multipanelplot(trials=self.trials[1], - avg_trials=False, - avg_channels=False) - with pytest.raises(SPYValueError): # no trial specification provided - self.dataReg.multipanelplot(trials=None, - avg_trials=False, - avg_channels=False, - fig=multiTrialMultiChanFig) - with pytest.raises(SPYValueError): # channel-count does not match up - self.dataReg.multipanelplot(trials=self.trials[1], - channels=self.channels[1], - avg_trials=False, - avg_channels=False, - fig=multiTrialMultiChanFig) - with pytest.raises(SPYValueError): # single-channel overlay - self.dataReg.multipanelplot(trials=self.trials[1], - avg_trials=False, - avg_channels=True, - fig=multiTrialMultiChanFig) - - # Panels = channels - multiChannelFig = self.dataReg.multipanelplot(trials=self.trials[1], - avg_trials=True, - avg_channels=False) - with pytest.raises(SPYValueError): # multi-trial overlay - self.dataReg.multipanelplot(trials=self.trials[1], - avg_trials=False, - avg_channels=False, - fig=multiChannelFig) - with pytest.raises(SPYValueError): # channel-count does not match up - self.dataReg.multipanelplot(trials=self.trials[1], - channels=self.channels[1], - avg_trials=True, - avg_channels=False, - fig=multiChannelFig) - - # Do not overlay single-panel plot w/figure produced by `multipanelplot` - singlePanelFig = self.dataReg.singlepanelplot() - with pytest.raises(SPYValueError): - self.dataReg.multipanelplot(fig=singlePanelFig) - - # Ensure grid and title specifications are rendered correctly - theTitle = "A title" - gridFig = self.dataReg.multipanelplot(avg_trials=False, - avg_channels=True, - grid=True) - assert all([gridFig.axes[k].get_xgridlines()[0].get_visible() for k in range(self.nTrials)]) - titleFig = self.dataReg.multipanelplot(avg_trials=False, - avg_channels=True, - title=theTitle) - assert titleFig._suptitle.get_text() == theTitle From 1b9c38e40f9745f65a72c1e0b6ab82fc59875cd4 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 21 Mar 2022 12:55:45 +0100 Subject: [PATCH 118/166] CHG: Use also negative frequencies for Granger - I apparently simply didn't include the negative freqs so far, and the results where still looking Ok, hence the tests were fine until #243 came up - this now also finally fixes these weird intermittent boundary artifacts, again many thanks to @gabbyshvartsman - included last new edit from Mukesh, this improves the convergence via direct inversion a lot On branch fix-granger Changes to be committed: modified: syncopy/nwanalysis/wilson_sf.py modified: syncopy/tests/backend/test_conn.py --- syncopy/nwanalysis/wilson_sf.py | 18 ++++--- syncopy/tests/backend/test_conn.py | 82 +++++++++++++++--------------- 2 files changed, 54 insertions(+), 46 deletions(-) diff --git a/syncopy/nwanalysis/wilson_sf.py b/syncopy/nwanalysis/wilson_sf.py index bd8eec7e8..f3310ee20 100644 --- a/syncopy/nwanalysis/wilson_sf.py +++ b/syncopy/nwanalysis/wilson_sf.py @@ -56,12 +56,16 @@ def wilson_sf(CSD, nIter=100, rtol=1e-9, direct_inversion=True): Ident = np.eye(*CSD.shape[1:]) + # attach negative frequencies + CSD = np.r_[CSD, CSD[nFreq:1:-1]] + # nChannel x nChannel psi0 = _psi0_initial(CSD) # initial choice of psi, constant for all z(~f) psi = np.tile(psi0, (nFreq, 1, 1)) - assert psi.shape == CSD.shape + # attach negative frequencies + psi = np.r_[psi, psi[nFreq:1:-1]] g = np.zeros(CSD.shape, dtype=np.complex64) converged = False @@ -72,7 +76,7 @@ def wilson_sf(CSD, nIter=100, rtol=1e-9, direct_inversion=True): # the bracket of equation 3.1 g = psi_inv @ CSD @ psi_inv.conj().transpose(0, 2, 1) else: - for i in range(nFreq): + for i in range(g.shape[0]): C = np.linalg.lstsq(psi[i], CSD[i], rcond=None)[0] g[i] = np.linalg.lstsq( psi[i], C.conj().T, rcond=None)[0].conj().T @@ -91,7 +95,6 @@ def wilson_sf(CSD, nIter=100, rtol=1e-9, direct_inversion=True): CSDfac = psi @ psi.conj().transpose(0, 2, 1) err = np.abs(CSD - CSDfac) err = (err / np.abs(CSD)).max() - # converged if err < rtol: converged = True @@ -102,9 +105,9 @@ def wilson_sf(CSD, nIter=100, rtol=1e-9, direct_inversion=True): # Transfer function psi0_inv = np.linalg.inv(psi0) - Hfunc = psi @ psi0_inv.T + Hfunc = psi @ psi0_inv - return Hfunc, Sigma, converged + return Hfunc[:nFreq], Sigma, converged def _psi0_initial(CSD): @@ -125,7 +128,7 @@ def _psi0_initial(CSD): # Remove any asymmetry due to rounding error. # This also will zero out any imaginary values # on the diagonal - real diagonals are required for cholesky. - gamma0 = np.real((gamma0 + gamma0.conj()) / 2) + gamma0 = np.real((gamma0 + gamma0.T.conj()) / 2) # check for positive definiteness eivals = np.linalg.eigvals(gamma0) @@ -156,6 +159,9 @@ def _plusOperator(g): # take half of the zero lag beta[0, ...] = 0.5 * beta[0, ...] g0 = beta[0, ...].copy() + # take half of Nyquist bin + # Dhamala "NewEdits" 28.01.22 + beta[nLag, ...] = 0.5 * beta[nLag, ...] # Zero out negative lags beta[nLag + 1:, ...] = 0 diff --git a/syncopy/tests/backend/test_conn.py b/syncopy/tests/backend/test_conn.py index 17f208d91..527dd491c 100644 --- a/syncopy/tests/backend/test_conn.py +++ b/syncopy/tests/backend/test_conn.py @@ -174,47 +174,27 @@ def test_wilson(): """ # --- create test data --- - fs = 1000 - nChannels = 10 - nSamples = 1000 - f1, f2 = [30 , 40] # 30Hz and 60Hz - data = np.zeros((nSamples, nChannels)) - - # more phase diffusion in the 60Hz band - p1 = synth_data.phase_diffusion(f1, eps=.3, fs=fs, - nSamples=nSamples, nChannels=nChannels) - p2 = synth_data.phase_diffusion(f2, eps=1, fs=fs, - nSamples=nSamples, nChannels=nChannels) - - data = np.cos(p1) + 2 * np.sin(p2) + .5 * np.random.randn(nSamples, nChannels) + fs = 5000 + nChannels = 2 + nSamples = 5000 + nTrials = 50 - # --- get the (single trial) CSD --- - - bw = 5 # 5Hz smoothing - NW = bw * nSamples / (2 * fs) - Kmax = int(2 * NW - 1) # optimal number of tapers + CSDav = np.zeros((nSamples // 2 + 1, nChannels, nChannels), dtype=np.complex64) + for _ in range(nTrials): - CSD, freqs = csd.csd(data, fs, - taper='dpss', - taper_opt={'Kmax' : Kmax, 'NW' : NW}, - norm=False, - fullOutput=True) - - # get CSD condition number, which is way too large! - CN = np.linalg.cond(CSD).max() - assert CN > 1e6 + sol = synth_data.AR2_network(nSamples=nSamples) + # --- get the (single trial) CSD --- - # --- regularize CSD --- + CSD, freqs = csd.csd(sol, fs, + norm=False, + fullOutput=True) + CSDav += CSD - CSDreg, fac = regularize_csd(CSD, cond_max=1e6, nSteps=25) - CNreg = np.linalg.cond(CSDreg).max() - assert CNreg < 1e6 - # check that 'small' regularization factor is enough - assert fac < 1e-5 + CSDav /= nTrials # --- factorize CSD with Wilson's algorithm --- - H, Sigma, conv = wilson_sf(CSDreg, rtol=1e-9) + H, Sigma, conv = wilson_sf(CSDav, rtol=1e-9) # converged - \Psi \Psi^* \approx CSD, # with relative error <= rtol? @@ -228,16 +208,38 @@ def test_wilson(): ax.set_ylabel(r'$|CSD_{ij}(f)|$') chan = nChannels // 2 # show (real) auto-spectra - ax.plot(freqs, np.abs(CSD[:, chan, chan]), + ax.plot(freqs, np.abs(CSDav[:, chan, chan]), '-o', label='original CSD', ms=3) - ax.plot(freqs, np.abs(CSDreg[:, chan, chan]), - '--', label='regularized CSD', ms=3) ax.plot(freqs, np.abs(CSDfac[:, chan, chan]), '-o', label='factorized CSD', ms=3) - ax.set_xlim((f1 - 5, f2 + 5)) + # ax.set_xlim((350, 450)) ax.legend() +def test_regularization(): + + # dyadic product of random matrices has rank 1 + CSD = np.zeros((50, 50)) + for _ in range(40): + A = np.random.randn(50) + CSD += np.outer(A, A) + + # get CSD condition number, which is way too large! + CN = np.linalg.cond(CSD).max() + print(CN) + cmax = 1e6 + assert CN > cmax + + # --- regularize CSD --- + + CSDreg, fac = regularize_csd(CSD, cond_max=cmax, nSteps=25) + + CNreg = np.linalg.cond(CSDreg).max() + assert CNreg < 1e6 + # check that 'small' regularization factor is enough + assert fac < 1e-3 + + def test_granger(): """ @@ -294,7 +296,7 @@ def test_granger(): # check low to no causality for 1->2 assert G[freq_idx, 0, 1] < 0.1 # check high causality for 2->1 - assert G[freq_idx, 1, 0] > 0.8 + assert G[freq_idx, 1, 0] > 0.7 # repeat test with least-square solution H, Sigma, conv = wilson_sf(CSDav, direct_inversion=False) @@ -303,7 +305,7 @@ def test_granger(): # check low to no causality for 1->2 assert G2[freq_idx, 0, 1] < 0.1 # check high causality for 2->1 - assert G2[freq_idx, 1, 0] > 0.8 + assert G2[freq_idx, 1, 0] > 0.7 ax.plot(freqs, G2[:, 0, 1], label=r'Granger (LS) $1\rightarrow2$') ax.plot(freqs, G2[:, 1, 0], label=r'Granger (LS) $2\rightarrow1$') From bd319c6c406bd92dc85956b498ef5c2bf155e6ab Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 21 Mar 2022 19:34:12 +0100 Subject: [PATCH 119/166] CHG: Remove de-meaning after tapering - granger results (especially around the 0-frequency) look actually much better without this On branch dev Your branch is up to date with 'origin/dev'. Changes to be committed: modified: syncopy/nwanalysis/connectivity_analysis.py --- syncopy/nwanalysis/connectivity_analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncopy/nwanalysis/connectivity_analysis.py b/syncopy/nwanalysis/connectivity_analysis.py index 7ccbd209e..2c0403abb 100644 --- a/syncopy/nwanalysis/connectivity_analysis.py +++ b/syncopy/nwanalysis/connectivity_analysis.py @@ -270,7 +270,7 @@ def connectivityanalysis(data, method="coh", keeptrials=False, output="abs", nSamples=nSamples, taper=taper, taper_opt=taper_opt, - demean_taper=(method == 'granger'), + demean_taper=False, polyremoval=polyremoval, timeAxis=timeAxis, foi=foi) From 8e19119951a4905add782af5a8ce8ef06a98fef2 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Tue, 22 Mar 2022 17:50:18 +0100 Subject: [PATCH 120/166] FIX: Trialdefinition for preprocessing selections - the trl definition was erroneously copied from a `process_metadata` which has no real time axis Changes to be committed: modified: syncopy/preproc/compRoutines.py --- syncopy/preproc/compRoutines.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/syncopy/preproc/compRoutines.py b/syncopy/preproc/compRoutines.py index e6befda94..e2a4af193 100644 --- a/syncopy/preproc/compRoutines.py +++ b/syncopy/preproc/compRoutines.py @@ -293,8 +293,6 @@ def process_metadata(self, data, out): if data.selection is not None: chanSec = data.selection.channel trl = data.selection.trialdefinition - for row in range(trl.shape[0]): - trl[row, :2] = [row, row + 1] else: chanSec = slice(None) trl = data.trialdefinition From 737595036ad31c198671670385909f1eac9b1e02 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Tue, 22 Mar 2022 18:22:05 +0100 Subject: [PATCH 121/166] NEW: Butterworth filter tests - testing of filter validity, cfg, selections and parallel execution Changes to be committed: modified: syncopy/tests/helpers.py modified: syncopy/tests/test_preproc.py --- syncopy/tests/helpers.py | 6 +- syncopy/tests/test_preproc.py | 180 ++++++++++++++++++++++++---------- 2 files changed, 133 insertions(+), 53 deletions(-) diff --git a/syncopy/tests/helpers.py b/syncopy/tests/helpers.py index ae17ece37..fade272e8 100644 --- a/syncopy/tests/helpers.py +++ b/syncopy/tests/helpers.py @@ -140,7 +140,7 @@ def run_foi_test(method_call, foilim, positivity=True): assert 'bounded by' in str(err) -def mk_selection_dicts(nTrials, nChannels, toi_min, toi_max): +def mk_selection_dicts(nTrials, nChannels, toi_min, toi_max, min_len=0.25): """ Takes 4 numbers, the last two descibing a time-interval @@ -189,8 +189,8 @@ def mk_selection_dicts(nTrials, nChannels, toi_min, toi_max): while len(toilims) < 2: toil = np.sort(np.random.rand(2)) * (toi_max - toi_min) + toi_min - # at least 250ms - if np.diff(toil) < 0.25: + # at least min_len (250ms) + if np.diff(toil) < min_len: continue else: toilims.append(toil) diff --git a/syncopy/tests/test_preproc.py b/syncopy/tests/test_preproc.py index c42d3798b..2a0f53aba 100644 --- a/syncopy/tests/test_preproc.py +++ b/syncopy/tests/test_preproc.py @@ -7,7 +7,6 @@ import psutil import pytest import inspect -import itertools import numpy as np import matplotlib.pyplot as ppl @@ -19,9 +18,10 @@ from syncopy import preprocessing as pp from syncopy import AnalogData, freqanalysis import syncopy.preproc as preproc # submodule -import syncopy.tests.synth_data as synth_data +import syncopy.tests.helpers as helpers + from syncopy.shared.errors import SPYValueError, SPYTypeError -from syncopy.shared.tools import get_defaults +from syncopy.shared.tools import get_defaults, best_match # Decorator to decide whether or not to run dask-related tests skip_without_acme = pytest.mark.skipif(not __acme__, reason="acme not available") @@ -52,75 +52,155 @@ class TestButterworth: data = AnalogData(trls, samplerate=fs) # for toi tests, -1s offset - time_span = [-.5, .1] + time_span = [-.5, 3.1] flow, fhigh = 0.3 * fNy, 0.4 * fNy freq_kw = {'lp': fhigh, 'hp': flow, 'bp': [flow, fhigh], 'bs': [flow, fhigh]} - def test_filter(self): + def test_but_filter(self, **kwargs): + + """ + We test for remaining power after filtering + for all available filter types. + Minimum order is 4 to safely pass.. + """ + # check if we run the default test + def_test = not len(kwargs) + + # write default parameters dict + if def_test: + kwargs = {'direction': 'twopass', + 'order': 4} + + # the unfiltered data + spec = freqanalysis(self.data, tapsmofrq=3, keeptrials=False) + # total power in arbitrary units (for now) + pow_tot = spec.show(channel=0).sum() + nFreq = spec.freq.size + + if def_test: + fig, ax = mk_spec_ax() - fig, ax = mk_spec_ax() for ftype in preproc.availableFilterTypes: filtered = pp(self.data, filter_class='but', filter_type=ftype, freq=self.freq_kw[ftype], - direction='twopass') + **kwargs) + # check in frequency space - spec = freqanalysis(filtered, tapsmofrq=3, keeptrials=False) + spec_f = freqanalysis(filtered, tapsmofrq=3, keeptrials=False) + + # get relevant frequency ranges + # for integrated powers if ftype == 'lp': foilim = [0, self.freq_kw[ftype]] elif ftype == 'hp': - foilim = [self.freq_kw[ftype], self.fNy] + # toilim selections can screw up the + # frequency axis of freqanalysis/np.fft.rfftfreq :/ + foilim = [self.freq_kw[ftype], spec_f.freq[-1]] else: foilim = self.freq_kw[ftype] - plot_spec(ax, spec, label=ftype, lw=1.5) + # remaining power after filtering + pow_fil = spec_f.show(channel=0, foilim=foilim).sum() + _, idx = best_match(spec_f.freq, foilim, span=True) + # ratio of pass-band to total freqency band + ratio = len(idx) / nFreq + + # at least 80% of the ideal filter power + # should be still around + if ftype in ('lp', 'hp'): + assert 0.8 * ratio < pow_fil / pow_tot + # here we have two roll-offs, one at each side + elif ftype == 'bp': + assert 0.7 * ratio < pow_fil / pow_tot + # as well as here + elif ftype == 'bs': + assert 0.7 * ratio < (pow_tot - pow_fil) / pow_tot + if def_test: + plot_spec(ax, spec_f, label=ftype) - # finally the unfiltered data - spec = freqanalysis(self.data, tapsmofrq=3, keeptrials=False) - print('unfi', spec.show(channel=1).sum()) # plotting - plot_spec(ax, spec, c='0.3', label='unfiltered') - annotate_foilims(ax, *self.freq_kw['bp']) - ax.set_title("Twopass Butterworth, order = 4") + if def_test: + plot_spec(ax, spec, c='0.3', label='unfiltered') + annotate_foilims(ax, *self.freq_kw['bp']) + ax.set_title(f"Twopass Butterworth, order = {kwargs['order']}") + + def test_but_kwargs(self): + + """ + Test order and direction parameter + """ + + for direction in preproc.availableDirections: + kwargs = {'direction': direction, + 'order': 4} + # only for firws + if 'minphase' in direction: + try: + self.test_but_filter(**kwargs) + except SPYValueError as err: + assert "expected 'onepass'" in str(err) + + for order in [-2, 10, 5.6]: + kwargs = {'direction': 'twopass', + 'order': order} + + if order < 1 and isinstance(order, int): + try: + self.test_but_filter(**kwargs) + except SPYValueError as err: + assert "value to be greater" in str(err) - print(spec.show(channel=1, foilim=foilim).sum(), ftype) + else: + try: + self.test_but_filter(**kwargs) + except SPYValueError as err: + assert "expected int_like" in str(err) - def test_filter_comb(self): + def test_but_selections(self): - call = lambda ftype, direction, order: pp(self.data, - filter_class='but', - filter_type=ftype, - freq=self.freq_kw[ftype], - direction=direction, - order=order) - fig, ax = mk_spec_ax() - for ftype in preproc.availableFilterTypes: - for direction in preproc.availableDirections: - for order in [2, 20]: - # only for firws - if 'minphase' in direction: - try: - call(ftype, direction, order) - except SPYValueError as err: - assert "expected 'onepass'" in str(err) - continue - - filtered = call(ftype, direction, order) - # check in frequency space - spec = freqanalysis(filtered, tapsmofrq=3, keeptrials=False) - if ftype == 'lp': - foilim = [0, self.freq_kw[ftype]] - elif ftype == 'hp': - foilim = [self.freq_kw[ftype], self.fNy] - else: - foilim = self.freq_kw[ftype] - if direction == 'twopass' and ftype == 'bs': - plot_spec(ax, spec, label=f"order {order}", lw=1.5) - ax.set_title("Twopass Butterworth bandstop") - - print(spec.show(channel=1, foilim=foilim).sum(), ftype) + sel_dicts = helpers.mk_selection_dicts(nTrials=20, + nChannels=2, + toi_min=self.time_span[0], + toi_max=self.time_span[1], + min_len=2) + for sd in sel_dicts: + self.test_but_filter(select=sd) + + def test_but_polyremoval(self): + + helpers.run_polyremoval_test(self.test_but_filter) + + def test_but_cfg(self): + + cfg = get_defaults(pp) + + cfg.filter_class = 'but' + cfg.order = 6 + cfg.direction = 'twopass' + cfg.freq = 30 + cfg.filter_type = 'hp' + + result = pp(self.data, cfg) + + # check here just for finiteness + assert np.all(np.isfinite(result.data)) + + @skip_without_acme + def test_but_parallel(self, testcluster=None): + + ppl.ioff() + client = dd.Client(testcluster) + all_tests = [attr for attr in self.__dir__() + if (inspect.ismethod(getattr(self, attr)) and 'parallel' not in attr)] + + for test in all_tests: + test_method = getattr(self, test) + test_method() + client.close() + ppl.ion() def mk_spec_ax(): From 34ac192ba20137baafdaa5ec242f71809a772e52 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Wed, 23 Mar 2022 12:47:16 +0100 Subject: [PATCH 122/166] FIX: Yet another Granger tweak - the covariances returned from wilson_sf where not real, but were casted to real in the following granger-geweke function which still gave very good results - we now enforce the 0-lag component to be real, thanks to Mukesh again for pointing this out Changes to be committed: modified: syncopy/nwanalysis/wilson_sf.py --- syncopy/nwanalysis/wilson_sf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncopy/nwanalysis/wilson_sf.py b/syncopy/nwanalysis/wilson_sf.py index f3310ee20..dd69a0809 100644 --- a/syncopy/nwanalysis/wilson_sf.py +++ b/syncopy/nwanalysis/wilson_sf.py @@ -158,7 +158,7 @@ def _plusOperator(g): # take half of the zero lag beta[0, ...] = 0.5 * beta[0, ...] - g0 = beta[0, ...].copy() + g0 = np.real(beta[0, ...].copy()) # take half of Nyquist bin # Dhamala "NewEdits" 28.01.22 beta[nLag, ...] = 0.5 * beta[nLag, ...] From 91a609257a2bb1f77f00dbaa01f98c00d416a952 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Thu, 24 Mar 2022 12:16:52 +0100 Subject: [PATCH 123/166] CHG: Test also chan_per_worker parallelisation Changes to be committed: modified: syncopy/shared/input_processors.py modified: syncopy/tests/test_preproc.py --- syncopy/shared/input_processors.py | 2 +- syncopy/tests/test_preproc.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/syncopy/shared/input_processors.py b/syncopy/shared/input_processors.py index 85d037c02..56816e66b 100644 --- a/syncopy/shared/input_processors.py +++ b/syncopy/shared/input_processors.py @@ -378,7 +378,7 @@ def check_passed_kwargs(lcls, defaults, frontend_name): return relevant = list(kw_dict.keys()) - expected = [name for name in defaults] + expected = [name for name in defaults] + ['chan_per_worker'] for name in relevant: if name not in expected: diff --git a/syncopy/tests/test_preproc.py b/syncopy/tests/test_preproc.py index 2a0f53aba..92ff732f9 100644 --- a/syncopy/tests/test_preproc.py +++ b/syncopy/tests/test_preproc.py @@ -38,7 +38,7 @@ class TestButterworth: nSamples = 1000 - nChannels = 2 + nChannels = 4 nTrials = 100 fs = 200 fNy = fs / 2 @@ -196,9 +196,13 @@ def test_but_parallel(self, testcluster=None): all_tests = [attr for attr in self.__dir__() if (inspect.ismethod(getattr(self, attr)) and 'parallel' not in attr)] - for test in all_tests: - test_method = getattr(self, test) - test_method() + for test_name in all_tests: + test_method = getattr(self, test_name) + if 'but_filter' in test_name: + # test parallelisation along channels + test_method(chan_per_worker=2) + else: + test_method() client.close() ppl.ion() From 3f24db1d823ebfd550f711b65cf54fb0d7514152 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Thu, 24 Mar 2022 12:36:53 +0100 Subject: [PATCH 124/166] NEW: Butterworth filter tests Changes to be committed: modified: syncopy/tests/test_preproc.py --- syncopy/tests/test_preproc.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/syncopy/tests/test_preproc.py b/syncopy/tests/test_preproc.py index 92ff732f9..b60884640 100644 --- a/syncopy/tests/test_preproc.py +++ b/syncopy/tests/test_preproc.py @@ -30,9 +30,9 @@ minRAM = 5 skip_low_mem = pytest.mark.skipif(availMem < minRAM * 1024**3, reason=f"less than {minRAM}GB RAM available") -#availableFilterTypes = ('lp', 'hp', 'bp', 'bs') -#availableDirections = ('twopass', 'onepass', 'onepass-minphase') -#availableWindows = ("hamming", "hann", "blackman") +# availableFilterTypes = ('lp', 'hp', 'bp', 'bs') +# availableDirections = ('twopass', 'onepass', 'onepass-minphase') +# availableWindows = ("hamming", "hann", "blackman") class TestButterworth: @@ -227,7 +227,7 @@ def annotate_foilims(ax, flow, fhigh): ax.plot([flow, flow], [0, 1], 'k--') ax.plot([fhigh, fhigh], [0, 1], 'k--') ax.set_ylim(ylim) - + if __name__ == '__main__': T1 = TestButterworth() From 9eb7a62fb4ef0bdceae907d174b55a981de7b9fc Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 25 Mar 2022 15:36:04 +0100 Subject: [PATCH 125/166] WIP: Connectivity Quickstart Changes to be committed: new file: doc/source/quickstart/ar2_coh.png deleted: doc/source/quickstart/ar2_coh1.png modified: doc/source/quickstart/ar2_nw.py modified: doc/source/quickstart/ar2_signals.png modified: doc/source/quickstart/ar2_specs.png modified: doc/source/quickstart/quickstart.rst new file: doc/source/tutorials/connectivity.rst --- doc/source/quickstart/ar2_coh.png | Bin 0 -> 34176 bytes doc/source/quickstart/ar2_coh1.png | Bin 19912 -> 0 bytes doc/source/quickstart/ar2_nw.py | 8 ++-- doc/source/quickstart/ar2_signals.png | Bin 70280 -> 70998 bytes doc/source/quickstart/ar2_specs.png | Bin 22892 -> 31102 bytes doc/source/quickstart/quickstart.rst | 63 ++++++++------------------ doc/source/tutorials/connectivity.rst | 8 ++++ 7 files changed, 32 insertions(+), 47 deletions(-) create mode 100644 doc/source/quickstart/ar2_coh.png delete mode 100644 doc/source/quickstart/ar2_coh1.png create mode 100644 doc/source/tutorials/connectivity.rst diff --git a/doc/source/quickstart/ar2_coh.png b/doc/source/quickstart/ar2_coh.png new file mode 100644 index 0000000000000000000000000000000000000000..fae4b7c95bb47bf5db240e115fbede56edfdde16 GIT binary patch literal 34176 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 literal 0 HcmV?d00001 diff --git a/doc/source/quickstart/ar2_coh1.png b/doc/source/quickstart/ar2_coh1.png deleted file mode 100644 index 248ef60dcf9bd76612c0bb58c2ccffdc97c09530..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19912 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 diff --git a/doc/source/quickstart/ar2_nw.py b/doc/source/quickstart/ar2_nw.py index 034882305..2b5b2c358 100644 --- a/doc/source/quickstart/ar2_nw.py +++ b/doc/source/quickstart/ar2_nw.py @@ -5,13 +5,15 @@ nTrials = 50 nSamples = 1500 trls = [] -# empty adjacency matrix - no coupling +# 3x3 Adjacency matrix to define coupling AdjMat = np.zeros((2, 2)) +# coupling 0 -> 1 +AdjMat[0, 1] = 0.2 for _ in range(nTrials): trl = synth_data.AR2_network(AdjMat, nSamples=nSamples) trls.append(trl) -data_uc = spy.AnalogData(trls, samplerate=500) -spec_uc = spy.freqanalysis(data_uc, tapsmofrq=3, keeptrials=False) +data = spy.AnalogData(trls, samplerate=500) +spec = spy.freqanalysis(data, tapsmofrq=3, keeptrials=False) diff --git a/doc/source/quickstart/ar2_signals.png b/doc/source/quickstart/ar2_signals.png index 7ff78d4b9e5972c5b62d49f037075437f4c125a0..ae5c3063246ac88739224eba408515fc802a6254 100644 GIT binary patch literal 70998 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 literal 70280 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 Lno3zWOrQM^;e2R- literal 22892 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

2%0fjE-<}u^UJz${#EUMeiZG0 z6LsWrBZFE7zY|5nfXG%zoWIQlLfg+3+;-iP}r#;G0KjErMGvl zccz0%Oa%Ki5PO!I{NH39gX<^iSOi6<629I%GPw%fJAX&kV_H2iRbs@41I<58O`g5o zr{kJ+xxTFd2pn+PZY~<$IhfM$vhxN_6s8Pa7TF8q?RyhC;RhMfquGUq`~Lz`kq8J5 z4C2@Wayu&`(&ENV!KpCFlV`pg9B*6O*f4ZFA6k^k%g%;7D&Kggqa*MmP!gQ}tUlw8 z;UW_A`_#dL?ntE}?{pvgfz960Z)e}fGbJ;?|Bj|xcjF|UB05{TYenDyi`=i_dwltP zB1IPwHDj9m_-c0XOK=DSbdaBWVS^qw;3Ysh9WXon*bP4X$$BJ!bcgPMSv!;84gA7* zsRfQZ*O4A7`%IS02J|IVqq9@tr#`F{3Ot9M8&0%NSp2`5yFP7^??jrlB>9ULB=_W~ zvNQ6sz_}8FhO0xp{-^Hd-p|Fw2nUD%x7(=St`^%PqI)&x$w?Lsj>0D8GQVHEA)e7# zk!w*y6wT;hD2Nd=#j12UksD?e`F6{IF5>-e;qKX$*I&;wXkGh@_@4ZFEfcO3;NADl z+`T`d`cb<)M!$OEjQQ2(|CV62sfuu*`9|2Wl_QfI{yviM8igE8wuj|!u&s`08<{=Ho$HVeD*-V z1kyJ+huJ1h{Ik|hvOJQE{~|@gq-v&Y=OP#f`rJVC+1ec;i=&L4g*U?ZL)LPMFY?1( zb*s=f92(B=I|A=Ne83bS%dJIyYOe#SS~GD^OIMe)j`zQ_9n$&1w65TEG|e~5t3~Ez z<8v|>Z(g%+9Wey>s7Q44zedI1LQ(S9&g!15_q1!Xqyp!=MX!kkB@@=th-0=<1 zRlByyrRpE@jAZ7gGu)x5u;a7mV|cls=YC#gX|tOEkm!uWJjOfOz*19H`RkSR@K}j^w^Cw_o9n`B>g({6+d`xJz@s^|sI2J&$;`?s4%p z<#ZG~dzm=-FgPuf_^ioWrB3jho5K>`yGZAs;H(D-Rna8+_|=sQw{t;9rPZj>$r#6Q zENQ9f&P>L@_Fbr829*Rla}8QGOC^|Ts7o`F^AG0l&Nmt5V4;sqZWte*_6AR3#DyZB z=|_T(z<>lILd*&**;@ob_;ZiQ!`&SCRD=?ydPHb3I8@!;nVK)QR8}UqIOk6}e&{A+ zU6t%k>07jWwnQ%N3&FQFoq2>G8sKzk)5*Nl$&wvg1;^3jr8EO6KMIz#K5yVQjxbmC z6^S*!tQ<7%v{f>(YR8rvk2>wH-Fr_jI&gR9u_)&DDXUduZc|@&sM=vmi5q=v9ewmY z9oS(|CB80!x=UfWeDdbD;l+ALg{A))<|}*u5VASL&7l3`yFn$QbE-@l+5-t50v{y0 zuPO4HlYA$)qUQulWYM`jIw8GMPX#QwpA#{fLxpk`OQs7%}C*S_3}TzKvGTpi4D&(y#n{4iSWaUlkr+&q&6{Q)*6d0b%|)^tv<-^|iXvbJjxH7^gd}I8(ZJzuJtxop^T3zb~3! zdpf3$0_m0Bx?iGs^c(bf#BcHBw8d?R(yVFr4Ibus;Ba1<9brKeFbe35KKb6MVNtpW z&E3VIntnj|QdNL`2op8OWRv_*e1j8GzN!d z6dyGiXusvb`J_|Jxa4jcK&q~s3uF8wN7Ze5OF;?s6=#sYx7Gr8KmGN((CSvn;zS}O zxhS|h-U}7r!2T31A=r!Hd;A3+x1*F71c)z`6{%tCL?*xQi7x$bksVf=;Z@hSkH;L_ zry*>0+|`gTAA$9?mly;Zr=8n2<+0m1kik>R|3Z7qG;V~)!e;kbH%Cqh1rO%ZjO5uw z%a_^Fpj{tU15q*1Z?V|k>l$BG6%h0KmR>Lw<eOyF#Kak9y_!uW)ji*2^2w9g-Z+V#_=(Oj1( zwlBMWxtu;CICB7MURIgCtm{RowdaKn>u4tW{ND>wk_;^uzn`TVM1%Hmtx;GUYzlaA z#bSuRFmqvtYc*e!!o*LWbq_Ce|ESJ?<43b%)mw@NmZ;74a;z=9k+Q({K$8BO`IBfC z%tCrOuUa*D_5n|QOit;1xK2kz6#g_p2d+Sel3K_S4YK%R$d*;+)a_Ed{#Kyr!rq|| z{^NAvaeS@y)yc5?sdzcX`s&Tb8}E!2S`u;+7?*%L3dFLS|Ies`Tq{-q`wFxqBw0@M z;KEMKfw#iUQAh|8G{b4l-{RTwHVU@vXB}Bq*zeHYbsC9%GCYM&sva8%K=&&HJetI~ z%!UhHCmH=NR4~gJCfXP2lkV`Ahg?m`~*MMqi^Qzmdc;4xES8p=n zK=Y$LGKYWjp}WIBM4fAafkDi5MbfJGZ-YQ`cC_y-{&ls&4bG@!x7<+VMz;hO&dGWw z5ou+S{yo%CZe88AeeC47gW%>&$C8?N%wI#kPd@g>rs~dOH}%amx?$>f{%ju2;(^=q z!$N9cqy4{rFNaCq7ACR%%tviV%&3-P`kS1tx{ptIKIa3OmsK|`jq?E_*YFHE~9Yz9*6vQ!`9tf$2WIO_Ckolt=Js*wo6~84#*dX1b>4= zDB!ni{P;<<)Yh+ozBSPeVWmS@Wkby8Xae?z`j|PfOJzHs6U2h~do2P2hvqN|`1RzB zYeu>BI0@-7q}Me4&Y@n@@F4i5t(zu?;Buno!V)4~c8`Y7o@RN!a8308YURA5n%=fG z4rU7$kfI{85oCiDQJORhy=;&GQUVr=(h-yJ}4+^hl1H$=*d39&ld4)vG!(l zI0dQhaUzY=YjU&`f>^T?^zH`bgnszSOM9kbbe%4tI!-_@)0__F9oU(FHmz;XYF#T{ zc`7LJoXz3#*Q^^==^GgQ;UpGv>RZGjDY*0iIb_II$miwmPSvtn{8>Y*kZ`3nztBF#^x|r&z)7UJ)@e2L7Pm^DL~pJpb*y)Y zNm^~c^j1{J$*C!McE4bb)SZ_heNw{<#Y;+IL{)6~Cvq#48&mGk7DFCl&DiB)OWG$J zgS@{WX5IO&5hCSkP!O(m51(NVaBQ+#DYLM*-=rlqjKx3XyG?OQrSB`b%z7+zGS+9c z%X&0F;Wb;+3c|Uj_kh>{&1uE!&xXg+rHwy!WXiFphFng5Ct)!TTN@2C1@~72`J{Zy zxnB}{(|>&FbWtr-d5kwsKY!tI$klVv=UszvQF|6I1*9J1$5r3gzu?A$vmH-!y|Q-J zb9hy--SFv+23liOX&%25=|G}n`-jUJP=|s$maXZ`YKT<4%n2t-xG4d{W+orYqT)vD zb{rMcM>N4kDaF5B++KVnYwmva!Xf5f_8clHXK-vTiOyu^+)eN&Ye{qPx3fBy2LvSe z7QVWs9?kt$vf}-WMLcX>)#m^{rdr$>qFRYJN zdO5p&wy<)}eU0c%oBL!G7$1GklX-Rag@TzsugB1Vq%6aMgKJI`hnDms#QpY@)qsCmkq;3y~Ms zC_!9j2FP|vG-??>4u-sUzYxwOysR2yEsp}l$ z4enfSK61>fL{_m8uWx5q3ecNRb@P=9zG3T{iOG#S`@MXNFx&+(shoRPBgA;4Og>ZF z@?81Pk3GVTMeJEsdvY{2*%TQeRcKpR)vZ5eP~~X7ym06@fdbA*<}|`xKDxD1L6478 zlG$csd7&_Mm2fp`Cdbv~jalC^=A+#OUs=X?mhg%NZAQ~VL=W>#S)31@eHtzH!stu( zW@Gb%b?HQfvs_s<0^Nd-=CQ1otrw0yz1AtquQGgj$&zjsTc&4d*i0hpxH(f?(}$f! z!!u75#_b+vw2P`GWSkZH{@(tO;ky5x^LWS3xEE{4xMhzZeP}t49*v3PeA}`r&Qor60U*nkMdd`Y^ zwBVw0*Qn@DdAH*^Bq%NU>W9_Xc0-Rd{`#i4vMs_xiOXk+MQ0*CYrvJZI1!~aqdfNU zd4@V!tm0uc%%b4#6;aafyUJE_9_?^qK_f~Io zy|;l}n^22GBphKQwVYS9-XBk}r<$qV0xl9fk8yXth(LAllPP>)Z4>A5uj&!TZOT?z zVBTN?n;sKfge}J@;3BfH&;{obFi=1Z4QL&Gs1bJ9AJyE)B|0~}79v&l(24uK^9r^= z_RK9L-y>JkQ-?sm2G#pN1dj3WKuyNL(QzuR9nAY+EO#oq1^#L&skltIL$qWnxN@_i zP~iH4zjiw8*?g`Ao*$|BgJq+aW=p`3A7&3nre~e*i&3@v(m^Q)n4-^K9;Ipc+r5Po z-~GXzMq3t!Cdeazw0P?vH{nOvxa3?l##}fu##H>DSc{b!l2?skvf_6t$h1Boc;IB$ zmZqf+A;i@7slxjeX18(=1pfF^n)BP{`UrKG7oSt@a`j8-02B|B} zC#Yg4RODC-2$}xFP=wB+EW=}GW@cW2tWd@R9)B53-Qc*nIw`<7&H|a_X1~~>;df4D zeIq0Ot*PM#^!kT9plJt12uDJU6~h+`xGRlf)o9Vd&wAC$zc1<)J)xZjxp)8+1Y@c7 z2#E;3W6(?!sf}^+6bS{M!h~l`eEcyzMuq0&b4a_4xK}n&4`MpCO-xL%B0#B00h+}v zNChfA;_)9ZHuBYEDR^NeO|v9x9i`8G?{<#M(H;ED)6`^ z&)0mStNJ8W!Moo{SeU9H*KZ@N#52ZV`!IIh5k@u(Ui{A#H3=pKBfxR747EdAq~(2> zu>&YSOH1WIbuXJtG`Fw-BWssxUXszuiW%w^cmqvMOf(=k8mU?pJq~V-k`?iF6c&r? z*AH-tkJODQd3DF3)pMh8|dgfb&^YzS3j3b zybiVwEEQ9~h0n{YfRBZ?+GSVIY*lg$s$vSZ!f#7IX#8afGO-yi4UI6TitC8zDU~70 zl{<=Y6z7-9kqA+Yz7HcEhDlk=MuxYX^M>h9MAH|H%LTg->QEriiRF~nUaFEw-+|T zoh$xXQ%-rsKB(FsjKI*xbU-IsHAb4LA#+Frj0c4pOlvnBqK{m;Gvt=v&;wWzTmS{R z2Q&_z-$P&;dIJh2Hz$YpC$DRx>AseZN-H-EEWY-E??RtTl(pPc2YuyvHe{NLF(@J4#JZhckp>r>zDNjU&d z5H^Ve2B<-eDnmuE#`R2+BS}-<`Wx)Ga#mtj7L3UrkIP2g9h{x%i9F&8jH7 z73#TbV;*TM&=XCNl{*xH0O$ocs;0b6fW!sL*po0qR=LEs^$ld)pnw>Ik>Zg7!edt5 z;R7LbE09Bd_R`YQYFtVDK%McfDIRrqA1&Rx>s#szJvv+XQ5BQ>Uz0Bje;Vc}f*jcR zb5e(Fe~cYRZ2PgXdp%>|496j@{yQk)j-vVOe}^q>;S9)IkK!Sa$hgyKoc7Nd7=y#$ zApA|o(z|?_Z;+$36DA5oBVq~@D8YbUtsIRec6I`eOtR2LIR>OWF<7j0=VM-s!!_`~ zGNVwP-Q8`sD_FX^jKW_b+8=?i?+lLtmYb9gK?lMjAn_c-4M^+^*&!jN_sk3YN4&6L zLQGd{C7SibCXJPVQUh557HbR^-5gunXK%qOC)5TK!P9F)$5|k64&LBDEJWDa ze+62a9I?yaWhLCqq<6Gdz*MQWk^+HwGY`n1-vagqK!26IPpVTGrwrLp@|Y$M`c%Fb zU{H>WNVFYc5DA*$vrUdL60E=|Gx?{uff9U_}iZ;o@v;y03~S8^wsYv?>Pt0OiufeTHghR zj&o)P1K|ytZ%;2>9f)$+t_dU~sA0ef!FOSZd7C3QC50bUF{5C#1rHTtDfRZw$<5tA zUS)}Tm7jmc9Cb=sIvR!;IA^2ft^*3Ks^bffL-2E;A_LQJc5yK$Za^R0*I@CZDan*5 zr2};<4StJ4F?f8i%`#Ty-{c=4dmSvdEF!lzCkjn?*MuYa!PxpDgRM-5_g<(Q2v+d; zL;ePKKobfLNinDnRC~gtei+P?PG#2^t88jfe*Q-$_Fjgcz#UfSUMAw)y_JiWU~X^R z0^9WZVtjl&j`=$H%^Mf2*Hf}xcVEs#1a6Uil8Do)8(=_i4Yjy-?Lq5IKn;o3v}BQo z`g6AD&$OdQj{FSV_BO(qA=X>Y_tqdqF%~8!va`{n?n9xfCDw&>1#9wle7pBrV-e@X zE%dUj z6T})G2Rm4{gbl413oWZW!bG0!z2-{vyZw7PcQMJuLwcd#JlLEODHX$Oz3sJtxq+b?! z3Z8D6bVN-?#vUjiEAvSKyBW=^hKIsLEPbSX3xnivieUX0w<~hsK5j zEStyKg=@B_PQ5NdftDW@Etu(eJm9D_8V5Oe48$9!=>Kx7HtmnU9k zc=^#}&p!fNwmpl~xP_xC=tB_zuR=QW-!%*;d#r)|PdL)sAC@>UP{6s9ZS=9+_?W0> zBoD+?LK?$ZCoL`LxjvO8*Z@6?C~Wi3@^}}I^#M7nWzbeEaQFD`O?AA-!fp)F2*nyr z==8LNe$St@V=n|)AqHV~TjW(xl%vCbR#L7UyuDxcI^$G**c`$D*iW!0n3lWZu9c zYYPjW1l5bZ#x^$OXOog8#v(EI;ye8{2Wn&?odR`fZ@jnuErZVdb(6@|OSAeW0dJH-d9Q1QPFd?@q(W z`BB5ez<^_BVro#5@uTXLpxO&ciQuYf=1A3WGs{5?kH84@4fA)83Wl`z|fU zf^ju^z2a##H{*VOn6XYl6|_0^hySUIev3_at8C5IsIjn;!X*IaZf2E6`_:: - - import matplotlib.pyplot as ppl - fig, ax = ppl.subplots() - ax.plot(chan1, label='channel 1') - ax.plot(chan2 + 8, label='channel 2') - ax.set_xlabel('samples') - ax.set_title('Uncoupled AR(2) signals') - ax.legend(ncol=2) +We also right away calculated the respective power spectra ``spec``. +We can quickly have a look at a snippet of the generated signals:: + + data.singlepanelplot(trials=0, toilim=[0, 0.5]) -We shifted the 2nd channel up by 8 units on the y-axis for better visibility. Both channels show visible oscillations: .. image:: ar2_signals.png :height: 340px +Both channels show visible oscillations as is confirmed by looking at the power spectra:: -as is confirmed by looking at the power spectra:: - - spec_uc.singlepanelplot() + spec.singlepanelplot() .. image:: ar2_specs.png :height: 300px As expected for the stochastic AR(2) model, we have a fairly broad spectral peak at around 100Hz. -.. note:: +.. + comment Careful when using :func:`~syncopy.show` on large datasets, as the output is loaded directly into memory. It is advisable to make sufficiently small selections (e.g. 1 channel, 1 trial) to avoid out-of-memory problems on your machine! Coherence --------- -One way to check for relationships between different oscillating channels is to calculate the pairwise `coherence `_ measure. It can be roughly understood as a frequency dependent correlation. Let's do this for our uncoupled AR(2) signals:: +One way to check for relationships between different oscillating channels is to calculate the pairwise `coherence `_ measure. It can be roughly understood as a frequency dependent correlation. Let's do this for our coupled AR(2) signals:: - coherence = spy.connectivityanalysis(data_uc, method='coh', tapsmofrq=3) + coherence = spy.connectivityanalysis(data, method='coh', tapsmofrq=3) The result is of type :class:`spy.CrossSpectralData`, the standard datatype for all connectivity measures. It contains the results for all ``nChannels x nChannels`` possible combinations. Let's pick the two available channel combinations and plot the results:: - coh12 = coherence.show(channel_i='channel1', channel_j='channel2') - coh21 = coherence.show(channel_i='channel2', channel_j='channel1') - - # plotting - fig, ax = ppl.subplots() - ax.plot(coherence.freq, coh12, label='1-2') - ax.plot(coherence.freq, coh21, label='2-1') - ax.set_xlabel('frequency (Hz)') - ax.set_ylabel('coherence') - ax.set_ylim((0,1.2)) - ax.legend(ncol=2) - -.. image:: ar2_coh1.png + coherence.singlepanelplot(channel_i='channel1', channel_j='channel2') + coherence.singlepanelplot(channel_i='channel2', channel_j='channel1') + +.. image:: ar2_coh.png :height: 300px -This shows us the following properties of the coherence: - -* **symmetry**: the coherence of channel pair 1-2 is the same as 2-1 -* **range**: like correlations, coherence measure lives on the [0,1] interval -* **sensitivity**: even though both channels have a lot of power around 100Hz, coherence is as low as for all other frequencies as there is no coupling - -.. hint:: - To inspect the available dimensions of any Syncopy dataset, access the ``.dimord`` property. +As coherence is a *symmetric measure*, we have the same graph for both channel combinations, showing high coherence around 100Hz. +Granger Causality +----------------- + diff --git a/doc/source/tutorials/connectivity.rst b/doc/source/tutorials/connectivity.rst new file mode 100644 index 000000000..133570083 --- /dev/null +++ b/doc/source/tutorials/connectivity.rst @@ -0,0 +1,8 @@ +This shows us the following properties of the coherence: + +* **symmetry**: the coherence of channel pair 1-2 is the same as 2-1 +* **range**: like correlations, the coherence measure lives on the [0,1] interval +* **sensitivity**: even though both channels have a lot of power around 100Hz, coherence is as low as for all other frequencies as there is no coupling + +.. hint:: + To inspect the available dimensions of any Syncopy dataset, access the ``.dimord`` property. From a879e42f99b8ef9c0ec311c22a4cafa6d546908c Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 25 Mar 2022 16:03:04 +0100 Subject: [PATCH 126/166] WIP: connectivity quickstart --- doc/source/quickstart/quickstart.rst | 21 ++++++++++++++++++ doc/source/user/processing.rst | 32 ---------------------------- syncopy/plotting/sp_plotting.py | 2 +- 3 files changed, 22 insertions(+), 33 deletions(-) diff --git a/doc/source/quickstart/quickstart.rst b/doc/source/quickstart/quickstart.rst index 128ce360c..799ceff0c 100644 --- a/doc/source/quickstart/quickstart.rst +++ b/doc/source/quickstart/quickstart.rst @@ -188,6 +188,27 @@ The result is of type :class:`spy.CrossSpectralData`, the standard datatype for As coherence is a *symmetric measure*, we have the same graph for both channel combinations, showing high coherence around 100Hz. +.. note:: + The plotting for ``CrossSpectralData`` object works a bit differently, as the user here has to provide one channel combination with the keywords ``channel_i`` and ``channel_j``. + +Cross-Correlation +----------------- +Coherence is a spectral measure for correlation, the corresponding time-domain measure is the well known cross-correlation. In Syncopy we can get the cross-correlation between all channel pairs with:: + + corr = spy.connectivityanalysis(data, method='corr', keeptrials=True) + +As this also is a symmetric measure, let's just look at the only channel combination however this time for two different trials:: + + corr.singlepanelplot(channel_i=0, channel_j=1, trials=0) + corr.singlepanelplot(channel_i=0, channel_j=1, trials=1) + +.. image:: ar2_corr.png + :height: 300px + +We see that there are persistent correlations also for longer lags. + Granger Causality ----------------- + + diff --git a/doc/source/user/processing.rst b/doc/source/user/processing.rst index f7c57a303..22ebb7542 100644 --- a/doc/source/user/processing.rst +++ b/doc/source/user/processing.rst @@ -70,35 +70,3 @@ cluster. All subsequent invocations of Syncopy analysis routines will automatica pick up ``spyClient`` and distribute any occurring computational payload across the workers collected in ``spyClient``. -Visualization -------------- -Syncopy offers convenience functions for quick visual inspection of its data objects: - -.. code-block:: python - - import matplotlib.pyplot as plt - plt.ion() # enable "pop-out" figures - - # generate synthetic data - from syncopy.tests.misc import generate_artificial_data - adata = generate_artificial_data(nChannels=12) - - # plot each channel in `adata` inside one panel - spy.singlepanelplot(adata, avg_channels=False) - - # compute dummy spectrum of `adata` and have a look at it - spec = spy.freqanalysis(adata, method="mtmfft", output="pow", keeptrials=False) - spy.singlepanelplot(spec) - -.. image:: ../_static/adata.png - :width: 370px - :alt: adata - :align: left - -.. image:: ../_static/spec.png - :width: 370px - :alt: spec - :align: right - -For more information, please refer to the documentation of :func:`~syncopy.singlepanelplot` -and :func:`~syncopy.multipanelplot`. diff --git a/syncopy/plotting/sp_plotting.py b/syncopy/plotting/sp_plotting.py index 81a2664bb..e8d2260e8 100644 --- a/syncopy/plotting/sp_plotting.py +++ b/syncopy/plotting/sp_plotting.py @@ -160,7 +160,7 @@ def plot_CrossSpectralData(data, **show_kwargs): xlabel = 'lag' ylabel = 'correlation' label = rf"channel{chi} - channel{chj}" - data_x = plot_helpers.parse_toi(data, show_kwargs) + data_x = plot_helpers.parse_toi(data, trl, show_kwargs) # that's all the methods we got so far else: raise NotImplementedError From 04107120a5ed8258b7c0a7c6a7d88522540e2aed Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 25 Mar 2022 16:55:56 +0100 Subject: [PATCH 127/166] NEW: Quickstart for Syncopy - covering timefrequency analysis and connectivity - preprocessing to come in next Changes to be committed: modified: doc/source/quickstart/ar2_coh.png new file: doc/source/quickstart/ar2_corr.png new file: doc/source/quickstart/ar2_granger.png modified: doc/source/quickstart/ar2_nw.py modified: doc/source/quickstart/ar2_signals.png modified: doc/source/quickstart/ar2_specs.png new file: doc/source/quickstart/damped_signals.png modified: doc/source/quickstart/mtmfft_spec.png modified: doc/source/quickstart/quickstart.rst modified: syncopy/plotting/_singlepanelplot.py modified: syncopy/plotting/config.py modified: syncopy/plotting/sp_plotting.py --- doc/source/quickstart/ar2_coh.png | Bin 34176 -> 28775 bytes doc/source/quickstart/ar2_corr.png | Bin 0 -> 69737 bytes doc/source/quickstart/ar2_granger.png | Bin 0 -> 27174 bytes doc/source/quickstart/ar2_nw.py | 2 +- doc/source/quickstart/ar2_signals.png | Bin 70998 -> 59122 bytes doc/source/quickstart/ar2_specs.png | Bin 31102 -> 26179 bytes doc/source/quickstart/damped_signals.png | Bin 0 -> 60586 bytes doc/source/quickstart/mtmfft_spec.png | Bin 33650 -> 42343 bytes doc/source/quickstart/quickstart.rst | 50 ++++++++++++++++------- syncopy/plotting/_singlepanelplot.py | 6 +-- syncopy/plotting/config.py | 2 +- syncopy/plotting/sp_plotting.py | 7 +++- 12 files changed, 46 insertions(+), 21 deletions(-) create mode 100644 doc/source/quickstart/ar2_corr.png create mode 100644 doc/source/quickstart/ar2_granger.png create mode 100644 doc/source/quickstart/damped_signals.png diff --git a/doc/source/quickstart/ar2_coh.png b/doc/source/quickstart/ar2_coh.png index fae4b7c95bb47bf5db240e115fbede56edfdde16..57e22422337ba463060ef70653da0f40c1a85d7b 100644 GIT binary patch literal 28775 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 diff --git a/doc/source/quickstart/ar2_corr.png b/doc/source/quickstart/ar2_corr.png new file mode 100644 index 0000000000000000000000000000000000000000..62955ebcbe0d1e1e6cb551d9a0d5abecd3d6191c GIT binary patch literal 69737 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 YmV7ft+jlFr8H3ldB_6If7ja_#2FwU|Jpcdz literal 0 HcmV?d00001 diff --git a/doc/source/quickstart/ar2_granger.png b/doc/source/quickstart/ar2_granger.png new file mode 100644 index 0000000000000000000000000000000000000000..90f15640a881464325ac73338c75039eeddca110 GIT binary patch literal 27174 zcmcG$byQW++ck=U2m%6vfFK|u-HoIoC0){;(%o^SkyN@-knZl1u7h;9bax#NcO8H4 z`;PB^ciew&jG;sE?6dY>Yd!0E<}>HqepQf@z!YNo5)#rw2oll*VKh|m zFFf#U#0PJk#MGUXZB3nA4IE66WDT6`tZbdE%ne_;m^e6^+uCq2b1}0qzWVIsWar4o z!eaeD7ckp8n6XS7oHT<$(Cs8O9FdT)4G_N{DWPwh3Rd!^N@52sZh9X1Eu3JTYv*$_8h>j9Hn3QByc@IxCmh2VDOkx~7^=Urwd%MfyxREMw>c#WPzj7oP zM+A)`01bV|m-)-TzitUXgP~Id;D`j^pu@o*XcQtLC=kR&I259O!iY~oa9+1(AU+XA z`@SrX_~dnf5iiPTu=6hqHfC;y zPh1+ke}uh8-03*t|Fc{Ezj^W(4(%D4wD?6jJ$r>_mu?xbiEHqKMEk7s-%=>H;dTHK zQDtem;Ai1E@W;@}<9KW*wwCyISj+D}KDNVR{@17MvBC`g*WQ6YuNR-@FPBzT#bT8b zX40xMIy(!_=AI+R)PAOL42{}D!s%646ZGOXa%jRl^pc5vMkU#$Hz zr;EB;Q+eIM-OHzsxl@>qjB4ln)ukvz20|aJ9i3HLvYI4=Uv3-N!zbf0-fG#(P31&l ziZ^?1ANmR;)D+z-La7GgF`X1UUlHyNOVKU`xBGOT2Ria!?XJ#+k9_VdP;Z>KJvWkP z6=0(};+W*5Ru{v+%j;V=*!+6){&?)~iC6K`&uV%KDe}I-7YS9j&y>})dce}WW_lY3 z7Ntf@O+~#Ee1el5WjuQI6|7D*|AAl1W=tzz>%iS`4cX_~68-%Kxq+pdKt@+p8+{G1 zSby#aD;9UGT_Aa_r5`@V%yr2Pd_~;M#k+Xvy%38aUg)I(L~yNzZvP#j{;STe9)ohH z1yj2jmP51SX4l8MbNepnNW%|;EmG=y7AAd}QITw!Q~Q6#Zt3nwC23Oy6^OPE&3distQ$vHu~H+9>A{+T@Dr=ed$u_;efcs@9kNI@(?&KOjKU~ z4UGB_FoWHl)C7c%^2R}~17u0tcwLL~;DspA9UbcQx)9aN<$`LB`=ueB2 zhVOWt5Ue{Vp9_!GAGJKC3f`;M)`KPdd$JAqw~zL8&9f4Si4}l}39Q|SNTNdG`J`)i zJAVI0!@$_h9SwoA^HKQm5XzBm86v?e&VpJKJRk619{i5owmqG_NTGVC>Y}Qzu}F$h zUm{C+&?JU|d_SBP7e+TG+SeWJf?axWacI+AKQA*9^qO8?)qs|-8{;e{2;#s(k_4OUW1%=o@aX!Sg>+d-%AYzS>d^p-f zx;HIu0Q_`jabHaZ?3yc|X zKn%B864I(QH?-aL#O}WhOCJWTdQa;O*V$S~o?mb!r1a^f4L58~DXMCAgG5!kJE5$k zJ^zys3MQt2&ma~k298p>$!WYU#1YrjlWp1!z=EPKKmsItv_oCU5*)+ z9u7S5CfYO8)+17C&!8LY1b4kU3zOyKCDAzFDtKp%1sBgKZg!m2tgu3N+!!)|HoYzK zaQXxx&c#+myd5dF9|Ku z4)<1nRF3#UfB))em?>(1JO@MWn6;MKT>U}v4B|XFI5^&(wag!RP`JA?3fu-ULE|o; z!yrgmXWtP+Z8$jUZ()qMLgR*|%Y+$zdXb~vzL+9Z!Gek}y(2U%tDLdIl$Tj?8$vfU zGJ4#=`>%NF&3)_tPhkzQ761RMj-p!}U7C)Nv=sS;LJRuKFZ%m115*Y|p=|Gs%}rgb#cZ&9sO zQ68`wL2~`$FEXeg8-r;bzcUzDLm5i9hBMC+|AQ@f)y_O^^|~?@6cyV+f}|5xiEP(z z%cRxzoZt$VdC`8Dexm~+{P+)R7=OOq6=fl4YWq*D0;E7`rpmml=bl8ev4}IBqrcBb z=Cfr`W3%`>Lk}(uc`o~(uc1`Wx@ygEKbb7a(zkv9P)62B?>@$Y-ddTjM(4IJ!(ZYFHyi(bhj{#;svkAijkep* zKtepeXtcegJwiqVZp^SSmT7qa&WDg3*_^||tjVXl_sy;G;<>rPVQ;`cYbyA+#zhtF zITNsS#EzK6mEpWTtHOf+=G-UOaE@U1xtV1qTTowI>Ut&;Lcx&NzGOnE*I!d%ty7ZW z1%I=>z!0z%Um|;yJ5>WQek-VpBsal-TP-7k#uodC>xyAPGqlTa+u;tZhTGj?>7z0(QQj+c4kRM=rt;g}0VwJ`4F!@6-KbZ_@H8U>UB%gkxxsv{?B zKEvqYkG^$1ny&y{Zq*B6!!gB;8m%&hH#;_Wd1LiG#8K5YwEI9hxNQLUbChh(+D&hY z?P;-rpmneG|H;8?OZ<_N%7&*w8Uyysp0;ET`(c@Vv{q0h5M z-d$604EWSzW{pheu0Inw6ZGWisN<}zx>lCn0Z`V1G0$IMlc~SMnEia4?Nn{E_N6BU zQWr^>hS#P}k^4cC&1IifEFszjzSs&Plbu66>*WWi1GYRDB31}LpVE{AV0@;o!0#n+ zw?{n6F?xHJC@<8K+$84iJ5^ulK&P(tXI}`6H)nDkE>0O*y{2C-64KstVCQ(azCF)N zmEF-`nt*x07=Y6}VV2aov~f=mC)Fi*xqzpB0Zz8y?B&f{p0m&Vj`c6Z6#0rvm=@J# z4vOaGE;)!X2U|S2cl9))0EW`G>3)O!5rgf`IKKeVYp0i$e0)Wg7JV@1(IRz#+X-(N zue9K8A3D3bsxjajO9JF^QWNIEQL6NDPdpvHqo4-dDf~J%oW$kyI%X&7*BzCJv^W~A zTn|E*T_UBSnA(N+CGX4J`#;nB7i4krXd|^RZCIG25*Mo!wple5=AcX6hk z@dzR>eCrg%So&Oa(UN1-9|pmXE5{i>2m4 zvd3+3@fU0q(DiBErtOI}eK!k7rx3M8@eYb=8UHkFAg0*6Wtdh!A3jllGK-wdF6(vE zMU9Dy4C-Op0UiN?I7ooYG}P2NwmuIiL=#-n^RSIiG&P9tl3K4MNxyr@Hd;e2M+~0S zsv#@wGk|>^p-E~@ciG#LlIQnIWS7n5w=%B5zG&Fl1B=c^6iyn?(v)SCqlCuBnrg!` zRXW!{j}dg->Ad!EAa2rPNs^SIg@d@U^{pxiwN3?qc>=hei=!qd5oujSMNgZt3b>S)Ury4D8 zTXVwBIDA6oG8dgj4PgM~(kG&dsf8%WvPQmSs%u>L7Wgn!;$(VmG*BZwo!~w>I8fH+ zpg=!TyCr^Il{W#08sln0q~j{+`SFx8XS{#kcPX-iW^=@`0M#rj{F^fYcS^*{SGt#_ zZ{J&4?Y$$E2yuj-W{|JlbH_i{CHL5JK=L^~{NQER`@0pwNBTu@;(SoI@%b0MzsYV* z)50)itGf8n?1VGN{6HMT&%{ckV7i+*$hpLPIyUAGM&Z0V?{P~6?|O;TaQbep3GEg- z|F*qs1@#fioacD!Cv2%&R)gf|?w-3Vo0K%)_;*pC@BZDrNG|WB90I&$}vEcNB&$V5Sv(*{l z_E*q7{7j|%-hOEGvU~_mO}yiE&_N{al#)mqB2+gjtL{_ANeYr@w zAvbTK(KWfp5CcsvL<{723dLr$#DZ(~px}dzZT2aq?CbB(EUIEW@|Zm{kBy6Su5ZR! zozJb>9>*p8Nu^Y}k~b<7*d=_S=b`P^xKEQUnq@gwBmXZf_`wo_T5UDc#C62yIXmu> z)be{uw@2INsVQy%j(=))pO|*Q)1z8sAr`i1u+4cD+_eL!E>tyCSCvu-hC=Y!_%e&F zBAG3ITDq|73ModpI-^XI-2>sKbM8=X{O@C3RaPP)f9y8wn2nxM&Oi#j{4%RKYHbwls_H7b0ThY7laEVF0TK*ga2C3XGpMwMq8KSM%)%%^st zVjRN|h2(i(3%xJflvtdN0x(YqQ-VqN`4$l>f|JSY0q}!g+dk3SWNwyePPHb&^PnMR z*9;O>yXeM|m`f;v>5xILN?U4erMAyc#TArKX_)U?hy0;6HG)kf_#b(5CJFYJCPRJ5 zx|SKOsf1OH)v=qZ$kT^Zt7Y~#EM-#~iB=R%>r#vgX)PvemZ^0u6BkLa;Np>F}U<|BM+gG+DzuR`7V>3%+E0tR=*+hO)bGXBrI_)%)M?pY|_eCWmT}>E4 z-}$M3E}Ym#eO$i480&q@(jw8*M~XKbZ^T`&AE}S842~Tg*kHgh036!HOvg;Yz#n!N z?u2COZKm32e;9Qc;_A!JT(RTQ{`xL7(h>?UQx{gJ(Gy35ee&f?PMA@obwjDhp zx=6EX+dV(;I}X5E)qXxcn9;7UzqzQRlE@Ge7S*B8TBUV=;d$sp^cdWfSgslc@>Q{+ z(rShS{X3t4F8R5fyna6qKtQDD19%mc1v>uiuY5CbD2kjFOpQdBP#|mu#7M*lGA_jC z(K-4xzz${$-TIskV1Dh4K{$pu7EbO+A>-$=yD^EZ_iKZbd!}wy!`+YZMM4zt$Cqv4 zL#K-Q4H>b5U=N~%Z{H+#;fP?0V98;FbFC>0ZZKkvIFb|%1mNO31{T-)gY(wb?qG7S zUXE4>ccK`$TnStb3Sb5)OM~nNKv$wuU~d*!FN18R7Vb{FV(}ADJdiLbL?ryjJ~)x1 zLW-^7JERESP}J3rZ+u{8Fo`DtoNw}8L<*DCGlOTI5rgK#1weB!Ev(N+k9*H-L;~`} zbAu&ymL5SshD&6{`tOg`ZS^Ya{lNDVwig1>6eE~f6b-xyVZN;k84DCi2(T&FdUkPs z-9Tm!naFBI|Cd5knmc4w$W7sGO9%3^h$p!OkKBB>$iGm+lO)4lTZ%(bFtSa-R$!as zV)?vyNz{!MkLM6=@OucAN?!KH(Nt+GUdyEnvAeq5bO(g=VfKuwpm6x2HA@Y=0_w`T1{)_HhGo z+Q;@z^etT;qnaKtfb(gW7DWAG7wmci{)o;GPu^)qjq`aW?ET4whkDaLc@Kp42OFmlWY@O z)klDn1LVjKx4QP-G#+T&(P80S5!?1J0MA;RFJdqoGX0}!AZ*~8)%UwMr2UnnYpREnvL3J?`m6EvN>S|_LUsCo)HsowUtlG)o`Ze>96 z!InOAX3|O}%(Rx3u>xuVS(uHDZK*C?BxG!dSm({zF%hKr^;z^aIP(W6Tvr2Hu{|VW zV!l&=WeVvsH0Te&xLPT$Rjm3Ml4>)%nAT^^?%bfQh|k`jJ~bgEp7lMHx!^n4#!q9* zZzz-qV~r%Fg0T^?{ms8M5O$E1i(HC@ifr3_%gx2bWkoyx4sa>DH@nh;Lg{p40VALH zIJKrfJ$|a3gRY#D|H1P;0M0fsSok7m*ilo#*?qDiDt3Ot+eaX0Ens-0v~rkT1G-Fl zy^nr}6su`rbgh;r9&Y%8sw=n9GVDoMFKDyeteJTpcR#eMKH({(g?Sn@dV_vDIxTIX z&=G^8<*jrKM#eB z%RN`jK*ziiT)Ws^a@WXTL}F#dP11p<%&Y5Y_$T30NPF+*>2V?Cr9J>{o7iEi&?#4C zSf%YFle~oc>#6rG0*31g{5j{><$)mIZ62dAkRoF1z9D|zc9Y#iiU7&P#00Rs zrc#sm2RaV6{}2_*-cxKggFB8273~2W_DNHo@|6!ZoD4w2YWN}iLio-QZ-87KYI~Yt zxYo*p8oyNQgw8;&@nUZbg96ppBR5x%1A-JUPDmNiZmC5JaPFs8FDBjdEqYbKhR`RD zkC0yEgu{6O0{aG@^p@$=Ec_^=XLj!(;S`;sQ(>&s`Sr6ibX*TFrv1Yk2Jq?U{0nP` z^MUIC9K$^HoB0+19w+~do^sb@utUD{aq&eYH&FK>>WCSl7NdN!ES;nEzBh)0G8SZ4 zY>Ys27>J7JSFpG;+;(m@JXeQST!lEo%m{r8t>DH73VQ%rcy(ebL>HEZP~31;f-OiM zNQR7_&%KSwI#0FuhLGX-C;=9Eou^AD*lv;mvCl*IMRQzG95nb&9psB)f^zcFou)M8}rl8`+d{=M4SMDX$#ord{(vP(`$slqE&0PUlt*i z{d-ZH{6F7wl-^*;%;13FGATFFS)+QucS7P4YE+IPQ<3|Z+#z655<#N$+%DY(eZYU_ zW9f3s0l8b)D6OPh1e6-IhMvD8^IYI8ZRAdq3>-`ufCo%W%pRQ{fk~CrXV3C9WfXcp z0O4%Kl++5^ths`{0P_ia@}jk$hv6IY8Kmpwh8)3U4NXgcC&EGg*bO!pMK-3fdPwhH zt+G2PBYo*IZxr+0VmPkKIG6qW#nQ1|E?qX$nY zM5GlmdO>{$LdZLWI{9}4tlGm}aITN8y#%hWcJv9rP+yWC=hd=Qm0k!MIO61d;-F@W z26ddikP-*>F-o68G!K0T&mHJceXJ&B-j`1gv>JYO+_>{BNR9+${$~JG_S31F*-Oyq zNXZZZ7*I9MN?$xwhnysgXkC6%<;njAZ6(2CInL2#3x7EYi+f?UaDG7S>M!HEuK_+pll+@K2D?` zw5supyNRFnpg@W}g?b*I-86}WEZWyKBP*?A>=$}RsVBck!AF0WOp#h4MN$$UNmh+u zWYE~&d(-PBIZ_ppd-_oZM>(mh8I&v|D5<`o{RspKi2UvUq`~eS!vXmd+w0XKau!CN zVD_%S>w)3#YEC{u-g#XB>-B$^eMTBJ?3i|~peRd2q-zmbg^YfH%254B+B)s=+^&8; zM`}9nPMLR{T0rb7pwnSDJgo;4Y#Cl&T<&2f4@(`*)|JX?en{NXhx&CXO%;Ot;~{76 z@F?ihgZo#*cY>w1g#rF^2i0x)~1mnE^i*mIdp}SxMdFnI@n*vtM?*H}mBJey!wY zjiAKqVxVo;G{Lov9iqt8zO)%!oTow$we5q+lAEJhIj+zmH_G>(MT3<0Y@8c{gzE3h zH7wYXyqW&9s0#Dyh69o~l;-*rh<@tNEo*NHY#9&ipI3IQw5`~o0KN1golq$Rhl}Z+ zBbK8;p?Jt9vjdg(lyC#m4l8B6?`%#BcB7oBpJjq}$1F#gn(F4+o z36R_=Vz%Gn(quvH{Nw${wQki;zi}}%EQ;UKx#Uy1VL#_j+J23w+M9A3H{%IPy+THj zfMmcB{k^j`Yiv7&z@5Lzng-^vVu4Qo@h|OM4#<-!c`2LPgRBg_5UEFvg(P3LKi(V6L%6bYCE$wV>taE2`Bjo_85|cu)Rqspv1zZ#GLZ z7P>C1=lBKF`;2#->N9DLoytpqlVHzW+4IxE|AH3HmOh5yoT-;5u|%cXIIvy%8SGC2 z>Jv)xV^Ghsn&poP5^>hl57m6*P#y|VNpy?qLx3@i&({hy><0YWDu8f*)LIA=^pfU| z|3UkN<$m9!B*PJTncN>C*dHadwv4a1<@}dQ`a;?CR-eUHb#wok(V3uMYJhe|{o$AT zrk6+9FN}aOAr2RTg{@tWR@FYitTt$U?(-qMxfwd_(O7eZbJjct({0A^%-L|3#AjKD zP9)Wu>lw)EL7uoG0pEVOUePXS_5)SXfIW$pg=G^|4xEf{ZGb25h#%4dtM|7GeV$;U zJR~@wbnC=T8d4q}y=o|v1W)<^q{>;WJ40q3<*J{wtVfQ3J$Lan0PWEuOAHS|)+(IL z<`31fbppCJ4f;fDF|TJ_3%&>BjlG}5t_XGguVVH1c`5_PTXF{q~^U;uBIxgy6y-J|Nxf$J$EAGf_aCg~S5k zZ+&a67Jq{SgI>f!#|(t4208!6%-^q{=y{TUS;uCixty!lF~tnTS+GnKq~z8f*!$Wy z*u8{WHsPI+8-u^#-d)_<pwDyHPZdZR>Qajt#Z5-@jO;Jnu@aBx%h!kU z#w+2t$SMVk98xL56rv&M78{2%!a_X(Y@0F3oG-IRLNaNuyo4%pO;0z49WnrgJMPbuk7MQ;_z->n?~pN`Zu)**(JRd2rL7Od(;hi^OHUmTv=!3hH>bpTGAMaxm>akEiH0mp$v0cq1FLVZB zhf4JFuv?2V(v=&)ENvqI)(ZLdheiGXTS};Hb||$$X^5fokMiN!H=KNuEkiFOP%rI2 z0{uLBIO&TjXg2Z2bG0?hUrgx;l4OUq4R4onR;AFBogwfjJvTfB#LOu@a*ef z0Q#-@-(SR}@SgoyN7i`>;BW!Jbgp-Mo9c`NpiBptkL5l(RS^mCQ9V4u;qRS;osXAs zkG0N_9=u~Q)v8P(hdV!(*AafI{JU*X5y(&8Oj}8<;JlygF2VQ`b8$sbMz)r`I$`Gd zu$xw>W$pap-hs0}=8XT7}U;1<)|K$I8dXgHK zCbscenN=R-(8nyc`4*S6o2LdO0JeA*SZ^FAj=sEt>@rfPL}}hO15px}5wL(@Kg)lb zJk_P>`~xgMPUZ7qJ-Z!tkxr4H`QOOQEf0@wWn6#Af#8XxN`q)f=d&gT=Qojk<)MvU zZ1BkESIo>M9WJADY9a0UIql@I&%;EpIxb2A??{zfxsMSPn@w(rj%@5nAi12pSh4CO zKcR#k>8!C2#@OAUXANm*200gSqixzYsQTB)*v<_RXFw1>D#&+z_+zT3^i3Jpt$(JY ziFzzrT2QrcVkZfIfyT|GPz$f&`YI-+GZwUsn+r=$e0w-zaR^jMf^nM5vEdT*jg>&0 z_o^n2NH{s>;mZrpHaV7%G!KCta{X9JaLs0loOPzIatKUP-D-yG-d0GFaIRo zYJdq0xA?5gpTJtV#-{ogmG(VLFwaLP-M@a1o#K9|FtugqB=W{0j{Xl^5s1_cKOok% zGgkOTOl{h_i`yQ`rqq|r^*qyn0z6Z)mX@Dl5brr%`N;<)KLaM~d#k6|7I6hSEHjQ> zU_h9O<8!%MLg|>WZN+jR2BtB!Hlr2`Q0*+n(;RjQ)GKNDm5^mXNyr7e_ z|IW9e!y5j3f5J=kC#DmEP9?zrg)8)Nf>Aif$u?n1o-3z5E78My$nHWd8xf|!>8;s6 zDtJm|3c+*)muKr_G|NtuR?m1tt1m6%l5qN%X6RrycOTSK7CI$%++Yp%Mes#lq?a~_ zNC^sP6fKB;=|4=|E`A^E{}8aeheRd>?dHEvRqozE++EJ=0_-j zlQr_(Rs0f1$-16&3?iNU1UN`9h|ZrWr507!Og?3nc}(qUcGvg)k3bxMX<7fUPFfxP zCp?~w%fsYEL4K3VrCig+F}ffpQZR2h-nJ+@dDtukx-xbemb04tegr$3oTHsp5QP@a zBj7xkLy>DVeld-9p*Vo%Rp{^?;^F;4JuQ0#Q<7@rH~X2!c0@!bDam&Vz`xo>?-W3K zwFUjg`a9Uap55AlkC2I4P;=*`t^H$w`UKkE@|D?(q*if=XPW}f7<;PhFS!WataxrP z7Yddwrkdf@bxJ_m#nBY{d23R1^4K+*Xx6HH;6*AoNPZWLozN3joK3h_zqfN5<7PWd zH*Rrn#|?|J|a!{}U`r&`+WJPhRpxgWc-Ag6II6pC*u0CV=K+l&C^LKlW*P98e zwc?*;VWV_|L!ttF5Gm1VVt|3vPX{RorT78#Nd5i?6!7SKOsChAL6S$gV|gEWvy`Ph zv{8bYhLBZWgLFm$QWfC(qJgymc6k!)?@ml1O3rWNh(nl9sl91?NI+78rE;5I#sA=8 zrFTFy(A!)b$p9A&ICgHx|F!L9H*uOi017_}BE#zzSUY_mr`|O|n^IET#P*Owq=rtM z?V}M@ac$n*oiwJgxwJ-qZ9x54zxVcFsxuJ+4IaSypb<9NjV~sxQOsAFPTBQzo(Bvu zQ3NUy{y#f}M-?^#hJ68OeXP*&39eL;Y)RXd4-(M1BI$J4bhL7tw!TCM^ua+5kto}T1@)a`(03_g5MA#e~ur?4L(oaz(PnvXrV*=qw zIBdF<%ZQf^aSR;Y>&CIaIX>DiM~7Wxb$D5*@=mbB^=Ol0pUE1=rq{ZpTM@$Cn}p!? zwLyoG(rA4@)X~_g5KLx2bm!Da==#dSWq*m(4Iz~6mZUG39OcqJ0Tj`{E(Yb*VPv2- zpAj`6lXGO0b8pdc`-^04#A(D)b))1XUkft(Mf*Fe%#l*eEMmjb-nC@J{l?H!b(lL- zVaJ0VDw=0UfRxI5=w~3{vrzYdIU6txwvygrz!x|7$lls{s<6&!Ky`3qY^*m)2L469 zoIcIw(2lok#>$txaRDCD&(x`q#aSO>8FQ_iuOdVKVjzfg0tk23k~No>=VQ|1(a*BX zpcp2pWw)MUYPC*&bo08h)jC0bxFVP~-UKl3!`Z`Q!*&dvk|^}3gCjf=T>l}{-Q%~j zsCL#s&MFG69&iSSq8AE%%E$nKbn4TG!&RWQAxcktcJ;!Ncx;XRi9NGI^1}Ws?19Y3 zh@T_d4_;Mf?`r{n* z#rL!INZFHU)bZmEKh~Ps3dliqKT2;xff%iFa;1b z-MqOvy-Xh3b4dXhl=NZMUj7W&h_zE-_yE%4;5z9X9HDKvEKNS8D1IRfygY)DI6o~m zzk2w<`k#_5Y>{J}YG<_Ur}T$JEi1@Uf*WT^ada5|CYod?tZ=#k4{i!9A0RRqofm2y z-EU^NWcrdvZ8WYa9252r8Y<2R2WaVp7AkA=%0m9Rj}Wd!#x~XL>}=4!f+Y?FmKI>n z1!@xT!`fugiqgJSAM5S&fJ99|fCo%&c$a~XN9b)R*e4F60V!L31Esv=n{{Z*IfSH1hN z^tqV9#iyx?)%JB6VqvzUO7NHFrvibzgrMi|{{)#IWYCIw93N;t(HUfpTBhQ^boG}O z=|osSMyV=YTGk%yW{}t}f(RuVzxmZ60ldfVSzA~o8Fzw_)g%=3?0(2r!jilW z3O%c{`(0(>LRRazAMl3L;{G=78Hu=aGlHn~SIaPlJD9CI-2R@MxjKx{7QXegTB>Ot zu&#}>Uh|ytiQ)-e)L*A8*O=VE1+TKF2|Wy_uqPC#RNys`a{1QPg!p)eDR~GKxCY^PRVt*{ zUhpwv6SL#N!wjtcrq*`D${CPregitAv%5Rp`!NOT&;QtM;ts=Q(VTV?UE5iK$?5d( zWjDey3Hayh87j)R5OsWx(FkO%Ws1!HS(yR~fg_I|dww4L*pm}Kj5^814}fhR2TnKD znVA`cvkEw5z!V?l7;_}afZKmJ=-=8^enO$yRI`tAxF^&V#v{M;$Mi69LhpI~$qpL~ z-iEaCbJLl$@*u;wu)hhHufXW3k0fa&mJd5tH zz~yr!%7~S4+0Z8kEV!=$ni&)CR8pNebsqq{x9~@Qe2Sni6XYuTC6qt1Rms4J3X{0*8Rqa}x@E8vrRdN_dxRsCvmNCWyxT;6$FIvG%?Q}Aj2nJZK zVQ5NQsj11yIxd4N%$smZs5QR$+x$|X%VlR^d303N+Vq`;Vx=Z zF+|yB>M}%tqA+*zxZ25!0CTZxXa;cKu4{|8@8mpd+KwsA^w)8UWw+f#o#@_E9zepI zErC|XkMVP}cOcG^wyZzKm#=gqTh$ibG!o^t*-z?-Ee$cj(jRTL)5~GNZwl=SG}gWKXPi*NJ%=r6p%+Ez#_v)9iGT~ozC;;pz*bT z_!S!1_a>djp+*+zO}}v>c*8j_G+vHRtC46lm!GVU;0c9?BqGQkfUv}##aweJ%lR(N z9zO5s&bRa;1xLMPs-S-J#0kJaYK6e78G}Huy?8-luDm5g0zhyyjrgb^qhWBLFD_2h zF>_lejkvhGT4m<`Zziq`Dek~V7I%D0eL@DzTP-T`ZW0<3?tNwij95MRFOV6 zG(T07hNOR^oZ;A2si!oyj0t6V6~IdalsHjrp!TpGAIm4T`rOX&9EfbQkrcwcLo%9M z_{#>@k55K^^7#PYD8Ud>`$9tYX-0;3SR*rOmpn7^$hLhIG1m+G70|nn?wyoXk3ze) zyGwPLMCSadO7R0PVVEXgKuG%`zY$*DZH1F*bv`tM?{)kKY@Mg?Ohs-r z)C=~X>MnYuGD!5`k#A=-9D2q?KY*0Es*R9kc5uRubI>^(n+5KUafJlbdzfZA=19%< zfdx`wthZx{R5mvJl2%kQ2G}IYKJ$zA0K&XNqVvMye#zS@5o9Qn^UH2UhqA~eAE(6n zm>A@-n!774bJ7CW#+W3Wc5-ggY~a{=tGQ$_%gyGNuTo%RrpeYNQRR7OP#-94#n%t6 zz+y~^CjQ{zg`yQXGwq$3PH3AnSoIyXE#v%l)$Z1}jnszaUd^g|^YnWfLC268N)VuQ zUIXLpSsQp55DvJ_;tSiDBnxlt3Vd^gg$BWz9XEgNgS}!1q_^bYmmjYLDa4ZIP5EkL zmZ$SzQV#DKF;{jR_l68vkml@d8V9gt>OF)nbx}UJy33W|w>rHzPzEm_9M_KG0a2%Z z+!(Wn!~}CQZ}er}L)5Ef1yb<-0`P2azgk(Kn^hdjd6InzY(0j5fR{A&v}i~$MZOxC z?reqm221dAE4@I*0>yf-poE5>Dd;dlzAD z`c0W1$-Ooru`v^o`Aa!_q<*XMFSd89AzUnzpP8DWMwuH(X+ort#EzMv}E6cpm zq9Emyvf{5pi1`gYHMDvdA~vX1ZB zm=|6yrXw=j4-l%_g$D|;}#N z%qXliUt2TO_`};zBEd;o;?-W%4;JU$qYcZEU!jcn&*J2rGmJl<^v7f9(VQuMiam+ zYIo)sJ=RaDvqev*_sV}|jNml0xHk)>-<_c?4NDoQ%S`hKr4Mheg;A=y)T*rb*yTk6 zX;yl1UP9JCCXhp%)khuQc8~Wfosb`Z6@ZTO^0a!WH9J^RcAgjkY^@sJ3w3SPy;A4$ zjh{5ljRM$ST2`i5rZ0sqtPCg5+mE0jiqDjsZH~+6d(nr&BgzP-y-$%e z-j&-#4|FuSuEWdq@=;0-Wlz(@yQ(*7*g;JLtomB8Smi;O(VnpLjWkp+A@}r8<6}2- z$LBd$Xg+7foI=7yNJlHftzBt>p1gldJ;4%CPqg^|`I#)lzuAXvpBfMW<=WYcg^ zM0+kx53npQ_Viz3kL+EX00!cuP$S99R*4-9SKHi_rf82l@9KW@4VoVMlZJ{qe1Kz( z7re3rfW;DN2HI8S)umoiyV>(6)mU;4J#WD6?1rvC{_43FUel{FZfxZRuXr zB-rD?>#%ihKk02B4qhEftBFFs8e!nSI)spQj&40%?)X5kLn^rYVxN=@@Ce{H%b85!Ru5d+EP~i(pgycO|Lm${HHuy?52hlbM zC0Td-Hk~xNmAn;3ua@dEs_Z#A%#fo28XSLE&dI5?#Ib#SAkaImrdl)|dODHq>>IuV zSn75#Mp>t|L5_M)6cDDn(3h#^>Hx_4`@PcjE*6FtcPUX1JBWN+HM&yx3`lM^yF#Lvi+o)a(zHvYc~`t=|X<~ zj3j5h6oYx|a&Tl7%QLTx^Z4R-J_rXH$PPz(oy|)}7NW^Dqdft|VXQ~lKy)L}i-*RJ z6XisNP;t-eEtp%oaGdwaet~?OlPDz}m?xA<@@!{?Zx)=+k`AmNSDY-Z&%bqiLzkw4 z!N0KhE;!Vp(SOFW_GH2SD{S~v7P9hZu10}rQnKjGIoEHzDr3E__jO4by6n-njxt`C z*eNeSyqi-Rx;tehSr0H%$jre#M`_jxe9roxwvp=}crI7C?0O54M=+OS<}jef4y{D%oQ=`_H@-)hgJ08FA_a|3Qu0N?S1rC*8FMeN7-KU5L$oYeG0a+669MFn%oVXGt zfJ$aH)qFkIDwx(?$DVzCp8QUN=1XBWRfWC>eMU0V%W}@s12PCnM{Wr@Inl3%&l1i* z(iq#ohB~ap;z!QC9OrIGbP|_$i<;^YYrRcHU@YtkrItzk-30*3M+!cPqF%RfO>tKo zP3XW@a?rzNchu8ZM9V&I*nZP!t$*^#O)&7{vfeN>iX}=t>C*7Uy+xm~H|f(_!nqgC zSNLXl!Fy?!U+#wCnu`mL^nX55Fe~-c{mpwv@+19b%uM11sOq5PsGlAi5HvS%uqxgG z#}SKl$-`F@IM5T|AZqHA^l|Bv)MXNE&%YUSjPbE6i$uiJI@2eC`B zq~pfnw*{P)0Ns;9mIB3#R~HAykbTMD)ya)f93kJoY~Vl0=<{YmKGfx5`w9WAbSYTH zD%^6?hr(Z^Io4>kT+lJh!k5<{5jr8%m^Vjw$a1-gu4phMaNH0V;da~EK;KbGmcKVM6hmY+2=dD1$3V8^>_EaZ^-Y0`WfsZj#VwG|$qk{$@H?IKm zX61ED*w?Ue=lc764;l=;g+4@&I7B9=s)=b+8U zZsI%b@wyd&aNF*(r9c|;uskKB!agUgHxAlMK4txK^u@Rmi#<0iiF zjViIHJa^gAM4GQ7X(jh#c`Kkq-Uw&B4F$G_ zrsmx@Q>hu#>q2Y>nq*dEr%EA??FEHBPW1;sP^t5B5&-WTiEeQkcS-f@#+4jH$RC{ktqnt4xAeDV0Y|DJ^8sf2Yb3A{! zp_?OouCeSj?CpU9kY!6uaK5!ub;_yRraeC){K8R4L%@Hf+<ht#1Zsy7F%p;eiM7ZalYF=}9ki}7 zWcQq9hh~@Bq7Es`=J)xiCGatVpq`-PUM_fh=n?>4*w`9*=GPZ@SI%n)77awZypLO= zu(wO>@Im@}kyx_axv-8klgUG#+S?kR9x=+RiK2@Ku0zeE0ll>eJlv-#%>Y(>6xs z+$6ja2>lLIcu2TdVZ6+a!syo75qJgds;`Uk;S13t;YL6Opxw%GA1&CU0~8q~m({&? ze!N*qe?0{0XA7`&zw6Oxs@}y1B#h#b2^9oh5DEyAMi!E;VdfWoheATXlcTUth1Jq} ztjCl!F1y&s7`Pp~w%kIo$Jv460{U1*O>;}NlG~ErVyRFjwa|Lfcu*~PRs2gh7wSu zCtyiRQ!s#q!`Jk!;K<*Ts>T|_Eh2|@JxvGt#*4#l@V`#Pi)~Yy+qo>v?d|T3sh0V- z0)e`ZrAbQG4S1KP*^Uy11=M6ezA@XhxCLj#t^ zZvPGPMIxmY|6ik67vAB)q`_Lv7Qx%2?|eC-kW!&}Ga@yP@L8gmBZi)FN z1M0kvL<5U6U8(k2k{qITkVra(7Cs5exCrjWC4)^pPyDA7SFPR>!RBA1#@XnZU*^sY zBuEqiOi2ui99$W0K9HX7=(Ehr&qz~uKlbe6Abj)x=DEDau`gj zR2qx=!`&ja*=eKV7Bfro5*_DDuCw%LRY27CRLA+%wLv-a$1CH!_@3UaoArd9If@!$OuOV4^FEh5Mjm`UUDwmT~Mt zeEV6t4=f0zx&5&Dkc-vmy#WG)6GyuzwW%FvRYsJb3@8^HG$h4C63JBrYu{RiX(Ya} zmZcj=yn;M)RZ2ColPz^19<_3vYB6B-TMtva<(8cU2wIu(^!2#{42Y1X<&UZDK}~%TmQ^ zJC-Xbg$kYk*Mr(jS1-N$X*3ld1}u4PP+6hC4vZd$=?1M5A2mTs)n|<+JlK4Jvt(#UmWO5u_Tn zR_M~bV51XzH|D?;0{t^kAwW^=j&R|;C;ao^ax_n`9YNU~tfxNr+r@QeRh89Nm^(_+ z5XCb%gO0jxb8$x1@j|mZ%FEL!mk1ocyb3iAq<}9s<7`6Y!sg-!;Mv*az%0pQDwb!J9!XxF6ITXEpr#Ym?rMP~ zII>sZ8{IkRCH_=P{au^r#xCcH!?uab*4EK1jfa{etNmIITV2?W;f+HmWM_y<^}GJ@ z9dGrsuyCl=sI}wHS=G@<(lI8M%Zilj<-e1uo|kL;ff30XrKqTG|5%n`T9*3Z+r8h- z&6u5mHB{-STTmt;eyLP)cr%7UdBSzm+?kIgZ}^>x)uv1R+hH|MW}&dG&0Z>VVG-L^ z>L+!2tpr3!>Ty1`?dxX@o zWa$VATUw!*p7+RXTwg+B?ya%m;aT%q7v^Vltj|4*F1~Z0i!Z_eQh{Xil2SRr4%_#M ztBY=`2N$xeq(T_&x_=`~Het2Rj+p$y?as$}yYp2`zeN4SS-`Kx(uSVG?0akFsdEx4 zqGwurv|JgX#;tek%O1||aSqCa2hsC#W&fzQ?lm=ZGDtCfq|0mf2qpt_r?j7XKo@3V zc3{8Sv)k!I(&|0r%Lp+YE@Iy)u~!a2iE8|?7fp(2IeU8=uze)7_+HV_)IrF|kR(BW z&9yC89UN@D$_bgIB!%FZwqj}9lLEGon6c@*%K2t);*_6`>{oS&M%Kn8vef%h=GpN* zUiSXnIkZ2nTOOjMM(}G{vD_0X3BF$*Bf=pVaFs;r+Qjy8*%Xz|kE=GrLX?8$`39Ea znlqD#;yAeYjW4$YiGvBOCw`()`*C)hWAQ$7*4PJrp;+Z7BtsN+!<-A-A+Zn~(dIQErlb6E!VV8Hc^Je0V~+egUrJoYg+QyE)R~Tn@TU zH;NNpa5HWhhH2FeNg@ox9q{X$-}D;X%0S+%?-X-EYk{=SWg@@Jun#xlK^ zM>eMA`_FNUQS%3O4GfUmZBDBYRR+`6S4I+dVcwXNhY{2EO*@@~x=@R|yMiL!%2g}< zz3)7>DO^#{&GwLtDl_q5zV&Cke9STF!L2$@3at~Q$T-92v*QmQ zlwT4_z6lwT=W}!N3x|;*i?e&J9+C_{(DsY9l1%PfTG%zO6M}+pAS}c{sSBk6$07B; zJ~8(s(cLz4w0k^dQpa>rP3pl%X@uK8ju(q)@AoVw+fIe-nyIEh1{PjtO{gU9%so4u zUaGKrZ?Eljxn)e=VV-;lNm$CruroM%vffGm+p^$;=4v#0wna`{;v#eCnVXfG5xs1ET2XY`Dnk`T_f_=hJ_EbHb2Myo6dMFKuts) zHrSl_eI4DJ*b0J-UnAqPXS5z_$97M2)8eEmy&g0*_3-c=n{QXW8L}cblYa2tr+F7Y zGLhqDM1-6;>&c2VCtuu!LfOzkj6u_~b@o@nrPh!R7tbl8wrQtrj(xA~b60A}xo+%+ zgapo}#T@6^mPAt{dS-F`J5cu~lW+305P*!0*XD2RJvGE40G! zYzr#bWGGu`jLhu!$?Od64j4wo zC~jr6X4_b4dB3LTt*6q#AtuRmSAsJGgWRtEv$VqZh%yF0yidx(g>;^+-9s$UBjWD^RoS!q&|BBe$N!Y@^ z_PmLctFSI(ie^u7X+xJDkg>)?3UFjqE$b?P;T4w8Z45WzF|Ds zc&HN;wsu>AHmqf(4=0T*`hmE{9larHn-}Zn7|5do2c^;llHiKmP4OWQ$rO^2K9b?V zxRoTeT}`@HZPkershPXZXmLsd%lqzyjGUudIN$PlwPW~VE5kRgSi2j{T~^I~W8IOg znf!#>u8gFp-_TL8T|xS>KRzuI-qRCXvb5xaTcVxqjSaQe;zpad7=*tOUX+Q{~_~&{vgq+`&gwMB{ zmEIXP7UCx5C%_zqvBT97Sm{Tp@@WQsTR#8XFjTf@aomWlp_%vTt z`>jBfhNl2y(yCy;s-jx-{Y_-~>8aVvDO>rpZYn8s|4)l)rRH4XoThc_eZePSUp^@E zpwd95!c=goo$Jo>a>31+4l2JKxTYffx-bTgHYQ!WcH)EEczT)2sE3pGUsT-^Q}j_h z23wl_7DFe81HbSZ)?jTCF*hH8b9vO9B9E$-$m-^Gx0`HnK?!*ls=VU@w$9h{M@foC zP~9VbEZwpmE&?iZ%DE^WhtKN1>V>W8S97u#4tej>5vKJ?6=p`#6XF9*>bE8@v>xjg z2XQEpPE)2zRH=umV~p|_$Tedq*^zZfT1YUjUzc=H=KfY~n7cc?b-Gm#0n2dL01W!` z%HG`WVO2Xv>IFhZfpOJ=v-_oBuX^DEhHb@ zyl_`Ohe7jZOUsdkhv|>-hcv#NW33~J^H8-+)YgT3*g*5_t)FciWvHfqGe)9VE%Vum zxxLH7g-fdDlh<9!LSF@ucahgol7(^x1P)pB*8=aN<_!nWz}m%tVv2HE|IwvK^MSXz z$sS6)C8@+)r9p^Lp4GqGwg6YYX{o*Jgt+mA{!P#(D&rHvPK770XI^kIPlC#(ZAM1o z*VX16$EWY}KChpv>w!v&I@mQ^+9j8u87Y?gUW`-a{y zT~JhL{3q0HbN{_4qfaAHDY2BW?SJmCMSH&7!KjIptVR-b9UJAw6|I#e>nGC%I=Iu& zswD{;7vI&)H6i~UtF9=QM$ z`xXwBT7ph6URX*nQ`uOTQZGsSiaW`*J}6Y?WaGP{A}>Nh(7{uc;-O))SQEJvePh;c z$RkU4Hq(-uQ%IET?t5<5+d>~t7Pghc_{U_z`EI-)F4YnYY@TNjyq$PCG}d(``tZf@ zxrQX6*Hs^-VTzm;Oa?rpeQ0S+pk>K?IJ=xjT{c?M?BJs#%expGB4KIFO3{z{ilN_{ zo0qN^FF=O;8KLaNVBQR@S~%6d8I-z4yI71C)-HZ(TimkYu(>|ONnJ9kJZ%0bXXM(u z{@E>83wz=Id|nB2X8}>fVz#1ToN^DH`+b$U0FvVHpC8INbDv(~;)HzDb3|pWG{vy7 z{53;(@RirbY%&UF)=A6}Be%GDxnA?SBb#SYs1*+E%Ew!5h&GvNNiXJj>&Ro0!}2!C z{K-07*SfQ{KnY2`<|Avqk$`HPN3Yjbw{^(^RXNbq1? ze@)6sPaR=!X&`>Be{l3*LSlq0_+uqaQMjLh8fKz#%Q0mwfD;(h(FIxr(#PwQOUg<( z)v``NA|cXvCMSZlt6hNqZVNSibqgQns2dGQMUel~5=s#*+*4wkFWk$r$i+aR5}%N) zplQr1RQ{UXGR)zjHoo34wUOgGCoGE5aTnr7<>DpfWF>f{s0yINo;6JJcDY-Ivs*@) zTPsymN0?&E`p&>IT6u8t^TNFZ>%zjXm>o_AzR1SdxRPo0dE&6&*MEj4fHyv$iS%JA z4D+I%kdt&0sg1Ej78O0nQ6F4G;c>Y%(;ICbo_LuPjcR?%9nO6^YjK#t!!4MCkyJ3> z7v`_l84{!fkL|Mb9YOhnje#OyD`kjNm3QnQtHx(F+ieob4)27i@N7SD*gujK@i@Be zNp*cJI6O!Iq|oBb{hFcyRHRd)6>+{5njQzz!8{S92>;=+5Z%mo71h@rJ!t~(n^^2s zZ5?_D)FB`lf&?XIJ(gsxH1!K9O}K6*oiH-ScTbZqMMOSxLdL;`hcYHni!0~bGMbaC z^Fftln;9co0yJ0Ap4t4gm9n81`j;M;T*& zN4~IS*4dXhs@ZTWcT&Rg&zOB_J<%%`Wv+xc1&^zJN?LcWd+CBVEkombPY4--DzGfunrrHzw)n`!-1%w{^R9m#@|iEe z;dxsYFe#}&4qwYBW`qZkiyD-{51-TUje%On$^3U+7|b$e-pcrFcq=W!ZF`2_Bt8}Q zN|x8+rga8SV1Pu#4UW4|_Q_lIzRlPr4a`zQ%;0?M%I?LDuC)W6)gMH?!7`r%!h0C= zT|K(U%$t+_R_J8&&6FoO_Lvwc?BpeLl)3pmZ(#Ei%)1(D~xVvU$R_EvT z+;W|H`qxlO%FiDAU`^blCOys{um4gn0JXRAu_!-`O3#cIjs_~ya0LD-&c9F= zE#9M`6b4M{)jB@Fnam&UI{oWMPdT&3p%GCpg*LbMM~Umze1y(Tjb|UxGyqn^0_(!} z1(QybCjdACaP9cl#+#$qW(c@6KxEq0Kv9gnQn4LyTWOdYV!dhC(Z@J(0B8iUC@Y}o zMtAtqWPBPCxy8kJ`UMu<|L3~`_QZ6Vq(;Q1TRpYd%jD(dWjk1^y?4H~0s_^qU%y^l zpw2M+sY$@*Ac{*|{f}?3c6d{!Sh2tH=~19&A;bOqg?ha=Nl55ZWO1+j`|C$VEi<=A zjW)@dN5zFauy^pE$>F{d8XDxpY+g?nNf8(S%c+fWb*}yeg8T)CWAO(64MCh>TjcUz zwBx_k-dbFr@8S~zPVt`0OPcEG-n-4l83$r5@=3^0!JWm66_HjX)Ik{^XLrS+t$Yh4 z;Z7P}75h3xa?`6DZtb329NQr7)@NhK(+4Q8lLq^~^x5);Cc*IWBQCEC1-p-8{*&&T zaz@!7H{bAmR+V4N3%13LGOnf^@#>iOu0#0mv{IemcWz$oPuVZ90=gOiJ4QT|7rHWjM*{N} zujnZqwtqVLx#0TLamCN^i_eq{uIJNw`2gl^$o6n->P0nMQ~&U8#Br9_y`V2XTK`~` zlivGfAp4aw>ox|I6!eKa`F#1-UvS58GKu@ULrKUQw8Gd1$GTI|jN)q{WQo?Y9}=b(m( zmoD+Hc{SMO#i@|^RMK;=G;j7*kJDam6Qq!D`CFmN&2SI z=)W)V|85t6B_8}&Hvq2df3ShT8^>fXbsy(H=msc3c;EX3&G+G0dDlw=HhylHKUsHk zfPFt++$7DULh6qxN_e8J?NPg+bR^9Xn)ZFYf!j=5{4KI%kb|0*NJiOc=2|v7yB4VDZy1t30f8ib@f545E1G=y2 zSh(M9b6_0P%hCqI(V!kyI%@xH^=e>18Qu*R0|r*&J@whdzib2=EW0IYrMDLY?MoefeT6CD!Lw30M zns0#9!8P%VJpg=(G~hykb_39N^DAbfI2YZ#G*?;UyLtfsV*tDnPLsrV_j&ElwB}e4 zE#L&O@l`U@+qtiJ9vI#wR%aUzg!vvT#S5U%2W4=WZBPRUR&L8H$aQ;bcsh4YR8;IB z;8QgwM~smTnF@Nxtdod@oYZbJP@|xitvLaHh*iZ@GKIr;;KWHA_Jmjv+D(8KZZ6Sd zn+F;QHpF1TYCjq-^&JPat;qE0I94YlPbQbzlDrRJn-!k>)pCqxOhJRSor9obJT^dt z0W$xkbf>P7bA96YbSKylelfJ){0;~lK}$tWfa_4(E`ddOt#Qp@98~>Q9H?oJ+H7uXE%63pkM>AucB+01)(`b zH|V;^t9|-;WxuhMPA3GH9%msy$kf{Z+J#5ZvHkTGDDf=G4$gt%N55Cw>vg^jP87ha zfG6WUlMhgB;Fx%SE*=JE#EE2+9^Bq)_l{x`k`O1~p{BG;MR75jo27E_R+S9{3T_R? z7Rx;NjMBnuN#(ZNc-~c{5ff_!jC?jWWYGlc+-h6eGWrZmPh{5^>A2XtvUyx7iubDg zy*!A;7{SsFKljwGw4#f(CIW8SFs<^vvLftsa1h2-d0l!D`D10}a6(8ceOPm(prD`> z!2)j`=CyNQ8WC1yc8M#`%qXukx&HHXL9+^L#Fe&uF&D8Vkh>)oN}WF82h_A7B)ue>gs&AmVSj!#X1{w9e4mF66hEMwqs*u z6{Y$4g%}+=cP3yP+y(M9Ky*vdbLKW(=H~aJuNlt#)q}sq@(zFxGx$ztoQR4G3|^M+ z9~F;Q4Ts16R5cxVN-iu*HD9e|1LlM~_Yg zsb_^_N?+1cGC*GcvFjy!w{|kRub-p4!4AG1!BzpM*3VI#;C!*LZ{QnkhkMmieQ$kh zF&E8_#QJ4L(S*LBfLLzlqcVj64sa*m~w&^Q$&uStXrD62H#wpbtzV-#>2F&&kI3~oba`+ z`w%*NcmE8w{J`VcOawPf!9eoMiA$jDnxQ@xGy=*Xu(Y4MA2X%2uy^s+0>2xd>lzFn z>;eIn(s_=>_gV%$Hc}ZzFl*EnwHX~do5Ea!8GBiEc(3v%aS=SC+y@VTa$~GJdh-wx z7%Va$uqYZ^Gx*?F$3~IrQyKmszWM~Q&Qs${>OFvrceEO&9zGf!Yy)e4jg3B>?GTMF zVP7{uxQpEnQ&)gA85cDdMI`FzE2Bh;-O*&Eal6F$j~}^T77z%;ljpk^PG^m461^&m zZ`!f8e$WVTlA@yPHNgVV{w^4>pgLG6T>jr~9`xU55ZF8a|6Pfkie08ZCE9thT?BhJ O;mABzkSG$@_xTSMSeeuS literal 0 HcmV?d00001 diff --git a/doc/source/quickstart/ar2_nw.py b/doc/source/quickstart/ar2_nw.py index 2b5b2c358..bbb91b93e 100644 --- a/doc/source/quickstart/ar2_nw.py +++ b/doc/source/quickstart/ar2_nw.py @@ -5,7 +5,7 @@ nTrials = 50 nSamples = 1500 trls = [] -# 3x3 Adjacency matrix to define coupling +# 2x2 Adjacency matrix to define coupling AdjMat = np.zeros((2, 2)) # coupling 0 -> 1 AdjMat[0, 1] = 0.2 diff --git a/doc/source/quickstart/ar2_signals.png b/doc/source/quickstart/ar2_signals.png index ae5c3063246ac88739224eba408515fc802a6254..007bd1c0c27a323a4efa828ddac92533de0d2170 100644 GIT binary patch literal 59122 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 C{ulQE literal 70998 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 diff --git a/doc/source/quickstart/ar2_specs.png b/doc/source/quickstart/ar2_specs.png index 7a10975b4b9aa2451d2a64f19935269ee8c9ce9f..062cced2f0ac6ca0db848fb8f069c54c4fa59fdd 100644 GIT binary patch literal 26179 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

;&-oZJz$}y{y-q-1WU#A}@9i{&N0k<|ku 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 BEEWI& literal 31102 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 Lno3zWOrQM^;e2R- diff --git a/doc/source/quickstart/damped_signals.png b/doc/source/quickstart/damped_signals.png new file mode 100644 index 0000000000000000000000000000000000000000..8f5a81794749c1c3b4cd4629ccfb031a08a1bfe9 GIT binary patch literal 60586 zcmcG#RZv{f7A*?FH9!dN!8N!BcXxLS?j9^i2<~nPE&+l&1Z&)ayL1{28l-^+-sYV1 zUe)`&AGfNzs%!U_wbz<5=9ptetE*Z_VVFRaP;pOJ!>g8l_N#$eX;c4&c!p+Xh&dEw;=jG+*Da66y{QsT6 z?&|T8W7*=e8@LIYo4kQ19NcS*moI#USh+nM++RWkX$ft=yuaPPZDe{u&(8%LE2geX zY|MHaCLXT%}>E@)qIurloL^Nu^I&GjG zKZ2O1uZ`b`Hgflh*-ouwudD>zo_6aA)$Pw(ZLx8G^!+$>mv>w4K5DFp9NuUCo(K~e z9QTWB3E@9Kfg?PxZDE|iQQ&8u#Cve!|IWb-BM$k`!S_TTaVDTvFBgXQ351)2DF1sK z5HTY6(|@mfKSh;wG5p^fx+C|+iT(FUaoni?7jGS0i=v(&Pa7sj8H|`H+VD0}?9OlY z-|d#KSXf`^{VF5hMt9{d=du6m{|)!Bzf0%I=VUeV<$`2+{2ld*gS&^bPD;3+7YS?i zFJG5Oq4m3Hwe%0RAI@fJ#lU`tbE^PE2WF*0@vfk4$W-zpcZRcoj*MC24VkiPVRmI!x-Zfc>h(N zwupNdb**4?@#pRTo{;A%R*#L0q1GL5LkmdSB2fcH2sn zPg4Ah{hA_s^!$Gdx0i=>cyn@g#vdet7j143y?^ANIIt5suv6ih^uN{rmxNRayXH8P zdrvgDE$&XX^~I5&=kJIA{aa4@ZxhP$Hqm!!Ya{+-4c(x_h;k(+-q)!ll-I}2#XC|mqQOPE*4^aiAfis+6VS)A)reU39#sBKuXjO{kGC6Y8|`%`ept7` zcc%C287TbVr-#$w&F}}WVYb@>#h*g|GfD!Xe~ZxE#Q)b`L6RRgJA;V-H>mjV|8v{5 zO+VfW3euRIyz9R{>!G*}d@5+CK$eO$VZoI3{IAgyE#Hsm8b6;N?*|aY`76`n^*Oh$ z$81hfi;)VkYZBG4lCqevLPkNxjh1~ePFzgn`khrQ z=%R0Ld`+T@3@u*v@eU`^TZundcprM2aM+lmkB&g6$# zF9qNSUv7II0$>-A{rcd?061}4k|D9Dfe$Z(yIXT`ju7eU&iu-%<4NwfPyToC%uLrawU*J9?vM<>JJVtwaZacx06z$d?a#;Gd>!-p*__ z`|3&PUmY9-tY8L8tNB@9ke9@|+X?c9<6@1O9OWAHV(r3Ig=+{T>{9;R3(Wd~*=FL2 z8W5n{Ae&~VcYU8FoPxYqzA(t*08WaDd@;lkgqsN4O-8bZk;kfmAJ>9;mIl7TJO&DP z)-z;udY`;{bWeNFf&*`ARZx+`7lO$#L3uB0Zk1p#yVuX*S}5kw7sIEgQn&SV<%1aB zM?)tatBYlC@5~esJRY7~e^Tj8L`L@Fe?>B-x0wOKx3wE^p`N;hHJ={uk*cPCoHy@e zl0G@rMoAd#I94BCmmfEKtg*FPZghCJ0Y;@8$QI5?`C`VL!!K)<))FaCg~NKcu9ImJ zow2Cv)bZ&JpK&L=o{5i*%30kNtmFFl95&9OE%^H`KDKqCq=`+MVD-PZGzbynh7n<) zzr^-9;+LQ)O@oVxjhUh2H9-Wp_o+!|@r)~bf4t;9Yea35+&s(sB ze0jp_am-hG;{4?pbNjK@f+H%GJ!+%+E5d+fTlO24UBCHml#WM~jM^gF&|Fp2I(Q=6 zz}za@klwLHBHXf85$ksR&S>A6FnTV3?$EIPB*VykXNYhIfqyGPo>uailL!MI7~39s z_yx`;YT}MGjR@dN+M}(#x8%l+76_Z4Hm(m&{`Tzw!KLi=ruIcppE*upx8s{r*kJz4 zA1&pDAl@Yx@%Kb_2g{2!Mp4xqy)g+VIgWLlK_@^+8nE&OPpPd(17={T88IuM8s!*C z8wCT4G0OBcKgX3?5yjo1W=@p;X{Q*Jq$Y^{-Wh}|TMpX3eb^5WC}&%fOWa8yr07fB zypk|pXb0A8;3ai}?2)vdi2fy%WFf$n$harS^#e!a?xvJd8E{7@3k@s(_g0+L7;qzd zk%AOxFNtkWIOL-L9du|Lh*qF5AZG)!D#`ow+rR83se96Mp(!_%{N$#P7<leHFkP2GTI?;ppw7gR(I$@FN>1pw&pWFgBc?5U34ojdXhA`hAY1 z#NAwHkiv!-{TVk=TpHsH)Eqe5`=KGLH^=;Bg8((zm}J)2wl-=OAFrr=?Y7MUHtDSx zVu+tK8f)j?w&z8L3CdPhoRCN>1;V#g0yT)z^I;KL3m99yvCmX|fvtqGp#AJO7~MO# zPrFB56jWP=MlsLVp2P( z`|)R@5MT);#-=aq2(TxhMpV)s_Ja|uCEVuaO7zt~wAG)i4a!`$+BbJpN>Yi`81}p{ z<3Ju;oBQXbAAYbcNlIkc&<>G#Xi~Hru%f-4D!WMUp5FC+7xnRkY{5+g7t|Tj8%ZLR zo@7H@r<(ub9&xR(n+GiZm=>~)&uZOQ%Uz7ksW=#}18EOp zvKUo9FOhm<d4Z{W2x7&K>Sxt>Ru>fm;OJSw1GTn~n3pC9aiqxtS z_U;k4d}?wrE4ud#aypFKv&9gTI<$Q4MiHsm&fRuL~ zVz`r?-K#=G*X|5yBC{Zj zw8AQ!?!7Szyxoq)`mm=wvL}O@f`}c^dKPU>L>Btbkbqif_>H?FKGhhW2WyY@CRe%w zP2M<_QKSUnxJ}xU;>VT?Iqd`olUUDa-Q?`ai&Jzgge)SS+d6#; zAU(Z3PPxE1G6Q{QN2K zH8QW~cUq<8Vs@A(xdUFc=ABZEV4HvEX}xE-i4IGMm}twJ!hk!ytFtrmKs3_AkhTMF zcj{P|*4M7b29X=zldBR;624nExg@F@j}$GPL`8y0I{1>Ti>xjpv;S&E1MhmW*-hFS zbf9Y-w4(C@VXn6Riw@qzOVtWcJHhBhs6vS5(Csma6nL`g>S3cD|DstsHQ))1lDu$gz^uyYD2K z_?%BUr7(qth~8j#mM4MWE~foz0uPg6g3!y%#rB|&K9pk(adll6m`_j5Tf2nAw==_u z3^ZTS4SryG@pOm&WQ5dT51Nt~;_1EZRs0vCInw9IO%%cw>n6Zwpkt{_n+fvbQW2cyo;0Cs5LAXr{R1h4|O#kOUG{1&?}8NXE z?|OnYd+EdN$ypGo*xjAlc#rGW&ZGRE=JF>uO1@3^PpPs-VymNV$zRlrCnUcVJuPTa z{cCrZv2gxliL1rXjva+UJXzu)LJY!l#W4I^E4UR|KJQMKeaX);3XusY)SkoIORF;0 z1Jk%5PRC*GKFcPU>mlH$%1Yv&ME7!;sL{|9lH&{O5-!Li#0ix=>D_p_`9NK6S%efVN%P+ig z88(AMWLs|S#{E+3Vqc_m<4Yh1hIZNf^_>DLW1E>>r)9Qu{8f7oBkp8W!9LSSZ zxA&uyXydo|UQ)4ddVgC!bYmTJbhstRa(_zND44YT&Pn+u8&Otk0mXK_!MN$)Z<3U7 z?~0HR_4r=dj_48go`FLN!+dRP1JI~6qS6Nm#1mEPOG}O0LsB9W z=5mnCyU<%X6zCl?E^fn%h2bW8mHQpidkGm-x4YGz%~jtNG?CV2+de$)LMZ~RhXT4P zt0~`*ltQm(7pc$ZqH6yb$a{1n{YbN1pkMpwuagW}Y>u84ZN_K7#F zR!x437x97Bk}mz+FA+mQ%pum7hi(COHxMtr$cU@~O;?OCpVDgCwgk!PUZG~nko&)C zAB_p)KrlAANb45)Xup~e0hGOf zoP+$M>1*o(2GWq6^WfBRfSA*$6_ERQETLyxmA#7C>*acU=IYNp_3!R?mS1Nv@! zIjcD!;N~;PVKA9#of+e4ZS=7D5R!AiDOW!~AI(>GBvk9r9OKIE31h56=RBT+%<9G> zI{~cwf*@VG4rQIXz&@;Ivxz`uvT)V>|0}-_Ec9fQNBUhozYUo#ujI)`*c+#2AS9#B z#Xeedg)w}=KJuxMzVkG=%K<-`=2FON;?JDA@z0}jaX)=Ea*1ZB$o}e>c{x0XhMH2s zjqM{-JTx6SJ1P*r3=OUH;zmNg#ro31p#)lieqU3W>=q#-s|f0%i+W4 zBCK=?96Kz&PU!ifT8={475Qy$Puq#zKT?a4GDG$Z^Vc`j1R@Q3oE-zzvb?iDsR{K`MnsT zkn7FY3e*^dleI>a@G{MaY0XsySRs-lMKUI?s>3CTbN$e@W=Fk+x)Za_IxS2ww|^?j z_XoPtG|5Fvn~&|iLkIIjkG_6C-J`LZaYjpVe|F;h6h2gS<00qAdU{;?u;w=2_#6+! z>uUDp52=Kl1q$2pPKv(9aBbh3JNLdV?t_ql$p)v~3~?v4&!*sex*7WW`J;Gfnkp!2 zByR1x%|^Esz&UXdXCb}Ui`A_WOl=G*pO^VQ>s)7=BN8n&J4$@*ls`e+2QR=fW1w;O zrbI;cH|LMbRw-oLW<4k4k=@Ml8}PvR(mQisK9f6Pvi!j>TOUG&P5lrH1Me~geq>{W z_U{^vW(j3~g=2K-T~_|>4^Od)&2d(t$T_BjMs<%ZvEfWpnZ;%fZNJ=a z6x`W!h70u~20N4+3YBT=57~8mgDrI2rC6h(@jm?=+WB3B@#ncwvw+~ZHnN1#Jb65d zz5KRG`Cfo6u9xk#lkD>)M5lXEf02#|^A#L2O03PNr~&xiEw$&nz#ihnW{RSA%vU~i zxlHudN#n6)pW8w$myMTK!}ZElca9$liW5_#?*-#O(d3N(vWK|)e5(HOU6@!q>dC;Yp?G@TgY)$9rnz2Vag} zc<~GRub>9qwn7D;D`Vy@CXLW>hw!bv3V}@^@T|io)ZCV;)wZLCP3Jzyr8R|4Myr++ zhI%~4mEgNd6%MQIlQnYDlaH5;Fw1I!nzbi{O zdl@)ct}!J?29#JRU=2bYn}j0<3#Lj!Z@l=Teacbt**QRS7Yz=t+?Q~{u6(JY6rxYawMWJ= zF0E=X0{4bGHQ*86 zlSaYWT{mhb^eYN%8!b2eA8kdR_9i<_?h+dKdeGIZHQydCfS9$R2d!=YqIw~0?tKB) z6W}OIJRe;A-7s8Wzv8vw^Ae>i*Yub?i+D|&_-zy?sHyj64zGUMK)m5wfO4p`fXXzR zr09*fw*0#TH;c$REPn$K+Csio^w*@%wBvTZe%sB0%&I-jID`%7t7L8lIuH(y-`c?m zVDlYTU}e2a&(!w6Uh0Ji)0|-YQV+cV(AOVsxdW}9DM>xLl0YYG6#{|^85Nv-Z@{ix zsnSsc;q2vv_Sg4JL!;J+e6dL5U2NQJOh(;V4X<%3JkE4uH3<0v-!INtCK8FywWD6w` zRQOH|@~TVzWCcrsw}1CSGrd4>N8|eklllYg7!M@L%89agN_0 z!DV)2vw!@_AoKY~3EO6lM;Uo0q^Bb3GvF9=V{_mP#d|79T0hF|2|Jx~ao+K*HdmObZzwZSBjhFsI!JoHM&|b^F@+BbvABc^(pgg1=~LcvZ6vg#Px#W*Yr{(7zn{ca!Hau=^CwQnJO` zL&h{EX)0F6`w8xsPUNNB9#VPwuos`qzJKme(ZeR5B(|yPN}Qf1zOU-*;<>{o z7(yy-P_%+QDa93BVhSsVGM*QawI2mprr@Koh$tJ|!n)nMR9uD1F9krUQ4yCakG1f^tHWr+fMUJ z@@Op9+S{Rprl9iXpG3nEKWZIf>xEEjDn$&4y}f~hc{=Jjzmt{72Y>NG|>-y9I^?a>*p&`$Uu%%i&(C z?4jG6%MX6q;mS&Mk=TqxKb1rzA{v;0kD-mE<2on;698zTVZ!b6vfJX;ul8G5!{+*P zH;rgpJeo;-lJPw|+JhdJ>r=oB2`i3eFq;>rsOOu3r*s{5%qbXJNKyw+PosNa)krln=Do^8Zgmweh7rg5F0uKJGlW**l{gwijcLOg6Pz_cd+}>WkE-%WuJ?$hPWSE| zvRw0{O*M5XsD){zC#L>R=rd$f7l<-}d~zCBc4W(55!sS`Gg=j6C)w9f!YOjaTz?SQJB^ z()Y=oFOiMgVT-3Z!beFiOz@p=9AA<9`8U+t)a;Si!X`bXnG)Nwq<-y5$2y#o$EjM@ zFsC{mZvemo$dl(QWNr=wgbh-@7a|#cVLUDj%+?jS?X{((oz;JXIrFuQgM)JsM7@hR zRpkM$rB?ra%K7(=N{|P!-@+)eI)kMYOocVG4_KfbHj_u_0F0{@qe}ki#@Jw{H zvgE!H^;W9;&iinp-zTbSa|Esi85fg}(^e*q{7-%xW?=FU)fZ}=!Dd>2X*7_Y93~+G z>2XtSj3?Rv9En0<33P^nq3&H26hrCcO6N!O>IS7XWB~oodPHMfcbSRrDGxycoZC&` zA1LybOO!~j2P0+D_OglcOQ}c``J!?~_x4WY_lJl{cbdghXyE`TTs|H^SRw+@w19oh zcEcu?IbSGh^WpjTs`T&lkDNabSk`oCV{um8&TF_7<5zZBjs!USEXZUHt24(m?G&t2 zX8$tiQ-u>3$I6|>(@Nb$C0FI3=T^WhEfNCPEocDfCaxFR_aPd?z^fDX0_Czx^mJ&H z+Orh6a_U<-zFBuM*D*cjCXttd3XhK!Qw{ zk%VZ-H$=$usf-gD&#ZpQNun*>w3~PBEri^8X0yV8uevPy+#4L zA+o;?aDL4zJqQ;waf(GS_gw*>NZIql#@eu`R#pw4spEsp5|d zt!O>Lrt@qZhYa|YHC<+^V+;zh#$0CROZ>24`l`55#rY%>Xv1$@OkJxfuF+Q3z~m5^ zGSoQ-iKJ5KC~7PDGd#es3GhkZb`2^#{{C5Y{BgACbsQihSg(<($MAG&qd5($ z)-kdZ$A)0(+{>eZ?SpG!UZ#Wc%!*Q)avq^7N(lpsmerJc`XS7B$~04}gkg6eTXL4fkOG8T_x@t*4ON~JSUTPPQ&^gu z?%3SfrbtMZy~SY*pKz&N;e6NH|r65tLLtTvrsi;I!z&JxmSpc zvZ{iSX*?CJsqYcOFEd9mxDycOzbN3yW7Jx|Nfni!p!`PUpTat)fb^>#*rEK z4B%!WRT*F&`fZw*zQNFb8vP(r9rA)m+1?v`T_a>1eB*F-r!~r$QY3!npTc9SUEf4H z4iK;J9AVK_n9juT+E$^RWsg@=Nw_JVNAAQ+60+A}8(@KZOpDI0+l%-eYq8@FC zGb7tVfLkUeVGl4<9n19M*rzKs?A&bu7TR(ak-VuB=PlQ~_rvPd`vivmHs`y5dqH@r zcMb~}HnW0CI`B=%@a?m=5wa{DNGX=s?A6_6p^ueL4OPm+YTYoF^>U^OEI2XB_3#fn zoPnoZtut-3aDLCbjTf_T33sK&mn@RU^*F;bm8V>Cs115^9dfE1U4Cd=dCL%s23_+*pM9QtYU^?2 z6KC&AVBW+j7GKAcXh-ep$^kgv?INb>PiGZa$xso?R4Z+Wf3WA_8b04!a@2oQDT~Va z)MT|vsj(jEhyc|7n-w6B07&9Qp?KdUI%DqHR|Q5T+DwV^fEr$AaAsWM#6o$B26TRz zo6DAaj5q{4wrWO4{Rvn=XH|$$z9#>jY-=4S^4idva-!0GUF)(5PuZd!OvM(@t9Y+j zP#eO5#o-z?X(>})Ex@P4Z_8PIY}YVPIyfq=Z$fF^RB>zk>vT}j%xJ+q1T9DA2U(}d zq`qkW?$b-4r*NBOnIdtA^b!ooY6d!?K()F9uyY+t06=_hJ#~hz`@70?4w%g1sO&{weAIH7m=wQzcchX# z5U;d)@8*1~XuQ-)0Ow|MM&jo$+!Jy@H+Q>7H_B6gAkFoP_*<3p#+Vr&TJDf!U1n}( zer%sK(H1vxVhb-m7I#wIm^5ZoG54{hBp<~eQv`I0yU7VCodTYC-w3sAKn!!p7}#BE zWYFNVs_{xaVrWsVosxnMO+I1$*j7U5q=t^M^L62nY&x+6%pTMvCX}FB9yJvm8Sk)T z8ix+b5?k=Jgj6~p!qxZ?N~_>nOR{E`DjF20h*}~8y(q3%g(}(4Cb~V!{c?v{VhP~w zhE-+96Z`4oM!|!1Y`B`6T@O=E3CeI2X2j5WG$;2qbhN-P4?;|TDn84k^}4aw;L>&3 zDDe_6eX%JZ$di|~gUaQ~I`;xJrvoIBHpLF>5Xgw&g*nF4#1|Jgj8fdVMsgY(T0gmD z87tf?({3P3AMI3MQYhgsi{0O8S_L?a87S#VtuFw&ZX$-6k-s}yG0Fcx)<6$-hhec^ zAYSM>vP%I;E%n1nI%SMX4uzgk4Y_(;TUtdMcGDQ8AU|hdd4mEXPpxp2EH5oi_(H$I z3}haq6R7=3^wQTi2RHugoXA5W8?Yvu_iJq}y6jH>$^rWF0k zi2%dNL2^d9Bh^mnu}Q!fTE`+LJ^%;vVK~(fVhZMKGtx^;IiG;SZQfqJw$Gs4fzz;u z#auAVXZvv`tA5+q`ObVucM^Ls+N`&2gi^rav658OBC2Ea)5ADj=03e0*+26{ z@lq7lNQ$cmxl@M^RVC|ABG-|OKnb)U9FoHat#27>aPY!3hiAWMuyJWBelI>FOn>#T zdLE%0Rb7!3VtgVXljz7+3ah*}KRHb)O-nfnL}vu~OHR!Cl!fM`o6=8B9lSL>a+XjX z1Dktkl85vqb^oj(jIz?wac%=L^UiW+3kPTx>ZPMYP4H?0n!6w2Nqn9cY3rDrYneP= zZCzKHnoI@3ohq%wkR%0v_kMUamf%q*WPr@uxrSlbk&NT{Hugj6ZZ_gB0ZxfX0h!R( z+>C-)ogN>+<9t*It$i%x0NE2^x+VP8!YcgG*IaGiAkowfK_=&7-hk#pZbwu@hE&7M zaic#sC!P=W^KX7E!{sq!>wugfmo#{mwL)MHjswdYAJy03N2RG*6?0qd;ykc{+<@SGfjp!A{v}z1U(}sx$0}#>6*$EV%~6_%oS+VleW>~r_&xw z)74s1&_pSYoyyr_n?$piH_`8Fp_8{2ziZ>~8Vy+i($0>ahZuz^u{i!C-R;!cC*7U9 z<0nInZw~5DnniF7t^+H5>}0H|?FMXvGj7#rl-4j+N9rkJ^_VZIZGzvnZx%9!V~FuY zzg~Ro`XcYtv^bZNNei?*13u7dH8*Ggc@{Q)T0Yjnr64&>lbJ|=om`LdS9Miw8Fh4T zk)+0uneGth?W6^yGAi2o0Tjt<%^Sy29`Kknch zt8K@8O&Jo-4u^;b5=XB^*Q6*6Jml2>wL_ZO^SWyY9b@=ydk_+nnGV?wla#wD+%}so zAj*onaip_zjDQavaA5OLN$j6_GilPolg}R=1f-eF7I<_AmOH1SRRzi88)wh9U+^7V9B?t{s0c{6 ztnHgsqfqj=sPh3U;fwg7(lyqU3#7l;LSm_o^8g1!r%_bL@jXpaZl=oaPk=K(hk;Lk zqRXIihIzKmfPY=rvzRMSX~3c$0IjpvBU$R{_@47h0Q~sfRCZE?ZW0hTW>&}mhcRri zbcM=F78TlkzRny%I^}8*F?|NBsa4t8IiMB&`8e!nsHML8!<dRl(T zN(vQzn?#hrUqccQpvd!++zY{L) zC?BzI>(MbYQdq;?{;EKxvkaI^lU;gmzsg@ox|8{3w!)-fSc@N;%!AsBjA_OUPHTb+P!x+CWK?8Pv9LMw?eaO7wzboW^89lN#tqF1*ff_rDO znn$b)~cqoRrh5C{7F~IUH+5=E>wAHPGr+7zU5jv z`Dx_eQArm~{>N0FNVIOj@^fPA+*$Y_rXznw*H8?gGRmv=siVeutswmoR+-N)e*s~3 zcuW@D-g)C{uC6s7Nc(mG=m|GX!~$&mK>yr7g7u!#_+HJRAhc3^AxX&*&A0K4w$9l6 z`Qet58BacO1ATuAP8ZZ78G&D-k+-R z#l8g~Gqx-PZj-RZ4my1Tz&?FuaSdB@7?68D#|Q|4xkxjqc)!q8 zzkiL*D8WiFa8urD;dU+n$e(ernJ}c395G&freg1aN*a5fAVVLY(B&+($NhvP8tdS; z!POk4{BG*t8{Ukz8C_F7`mKJoZt46Vujs!FMP=3v?3+EXsnYiAAF-WgD*N=X8A8-* z5M&hba56{lTPOnew=tUDv${V3oe{Dal^qlPV&NMvcC6e|b>SHU$vdzBcD@6rE?m}F z$-&@j{Ip&`3Hc5~KU1p?mQ4QdI(EAM)a?WOEeCx!fm_K&aCoL11tFAI|4LzA#-%#6%PD?v{6?-0N# z{PLquh^oDRBf>BGwKwoW19pD&+S0HXF})~~J>z~T7Z$|G7afr_#G!UNoZE3<-%}IS z19GV+7(zaYjeD1-!ctp_hwRSBVM&u(qM%|Zl&AREVaKi{MS#v(c64nE@s=q71cE6J zPT1Ll8_5$;MTipMM^k7%hZJBoT>)0z4M|CmCk=5QgINHkJ7-j@*Y%uAdIr!vcbD16 z+t`~#CDzk_oN7J-cq-UwmhSo)6DLaRf~=!2-auB>#f>GnnWWX#RP#Qj^M}alLS&#& zZk2=9zsUSvDS=$j>#Zw^4e%dfPe=UXL#LPwFNN4wVLpVDrVG0ZL~0;EhyFLN51wu_ z)vG}8?;NV)mK|+gxAg6EZq~7x$IGk*NFc{gg#&N%mCm=q>%d7Q#W%gK-RU=6`vM8f zA;AtXLCzfZgi^Y#Sc9_vghGH>`Em0GGA;A0gxMdFRJ_%#dIxs)O0A_!V~IeSbuybQ zt34M6tY3i>MI+^4Rl8Qqm!89%IhFM?fmO$GrDY{{zXIrcinZN``)$pM{R)>e?b|AN zAb^?jJ|#Hx=i^mT*YHwAqi5N6mr)~IaEm^<+`mzZF zx_m-u^<^7#g^+kScmCc*{JSacH)TY{yGRj$K+_jYu;sTGwl4V>*`g{=9m{IBs~LE< z+j#EkDiU5uwid}@{PX*Pe`Ah~XfyUaKp1SWQ<@~npDjHmqX9fIRk~gZHlPK_v4yFA zOe?|kAQ!9=L7pf5{qcR65U=|u#rDuJ4I2WpB5#vs0b_uJ`g2Jmkf7yT7iA{bMWYO$I>#}B26ntv=q6*up$ZeV2z{K!z&~t3$D~}7@E|z z$4YM|wp`gYu$}!IloLWzCyR{swpg;V^yJ)+2(QleszgXhQ zvCVEx+dG&!SGu>Z`R@bZz9+@Q%KXIk@qpN+*9;KuF*om7GjrglNMdH!1$U+f^I3i+ z=I>iYGK0{05LwN$*{C-hv3+dZ??QX-dSHSDU!iWUG?EnRdkXRa{UvM}2Ha_z zgFwP>V}%ZgtKRY*P7A^oVwz7y_eB#&3X zcO_%=N&7Bttn6JXp|@<^+*t0HK-ktfE&#sRxMcuxx{s1U72l*Wf^L)Qsp z8}obyBUzE(?9eQPiqqG~vxko=r`&KNrcIcBOFI9~`*c4a**Rp5>uu-^-ER1IaURXD zjFp?D(E3WIcG`mR?Hnb*bVev0KEwy zv&>&x>NnW_sgFUfs(qbG^yBK&8?MM@Y{wlCU7^Lu;D+wkI~HeH5FqG<-adw?i>89! zzdt2ag6&sY`AIUffH`Z0za5VL_UUj~B zW7GNei0+)@aN}OkMdyzNrVxO0b1imn*UBD?EbvvvsvPAT%1)OZ8gcaewnRQd8}jF% z%R;-q$wtZ2D_rM}0wJwk!%+lRYRc6UKV6FHMZ9KFzq|P44Luq<1Ks#OA@z9PJ-9#a zHvXKS3oE{`H+kUUhSX_0SxAl$5VIOG6aRImmg3I^68`nJoz(_9c{ygO=|r5^WT4C9 z&VYXA%)lD%+Xf)qFe|%}MX{%f&cmJe4&QD!+ziQ3RFyhpe)EX-2x-}s1eEbld+BBX zk-z|=kk%HPF$8YvQmG+72Azd1xyiGV|w`4GJ$W&Ay~C=5u89o0M@$CuYY zw;Ogp|G8*?!XYC$8X$9uSmGR-_3hdT;JnDHVJ89wsDyOljIq``ZL~hq_K?_hqkPrZ zAa1myRR`arjY{@O_KjuJ*kIRoCyU!k4OD>G0D7^i*@OPcG7ZKPTi=&yc?rrID{GKg}yUje?c9*#w?m2-`ccYsU6>JF`na`DQ*V ztvF z`arAkM;Zq)O4=C4h@!t1SS`BmH&I9xX9qx#?bkgZinjd?>`1alm{%Y>ob|iA52r0- zN*PzTk|!OQSHSqdfc5z{GE3c~RyRCG(>eL0bLr>MNr0;hQ+N|THNtI$0GFl`dm$1T z25~?_%tZkD*8$|EG7?(c+OC-PZ#yc=LW6nResoRw#Z-}VcJXy;VwWm{r-AmZL;ZGH z>HguamfroE8xLP;waJnL-aozCDYN#hUJqp9Xji(itzkX07)s+uiH&wE$uAj2zY75U zv($>@Uy8~#2V0o^#x3nczrNpE1q_R>*w5mU*2UDvbk;wbQ{N@w=o<07c9ZvIlLXLk ztnX<16W=A9{>R;RMtWXOsc$fA03)m4UWiwpqPwbCd!P0QsNvSjdeE z=NGWOY%b{a?yF0!cLu#3yTgC|vV z*j@k7wcOb|?#lnw%j}CKP;vyYr>P9MRv7F-(uLHeMo}~T!aJ5ad4=hU#A`JgFB9xv z6{K4o12yQ66{nA?mQN#+mfA&R1O=zR%dwEF&A7AEt75kH(Xv2>bUN3+YgKJQS-n7k zo?2CpH)(@2R#x%b!3$$3lJ!G~VmZvlPDF#(#q70fIF$kpPv4bjuGG?~lo5EcT0=e# zb5P39)(MW%#2r^4Vc-pqb2g1rs)b!e;JAQMQCDm0Xp&b^X|>##~s6I*bL-Q#KskGlDX zH(&*nVa8vY@RRuVIAk_L4CN~`710C+-8>EafuN+@nai9B0fryiaTb3>$;tB-<6-Wj zS>$wHP&4~fcHEZ!xjtvXd~lb8dar?oH8(y)?d(OJ^3yG&A)3h0GN*w}Ru`}~TE6(K zr0*`9p%zn@Yk5GRP`1XC;e8X2#x6TX7YNj&mkr9(;GL5LAHvmI_(+w?sMf>NdXG}m zrurT1Y8^tYT&{{nLn#v@^Um=@wxj7sAUy(`NBs28j*9-V>nwd!OvmG^?I40XPuN^& zQi0%l*D}2oJLJ=N8o9pubN;=9```X6EwJm{M(+9KSzxsV!^@`9-A3P_7K>qFtz`gp z0(|z|5GEs02soP-;PvO8!Vxv+xl;*a(LQ(|ELnxQ+p&Q?H%Vj{X^%auMV3Frt{RZ)ZSo-F2dxSjnO{ z4!olrcn`jx_~yj^41PA+i|Xf}HLpyFrpI91_RrT#GhHp=SB(iO2VgIiMA` z3q%V@8;xsHvUkk~=#Y_U+zYj!mXdg6SPI4_^*_P76(K6VYEBe z*{76)RPpErS_8jbAoU}VfoN&zaxZ|+s0|*Ly+gN<$m-zxP zMLvEfO;QNAsOipVwj1Baky)5H$^8a012yeRFg1Or4YhF0In%}@ zqit^NIHr}s9-}#IE%RHA*c#;O(DDiB%rc}T)6|D=|Gb3uEl>_((VHT@BW%9yv#n>| zd~ZgY02*Tnu>~|z!VkqP9Q^~sTQmNId)z;!wnXSEeO4LYo}nb^3t$9`c_0EvXtAQE z9+kZrDRtMSrn2fayTdUO%8VusA~!_M%$mF6(^R#)v&|(9`S7YZ1GZh&<31f)vT+&X z4#Ioiadj{U*JpyxYDRi>Cdq2$c}@rUWFpBS=W3nJIN=dfHMn<&SuT?A*eOp`7uqmH zaC8h6{ryN)ON)+fA-}z?G!p2<)99*t?9+SOl5{qtrcxr)nv+JkD*^d-m&tz2Zxwq(dYG=|*XgknZm8?pC^`ySrQZ&dQUZJB!C_s_~hPleO8|hmS5ui4^Al!~~&=7TqrD=SXA& zYt!m5t9*@~LVQUvX*M4u67(2bLs{e-eA*CEy&Wp@O??GC_>HUZT%hx;_1VL(+S3ll zZHuS&NfBCxP&3k0en6+WZ0|K>U=79d`o>+leO)2*_M>JVQ4FQjt1TQVzBn0hNScwr zTw6Aj{R6!qKzG(4ew0++z9ngx6drk&qvdiTGpnx9;pxHZE#A^|I#&y3^8h)=8-j>J zT+?*I@Yo@gGYF&X)r9fwrewLjDL3knp7iu)#X?|=d+I{wuuo+}SEQVe4B(ES4CMCa zj<|th3~@bnxtcg}fjdJof$q*rw5&kjxmsF|i_iGw)>CW0<`IT!_9yBu*}wP;`5mnC zs>a+B&T^oNgSkyxmy!ei=!;L^h6yXQdQV$ZrM)33@xj$ofGV(JyVa=O?5C9vC!(zu zgwzjm@~-td`;d9vF3XXb^%+4|OSdSMUjDDIj^xzF4lZhbvAKT~jg+VI)N$&S!u*%R zW&mR_WvZ#_!^VZm*Z^MtibcSJl40}*1T*+gwF0a{%R@li$g0o$FhuJYUeKv0GtITu zaN?XNmHJW`tWy9d=o9DqDd;1CvZkCVk(Hjtw5<#5vn!CCbH=d?hV;sNE1PB5tEu2k zQHHWl^!jN=aX9Fi5Lv|H4bhQU5Zn5Z+Y&=oHw9&fD9TNpEb-h-P=}0fJk4>Q7^A){ z&4@&5x>GfA@Xykf)V5+M{ghcBta0faJt~j!dH}5ox5XPY^D^rrSi-7EJT&=tjXdX2 zJ?(uH$r`OnPZMDs=`X)2XfNxo2g|=36Oie6V7f=V5o#+pZb|;0c4Smk(CU$XaPuX! zC2xw--;p8fSF=xV3;XY@MS$^Btv{>@qkCwGbv6|m|6r02wUHs@M5Ns7+Vg#HglMVo zkTAY_V4v)K%kQuQ_i)o+={}sd(fk;Qw5wz@yfsmkI!M)Yvw=(i)A zESXJ*#hffYam6~SC5CR+QDIAGs1j8I*?~D;gX?;W+r7%9X~Z41v6DhsLzL~ECI0vM zdl61k(<<4Mt3&bVH6VD>Qt4Il(iS}Ic$IGLNvxW`$b}eFbnUNZBw@uR7}Q5;!bmDz z0?lsnZpqQq0DP_5D|Si^i$$}|=a(rsgE3`JE`ZIjHRffday)VRHJHec?0UjReX65* zGK9})j*MU+UL>UE{4f6;gX#`44ScG^inaaqbIF!u;mpnjKwW8Wu2h>1P)Rre5N^13 zg>jGi9uh7=&VnPZ=EmWk{`;O5c8)4@?JUze`^2#8)6(;7!1J1}qmUi_9RJ$!2;cUv z{|*+_=)^v?B}xuP0~eIZB7*KkxTgmtXHfm~%zsXZK(;NcJeW6g@S92xE{M=TQU}5d zmV!W2H3JjV9wd(|U!hV_4^1TPG47_|Sp~a$deq>#w1N~7;lVYL@ds3%VsZP~k9IvI zSMW(zBRj?-kv zot!)5ppI6cw{6EXACkT#SJ3<^44ZxIMYsWhkY@7h0&B~m#8O)Nt3oLRi|kpzL$i;# z_x$%3FvtEZCU?X@R`%K$)J9T_v#0}!t#PGd#k}`yav>@^b*ReOtWN&T%t)nO-{|7o{N{c_IGu_b0{-+DM%&!K5k5IL znm&_}(HL!%?s@BsM3#1<<)6k|Y?ED-wW#iwbe2x-;5V>F4(Oa5bY6+-+g9S#OhAyJ zbRdc?vS2nv7*FGe61Bka4LOhkQ-V;9AfNN$@`~tcvNjXWzx-2jQso~En)sK9e}=|P zM91GOK@4NtiU}EQcYEa!UDp8*8mOhRq z_d2!fUGjK?Z%#Z|)Eq~Nwt8nsi%Gxm%2Xv<4tp-t)(#x=yU$$(D(uFI{5NS9d8oG511lKOhPI;i6#WZ18>52RP7r-R`7BVBL32O zK=vbgsgg(}GqBzF2>Rrn7a@AxZC;7CEFBw55Lpax*k96Oq%Y`*;GL@dyle{pP~z1$+r1w}6|E@9NCW=c|dn974|>$kL&z%FF0`aQMhptc&=mBO5>g8qW@ z{bvousDD!k>e_=z%fCP?Y_RyRHvE$RX9jh+Q)wBzX2bl1n4Em!h#6~^R&q7hIrB3> zpE^mm;PMnDaYmi>*FiI4wM7p*5nuB`T+0nE_w~nG92VNPO<+F8eb(>LD(J9xB%BL_ zuCB}Ruly6qQn*`gvFkP)hY$*06QzRMxSpY(e0{KFDFW7rkyV<_>Kq8Ra~Bj1pR})r zIg(*Olp@L_PJI5+8j{RXuLHSK<84_{+JnpDx{S zjp`F_Hq^71@YkzWZQUi0-zrxZU$@A6y_Z?aPx0{TRQd(Sxd?6VSWJT>n!1B_&>F`x zFIE4VI2F$HFM_2;c_xSDW2x7YulCdmP*vhRHXQ6zEpB{I@2l8I4r!YHBD8z4!Ho|3 z77J)~ZFM#4gCsP&j>4Kt<5dGAA=?I%zMCNp0Y?-f<&;n zJOKB0vGjm#!7X!FmEmdRZWN(QG;o;T^vE+{o@lQ(=^R?dByPyi2byA2@73YT45 z<8%_8sWk1uUT4tA6IG5_h6kFFQscm+31HDOv3mimXuyRwxqo5A{-#&U_txib91NcF zobc%Iy$(RhyDV+F>lo*hy*d?zqUCjT-Z5Uf#gq?K`;I(;a2pfae=NWMVPjykiLXhegKwJdR6XM`TaWA64tDy>$XFK}!|2s79me z=ffFdu{a5K@q6e|U^Rvc6>0XcaB$cXJ1T8d19}45c4m*MvwBY($>;e$mJJz>=TDJ< zD~|n;32*GXae~r|K#ZR8)e~5Fb*)Cl)$hVX4i`SUFPP!E`3V`cOY&APNZf6`U;Ec7 zS=Y8XM}+ihF6^V=;5xB2RS#>HoF>0^6h$al-TW+_A}AdH%D}kxeTfoD~+d>+Fzsh4-&|DJh9l@}wYUPW|$WJr@jaG8JJ~YfuRk>Ya-j z=xhn&Zmsik8+5n+qi>#Xp?b;^`9%Dhc$JC>^}Uvb5uYWo@r>)$!LDf{ay3jE3pC|G zly;0R;wQ&8xW<|K1w4mL$+C>u37E(TZhv^%q@mmH&fUH3U*@Uv%)oIW)u7pQ(?%6o`lJQIJN+xOBVW^22cF46Nw#hX&?h-f_C|29|E>r=W-h zFDZyyX;WcIRN8SftwL7-xSY{v)Va!>Wa;Fvh2TokNsKN${L5xEtM+xn?8w|nUmP>n zDMAeG{(07F(lr+uS+wlk`vgBjW z?*@`9tZ#Mduix1GpqbIv-Cd9*!Wpa&(0OWvB*I(rERXy-VZGjUlq51EOOS;+)GsV* z4RCbxkZr&id_5kGG0_>HmxJ<7Z^JHtau`1{#6h(NJH#;FU2WX*Mi)+b^z1~k2T{S} z?1R2+*rj5630Z_3UH^A08WEVP)DC4IF@xLMj|IT&Y!$=@qxQmQA5LwEU~Uipmoxb# z<3xe@WkvJDB+e3pWk!Mh6Cp|X%s2mN_(PE@(eb!xETtuJaAu=buYq047xH4ORP}Jq z5G*gMVb-F0+PxG~XlIq*``<4=9sT1V6KWdft&&bJl!@$Atk&5!F9prDNfNhW7&dU6*NXkEy~? z+4ob@NEuurKZK3y{)2D*1>VE8bL_nBiW8br0Cg;&DVg7KjVxLp9CrK}$8TFsQtFM) zYOEv@3S=m2(@!f+DclJzk@QMU^JH;lS1@BW+|uqvMH-!|aIIHHBu#V1i$X zRTF{=zNg0bzCq5V#ad4$qlkWZuq;my=$K)1I1j?u1DYs(&2r%%i9=)~~2k$Bci1(rygzj2DXE?Q*uZ5q<8|}d~g!<9y1X!z# z-w^)wRotr1J26|kn87vX*!3tizBQc*n)U7htqJ&$|0`p)P$0_})_UGwWd0KsD`FG2 zZ+S0-!W|v3s`@I=?bNlft75_CE$!xa)pc^8c9L4{ADeFtn`TrK_NB(8A_evtwH);R zejkmXN-RwkUWQo8dn(iDYzjxnV4s>ve^dW^GCA(_mKDejnY0$i-Is7fP~zu}bI~&x zVZGsC`_yb8V;DDL?$%6RRw$B1%7+Zs{KJe2E~O0drBO>l0!;#HvmT%Y{2U~gnKZ`( z^E98W00%^ZHjR&*3cao$_N{kldR#>krYjlM*>ehnm2wpw#CmhrJkz6Z$&=f~z`9@2Bys&M?pfWWux(HBA;W(3NtI?`! z{;FLqBhN_1<_b>ZLS9OZsWWs{-)jG5q^x><-gy6B!~WcVlq;>~A5}_2lvoJs7ijQu zIe$2PNDox$Se1&_bLzMsKjypId=M~P+ZTxlP5H=3C+AiID70qC8oU(32p$^08JMl^Nx;KBZiDrv zj1`5ijomB>oi*Q44ZCF!?e!?iO|TEE@8+?LUNrIm3pSu*E2<3_6jJz906i)E1%U(^ zaZhfXtx0%?M;kO^@C zAt0mpuWr&)dYeNJ)=2=_Q`&60xu){Cd-^Xial$Go+&f?@1rN_?VlU3<=y814hXfaX z(!@EZ(Wv;Grb=&374Zy(#o|4UgvVSZFP89iG*TdEah|TUnbOtrk%9KYikiwTkL6t{ zBahKG0Ye0zP-KB&g9wttmL<>R>qH=^>5ZMkY2~nmhDq1`t^Ij!$Bw?*`Ip%GpRNXL zbYz0c$3m8eNE!#o&H9g298XettB-`_tLs3G2pAU}_EIL>ae){LNs?tHzHQjw5(YLn z2zmOv;(kW3Cj^bq0RylB&N~9Ir@Q*YCQ!JF#PT4<4%N$7&QonnP}GgH|9~k}%_Gn0 zEkac6@s{-~xIyJmN&3M0+jIFLjWxURJO4=m{hS6>>mzREnEFyNn=`JcFr23tw;yMfiWAoJWe8#$X^t!j^5)Zl*emkapSo>=^Z&osCP zW%b%#gT+C6u^mGe{-;;jqhA{lE_Ip~Lj<%h)u>`Rz~as3@CIdiD>MQqrwcW`6fy-w zO^CnrhmT1CGzIiX4$PmvmbWDt@M1F|i|KBtX+C#p!@*mU9DVMpniCJbz@&KMIu=<( zEF%>sswH%``{yjfGadTpfQMuvOnz`4n5$PgzL~Qj*bvb+U^*c2DY(`-qjj7pYQ|8Z zu1v*EjXs%HD6YTi^-z z46e771|S?ybR;i|px@tx+EB1ljlWPtTWgPu=^UK4c@ROTkGES_;ri7=|141bv8qHgIHdWM>9`~Gm`htqB<;B+pesojpMj+F*q7nTZb)yu!DRkPIIhZeMhuK z<9+?lxcJ(jdi}=E7l<>WHUqz2Gg?j~idLUB0bfKyaB~9Sd)meEX-CpK%jALKFHHMt zL5)56>-c6tt($S|84+rerGr+YEvDFcsPlCV(L(UGJ0iHf1D&?>xXZ=zoUi4z6E|+s zEGLsZSr}h)F@8j3K*xL7k1}IUd6le8APKrh0P7Umf)xfH96RGr`nSudd9pscU8i7( zFqd19{5~$q>N{Gc^ju*}7bB{xd>dbQq5fI8)p1>P|1W0B`Rl(tmkHb#VB2|=QMh?K z-oI$iU9?2>^sEHm1!kB!APUycH8$|@UETk~yc@_(egYABKYG6c?dq@$7P97^Y+-+T znLB!Mp2_kZC5(a*7ti%QulvU6RS!Iop&_EhEbNn zF^)#Cw+qmA%e9tdd61+uehtn9n4WJs80LrwfCuAC#@&?CW3!OAUrc(5_i%M*b*og@ z?R=~-S~GMrNGkTT>?l8PQ=KAySu=uW5B=%-V@;f-K(PRfw6OUlV}x2XByw6E9JFQ^ zWbl575&HEFM*d}eZBjbeMNyVUIZS2r0M{So1t_A&&d&qFm>D^6gmt&AdV!IlS5Llr z(tVsK;v&W|5y?L0HFT6Orre|hy7wcdj1#J}Xb zD0h01Moq!x2Nv`}w*y9&s))tgqaH7Qg~W1AF5hyrko@qnyNfIp(10ma!y0ICY_4#< zz5$Id#A*K4!L|nFso{bPI>6PWFH?|2=F6588!8*HZS48_LpmmwF}aC4KR~eky=3gp z6bZ`>?=#fmyzjwM}b+{ z9i~cchQTI#v(J}2rAAOTd5?d@Y$I225yibtQr>ciGj|e8@T5)NCqfUYz5Y5141d}1 zvGNtn@p4-vuinN}INTEAiAAs5->)@Zt#l_7THj&zD}1aTuy?Xtw;f_Q0wdj=rz4YY zn)2ERBg@`*wd+N&ngjfE7>Q+3H9;SI;sm~*Z+d`gWOnJv)|ySjWg#admbll116N69 zC=yOE+36w@sB5{>hLuewKGCZJAImRE((B&<7tq`K9IY{r&1-qDQXr!zm^+T?-D-} z0H@5+t3J(R-(J*I1Uk+f9LaCk(Y)y`G*SNWep?Tv#d_0u%C_*#2e5&`b{yCwJOD3evUN1sayd}ZoU3CeI8H#WyZqCh#$`Ht*BWfl!~eCLKX5ks5FhwAE0;7 zh7kUQ6254k!|vkoQk!ispbh$A=0)4!FxQ|DX{_y3V3S`fPrT*O6?>L-$WFhQJh8eO zV{$*dLn0^hnL@#O^#_R-*>d=kceL|mQ5h=GD!UKt!{NL*8I$uyUVT_`Gxcd76>Fo* zEDyw}`y;K?WOpo6*E1Wue0ZB4C$w}f$baYzU0U8#cC#NksBd4ygVW9W6eOVI>N|M~ z?ticirDpqCcfJWG8o8*$7sx}|YWodBtYMAE8jUXt+B~!rOlvP5qgHuRE85$ARNX4U z7ljxqg>IIOhf_fHREO|de2~;!87n`|2ONR21(edXH_N3b$#~sD8o$apQheRd5fwzH zSPJ_OdYq|6rNk4I{w2EybbZeCeJ4*drOo_s2aG=KNAot=VF9f^yZ;V#&>eXV{i~3i z|L9BG-KCz#&h$7CA9VipEYnP{oth2S%lzcu?f!~D6&;AKwP^W{dt&FRR6^k(Dd zQ9F`89*Qgv;0QWLn-?r5uX6*1?UftCBs z^wiZf^y(Lu)}(~CC0dChuz?UG^d0g@7?)ZSg<9)HuLCY3OQKAo3kLi?qo!6>LYqZm zHQM}#aH?Bt$z%+LA&Nw`4}D(lJb)Pg_tz)uAV_;71C}O*X!%ZJ#PthPweuIhQaY{E zsC^og^Y2P$0p7iPBg7T%c2)%i1aK|mv#AsTjt6v_<$o&@9fj{bmnFUNRS1VOV(1Sv zU1D)8`0+D8m-mV;itxM7jGl2k^F(7;M^f@jrfL{b;CXsaPvSmzVi?)!u>qI4+9ACK zpGVlnF7T6?J<1NzMUY=HpCrU?Y{j92{es>l{_91hS(R$*m66H@NIJkd>7aT65zm@F zEdiq7=O^7f`NpQrt1VVw@PtMp1)MuwDwnCeUc3lj#Cr>^hp21{_gFbijB1V)#Ptaq z*oJS6U;hRg>rqot-N#tz_S?$7Hjw5Q>ZUo@$Dh8=pSFhO*UG=q z4k^HqE>)Gc(`X>JJJe9AUCEd&+QVOe{*zs$&gTnr+oB~f$NMU>-`M30&8`~DK6#Yw zy_7}Gp5HkT0tU08$OLI`)JDIFBy?))vcwVjjMAU}0CobE=2NcbSo~PO_IcW{y}%VP z=?siL6P?H~*T?ZM8`L?1B^!AuY*)qnV*WzNeJZ%9UwuyqpXn6Oxjm>w&#Sh(=pmVOrHXX$%P1 z)EESo?voM?|BnA(cm^ApJvKSdOpD_+7BHD5G#DjBae&3%=$rCLuYD_Jf2Q;6cSNjm zB4jFT`%OcPkNhd|Dh2zz_{PwLNrv z-yI9QDJpI3hIjozsFd^VGPBaxz~5x|hVBJmS@6E(VD|Zu7zS2Cz+SsFPX3@?aee$d zi;T8S2UV5a+C?VAMht%T?xLr?ffdFNVvjd=yBcBbl(gZlFV>Fs9xXr#weLvJPwwp^hqd=r8 zWcaXz!m>GTS}e&|x={1lqWR=97GpV>YMD)ns5;4IcNUh0Mr02xlMUBNnvAZ)eV_cJ zImn6&8{uL0CI60ZAtxNpO-y-@s+ZN4eKqL_Et6)gr&+Y&t%-qvy0VLFPFc_(Q-Zuv zJ(YKrT+Y<{8;vaWI0YbEfcn_FAWym?6fZdV4%jcB~qx^sA-6`*y)#GH{#6CGr|D%O?}b=Ld>kSDf-bC#}? zJhW<-79}H)uq8%xXxC%#BcgN*20_zJR1jmM?Sk}Fz`=%mL8}du2m+Qy`Z75$q&T)% zN#Zwet({rdHoO$i!C>@RilRF+Z}K*eQ{_-Yu_BACR^;-!WHQw|my= z zm0jR~`7%%3k%#r^Iij*KAXP&#iLudm9M8-tu8T_9z$(D<3PrF zsC(g?p<^V|C+Tj3uFfrs7jSuzmH4y=21Z#DoY}+!Faf4cOg-(#=(_4@TSwFU z)w1CswZc10jvwMK3fR5vv4y&DIWw{eo%$-(#g_Z1QfLh~mNW!^7Hz5Zvpq&ov`|KmK1&(fT%dPQp4RhD75@ z`a!f6X^Mnq{35o}IP6Hh%upg&Qdw}sf@drae0v$fw_9uU%;8jTfo%TW4@<4yIx+*) z*^}Ium|XFbM;G(NSA7e^4mOwJX=6%s;8*#E{PmAmC?>8-{O7bn^dnq4_pxsklx2rZ}%qgWpfTK zOO0wh1o!j5%S5|<(JCkOdM7=eO3nh&T5B@X9DQvUq9sT{rE&h-4euoPGUwYd;0?%u zUtjn!K^87!oThdWexn+A9*-J!gL=2O~*%kt;YHMOC^U63=P&uqT%)ws8@vFTRh zRddDN5}n`9b^1qiS%$7pi44mVS<-!@3qDzkQ%?&wGidZPxP5S9pwKo?H6x#>^R6Yr z8F>^I`>5>Lb?zmXv2-aY%NSK($}6)s>(!;{2<0kY*fE`Cey&l7b;JHCFK=nVtopea zJ4o#`jQI~oaRFeZ=Egwhc?bM}fD`auhU#a3Qz&tK3sfl&VO82e!)9;-{oO{SK*vcF zXrf}OTVLWeaaE0x<OsbmNF8a&feWZzRNI8t?kzz6OD3`u2e}~9We-=W2tvR z_5MFcO?Y^F{qMieDVEON+>CC22%8@M7XJZ?5m7qpW=oezJX`@(P=2vXy0J&P`1IlcLaS<%CFa`Di5GTFonX; zGqg{^LrGS!c%E(j;dsXHpV>(6S4#XjL-nZ#47-gCV4@=9R))_%QkDTv=(#_?C z&0@_)_h>6RDM9AVxcuzafcXPXr4n=v$gGTM~Hs-*MYS+4fx z2z&={d6QxA^Hg1^>HXorpWj``CTz~Sf7PaOG|jR+>YDD-3w&1Kmcm76#GF-}RK>c5 zfcIqF=7d#TBy%O9#+eMi4|Oz-9%>n!l||C zrp)}X`5bH5O!+}u)mJRFu&+yiujiU>0+YJlMnN*D;a@nip1m^YIN^j~4x7Z47{-|q~-dg>D>Aw2a zzVI~HT!OeH?HbwA*qNz?Z|jz>`yhJjPgQ3dk>2BuPZSANzAeOK-s{+H3t}QWz>GH! zSvVan{?aJk>O-~ZQJgmN@mQ?zNoJ&rDP?UGAt)rDKP%{16z*6FE4v#wUWX&pYpc~5 zaaYy(y&BWqy?%E1@Zh_*(&1GUV#5APD`>%B)PL#ksO;|ha1plWmsSSrWl~33kVZIU zBdgDaw*bfivx^0fkP$`hi(=$ z8kaANO69N9S%Hts=%bx@@o478XYm{;x^|CHl(h6JOt z6XK0eA25Li%dV@>_j4Uc^j8hmER1ko+!SG5W&+TK1zBnrtx&WTbQo4{6qS8WJO`cTo&5ZgkQW z{;e+`Ptn?D!liYsAA-9rdlltg<29Xg>}T**O;2BW$*pPvT2;mN*1qel<&b|R{JxK8 z>%H!1mjUc}Zw~HVi}ZBQ2h$fJi37lf^|ytg79Qb$pQf4Qei-HJ#8*K$ft&f5SE*Zck&7SkkNSOa}UxHz##Vzt-x3 zPXO?SRR1CPCQz}x-i-sex`8aAN%;)mI0IA`F|)n->YFy*8fdm$gC$X|-)gGn?V}D3 z(ZJ6+>XQ&b=;OZ6v0XK#^E+kV91oo}wQDutT_uM%NcR~X5gnl%aF~0j>NLOcAZW}V z)dp1aMvNO<%_$Y{2L~LqEWT$-Pku5$EHT%(wd%DpI}!iAXCjgfJMQO#wM)bv>((v$ef4kc=*6V!>|KKq zI_uvp4!5lM8zsuzSqh{fGvOvHxBy2PIhP-EmyYT@9Ty;rz&nSj#oRkW-@BXMvT?hO zerz^%*ltmUCK?6rxZBQhkIYi2}GXbRSk@i-C-};ucN8AT3e_fM)e!IpTWdy0JJ#Q zVZ{YGU$%o8HQ8DJ>moo20VHWXn=2aC+r@Ox6=86AKAiWIjA&DNGvjaAv##5TwVNGl zIPtvoI z79TYa%q(98^;Hpv^?&s|dGwgsJ-nvNuY=2-htO}vu*AHbhn#kJ;{+&#wK~qc_zL&@ zE1c2={wX1NDf;(EcYk%66Zc&iA)^@acVqG67o${+fponTx7SV85i^QLBNXbTc+lYhaND$(<#vdb4 zQBjC-gng6*xB! zleNgoZ@UgVkAquRhxM7qte?yliCWb_*cQluTyF@J+EjEbecyqO0q`yQWbd$Rp^xat z(uUB0bh|!77BKkrfXIbA-$!kX{|$1?Hy!%m*Xet5Y#Q}H(V7SWlP`w|{ebAUNMrpe z%IqoXlKolI(fr&T*Xm7?=rO1_;4*L1R{<%bD7c0R;3< zPq+2@;t5>y0#7$zm;bTPAY*mEw^OGRl-Q@BI2EP(lcoy4)UmlX(_W4_kp6UY`IKY# zr%PW1K`l%N6!bs(Fzx&M@NT>?u{1|tga8Lqw$wob_9T!%3I1kcGQTSBY}$<|AP$dQ zN2xN|&)}B8{p9dC3yJ2^yQZes`hgn5e?r6AzP}(AQy;3&912_kmW>v(SI~3xUnZ^n ze^89oP3sf-@`Jf{VEvdNIt}E@GXA9^fHj2Ha{u=R_2i+iAG)3~>B&B#zkGf*Iqn-7 zc&!UVFP}_x63=a-Tt-J02JQ)rJr_$u>A-?QRL{QA0Fygcoys*y`i}L}n zu-JF`lhW5(%bnqgo(9aD1TUs(i8Cc(BvuRQu45wDU$if~tl<2Y-Sm*MjiBVez%iM` zk&QqndkQ%PK~v?dcOaus<&ul#s-ZUE<^P>zA;FR0ePvI2oaYGkd+0Qh;oEYS%y|@m zu02(b=XnQ}J8X;DLhM+y*LSye?YS!=vwh$MKa*dY^w0YnBw{day$Ygj7x|s(fc=7a zc1%fju42Yp@S=BqW_uSm@2xaWAc?!wkzY%%>f)Z}3I3YhCCPvUw%q2rE~BO`^Z48K z@~u7bYyvu2o9liLh-7qFN3<;K)xeiX5Q85vENF{>4%D8p6^X$F>vT(r2D47&?G3RwDxiuaKBd z)lc5u$5NMhot!pPCicDfpQ-%wo)vB76;$SK;(@l{@o)d7rIX5xQSm|SZ2PgW> zI_Vfs63P5@&H&am)B)-vyyz$r88}hBCvWKa51c8fBnCIAh%+RF<76W!?ZwdUOdly( zg`L(n3E^2D)^_~TyPa7>IGfNSywNu=Oqj<6qeG^g_ySu?=darC*PGTAM4Rj(jEmtKwe)0@Jcxm_Um*j8Wyt4!n|@|*44nGfZP(J9np)fz*3stK>h!zD+u^=Kp5afb~>+uCTivN19gMMNOiOGtNTKTfx- zi3db1)ES6z5i^}Q5umAvNIv9FYx*=%hn?=oey(4}wb9EhEjb@RlR9c08Nh<^s#vty zTD9Wvd>uAB0J{4~YT+eFF7gl=Z2|EYlsPu6fg4DRofH0S-!%u_W{kI-4JlD;*fE}6 z)p7F3#xX2=ubXS76XD)wlOw$ikx;>Jp z^$Fv$(k&`*H@UC2WO94O;;njCoun~qMfTTm^UizA_4`5Wktr0C@ZE~ya1-RQ4bS@P zfn!j>qH7wVA4sO*+*@fZ-?JiJXov5}itVUiRU$0rKq|-WvnztLe+OxeK(rX(3>K=~@cf!|L z`gQrfk`X*at}|j_=Cztdnp5i{CYA@{a(w4Ae;T8=vV+9^dc0v2)z$mNXq zV`RAGo>7&JzorXwfyS})-BH!OYmW}fy?N$>@5gojbF}%e{4%$x9&xLR4-qfJ2Qv1` z8+7|!4#d7Es{;K`{v}3DeyAAyH7&UTrX%RE1b@|JG+dMr*O;i*=LE$9pN}I(}F( zFt?MVPgk2%-jq(}GYL-avkR`qqDbS!95DeIlAD$fy3m=FMfI>Ms?XO}1>L14pf)m* z#-!+Ce2&s4cJ8sDfUXkLeBW_1hH(>H815Mb)A-SE9!64`$Um%UI0z+rmd#(?K5a3J z+75dLY*#~x>{>BbA7T#m7nK&x9mUxRfd%P(IBU&A4+>`3XF|?D)0vMxKB#)0min?| zB$Bi&e*ST_>{=0$`P`V2>%X)2EZn8)Ko@a$)_CHRFUQqmfk`(bJ>v+~V@(cOGz}WH z6lz*k_kS=3Y=B)q(Z5jUdc)H`>M8H1b{kaRIv+0B`+@8yE5XtC|i^HipTES$oKx zWJu3HRiIP~bFhkJU7s@!|{29Leg zft8LQB;&+rZy0vs!-&Q52=2UR&A+@EM~&knrY*grUA>n^U2$ChSq3F$VZ4l{seQgG; z)PHwYJQLw#Q(xT@r>EfLa3_$bzG|n;pHTr=pw^=*h4FDj5mEUCW!-gntz9v;ur957 zdwb=ljTk~dq4&Y4EOP6X>GjswXkXREKIi%^ibP4x!e)ukBmJw<9+2V5P!(TCXl)V0RAN$u5Mmu6o*Zp^RWlhd-u$IOW>L_%Wv zwhu1u*ODQj0OAQ8-wqnz?(g)w_XQ2J!0nOv5#+KR1GI?98c7bTYLH;F9|8eSb6b<& z-Niiqt;NslWnJKvvP=;*N?h^s%K@-13&_4?@%wyS?)IDsUP19gBHu@yx2cehXNcOI zRvI&miNn2>vi2R%%wkWtatX0os0sZXI3^ZmDVwV4hd*sZ)HKWl&45LjRf(Uw`8y;; zyYAcZ{XPu0NWlt6C7~rq-_=k!*2+URPmL9gm`XP!r`|T;Qkn7vIa6Z$SXi8RU1^x) z)yU*`K+XIW7}H{ZD@*P8@9bku-7=a+63c5zwR4CuV~nWHDmal&X50jDy|@osCT{7=Q)TIeT}DW|LniY3$QS3?|Un~ z0k2gbz9dGiaDmmU+NXySHrs9F5<=7#JQ~%xFD^2719k5mCjt?aE7ZTG%ki*|bR+00 z2?xK~LqS5~mbJ_L#LkaIjt4Bl4LgU4uu@~$CVxySirLRI>`4lS54>fIZa;QMgQnkM zX-qbx-oo?ox3iR0$9UMk3rpO$U2^asdwM}0j+|At$Ul5=xQ*_w!%vZg{R~iIA9i*L zDL$~)JAo)7ykfC0IZ4JeXtd|Xlr`c`6Uv_4=Rph}`TQ;fJthiTt_nPIIC))&{61|| zU)02th)71>Y2%MZR|s?y?c+rKj&ZK&%rgI(nU}y-N*bHYQVK6K@Ygmjt@o>(Fjx z!{gHQtLN+S%jdGBe9t;8F%@+_QNnuJLgpdDx;<{$0zsV+M;2o5?fajhLIf5#f1N^z z_GGh|ta(_Yq>^9$QE@9R=yICq&mT3STil?qvA=yDeo|Z3`vNW{s=o@U82USm#}AKTlU&eP%oF>v%dOZ_oZo;?*NEO1 z7sk(!g$+I6@_OiFfg3MpSoJze<>cHfCV$#$pit_$OUGkr@3i{#c4=9R6Mg!?w0M!j z`3Y{tu8lX5B-W#};iK$rN~fj9@6Jd9Uza+_YzT!C?KH{#A7{ejiqO>=4ULDxZ0>J% zDGi2sJ2BKSqpG?|Fj*Y$3uJWdnrN@q(c>*aqEsJ#!IQINAmekTRnWkg9^3?copS8& zUsE*oSAOs_!Qzi2Y+_3F2ccS&vJA;~x;=4ucgII1sjl_QYx~O0@|%5w9?QCasZxIn zBv2BFQ!AbAA!qP;&|pcTEYv)AsWdoz-tuG%-HipSq1^kv7pGVqlG2I$2;)5!2~&eU9Qkhi<@ zK8FM20srZo&|SrlBr0W+%MYCGhVSBS10X;dgN2DaQA>RuVlw*6Zs`&2Wx%t{#fvA* zZ0A4BEH!c$r0!Icfx0>_L9BM(G%>&?OiEEp?~W1@av8ZEeA;Ts?@Gy#a`6_eI1&Dr zsS34Ylje?Sl8L>x)8fo)c!M(OmhbYCG{#G#af?l<1m&aevffyja*-!sfkdw>65fR+ zPE_e+PT2+|vP@kbYgH76WN`gL*PXCW#S;Wrq4R$WG&lR`A!atkh@9o{UShy?ZEK)Z z0>Oejh9filcy@KCM@gmGXMLVmv4xjW2dj%sTx5`dwIq8^;HhA6(CPhJF2)WcUb(&} zasN9$J4s1;>I4AIYdg_{-V#e~G1hmgZl3nrad5gkZ$#y~o&gJN;tJfVZCgE1K@)K% zs!VmNE!~nU>dgZ|e5+T*xg1wY z;#=sd^(dl+P)`~gS)%CyL)hV zcZcBa7Th7YyKJz)LhuFNeD56i#{p)x`*z)`>Rah;m}3NKWclCZ*`|SE6TzzvVdQgt z2s4N#OY&(fedDDz%{nsagFiEdt?_Rq)#U9jJEU}ke~kbv$L|whTo%pkcHe;t19qSW zLIh!dh=u-Gv$qWtJKc%mxU3X{w+<*F)|8V6N5QmGx`(JxFTv8?+iGi9L_JDpy9nD@ zRyP+rnl=j4ti&8P>OpsLe*pMoBF)5f3@G=GSeWy^I|^IZQp&J46^m_84<#W2jK|9; zs(D<0)pj55@!ZuBcmM&@c&Lh#7l?-3YtojbjcgVtuQfYLIXI%*+ zl1-yuA(M-FZ{m&uSfd0!|C2)~S_`q~uB942(!FE$%N z;Ju|g@70#G$Su9Glm;c&lLB6RcO_(HtN%6vlR_VBkRzeB=Uql-z~mET7Kl zunskSsjjc{owx2*%+-lBfuQeZ@6Rf2Qu7F9pFZh_O$BxQM#beaY$irC>0Dzofz+Nk z%>{32^6Z>q-%29t)(*&RJVOgtj~Vd3sB*xzh|B=Il=@ps%8eWnw01hl)Xz2YJji`O z>nzdlPsdDpxu?~~Jq|<$g5RJE&7Rvj?3RR29bL(o{_7%t8;&xQ;jtHEkFt`MC-by) z+IN^q3v;~T<7#@6j3z?bsD_T4m7+G=%?2Zi#@T2pO&gv5-G0sODRXGQx-XH*T?dp! z8=u`$UZKRsn!ne=Dr^C(RlB-0Qkhe7sDimYF#ga~CRrw?;n*Kl!@ovrNemZ7kqdfA zqj;c;}?&Xok8NwwMVZ|6c4l0YQ^z5T;z&*(M;c4oL>Si?&HK0 z8oB#U8p?-4D&qfai+(I(*U{s2dm->zyh%@%CMlCTxdEoiRO;n-O@98bIhQwL(%u7+ z{!Kv7;84ibktosT_CiQ+?XVjBPL^o?)S~egxlmL~P+@<*de%mgk0HN|)})k-%_K~N z(rg&#xQH7#h-ms2;Aw^XeiJx$4av($N8%#}JCyd~F~6_`Qjy{4b+x$tdImrv#12G_ zs{Jj3D$NY#2IpD5EPyzb(H8RUIDtBRzU7CK%zykIT?PzzaaH2?rRN{LZSt^CY+EY2 zRAQ2Px>`sj;GTFXT0wGICBc`e}0L%rBuQ&505kCQ~vr}?BB*!&;I!S zr=<$;e>l6}=OQBxcN0fGV%5M5f{#)*zgtdESnHs>PQRKd%^$TwU=iE>x#E+4y^3Or z8ai9#fzb&o38Ez1A28onCos~?{Y*6Kw({I+Xu{Zo{8HY&G@|aMC#C|Hn&CpzH2qGJ zo}$j`sEK?VnfA3F=Xorf5YRd?6V7S$yp4mdc>poj4%27$vp4V>wNxaY_}Vw&fYiN1 z0x|#shT=Lw{XT9TvTi;Q3-fUUc;*d$k1Ber?Y;~qq{1oCucvLnxa#g}-{h-Ol~*1U zNSeaq_~NQHh6Wpe`}JwlftTk>t&zbiiur)7{)E8o?dEf-ZVm}o`M@!Lo^`lT{0Yuc zq~3=Bdvo|3#VmKkw>w+w7=${UpJl#ds8VL)J_f}uVKQEHz&;+JB3d^DcL0Uy>I!HT z9`pPrm=kqyG8Ihq>rptX%c^d;buu9NN!<5w6aemz^U|m}<09MjuTuE<@;j7u>A$k@e8}pJ_ z*l#6ETE=q%n6fJT$6P<{u)~yui!rRgK)Hv^`#Z66%FsnphT9K?#R!*Oa0WvuaM7mv zxFX?Q{!?R+yHB~Fxu3#Ey^>7t9tmzWo?bxjeBtlw>i!j<8HSk`DE&fJVV2ulW5F3} zShV)o!iK-1fU}36&V;n6BiAoJ3oo$52H%)sm9MiLpz%C*K6Ed$R)^(JnAmC@CNwn7 zz+}uszvT18n=>$O)hV~oV|ZgEkT}8L#VOl5JPB=I`H;CDcs=NhtgPR#@Qqx}@V&5%5sF z1KPsJ_``;)-vK?&Moc?_LoeXHK!LU2BKc8?1TgEpz?L6S%7%4%Ay`Rnbg0xLtj~|e zpzyG2Hi?_VK@;3^Q>GD&$jorEB8{p@P|MKPq$A%i(QCVuqK_f${}@1}h}A729D6w; zVmYDzPcIoGEgta>w|rPD+DcHDoI#}|q2u?oPkgeLRKn<`tjR8H`670L<@~YhuUTD- z+-s54AejQTI%gnJXV#Z9tv_Yy5vXQh4FthJd!fGP9gfG50)63g64Ym1efGy*bkjxv zG~+jDGdOM5MwY|Km3?Z#(V6^~CZU^>GL2BuXt$0TwsfgG2qlf+4KOE>mbdKj%u2;4 zIVtT+>TO5nt$U6FRr{&ZF}*smLd82PuDyx>WlLSxwO&5X1WD@)-8Xt6J9J~DMCCH_ z+;~yPnV-S#m^ZK3XC`J^s&Iy3J6Sx)8YF|aSjvtIo!Rbw$%=M7!F5CbNvZg3%O`+Q zzib15X@yWWt(CLy3A;i2w5$OW8?MJq#IN$ZRk(qN;K{3k%FT!0W?#;I{2u>|2&~=Q zG%&*Eb@}%K(@5eEG1ZblfKCO7Ljm&jKnK-!m~G4Td#7);?^Oq&lDm};`AkVtaMyVh zC}05D#{K14eMFXT|EmFBG|j6H(kRKgG#E3!>kriNXsnqSkcZjzGjp5UtcZoOls(#J ztB-7+-seMn)2^sM;#`|n9EIJIGak&-_8Eg{O3mK=YZTdM^Qh!HxS$k z)IEg$lraFCVJFaWtm11-#me?3j?|_mix9&it~%mB_NN z?;dyVmhMKRNhP;M^i$Yz=FT1&nwL5H+;3o~aEs_}yE~j|;r%!47`h4H;YYS^!~59b z$_2+QY9S%)0W12L9&%MOExeU)PB%z2WgImY9ZZhyMO1q~t5%3Q+v_#x4BWgfxh<2h zgxzua+1y_Uvt*3byYT`8teF-7AfHD~U3ioY-(}aJ!yQZI!Y=Dmq|^+?LWDhq+XB2N zfD?pMuRBPPC`t^_rtmm_v85rqCsSeZXw#=xcT4n#tWr=}k)Ty6lkP5PbdC)M#KvGWprtER;XACseDv5CS+B1N)5`czZqkAXqPMjn-z) ztA8ErH4y5w*Q0j{vIha;gR!1~bBDErbk1A4{BMoS1b<^JYKd4Ap+NllrJO5i?r1Wm zA7q*yD7(#u(y&VbfccbXCg2B|+P zvf7EZRc6yfj+LEuQ9nmtl44JEL}3)d+1U4k8vt%XIeDr|U0Ye&0B|H}KgzLYj1ly& zML1@o55s+NFogOKpDWboVJe_h0C5bcv+76~#lk?WV#+=+WxhrGobm2^D!5S#6Kt0s z;7OA5a-3yK(q&8H&4m3ZXtE zV$7j76P%M_7OL{$gF;`Oyfi7D33-(p~&6k>KtP)e%%k!eE-<2t3>WLy8CGWCHUk+y87-`LY21eqy_3*2;;9=aNzTG{H zIW>t@e6+tv%t{hPTs7bTbh9=IVL?1?>=WHBV1)3fGw>ob6(qnfLyKNx51vMnQQUc@ zA|#}fP4~dHYcI%ZL2gJa_5S7VZqS^NbEbvwhaa0t_x}m_&Ok*EL6dfS!gsn!#~Fdh zle>&p$H)T%zyVlZe=lFSsi_nQLGB}Q*rmExJZPuXcT*5&H9vf<3L3s7WKxb6AD0Et zmyT%7$jIX0=EiQ+p1YQNzb!qP3BdOCgwbjL4clV!sD5&!sB)hkr*xyvhOVE5w^%AM zxg?M!&YC)8n8GlxNEg>hHp9=CQyEHXsEQ`+KiVOw^KIt&yPTp7Xjh7a)bS}FfmI-! zMYr49MxaQT3kY6I6y65yHV_H~#&s=;($RuVi%LFe3(_PY#1$w2mBL@$*5yFe01xaO zmYr9}0B*5lR|tG@k}2F)XmK!X>QtC*`0)T%!u6VbN!H^mGe4#kSo& z9dfUTc?Q1X$4t4Hi5X_}4$jLMi_*#A8kG||AkP;!f(ohWf+>rju>rD-3{16hCZ!1Q z!}e7m%A)FG$6dN^eiL#x)TQwtPqB0SnL2)L;;R=N;TK|K|Yk|Oa z{*Hw;02bSv6@K+b%=zxf^4aBfK!gJbo%7!v1@-pFXh-}5*#AP%UDyB}Gtl1#N)bCSni~5A$Re*Fej_4@AX{IIJ#@Fm|zG1l;*L) zmew5QJD}o>2@Gm;zp-Z0H$ag6r9>X`;;1D{Y*U_QZ!V@$2eHwyX8b`K8Z?78ZbTvP**Xt>i+v(PoL$$1#&(Egkk&x2bcF z6eMUzgj{UM2K{TU2eC&zC!D@MMpF$lHV$zPk6# zi3m2Ivc=)<0zJ=|0$V!lck3!_DC$b!1i}ynP{%#T<#=?mgC>z?b-U*afzPqx;-s4+MA)I#dDYX(K^*xRw*JD=?ZYF1m2+CWudLba((f1x zu?^bFDP21?ZhMU_XKkijPC9+NxGI3xq0+2Alh2q#?sm6uFf`d>Vo)6d2W5mixzUCZ z1w8WSZXcH=yx_0kj6o<|z5lZh{T6Hbqm|UceJJrz`=1tPtL**t`_^6nLWh@6Tw4}( z;HbO~c`drB!NLLk={?04rHWMrq+Wed7QMHbjIT41ONnIbu|r96>jwjjx|cRrz#=TO z;UX*KJihv&6K-02(oR)l<9WE|hI7oM-El(D)9HEmboi~u*PbuEy%T)yKcbG*gbQ4V z*6X(?M2`eF*kziZ%r41SQ%J%JJ4E;AT^bp{_mNtkNu%S)B0ymgDJ0z~U-ja}I4 z!1-2ReQxvtgm6R{sq7PPy=@$J@rO~1pXEd-Wt9sXc+^W0^l;ksP!v6(cD#JO9@*P+ zk4q(R*ChmD`TeCFf08k=7J}89H=nP?P-TM^Z1xIvFU0|&>u1$3+wC_(8%W;CB$4s< zv-{^y|H2di6DHkX4QcCw&A@$dBv!`;vNZ?Pd;3+B*iD$e}xbrRK>?P18KAFSrk9+nv3V{6p zo=bHC|Bdcr4ygsVi;cKp3(w&rbZRH$- z?Uos$V`FWiCv1>Zi1DybJ=7ANqLW2u|~#9k-V{jpz&U;k)<@^bq#P`%shtOo8&NMc8t|6FeX!qFst z9d6DBE0gdmJdqeqf5U17pnL2`kwgB|ZYmu50Q?0RL#S}t*1oiQpf(Q~=NCC(*zM~= z!XG2a#f)JE1uc+@*2Y#Y<3RluE0<|~Lp}2`FP*Yny?dC6mPx-E0DlkraFfRb&r34p zGGiWztXTH*433(%4%nz$hBAxysc!4wyuXvXeur-_f5!ml7$DT5eFX%IP*qw4G$_Fe#=nYzlCVfVbpd`7$h9%GqViQcl88sr?pI>?ypeEniuBC327J zNpq3XFn;8;Osk0xs-=)JaRA)_+R|@;NLLWuGLlH$Yt#OHaKMr_ZanwXW$2r!cHJ_8fh|`cLlseqbBQBtda$Mn1czPv3uenx{iP%!I*;)K$Wn{ z-k8Fp{GB$Vpb^NY3fHR)6B044S36Wxf0yNRL2;H_3`8Bc_ zmSnebfRhwoyKMi_aX(t-2X9HuDWcZ$zFRAkJkM<$%Qe#$@`y!6Ysl73tH&BrP=Wc-waNvC@hU7(?5z92yN%K{{m-PZ}m$8&c^V`SioQ#DW&MMV6 zvBzBgUidNgWA<1^Lt*Vg8Ja+YZVZsSFR*XKxzK`oR;MrV+-_PTf3i3*m!vCOJ*=_h zq5g4*xzK5KKlQ4HF@Q77*vyDM52XZ$`f&CRjG{ZVwQO{%N#p1vT^Xd)P6IN65i_96z?Se7V?K}P zR6Jr&`0MR#g8C*omQG$rA+LKp#!2Uw5{xE#2Y06G3me7@7(uwtrFh0 z@azmRO{mbXyn^LC8PZ-W=8aRoEwk)(u!8V&)R2@eyQ%_MygyzHVurhIw)+%v z!e%Mbw)$GD=%klxCG~7uu@m5^WZIo{m;Z(*W;hKdUfh5UFNQSRkxXd? zvEn5gif7`)1>v4IxBlGlSlP0CAN_g@2c`J{XWXoFeKSG5ZA6FXb=)(Kk@o|JZF8krf578$`^`4snZohnk}4?08Qu*IRjMo z?BzZ1B1E<1C0HmK5+D0*;`$XG1tJ5V^9-aF&C=vd7TB?^^Kc9 z3Ep;*RCv5AZ)MAO@|%MTDtnG3OT9fJZMw%*5z>YO2r`Cb0qwB*e6m4lCqDzqUjI^iWa z|6uU}o1&cQn*nbTeVs*Ex-?=+8+`~4N)LPJY3dKferuWoZRH35OPr`fhyxSj#jWXs zQ|u_h{Ms5Ubs*!8%EF~>jlL8RDhgz|Ue#U>H+G1fv;6Ra_R9NKr7ssn0yh%j~7^YYa zxCsZ+Y`nh-M=39chXXWUOJc-O;?sruH9J`vISh?GudBGZ0hE$wJLl=)T@v@-$AaE9 z{gY>cC<|bR6wcU(%v9+~5)ks`sSeRV1KuSu(u}c+I)m@Kl7GF0SyBOGXoEHeiYzt5 zPk7Adm(5w-y_U3m#g+>^*}b@VFXb@UCPDfIJJwBs6W8i=F9~+_96oh2m+t~~9*2q> z1`3p}z@Fb%sjp2Xo=3Vl55Kxc;%GU1cf6VzI;xuGdw1Nf(tX*<>C*jvw$c}*^C2WC z@n<8|D5fbwZY;c6Zo@D<=e;Qj(_@n53D$YU7-u@$0NBi=h4jxL%Cg!O;`>_&n$av$t5% zlMg?}C^19*ry3_M#YyubUrFAv8tshwTxWQ!!DZVQ{Y*d;f8M3H*18b4?uF|}YoOESd^VG& zLj$5QjbJa=ACG0M9VHr?IIaa7h*F{pm62tj$5!mOWv9;K%fOVd8k)})KzuYPATtKd z|3ECHDKjMcq9*?2By$`mB9$w*W5^*n?$H8J>r(gY!@&wfmXyUd^V2a{l{G&VPsc*M}y<09nv(LR>kE$1a%bkguOYY|K15 zrqA|`DAPwP>5OaCd(si><<(G_Aw8NvXqTB(%%(|=%+eR|)=J#ziC>x%zOwii zUNUhJJWN_UM1j|Vq7yGUpQk?TP2Xb4S}b)I&8S-1!tlkim~NG8pVwWwkfhA)$AKd$ zL5hTj>P&YF)So^;Z}0l{8hoPg&x+A0NRc9E=Hsh=LkOF6q`oVc*X}V4po`=P(4Dsc z9q9DL$%#AP;?~o`9Da@aiWnXo_3eOCl?voh5Oqe?$TvKw$`X>c_wlJmOkgb>UEbmg zzb{*356DqJhQOh^lK5Yal%+8nyp?n0XR3v-aUVE6xw#LSYEML)aaeVqgsR(FxRq>8fovOd7MmOU=yRjy;cibTVAisn_kB!3q&% z4aho5d;PgmIEZU(aOfjC+)%I-pCjsy(EbyP6L{gh$*14ZHU(3Xn89`+T6J5dXIroR z*9qomXqsZEFWs-;$`7y2uPMg-aRHgdh^cHPLM z7EQm|&3j2a<@VDRm7akuzaa7^Cdwk6m6$H~>->s{Yp+lwsyd&9Lcv!_d((uDTJCpK-HbuUKtgy!-E#bkhr7wtmx}DUvb$uIyqv)q+a8#3 z{fx0-IvE6Z;`*SU43AJpoi|${eBk`MCb)(fYYE<*Alt?8{M9D#KA9!l$k8+SZnz<0XO!JM zKbLS(nqAJ|buE9yJCvb@@7Gt0^Yf-JyREz}Q19Oxa!9|DrgR2o_kZ=QbE`HmUU^Qy zahg}tKtjQT`52z2x1&W{;-g)X?On*=&>SUEbE5-3F+-m>69g$TK#1pd7g*T@RG&oD z1ufq}f+6!0bZ7m|L9Vw12J$VmRJm@Pno6fr*ILJDfvkBWYNc9!>0HsQ>$&_{sW7=$ zOq0F`Ub&e5hln9hk8=aL2#CUMc7eD9*|q z;1O+ot{5q8^F7$dn)giY>9;f>7*dpsz8ZLWzb8k2lNI##apQz8Xo_qy!G3dJA0-9- zz-Tv9i4%(XW{~sKBks@7ulKDvx8YuuHim?^{5~&DZHjp^YgmaWRG^CD$4mKIuY*~n zM@9cU@!}|MHauVH_d8K`Wg%w8~sVzlwaII?)K1KPL5X9I}P#~Od5BAU+a&|!PL06{&Eb;rAP2V#zqeDmZdZ%k6?WKh<8&d^fb-|G7Zi%wYL6yOwix1^@X{q*6ySdf8L9 z*8U{y)T=x4f})_1?5tn+Ti@Cr&Wif{cKVOpqL{|c&J#V%zqa^W&IW;vWz{N%HdV;L zk4zV$hW9h;>^j>t6pVSbbhhU&)d3j@Q!9_>r>FC;z2wqbst1m(f@3C)zb-wh-}M+X zW}C5)Bi4V06W@KuXB_D;?L^tAlE>n*UjHygo4oR9hmAtD%$(yeThPR86Zg6EOUyCk z6f^?ejHPnq#?^@c@B-Do&7M zHM}0sk!)NfCuYKgYN#A4^7+0N{Mwv^v*i^B=iAnHcz+kLeQz|a^iNCPOEUUUg*oWd zs*0KkHBnS(_x%nVw0*2_em~g2WcX?5S2o6;a9VbmAwgDg?`5%)lmFW)(~h#MVhDIpk1EG%xG};}>2{)}VeXrP#n;%0 z2WU0}+2h(-+8uF@f0?sY`OiwEwQ9|iue}1xUTa4VGB&m@%sbtYBB#1A0*!o4_YJ(KB z`au;*6DQHJLX_NdH*cAsx#Gj8#X;nThsz6ko0+j#$-e7|nY;P*P=_mTI}ZF!kMv1fKpf!IO*Hq>#f@T5vUvmPy0AMmZAvHt53%2K1-4+I8VP!IcUQ4 zZZ@trtgtO8!hUQA+RPTr>a^F5gu9|Cm+h4_Y-D^9#Ei<{A@l+Onnd*Nt6sLM}>aipBy>*a&|QFbBp>X!(GYmt!^>SRqL#-n!pwcYCxY1g6a z)IYt0CR%cDNAVnO(Gy007SCa7(w_Rfa<0utY&A@N>>rBFvXaqsFl98}1v{Z?%tyL2 zcxcH5(;^IL_S`)l|3OLzf$E{0 zCky+)VvXyQ!Wh4#iy;3R!8xoP!qF(m-2jTU(|T6eynP(JWfDANXVU zK}GH_WpmahyC!wu)Uk_rn8a-mimOF4kKg4Q4CE-*Ps_+Bmw&ps1nzb)0?nEyBY|x} zhn>&__BLIMCqX~+_Q!|{&T@xw{S}Sr<5`%$qCPxX{^U=8CGX6Pq*@lvx;cB=lo3!q zA28j|19JMZvcF*o`Vfnkt25)o?oS$!AVp!r6f}+KjTA@QW9|MG0?T5m!ZH z=a9BrK={%{{}^e&hkHwWrKawy6y&4|6V3XwB3Absv6sS7o!{! zTKAN=aiptk(NScHj3U-Csz8cwuVqF(rJ(wSXI{c|*U$cX^YDia(|Fh;klUCObwk(!U1yR)CoVzl;)Jl*f{Bq@wdL;~B9e%8L_CXnHtt2h%YWI_N3u z_Q5hOdnU0y=dN*r3-eW!zm8Dnk>l%kS??F3WNC)(|Spn(rK8NZ|orG`5f zTyc@ox2mx2-F1(|^fUnT!-bQ!*C7+p&BvpiTBs#G+}?1o-M5ACfT69?35B+@n`k{u zv<>|yi7g-h&~@7|)U05cuH^tcgY7^zjhACIbLu1w^N*0WC-1jWQTP_VnXjKdYP{HW zFcXL}X3$v~Q{uwZ*tb?o5C6Ep*(kOn_9{;A%BXs^yq z?CJTLN^3xJ=XQ* zoOI=Fg7i9bg&1H%pdbF8Eg)r`WB7eCwbolb4jwT2%>d*YI%BTZyHpE3xOamW6?-dv{t>#yfh{mrTKV~gDykvUG*$4&n))P|RGK|y#RA`99@k#z%}eb2mWzqz^XekFFY5@m-;$X) zbNq$V`h>=RZ#2!zl{K30?Tq`=zz<(fj}PO4SE}>qP3!o<0I0V?ByU+|2+X#qQ18v} z^U)jP4^)@wHEZfjCcymXWhys&xZr?lkON> zbrU{@6*sJ^y)XhB6t!Kka;-2^*Z?sW6l4x-cL3-WP=VU(cBPLwz%7I^;>KQv3#-Gso!$dd zti8|Mz0X?Znx0Dm_!R;LHVq$o@liwetO0<&xX`H-k{}L@T-A8~@MJY{+Ss7SUbMvp z5z(pND_PjriJpk~?NO%LG-RhH;1lHrVDD6gS;M|e*@VbZ?V<-B?>V9zrocW=SLL;x zxJjS)*p1&5I3KD+L7;Il*29*BgPX!&T=NAw4y}m5H z@A^LM1ot;G61MVJ%zMrG1Wu?jcJBiP+r4!|OXRasuBjiD z7xvHxM~9(=oy=dQDn+ouwS{lL(m5fw>ojZP(7;1CKYQK_swSad6n|USQxI@(z6#O5 z1z@$-s);DMI{tm_(soE=6YSw!O{i>g7QUQ%`9Ny2BY0h$Vz<6c)xBb~_#`r$;{mmM zwhA<7FT$TTz{DuE_WOj|%Jrwm5IhC($U;Y5wV*6z|9ZoF(?=C8)obM_#&#Ia`AD=3 z(agAt`O68zMNIRQX&UKEVFTR%PHdT3L)H+^3Y_LBIgzW42M4e7`@T!wMTzbGEEIdysypmcAcFQ1_~SzYxv2ZZ*Za-agE5UTr1l zN-)lWOabq|rIxqISUAa7+2w7`vWD_P7g>ZE{Z0&dba8EK|JO68r1HpBJyS&D?1|HB z)k4}|JEY&X{$t26z}A3aAb4t3n>(C#8Ean8eeR`N&@OG};hvV@0QH1S@#R|h1m|*E z?{nYUwh=aG4+W)b{Ov;?S+wDeCFG>&=fqa5&hLhE)QR%-@HnUdRXD#qV?zAQB(_UT z^jIP=E5!d6`SD=aS3TAyX|Lq>TkMH$@?BSMYMj5t+1~(kUza|vp5JUi*B;1mjYAL* z;5mVdwtp=Sm*8KTu37j42?%nOLBTgMacx-}6J)Xbj3CyxX?!V1L z`|ngF%}rEnksv7;Hk5CKBStuL(BG%G@-X;fVV8#R8b3>UeGHOJoS5XvCsSqF{-WTq zT|M()+A#DxGhUL{@UF%WZgTYBAWWqdBEYxC${=)P@%p}Lr7_qtaS|$Esvm2HsIwn% z`S?F(u&Ih0?_zdGhN_T{?6pE=J&>l36Q@lS1JJFe_}21pfhZHJ?jMNepGmJE=BP|P z25d;t(FT-LzEIt>_S`MNTLu9TxQ7R)7W>8=04@KAPm(B?gP`ZTnAbIdYk+*d-W*B3 z4Z6DdR;_fYi@+(bXy);{(z+JSAK33h^|^gn?fy0c;U&5T!-8M`nnxeABzw2K`nE!h zk~BBzIjsmv@cva5s!*n`L14UE4dvtWhq)hqb2{Rt(!eLyJZA;o6z3>N4Re(>#xkEX zWKLIk_#$jT1>jxSX6YX@xr%&|>F_Tfo_;go1P&(;P>w#a<_lpSWXYQmaEQ5im<=X6`Uf$N~`{y!Mu-NC4Q*@#)J^nCd#{ zP#};2S$jlq1V==L9;rZHEJqYM30R5QmctRVRD;rMVx1eCr72$12#+=+5qY^|9<0e? ztSKY5CC1tXfb;cbmKL7>w%`^H<$|8BagZ7Bb|**$5#W>dGnftl?I2gV3gwQMvq4;6 z5!8flN4C%n*J40Lne$E2{0pM5@X9A!&qUL})4nz%pj?tqpFx+WsB9 zFWcOK9*fmtd`lPcO|Wc_fx^9b%3c|SL&tsQrkyxBp3Kr3${}6?m13mSt5s1K>TF+u zAebY~b%Cd{LgrDw#$U85ln4p#Fs-{5X;qY576 zNMV2XUoA;}zHlPuYuEH5>9`-Q!J@$6ir>xaAX=nb8>P z>fTEYjs|dFg++uE^38Y=XwoN{G(5hox_7EsW4<*5Wcl@m7=NJ0m%zIP@g7f6*`j@G z4iaqH>VkrY*;Xe6bTVw&ffDTcRj*pDU5kiSN=%&Jaz%Yh7G~;*LzPyvyB>1dYoA0M zz?jh$%VAG(jm)HaNg;Coso71%cU#>An zgs@6$TtQb9y77>*J%r4{bTY%~((9xRfEHJ@rX{KED*voEE!y2e!Q|N?Uw!d4uTrKpnQuf7S4=1s;K;J^eVdDY&>%>3h4s_WE4u5ev}A^%S{fCid<= z=L@BuBNP^nHLUo;kHhNm=7_RqDI*A^wp)m4GIGXh|C94#*idxx;1;e%a zZlxS%qZ+J38R;@L_I1U(AE7oypvU8Z-FhMuF_Fgmjb~dr{lTO!gu__tSa3cDs_`z7 z2NmGdflc^Lrj+HI^nU<2W~jyo#aw=+2RK7HPH~-GVz~V03U&ejAE~&Y#o;kR=|%C6 z$l_bgDyt}rFsNhKH>lZLO}M!$m8Kra-J@%S(tNnFnK04guXg)!{W=qTbgf0}V(V-p zAc*x42NWQVoE@&g+OibQkdS7il*!7#%QPRPsywk@xMfe9#pDCmv!`#zqGcFi@6g_~ zO$2u7p4wUkmz55%XhTd=5qo%H>*a?7UoPEY>PLKzn&z&Ut|1sFSJ1y&4xT=m@nm+l zdHyurAL>hyF}-mC8chJdsS0sjft_guWY=x;3!;r#lj*P52wzk1oQ61#xl%fR#rC`8 z_j{rbLDq5>ZN^}GJ+v>nt2jV&=1E-mC*F_G{RY1*1H3SyI<^#^USoqr?&R#~jUkPR z{q4gdl^R~kW_^XUg-4Mq8&pfSXwD@r40o|YeN{I{Y0PbO5<<6-#ugZlNHZ0wRwL!_ znO9QIp{Uk&+WYjCA8}9IFrlAbB5B|xdK;LC&T5u#HIWT+2C2v^`=f-QI-a|O6jn6y6EpfU!lJeUi?dAf$*GCC~& zTEcyXW9Y}9!V?f);?Jxgl~fBj(zv<`1n~yr!HE7lA5QH^# zn=Sipl+HI9(FU1(XPYwwn$Ai(7c8^@{EKd5iCHsp^|Vy@1bKMle3fW{HaG6&U^Iq( zv2tOyWbZdpX&%Mp>L-X`H$CK%8E=_o<)h$hX)Y+iTZ{UK6MEHfMFd7P-bJ`1v_?t9()x;2dXIb`5uF);ET=zLkX)dp zAR2=c=aVm!uv|*nB#Xfxc&Nt$WBT%i3gV%R0dwsAn68ff7(&pM4_z-$_~6E_jxM-* z3y}sMRdQ3Z19xsWZq@5+X8iulgq4UUobcO@Wm-f19Hp##j{sVAm5yiMb$-7XZwyEw z4$D2NlX?WaZ_8(dir7Itf)JYK{WdhJ0eMXDn4uY7*=IEc>Jp%GjjPcc5{%!>bogGZ z5HkH;TdI2Dg^X&<1)~VhGDVZKeNCMki;kC()=NF~&(UezupHuL6k0Gt-)lWaYWU@b zm}Dk#2Z0ZUwkRW_PfQ>_a_P)Qz}5PEyQGB;@OvA%7TRVyo$m#F9Px_puhvjEyo2?U zHP=A|jCZk#NcPAzwymoaTayi$pRi#%4cQDs%gSbq!`G?ziqV;>KWfss0G-@#7;4DH z!$0?fBTL5tML`b%!OdN^tCip~{=gdh()t^K9`itgZ<-RK^M z7Wi;Zml3zMRtFrFvR9EAqyEGF;phG$lyR|K9`bp>p?-}Dxcz>YEAYZ5zWTo@B za`P;45pG9qu&?z50_~#m`g@2A{Waea#!Ce}2{vG?rUrt9AF7sV%}p?1h8MMC<;Ip) z9UEh8vPb4!7au3GCW1{GAifhYL?Q$qBf(Fbz9&nZ4fyn3jkR(>yHJnPTkFxogAoS3 z-0D#cyZ&bes|#u*Tu#78noZ^y0>BXp*M7iBqA~xo?i55>^2fflWXO7S+zrvRNGxbJ zZmo_J7@1dGJ^jR8? z#muHm*coAkcQj0Zg>~bkLl8U<8}r`Cv-sDx1~mMb-lb~~6IIz+miCN< zWjZWC4WmbYUoQIbEWljim$q8vfa|k>erp~!72N2ga#s%R0+y#vs;fky;?rXBj`m5! zdQ6K`j?h*}_$)W_pU6v?**S=C&|?X57ua)OiTc@yW|oJwF=Wk!2D(@mnV0lX}+jE1WBu+EfJ>%2 zpKM&~0(>eGoEfSkg+mhuTLT&qIO}@I?}rtRBpFltTVmvf*F|#05B4_V6}k>-Wgr3P zO5Q*hMP&h=kC=?hY;RwN`e@1sRQ1{`A*bNTx({#Z$W(+XF4t9I43t>qrd>QfF7@*1 zF`0~y`262XF zkMcKaZvX2s0a|p}Fwkr>*X5aZ9w?>!snp}j+41E7Cbjtz*sNsLYi96cFgQVqwgg=w z>zl0kS$@X)!B_R78fO&TI6P(3yxyF zh?oKH@7cj9-PFW6QLX+JR?UZ7W-WC9VpxhPolf%!OZ{JS z-yIIu*S0$eLX1ujy+xuEC5RHyf`||y8J$FLL6lKObWuY@i4r7{=p~FcMlV5-M6`?& zy_Zo&8{e9|zxR93IoJ8F^Y3xF*cW^6S$nPLxu1JI&%O5A!)2|?Z-M&!^I=Ar*Prve zO`h%rgL;-{vVO5kg;m^C1k%pBd%=SwD`fO&AN%U^Xn$vt%Rr+HJx~|G;li)4E%D@~ z=9hN34J=Z;X|Nc5p)1b(QUOmPP05=gAhZ)8tpeCfK?iqUgIeiJF6QY<=56Y$U4(!j zBG1ksSW((Rg&cmr523pH^2Rr#4(8(_R(kLnSwIz4uZbchY|M#=u1!-<4T~f^?(zj0 z*X}2tKqRIkT8#IQL&)fxDF9i7iLvZMF(N(rL=qfIInSjL4R*_FgA(NGSMq!%X`wzq1x5Zz(xfuza*wSk~nWiBbhLj>_Kz0?R zItn^l)$59k^lu3!KU|ddKdWO1_+&L7A27-Uxo1lCndBBA4{CJjJOiAGOeE%aXUiTe zCmKXku2X^fE2uua^>MlpVW#=JK3gshrj{>W5Fnk(L)HKO!!M*9`F9^*cIBF;)7x5$ z*DlrAVjcM3v~mV)zK6^9&3k}3t}(+qyVVkVhOd)F!eOg5E30GY{V@HX3>A4aw06a>*NN$?-Ybz4D1t9NP3-PDhmG-F>#s2bZ5?hZqBiKib@RUv;HO$LYhn zgu;0JiPoO!JMu}w6t+vc8c8d&MX%^Avz6C<{(R}y`u2dfZbn|xn~8Vr?Z{y=Jxzs58nhA z4hAYMZ-cJ9UGRZO1Q*tHQfgA=M>DukoPLSun8*;8F(8%lB_nZ7J_uaQH#f89w1+M0_k#7XK zd$2)3xrt{nLeP3`zIE^xzT9j%$m+o8kkS3+4owN~I6&{j7+)@irxf^S4p5D|5Szw2Bltn42X$WmhPi z5!;wF*u6PoPq+8)f}I!G?Ej_2n2r>%is$$5le196JY!r+;855Ablr@61 z{UdvmHXP%u<`cg41+6_hP^x7&7~1dhv2Tz<4iV>9K>K{Q$j3GFz==@N0}{feF@Z3; z%g%!mEBHCaI_)=?9|AY3t&3n&m615iZh*h0`?WhhC(2Wt*6c<~#!>#|49ynO`)lhC&!TFtJq6-5ss{UZ<;)~S#c?Xm;I+e|FUw+AR!zX} zM&=}(+Tl{G&Uqt5NLpV|Vq0>lXmgOUzG*D}MP*r~xWi7;N|?{mEd85;XccWpOYjr^ zn1so}a5uCUEhq_qZft?GOJ5KfhT}&A%{H0`INI_=`SxzpBnZKE{nyfGFKlJx)kNlJYu7e5Px4UZY-aN2}XSQgM(NYFMy9m>|aBmKeFNP~yTULjr6dVQR};uV`et(AqSseZ>X! zQjyvG%9Eo0XT;=CIexEQoG+n32hutxKbCu5vLI3{rvi&qGauv*1W2%w@JY(mRmE1y zvQk_)S%5u$-~nh_Fxw80bOE;cUEk7G4gr0k%fMh(QJjP|&bpd!6yQyZ!1_<5vz za)2s1QXbdH&u`Xi&Su#~=Iyz}pMQEDRqiI3>|#HgFrPbIG@j~boUi>ffEVGd2xGIw ztjy@)I~@bIQAd%K967qK6Ufh)7jf%UjX_spfnsR%k^0#}K#S&cIRuCxpw1`A@^Bz0 z|6*&?6RawAN9t%8#}%&d!Z?6B|}F42d~ zJ&tsVcZFD4?Cb;GO>TlJtBxaL_$_Ajq}Hh+bM=}}y=CXrcehI6G3BJbW~*0FB{o}| z7q;Vhv8w5Au+OQmwk97Jw=U(=q0+oeBk#n=BaU6#q86!Bv4;nR1@G^ntgw}?@`%h) zA710*Utg=uM0ZPyg)=b|)@{c(kYG~>OV0)(G4n(ky{CeV{t(kqWUER%W@KFS4$RrQ zsox3}d#tgrQu`xHz2yl5sCsE5Tda#8?kG4e=RvwJ^g1_w`DP)fkU4rC>gl@S&XfIh zdU!~@bk$=9%w|u#D>lpvWD4-hOo^(#fLU0a?a+bz)JEa`=O{1;Ie~x7upIP~?^AiE zs3CnCMAn}vZb#p3;h!2=?^Hh9-iwimM@`%;_h~rEI$q-u9r)@1iOKbObn)%APm1^1 z^MuP!k`v4lY6J@#S1(R1eHm`5b`8$cAH2lx=9w{5nO5HVRP-3HsZz{`Z6Kj|n7h-c?&-+bUYw#-7 zjP@q!r}0#-1!DTM;tnk79Oaw3?rWp!bpnUJvY#W6lh5NwK_v{oy?4>sVy2z?p6)b} z$@mJ#h0~udMYIp)GwiLx*$C?Zke{myO`G8Mne&PWohK*h#CJ8Hwtdu>k;>66g_Ahy zXgvajcUm_Axx%$~Ul=H7MGC*=C`9zL-*YybDK;rx3T%)EvU~8-e4^is`LMB`MH%vt zbBxKR+>_m%IW3mWxzvn{wf&dg+{D!wNDX!81abooZ+VXXjoxxaMxoM|ptJm&f*fz| zYMeXU-}ZUf_n|uGd}-LDmagw*)X@In%?UonEWo&szZwN?g}-&o1O2ffZ)kX2Y{)qu zXV(tTGQH_}WGFNAiu>Z5nhc8uIxyoY_2v!@4?_5Ky{t$32sr0K@-1&oC_ptAUXEQ8 z)+#FX*os?SH8Yf8D&~JYPWlLDV_1YmR$yLmDwJ2b!n}cgX0pT(55U07UsznAGka22 zFnqC;pHLD25*}PLD}=G8%q_pFrh^L5Mv%H3)+Q}#{8jkUt#CQhTC!I}qhBNUiE&cV z#q0Q}`l{Baor_2Qj;<5)88#{0p$|Q`F{1d{U*56dbh5Y;G9az54|j3|srKG)zsF}y zz8pv~Q{c`fk*-5uPQN_5d!D~_N1%R^U1)*a?L>~n@~(H4zRMx9Coss zzNrT)b5v>F_h-2A@QAgycyHcIH*C3Af`*(hO+U$m&kClqLf?UkKJ^h}?V|oV6qK#Q zJ1yS+h@F}klPmLfP;*I(J5;Fj5Il>C1|sDF{#I+c6$5=m3tai8fe|Prf0gr1;ge)! zF-eXHjP4Bf!?Vm-)-n$Y^Rh(1{qXkKNASL{(#{NJoNaDQuS`}{u8G@pxyp!Yi3-|) zc#NJzV3SbFZGz1`=$XC;}?X31Mt7I`ny$`%KTR!)X4RQ@a<49xel3iPg6 z_$CPrH?k-jt9TnHyBW7f2@GdXQg#m@4K!j%&XAT>QH+FoK&psy9y)QywA(~uo+|wN&l2ni;eV5 z?pvc@eunL)_Z1P83q5`oM3$|n_I*< z4G}?eGkTT-?EYK76LN$OGv6_^SLE6-2YAycq7+Aw2+qqv#1)V<)-TifugVR+ajHmB zcsp+X!re&Oda6nE#wfCfp+ z`88IPhLT9~h)tJn->e4TKQmj#MML&U`CzyELJq_0H}-18D&oa+hec824^nq{E?W2K zt7pHR6R#0AW-l9w(V0z!2KlTlNkIH~%NYigBq2Ei3F+w$v6<8iVe!>`bQk&P{2oW$ zzb2KSPp^jMlKIr_$qOh$`AdzR6IwN*!I|s%WfK@F#p$w)l*8z8!(-A#$v9cX${u!)&;zdb;#Leqawl^KDxa<`AY9)pk@K?VOa98kcOwi48MEwJt^%FNK-d065BS7B(W`s}Tdyl3y6hGYk;?b#Tyu*5~T zSG2_EFc?hCj=5-R%-j@ypScJ}EEF*vA!2N} z^TLe12Gk#xbN{*T5=@H9ON1qI-dp6;bSR;DWO>nC1OIw(HTr*d!T)f?GoaI)dzGqX zbq@mJ6NmyrOp$KSsIPtV{waJHfw>3UkzUva9%{10Eko&<#q=NV7yS)+8wD#Pc}ouw z_fQIkzJN3xrXtRjjWi6ZqE^y3$I*-NieK&xN5jfG{%4qwh%jN?IT5+;TvZRu&HXHt z+ER0hN?~W~>I$AM@4Dxqd1Ped_L?M7;(IRJj|EINX}T?@r5!K2s4S=OfdD6R9Cz32 zYz9IS_8jd*NB+cE$OdChPYJf`zST=QgFSYiQaljpbT4yUXxOKwqWCj9lpD@s{y-wt ziS~7ftc({$g2D-z(&4t|U_(s-H-&QWdvnIDjeXHf`D1uX^r%%0Z&K{9Jvp_f3s@8t zxSrs{VvQ$%&c~PgecFQoJUZT}qt-)gI_$}{wYAmmb(#RX6xiL}7jFGjc5mUj*#7Sd z(o^I?QFfxL9$V`O`iNLG&cb|Y#(cQXF3x&#RFfkpZ2zUac&d{GaAyp1)AbIutRJ57 zRr4K465sM9nd+5$cAkDz`j?N)StIMrWBv~eTTyKPXjMA$r;bzEnC86yym41W1LEK| zMTJA!Cd(e^yV}(@AF<}{jD(_R=y#cD^{}Z=zK!N$AqM5H1KfhDQ%}3((KqeMpezo@&+`5J7V~C-y%%mxDOJbq-<;Z zQ34o1sD)B3jcV~_V5yLtsmS#7@tv-P9WaKwC>g41^O&nmN75i_TxGr9Uitg;0<2s- zpO56(2*!U|UYKUsH0B+Hnw}dqpB5}rMOq-Gv?IQW9rEsDF5CYCkPXajLeTI)*5LHz zboGDt2kfskOm9pbifeBD=1k)Ioa>wCz2G4d5g9pzbXvSkRV0wU$|5Z*{%A`Q8<9%RE5;A%%{(BW zf##6KGFag76MI<6|J=TgQFtuF?*uH<4rI@h2X_J+7&dM1sgjt}7)@ZuG1!18t| zWkrvO&qAdIr1IJIty4R3+!~kC>5|eex*#WZ+wc06pM;_`M%@3#WyHTik1la|;Kjdy z|G(qu{|#gRpMQHp>v1)?pfHF)e5xrqK&ZGy2r=!IDoK355AljyhbIq}?G{V3aHswo zyhMpyWS0mSZV!)V3nHhgLOl^%)R<}V-)M_xzv<|Bb!iX2jYc0xZ8X{>xZ1!&xH71J z7*GcT1gEv%Rr;byr5tXf8R_nD;?X~_jz)1dxNUX?9{i4BG7oxoPD7KX-6I4J0^ShA zTAd9wfXCF5?JAcaMr#{WaaYlM{c?&3UtebAq%SlGzXM%bT`j^cfvA1&{%9)rh?dK2 z%xYlRk2&F%-^zwYMk2uiBVb$>#k1YutoX!jO&a2%X$p2latL;RbRFE(#t^4Mqf1Tb zTGL?yNKcDTJX>3$xkr%`+~yqF=G-N6W3pJuc4>)*&GzO6(hJZaBxnx>H8mkzmxDvu zK5x+01elyZ@Wz!*x4Lax@ZVInwi;cNAv66>de>hWDM7-6(`e{8PneXC3;G8)i zF)da#`3)#2Ko_DPL$lhoQL4P&su~GENXIbtwL79qmI4T!9IMP zi0~aeh``*l30zS{8K6r6)-=y(&_SdJ3w@=ea%L4h^w>1ogVe&z}=R$o#dLC z2$|l4zr=Sors}Z=6&BvV?jLa9PZyXUJs_>bDz9^WT=aC&cIA!rEq0^?gdFR7V`B^d zI5!2t2Ep3`W>-D#<&CU%Y~V4V8%Q7OmWpBMjnEfga3$@030XZZZGuAXjc zU>S%lQNZT)`&AWo*8h}O@@YXJ2|F3YDpFTLiVoO6?j#7LIVY>a|KLeCyTAi`VYdrY zOsr;1Pzs7#Nrs{Eh{lmsbuRbZrR^8N3h~<9P9a>SuGbd9AbosSU`H9x&fW@0Mf21j zaJAkg!&OmYK-Ay*&f|neYaE0K&rF!v0DQjCbVdOk6MK`LWId{{aFKS_KhD@cJo z#bk|ahK#JX3}RIi`7Tl`JQCjXu$ns`+&BmQ_tQy{9PcZ?v{CKa8qs5K$;g(jwdj(C zk(v7*s)N`9iKnETVYN^WBlx2Iruo|0Z+xMJYAKbH!R|4B;&}T*7q#{D@xb)xpah^E zU>5Q6y#@2$3d3!h_MJ`|Ee%f{#lI9#3Ir|!5#Pq9TBN1U)BxWrUS4kTR1PYQ&*vqx zu2YdC03_wN$%ccx0;DD~Djx>o7%lU~X`KYb=XhRlj@<2RK$WAOHiIsic@@do@?pF8 zAu@b+%(IK5@2;M4mfani_F?GXG3yO8ac#VO(`UUsdFsekcYsx1KJ4b^sN`n31fc(< zU=9s1h5rOpq{6_ni_6{*?4r?1+UiMA4k(~EdWoWOZ@-QQTrLu-JGgKBbwzE2>8-czo%Au I+cM<80DUYdmjD0& literal 0 HcmV?d00001 diff --git a/doc/source/quickstart/mtmfft_spec.png b/doc/source/quickstart/mtmfft_spec.png index 1bb767fbfc547b3be793216d4ae9afbcfdc602cf..86c102b8db66b1f56072abac3b640ae23ec9bff9 100644 GIT binary patch literal 42343 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 Nx~ev;OxY~t{{W4q8fdK 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 INFO: Using 5 taper(s) for multi-tapering + Syncopy INFO: Using 3 taper(s) for multi-tapering -informing us, that for this dataset a spectral smoothing of 3Hz required 5 Slepian tapers. +informing us, that for this dataset a spectral smoothing of 2Hz required 3 Slepian tapers. The resulting new dataset ``fft_spectra`` is of type :class:`syncopy.SpectralData`, which is the general datatype storing the results of a time-frequency analysis. .. hint:: Try typing ``fft_spectra.log`` into your interpreter and have a look at :doc:`Trace Your Steps: Data Logs ` to learn more about Syncopy's logging features -To quickly have something for the eye we can plot the power spectrum using the generic :func:`syncopy.singlepanelplot`:: +To quickly have something for the eye we can plot the power spectrum of a single trial using the generic :func:`syncopy.singlepanelplot`:: - fft_spectra.singlepanelplot() + fft_spectra.singlepanelplot(trials=3) .. image:: mtmfft_spec.png - :height: 250px + :height: 260px -The originally very sharp harmonic peak around 30Hz for channel 1 got widened to about 3Hz, channel 2 just contains the flat white noise floor. +We clearly see a smoothed spectral peak at 30Hz, channel 2 just contains the flat white noise floor. Comparing with the signals plotted in the time domain above, we see the power of the frequency representation of an oscillatory signal. The related short time Fourier transform can be computed via ``method='mtmconvol'``, see :func:`~syncopy.freqanalysis` for more details and examples. @@ -157,14 +165,14 @@ We can quickly have a look at a snippet of the generated signals:: .. image:: ar2_signals.png - :height: 340px + :height: 260px Both channels show visible oscillations as is confirmed by looking at the power spectra:: spec.singlepanelplot() .. image:: ar2_specs.png - :height: 300px + :height: 260px As expected for the stochastic AR(2) model, we have a fairly broad spectral peak at around 100Hz. @@ -184,12 +192,12 @@ The result is of type :class:`spy.CrossSpectralData`, the standard datatype for coherence.singlepanelplot(channel_i='channel2', channel_j='channel1') .. image:: ar2_coh.png - :height: 300px + :height: 260px As coherence is a *symmetric measure*, we have the same graph for both channel combinations, showing high coherence around 100Hz. .. note:: - The plotting for ``CrossSpectralData`` object works a bit differently, as the user here has to provide one channel combination with the keywords ``channel_i`` and ``channel_j``. + The plotting for ``CrossSpectralData`` object works a bit differently, as the user here has to provide one channel combination for each plot with the keywords ``channel_i`` and ``channel_j``. Cross-Correlation ----------------- @@ -203,12 +211,26 @@ As this also is a symmetric measure, let's just look at the only channel combina corr.singlepanelplot(channel_i=0, channel_j=1, trials=1) .. image:: ar2_corr.png - :height: 300px + :height: 260px We see that there are persistent correlations also for longer lags. Granger Causality ----------------- +To reveal directionality, or *causality*, between different channels Syncopy offers the Granger-Geweke algorithm for non-parametric Granger causality in the spectral domain:: + granger = spy.connectivityanalysis(data, method='granger', tapsmofrq=2) - +Now we want to see differential causality, so we plot both channel combinations:: + + granger.singlepanelplot(channel_i=0, channel_j=1) + granger.singlepanelplot(channel_i=1, channel_j=0) + +This reveals the coupling structure we put into this synthetic data set: ``channel1`` influences ``channel2``, but in the other direction there is no interaction. + +.. image:: ar2_granger.png + :height: 260px + + +.. note:: + The ``keeptrials`` keyword is only valid for cross-correlations, as both Granger causality and coherence critically rely on trial averaging. diff --git a/syncopy/plotting/_singlepanelplot.py b/syncopy/plotting/_singlepanelplot.py index 35817b6c5..1a1021c7a 100644 --- a/syncopy/plotting/_singlepanelplot.py +++ b/syncopy/plotting/_singlepanelplot.py @@ -39,11 +39,11 @@ def plot_lines(ax, data_x, data_y, **pkwargs): else: ax.plot(data_x, data_y, **pkwargs) if 'label' in pkwargs: - ax.legend(ncol=2, loc='upper right', + ax.legend(ncol=2, loc='best', fontsize=pltConfig['sLegendSize']) # make room for the legend - mn, mx = ax.get_ylim() - ax.set_ylim((mn, 1.1 * mx)) + mn, mx = ax.get_ylim() + ax.set_ylim((mn, mx + abs(.1 * mx))) # -- image plots -- diff --git a/syncopy/plotting/config.py b/syncopy/plotting/config.py index 38277e558..0e2802d80 100644 --- a/syncopy/plotting/config.py +++ b/syncopy/plotting/config.py @@ -18,7 +18,7 @@ "sLabelSize": 16, "sTickSize": 12, "sLegendSize": 12, - "sFigSize": (6.4, 4.8), + "sFigSize": (6.4, 3.2), "mTitleSize": 14, "mLabelSize": 14, "mTickSize": 10, diff --git a/syncopy/plotting/sp_plotting.py b/syncopy/plotting/sp_plotting.py index e8d2260e8..0865f8365 100644 --- a/syncopy/plotting/sp_plotting.py +++ b/syncopy/plotting/sp_plotting.py @@ -56,6 +56,7 @@ def plot_AnalogData(data, shifted=True, **show_kwargs): fig, ax = sp_plot.mk_line_figax() sp_plot.plot_lines(ax, data_x, data_y, label=labels) + fig.tight_layout() def plot_SpectralData(data, **show_kwargs): @@ -113,6 +114,7 @@ def plot_SpectralData(data, **show_kwargs): ylabel='power (dB)') sp_plot.plot_lines(ax, data_x, data_y, label=labels) + fig.tight_layout() def plot_CrossSpectralData(data, **show_kwargs): @@ -157,7 +159,7 @@ def plot_CrossSpectralData(data, **show_kwargs): label = rf"channel{chi} - channel{chj}" data_x = plot_helpers.parse_foi(data, show_kwargs) elif method == 'corr': - xlabel = 'lag' + xlabel = 'lag (s)' ylabel = 'correlation' label = rf"channel{chi} - channel{chj}" data_x = plot_helpers.parse_toi(data, trl, show_kwargs) @@ -172,5 +174,6 @@ def plot_CrossSpectralData(data, **show_kwargs): # persisten axes allows for plotting different # channel combinations into the same figure if not hasattr(data, 'ax'): - fig, data.ax = sp_plot.mk_line_figax(xlabel, ylabel) + data.fig, data.ax = sp_plot.mk_line_figax(xlabel, ylabel) sp_plot.plot_lines(data.ax, data_x, data_y, label=label) + data.fig.tight_layout() From 7557e4aa1406f2893228f9841ed6c248888f083b Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 28 Mar 2022 12:06:07 +0200 Subject: [PATCH 128/166] FIX: Always return lists when showing multiple trials - addresses #239 and #240 - the out of bounds behaviour of toi (not toilim) selections is another pair of shoes entirely, and should get addressed elsewhere On branch show-fixes Changes to be committed: modified: syncopy/datatype/methods/show.py modified: syncopy/tests/test_selectdata.py --- syncopy/datatype/methods/show.py | 51 ++++++++++++++++++++++---------- syncopy/tests/test_selectdata.py | 6 ++-- 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/syncopy/datatype/methods/show.py b/syncopy/datatype/methods/show.py index d60713dca..bd3328728 100644 --- a/syncopy/datatype/methods/show.py +++ b/syncopy/datatype/methods/show.py @@ -121,7 +121,7 @@ def show(data, squeeze=True, **kwargs): msg = "".join(selectionTxt[txtMask]) transform_out = np.squeeze else: - transform_out = lambda x : x + transform_out = lambda x: x SPYInfo("Showing{}".format(msg)) # Use an object's `_preview_trial` method fetch required indexing tuples @@ -132,24 +132,43 @@ def show(data, squeeze=True, **kwargs): # Perform some slicing/list-selection gymnastics: ensure that selections # that result in contiguous slices are actually returned as such (e.g., # `idxList = [(slice(1,2), [2]), (slice(2,3), [2])` -> `returnIdx = [slice(1,3), [2]]`) - singleIdx = [False] * len(idxList[0]) - returnIdx = list(idxList[0]) - for sk, selectors in enumerate(zip(*idxList)): - if np.unique(selectors).size == 1: - singleIdx[sk] = True - else: - if all(isinstance(sel, slice) for sel in selectors): - gaps = [selectors[k + 1].start - selectors[k].stop for k in range(len(selectors) - 1)] - if all(gap == 0 for gap in gaps): - singleIdx[sk] = True - returnIdx[sk] = slice(selectors[0].start, selectors[-1].stop) + + # COMMENT: Do we really need this? + # We still want a list returned with one trial per list item, also for consecutive (or + # the default 'all') trials! toi selections without gap only happen in those cases.. + # another mental burden of mixing time and trial indexing?! + # If not selecting consecutive trials, these gymnastics seem unnecessary as well?! + + # FIXME: remove this part if vetted in review to be really unnecessary + # singleIdx = [False] * len(idxList[0]) + # returnIdx = list(idxList[0]) + # for sk, selectors in enumerate(zip(*idxList)): + # print(selectors, np.unique(selectors), np.unique(selectors).size, len(selectors)) + # # toi and foi are lists/arrays and not slices like toilim/foilim + # # so they get implicitly concatenated by np.unique + # if np.unique(selectors).size == 1 or len(selectors) == 1: + # singleIdx[sk] = True + # else: + # if all(isinstance(sel, slice) for sel in selectors): + # gaps = [selectors[k + 1].start - selectors[k].stop for k in range(len(selectors) - 1)] + # if all(gap == 0 for gap in gaps): + # singleIdx[sk] = True + # returnIdx[sk] = slice(selectors[0].start, selectors[-1].stop) # Reset in-place subset selection data.selection = None - # If possible slice underlying dataset only once, otherwise return a list - # of arrays corresponding to selected trials - if all(si == True for si in singleIdx): - return transform_out(data.data[tuple(returnIdx)]) + # single trial selected + if len(idxList) == 1: + return transform_out(data.data[idxList[0]]) + # return multiple trials as list else: return [transform_out(data.data[idx]) for idx in idxList] + + # FIXME: remove for the same reason as above + # If possible slice underlying dataset only once, otherwise return a list + # of arrays corresponding to selected trials + # if all(si == True for si in singleIdx): + # return transform_out(data.data[tuple(returnIdx)]) + # else: + # return [transform_out(data.data[idx]) for idx in idxList] diff --git a/syncopy/tests/test_selectdata.py b/syncopy/tests/test_selectdata.py index 769e47b3e..c31a0e1b7 100644 --- a/syncopy/tests/test_selectdata.py +++ b/syncopy/tests/test_selectdata.py @@ -413,8 +413,10 @@ def test_general(self): assert "no data selectors if `clear = True`" in str(spyval.value) # show full/squeezed arrays - assert len(ang.show(channel=0).shape) == 1 - assert len(ang.show(channel=0, squeeze=False).shape) == 2 + # for a single trial an array is returned directly + assert len(ang.show(channel=0, trials=0).shape) == 1 + # multiple trials get returned in a list + assert [len(trl.shape) == 2 for trl in ang.show(channel=0, squeeze=False)] # go through all data-classes defined above for dclass in self.classes: From 0ee6100a3c8e325000f58e4cffbac1e93a19c6a0 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 28 Mar 2022 12:21:27 +0200 Subject: [PATCH 129/166] CHG: Test toi/toilim single/multiple trail returns On branch show-fixes Changes to be committed: modified: syncopy/tests/test_selectdata.py --- syncopy/tests/test_selectdata.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/syncopy/tests/test_selectdata.py b/syncopy/tests/test_selectdata.py index c31a0e1b7..a14ca1033 100644 --- a/syncopy/tests/test_selectdata.py +++ b/syncopy/tests/test_selectdata.py @@ -418,6 +418,13 @@ def test_general(self): # multiple trials get returned in a list assert [len(trl.shape) == 2 for trl in ang.show(channel=0, squeeze=False)] + # test toi/toilim returns arrays for single trial and + # lists for multiple trial selections + assert isinstance(ang.show(trials=0, toi=[0, 1]), np.ndarray) + assert isinstance(ang.show(trials=0, toilim=[0, 1]), np.ndarray) + assert isinstance(ang.show(trials=[0, 1], toi=[0, 1]), list) + assert isinstance(ang.show(trials=[0, 1], toilim=[0, 1]), list) + # go through all data-classes defined above for dclass in self.classes: dummy = getattr(spd, dclass)(data=self.data[dclass], From 11639d76cec70ff1674a9ba18eee35b2682e9ab4 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 28 Mar 2022 18:12:24 +0200 Subject: [PATCH 130/166] NEW: Multipanelplots - both for line and image plots - given the show restrictions for cross spectral data, multipanelplot does not have a use case there atm On branch rework-plotting Your branch is up to date with 'origin/rework-plotting'. Changes to be committed: modified: syncopy/datatype/continuous_data.py modified: syncopy/plotting/_helpers.py new file: syncopy/plotting/_plotting.py deleted: syncopy/plotting/_singlepanelplot.py modified: syncopy/plotting/config.py new file: syncopy/plotting/mp_plotting.py modified: syncopy/plotting/sp_plotting.py --- syncopy/datatype/continuous_data.py | 10 +- syncopy/plotting/_helpers.py | 40 +++++- syncopy/plotting/_plotting.py | 153 ++++++++++++++++++++ syncopy/plotting/_singlepanelplot.py | 85 ----------- syncopy/plotting/config.py | 18 +-- syncopy/plotting/mp_plotting.py | 206 +++++++++++++++++++++++++++ syncopy/plotting/sp_plotting.py | 23 +-- 7 files changed, 429 insertions(+), 106 deletions(-) create mode 100644 syncopy/plotting/_plotting.py delete mode 100644 syncopy/plotting/_singlepanelplot.py create mode 100644 syncopy/plotting/mp_plotting.py diff --git a/syncopy/datatype/continuous_data.py b/syncopy/datatype/continuous_data.py index 71f621e37..61d6790b5 100644 --- a/syncopy/datatype/continuous_data.py +++ b/syncopy/datatype/continuous_data.py @@ -20,7 +20,7 @@ from syncopy.shared.parsers import scalar_parser, array_parser from syncopy.shared.errors import SPYValueError, SPYWarning from syncopy.shared.tools import best_match -from syncopy.plotting import sp_plotting +from syncopy.plotting import sp_plotting, mp_plotting __all__ = ["AnalogData", "SpectralData", "CrossSpectralData"] @@ -481,6 +481,10 @@ def singlepanelplot(self, shifted=True, **show_kwargs): sp_plotting.plot_AnalogData(self, shifted, **show_kwargs) + def multipanelplot(self, **show_kwargs): + + mp_plotting.plot_AnalogData(self, **show_kwargs) + class SpectralData(ContinuousData): """ @@ -625,6 +629,10 @@ def singlepanelplot(self, **show_kwargs): sp_plotting.plot_SpectralData(self, **show_kwargs) + def multipanelplot(self, **show_kwargs): + + mp_plotting.plot_SpectralData(self, **show_kwargs) + class CrossSpectralData(ContinuousData): """ diff --git a/syncopy/plotting/_helpers.py b/syncopy/plotting/_helpers.py index e3aab6852..29ec9b22d 100644 --- a/syncopy/plotting/_helpers.py +++ b/syncopy/plotting/_helpers.py @@ -119,7 +119,8 @@ def shift_multichan(data_y): # shift even further if next channel # dips below 0 offsets += np.abs(data_y.min(axis=0)[1:]) - offsets = np.r_[0, offsets] * 1.1 + offsets = np.cumsum(np.r_[0, offsets] * 1.1) + print(offsets) data_y += offsets return data_y @@ -138,3 +139,40 @@ def get_method(dataobject): if match: meth_str = match.group(1) return meth_str + + +def calc_multi_layout(nAx): + + """ + Given the total numbers of + axes `nAx` create the nrows, ncols + layout. In case of `nAx` being prime, + augment by 1 to rather leave an + empty plot than to have say a (17, 1) layout.. + """ + + # This works as well as long + # as `nAx` isn't prime :] + # so we have to augment by 1 if that's the case + if nAx % 2 != 0: + ncols = int(np.sqrt(nAx)) # this is max pltConfig["mMaxYaxes"] + nrows = ncols + while(ncols * nrows < nAx): + ncols += 1 + nrows = int(nAx / ncols) + # nAx was prime and too big + # for one plotting row + if ncols == nAx and nAx > 7: + nAx += 1 + # no elif to capture possibly incremented nAx + if nAx % 2 == 0 and nAx > 2: + ncols = int(np.sqrt(nAx)) # this is max pltConfig["mMaxYaxes"] + nrows = ncols + while(ncols * nrows < nAx): + nrows -= 1 + ncols = int(nAx / nrows) + # just two axes + elif nAx == 2: + nrows, ncols = 1, 2 + + return nrows, ncols diff --git a/syncopy/plotting/_plotting.py b/syncopy/plotting/_plotting.py new file mode 100644 index 000000000..a4155f2aa --- /dev/null +++ b/syncopy/plotting/_plotting.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +# +# Syncopy plotting backend +# + +from syncopy.plotting.config import pltConfig +from syncopy import __plt__ + +if __plt__: + import matplotlib.pyplot as ppl + + +# -- 2d-line plots -- + +def mk_line_figax(xlabel='time (s)', ylabel='signal (a.u.)'): + + """ + Create the figure and axis for a + standard 2d-line plot + """ + + fig, ax = ppl.subplots(figsize=pltConfig['sFigSize']) + # Hide the right and top spines + ax.spines['right'].set_visible(False) + ax.spines['top'].set_visible(False) + ax.tick_params(axis='both', which='major', + labelsize=pltConfig['sTickSize']) + + ax.set_xlabel(xlabel, fontsize=pltConfig['sLabelSize']) + ax.set_ylabel(ylabel, fontsize=pltConfig['sLabelSize']) + + return fig, ax + + +def mk_multi_line_figax(nrows, ncols, xlabel='time (s)', ylabel='signal (a.u.)'): + + """ + Create the figure and axes for a + multipanel 2d-line plot + """ + + # ncols and nrows get + # restricted via the plotting frontend + x_size = ncols * pltConfig['mXSize'] + y_size = nrows * pltConfig['mYSize'] + + fig, axs = ppl.subplots(nrows, ncols, figsize=(x_size, y_size), + sharex=True, sharey=True, squeeze=False) + + # Hide the right and top spines + # and remove all tick labels + for ax in axs.flatten(): + ax.spines['right'].set_visible(False) + ax.spines['top'].set_visible(False) + ax.tick_params(labelsize=0) + + # determine axis layout + y_left = axs[:, 0] + x_bottom = axs[-1, :] + + # write tick and axis labels only on outer axes to save space + for ax in y_left: + ax.tick_params(labelsize=pltConfig['mTickSize']) + ax.set_ylabel(ylabel, fontsize=pltConfig['mLabelSize']) + + for ax in x_bottom: + ax.tick_params(labelsize=pltConfig['mTickSize']) + ax.set_xlabel(xlabel, fontsize=pltConfig['mLabelSize']) + + return fig, axs + + +def plot_lines(ax, data_x, data_y, leg_fontsize=pltConfig['sLegendSize'], **pkwargs): + + if 'alpha' not in pkwargs: + ax.plot(data_x, data_y, alpha=0.9, **pkwargs) + else: + ax.plot(data_x, data_y, **pkwargs) + if 'label' in pkwargs: + ax.legend(ncol=2, loc='upper right', + fontsize=leg_fontsize) + # make room for the legend + mn, mx = ax.get_ylim() + ax.set_ylim((mn, 1.1 * mx)) + + +# -- image plots -- + +def mk_img_figax(xlabel='time (s)', ylabel='frequency (Hz)', title=''): + + """ + Create the figure and axes for an + image plot with `imshow` + """ + + fig, ax = ppl.subplots(figsize=pltConfig['sFigSize']) + + ax.tick_params(axis='both', which='major', + labelsize=pltConfig['sTickSize']) + ax.set_xlabel(xlabel, fontsize=pltConfig['sLabelSize']) + ax.set_ylabel(ylabel, fontsize=pltConfig['sLabelSize']) + + return fig, ax + + +def mk_multi_img_figax(nrows, ncols, xlabel='time (s)', ylabel='frequency (Hz)'): + + """ + Create the figure and axes for an + image plot with `imshow` for multiple + sub plots + """ + # ncols and nrows get + # restricted via the plotting frontend + x_size = ncols * pltConfig['mXSize'] + y_size = nrows * pltConfig['mYSize'] + + fig, axs = ppl.subplots(nrows, ncols, figsize=(x_size, y_size), + sharex=True, sharey=True, squeeze=False) + + # determine axis layout + y_left = axs[:, 0] + x_bottom = axs[-1, :] + + # write tick and axis labels only on outer axes to save space + for ax in y_left: + ax.tick_params(labelsize=pltConfig['mTickSize']) + ax.set_ylabel(ylabel, fontsize=pltConfig['mLabelSize']) + + for ax in x_bottom: + ax.tick_params(labelsize=pltConfig['mTickSize']) + ax.set_xlabel(xlabel, fontsize=pltConfig['mLabelSize']) + + return fig, axs + + +def plot_tfreq(ax, data_yx, times, freqs, title=''): + + """ + Plot time frequency data on a 2d grid, expects standard + row-column (freq-time) axis ordering. + + Needs frequencies (`freqs`) and sampling rate (`fs`) + for correct units. + """ + + # extent is defined in xy order + df = freqs[1] - freqs[0] + extent = [times[0], times[-1], + freqs[0] - df / 2, freqs[-1] - df / 2] + + ax.imshow(data_yx[::-1], aspect='auto', cmap=pltConfig['cmap'], + extent=extent) diff --git a/syncopy/plotting/_singlepanelplot.py b/syncopy/plotting/_singlepanelplot.py deleted file mode 100644 index 35817b6c5..000000000 --- a/syncopy/plotting/_singlepanelplot.py +++ /dev/null @@ -1,85 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Syncopy singlepanel plot backend -# - -from syncopy.plotting.config import pltConfig, pltErrMsg -from syncopy import __plt__ - -if __plt__: - import matplotlib.pyplot as ppl - - -# -- 2d-line plots -- - -def mk_line_figax(xlabel='time (s)', ylabel='signal (a.u.)'): - - """ - Create the figure and axes for a - standard 2d-line plot - """ - - fig, ax = ppl.subplots(figsize=pltConfig['sFigSize']) - # Hide the right and top spines - ax.spines['right'].set_visible(False) - ax.spines['top'].set_visible(False) - ax.tick_params(axis='both', which='major', - labelsize=pltConfig['sTickSize']) - - ax.set_xlabel(xlabel, fontsize=pltConfig['sLabelSize']) - ax.set_ylabel(ylabel, fontsize=pltConfig['sLabelSize']) - - return fig, ax - - -def plot_lines(ax, data_x, data_y, **pkwargs): - - if 'alpha' not in pkwargs: - ax.plot(data_x, data_y, alpha=0.9, **pkwargs) - else: - ax.plot(data_x, data_y, **pkwargs) - if 'label' in pkwargs: - ax.legend(ncol=2, loc='upper right', - fontsize=pltConfig['sLegendSize']) - # make room for the legend - mn, mx = ax.get_ylim() - ax.set_ylim((mn, 1.1 * mx)) - - -# -- image plots -- - -def mk_img_figax(xlabel='time (s)', ylabel='frequency (Hz)', title=''): - - """ - Create the figure and axes for an - image plot with `imshow` - """ - - fig, ax = ppl.subplots(figsize=pltConfig['sFigSize']) - - ax.tick_params(axis='both', which='major', - labelsize=pltConfig['sTickSize']) - ax.set_xlabel(xlabel, fontsize=pltConfig['sLabelSize']) - ax.set_ylabel(ylabel, fontsize=pltConfig['sLabelSize']) - ax.set_title(title, fontsize=pltConfig['sTitleSize']) - - return fig, ax - - -def plot_tfreq(ax, data_yx, times, freqs, **pkwargs): - - """ - Plot time frequency data on a 2d grid, expects standard - row-column (freq-time) axis ordering. - - Needs frequencies (`freqs`) and sampling rate (`fs`) - for correct units. - """ - - # extent is defined in xy order - df = freqs[1] - freqs[0] - extent = [times[0], times[-1], - freqs[0] - df / 2, freqs[-1] - df / 2] - - ax.imshow(data_yx, aspect='auto', cmap=pltConfig['cmap'], - extent=extent) diff --git a/syncopy/plotting/config.py b/syncopy/plotting/config.py index 38277e558..4b1b6e012 100644 --- a/syncopy/plotting/config.py +++ b/syncopy/plotting/config.py @@ -9,9 +9,9 @@ import matplotlib as mpl mpl.style.use('seaborn-colorblind') # a hint of gray - mpl.rcParams['figure.facecolor'] = '#f5faf6' - mpl.rcParams['figure.edgecolor'] = '#f5faf6' - mpl.rcParams['axes.facecolor'] = '#f5faf6' + mpl.rcParams['figure.facecolor'] = '#fcfcfc' + mpl.rcParams['figure.edgecolor'] = '#fcfcfc' + mpl.rcParams['axes.facecolor'] = '#fcfcfc' # Global style settings for single-/multi-plots pltConfig = {"sTitleSize": 15, @@ -19,11 +19,13 @@ "sTickSize": 12, "sLegendSize": 12, "sFigSize": (6.4, 4.8), - "mTitleSize": 14, - "mLabelSize": 14, - "mTickSize": 10, - "mLegendSize": 12, - "mFigSize": (10, 6.8), + "mTitleSize": 12.5, + "mLabelSize": 12.5, + "mTickSize": 11, + "mLegendSize": 11, + "mXSize": 3.2, + "mYSize": 2.4, + "mMaxAxes": 35, "cmap": "magma"} # Global consistent error message if matplotlib is missing diff --git a/syncopy/plotting/mp_plotting.py b/syncopy/plotting/mp_plotting.py new file mode 100644 index 000000000..06d606da2 --- /dev/null +++ b/syncopy/plotting/mp_plotting.py @@ -0,0 +1,206 @@ +# -*- coding: utf-8 -*- +# +# The singlepanel plotting functions for Syncopy +# data types +# + +# Builtin/3rd party package imports +import numpy as np + +# Syncopy imports +from syncopy import __plt__ +from syncopy.shared.errors import SPYWarning +from syncopy.plotting import _plotting +from syncopy.plotting import _helpers as plot_helpers +from syncopy.plotting.config import pltErrMsg, pltConfig + + +def plot_AnalogData(data, shifted=True, **show_kwargs): + + """ + The probably simplest plot, 2d-line + plots of selected channels, one axis for each channel + + Parameters + ---------- + data : :class:`~syncopy.datatype.AnalogData` + show_kwargs : :func:`~syncopy.datatype.methods.show.show` arguments + """ + + if not __plt__: + SPYWarning(pltErrMsg) + return + + # right now we have to enforce + # single trial selection only + trl = show_kwargs.get('trials', None) + if not isinstance(trl, int) and len(data.trials) > 1: + SPYWarning("Please select a single trial for plotting!") + return + # only 1 trial so no explicit selection needed + elif len(data.trials) == 1: + trl = 0 + + # get the data to plot + data_x = plot_helpers.parse_toi(data, trl, show_kwargs) + data_y = data.show(**show_kwargs) + + # multiple channels? + labels = plot_helpers.parse_channel(data, show_kwargs) + nAx = 1 if isinstance(labels, str) else len(labels) + + if nAx < 2: + SPYWarning("Please select at least two channels for a multipanelplot!") + return + elif nAx > pltConfig['mMaxAxes']: + SPYWarning("Please select max. {pltConfig['mMaxAxes']} channels for a multipanelplot!") + return + else: + # determine axes layout, prefer columns over rows due to display aspect ratio + nrows, ncols = plot_helpers.calc_multi_layout(nAx) + + fig, axs = _plotting.mk_multi_line_figax(nrows, ncols) + + for chan_dat, ax, label in zip(data_y.T, axs.flatten(), labels): + _plotting.plot_lines(ax, data_x, chan_dat, label=label, leg_fontsize=pltConfig['mLegendSize']) + + # delete empty plot due to grid extension + # because of prime nAx -> can be maximally 1 plot + if ncols * nrows > nAx: + axs.flatten()[-1].remove() + + fig.tight_layout() + + +def plot_SpectralData(data, **show_kwargs): + + """ + Plot either 2d-line plots in case of + singleton time axis or image plots + for time-frequency spectra, one for each channel. + + Parameters + ---------- + data : :class:`~syncopy.datatype.SpectralData` + show_kwargs : :func:`~syncopy.datatype.methods.show.show` arguments + """ + + if not __plt__: + SPYWarning(pltErrMsg) + return + + # right now we have to enforce + # single trial selection only + trl = show_kwargs.get('trials', None) + if not isinstance(trl, int) and len(data.trials) > 1: + SPYWarning("Please select a single trial for plotting!") + return + elif len(data.trials) == 1: + trl = 0 + + labels = plot_helpers.parse_channel(data, show_kwargs) + nAx = 1 if isinstance(labels, str) else len(labels) + + if nAx < 2: + SPYWarning("Please select at least two channels for a multipanelplot!") + return + elif nAx > pltConfig['mMaxAxes']: + SPYWarning("Please select max. {pltConfig['mMaxAxes']} channels for a multipanelplot!") + return + else: + # determine axes layout, prefer columns over rows due to display aspect ratio + nrows, ncols = plot_helpers.calc_multi_layout(nAx) + + # how got the spectrum computed + method = plot_helpers.get_method(data) + if method in ('wavelet', 'superlet', 'mtmconvol'): + fig, axs = _plotting.mk_multi_img_figax(nrows, ncols) + + time = plot_helpers.parse_toi(data, trl, show_kwargs) + # dimord is time x freq x channel + # need freq x time each for plotting + data_cyx = data.show(**show_kwargs).T + for data_yx, ax, label in zip(data_cyx, axs.flatten(), labels): + _plotting.plot_tfreq(ax, data_yx, time, data.freq) + ax.set_title(label, fontsize=pltConfig['mTitleSize']) + fig.tight_layout() + fig.subplots_adjust(wspace=0.05) + + # just a line plot + else: + # get the data to plot + data_x = plot_helpers.parse_foi(data, show_kwargs) + data_y = np.log10(data.show(**show_kwargs)) + + fig, axs = _plotting.mk_multi_line_figax(nrows, ncols, xlabel='frequency (Hz)', + ylabel='power (dB)') + + for chan_dat, ax, label in zip(data_y.T, axs.flatten(), labels): + _plotting.plot_lines(ax, data_x, chan_dat, label=label, leg_fontsize=pltConfig['mLegendSize']) + + # delete empty plot due to grid extension + # because of prime nAx -> can be maximally 1 plot + if ncols * nrows > nAx: + axs.flatten()[-1].remove() + fig.tight_layout() + + +def plot_CrossSpectralData(data, **show_kwargs): + """ + Plot 2d-line plots for the different connectivity measures. + + Parameters + ---------- + data : :class:`~syncopy.datatype.CrossSpectralData` + show_kwargs : :func:`~syncopy.datatype.methods.show.show` arguments + """ + + if not __plt__: + SPYWarning(pltErrMsg) + return + + # right now we have to enforce + # single trial selection only + trl = show_kwargs.get('trials', None) + if not isinstance(trl, int) and len(data.trials) > 1: + SPYWarning("Please select a single trial for plotting!") + return + elif len(data.trials) == 1: + trl = 0 + + # what channel combination + if 'channel_i' not in show_kwargs or 'channel_j' not in show_kwargs: + SPYWarning("Please select a channel combination for plotting!") + return + chi, chj = show_kwargs['channel_i'], show_kwargs['channel_j'] + + # what data do we have? + method = plot_helpers.get_method(data) + if method == 'granger': + xlabel = 'frequency (Hz)' + ylabel = 'Granger causality' + label = rf"channel{chi} $\rightarrow$ channel{chj}" + data_x = plot_helpers.parse_foi(data, show_kwargs) + elif method == 'coh': + xlabel = 'frequency (Hz)' + ylabel = 'coherence' + label = rf"channel{chi} - channel{chj}" + data_x = plot_helpers.parse_foi(data, show_kwargs) + elif method == 'corr': + xlabel = 'lag' + ylabel = 'correlation' + label = rf"channel{chi} - channel{chj}" + data_x = plot_helpers.parse_toi(data, show_kwargs) + # that's all the methods we got so far + else: + raise NotImplementedError + + # get the data to plot + data_y = data.show(**show_kwargs) + + # create the axes and figure if needed + # persisten axes allows for plotting different + # channel combinations into the same figure + if not hasattr(data, 'ax'): + fig, data.ax = _plotting.mk_line_figax(xlabel, ylabel) + _plotting.plot_lines(data.ax, data_x, data_y, label=label) diff --git a/syncopy/plotting/sp_plotting.py b/syncopy/plotting/sp_plotting.py index 81a2664bb..08a4431a1 100644 --- a/syncopy/plotting/sp_plotting.py +++ b/syncopy/plotting/sp_plotting.py @@ -10,9 +10,9 @@ # Syncopy imports from syncopy import __plt__ from syncopy.shared.errors import SPYWarning -from syncopy.plotting import _singlepanelplot as sp_plot +from syncopy.plotting import _plotting from syncopy.plotting import _helpers as plot_helpers -from syncopy.plotting.config import pltErrMsg +from syncopy.plotting.config import pltErrMsg, pltConfig def plot_AnalogData(data, shifted=True, **show_kwargs): @@ -53,9 +53,9 @@ def plot_AnalogData(data, shifted=True, **show_kwargs): if shifted: data_y = plot_helpers.shift_multichan(data_y) - fig, ax = sp_plot.mk_line_figax() + fig, ax = _plotting.mk_line_figax() - sp_plot.plot_lines(ax, data_x, data_y, label=labels) + _plotting.plot_lines(ax, data_x, data_y, label=labels) def plot_SpectralData(data, **show_kwargs): @@ -93,13 +93,13 @@ def plot_SpectralData(data, **show_kwargs): SPYWarning("Please select a single channel for plotting!") return # here we always need a new axes - fig, ax = sp_plot.mk_img_figax(title=f'{label}') + fig, ax = _plotting.mk_img_figax() time = plot_helpers.parse_toi(data, trl, show_kwargs) # dimord is time x taper x freq x channel # need freq x time for plotting data_yx = data.show(**show_kwargs).T - sp_plot.plot_tfreq(ax, data_yx, time, data.freq) + _plotting.plot_tfreq(ax, data_yx, time, data.freq) # just a line plot else: # get the data to plot @@ -109,10 +109,11 @@ def plot_SpectralData(data, **show_kwargs): # multiple channels? labels = plot_helpers.parse_channel(data, show_kwargs) - fig, ax = sp_plot.mk_line_figax(xlabel='frequency (Hz)', - ylabel='power (dB)') + fig, ax = _plotting.mk_line_figax(xlabel='frequency (Hz)', + ylabel='power (dB)') - sp_plot.plot_lines(ax, data_x, data_y, label=labels) + _plotting.plot_lines(ax, data_x, data_y, label=labels) + ax.set_title(label, fontsize=pltConfig['sTitleSize']) def plot_CrossSpectralData(data, **show_kwargs): @@ -172,5 +173,5 @@ def plot_CrossSpectralData(data, **show_kwargs): # persisten axes allows for plotting different # channel combinations into the same figure if not hasattr(data, 'ax'): - fig, data.ax = sp_plot.mk_line_figax(xlabel, ylabel) - sp_plot.plot_lines(data.ax, data_x, data_y, label=label) + fig, data.ax = _plotting.mk_line_figax(xlabel, ylabel) + _plotting.plot_lines(data.ax, data_x, data_y, label=label) From 9275069b88afbee97525915f2ceb0b99dd681221 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 28 Mar 2022 18:18:27 +0200 Subject: [PATCH 131/166] CHG: Add toplevel multipanelplot - added the spy.multipanelplot() adapter Changes to be committed: modified: syncopy/datatype/continuous_data.py modified: syncopy/plotting/spy_plotting.py --- syncopy/datatype/continuous_data.py | 3 +++ syncopy/plotting/spy_plotting.py | 21 ++++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/syncopy/datatype/continuous_data.py b/syncopy/datatype/continuous_data.py index 61d6790b5..02f0a3d06 100644 --- a/syncopy/datatype/continuous_data.py +++ b/syncopy/datatype/continuous_data.py @@ -400,6 +400,9 @@ def __init__(self, data=None, channel=None, samplerate=None, **kwargs): def singlepanelplot(self): raise NotImplementedError + def multipanelplot(self): + raise NotImplementedError + class AnalogData(ContinuousData): """Multi-channel, uniformly-sampled, analog (real float) data diff --git a/syncopy/plotting/spy_plotting.py b/syncopy/plotting/spy_plotting.py index f7d2cdccc..fd5bd5d50 100644 --- a/syncopy/plotting/spy_plotting.py +++ b/syncopy/plotting/spy_plotting.py @@ -7,7 +7,7 @@ from syncopy.plotting.config import pltErrMsg from syncopy.shared.errors import SPYWarning -__all__ = ['singlepanelplot'] +__all__ = ['singlepanelplot', 'multipanelplot'] def singlepanelplot(data, **show_kwargs): @@ -27,3 +27,22 @@ def singlepanelplot(data, **show_kwargs): return data.singlepanelplot(**show_kwargs) + + +def multipanelplot(data, **show_kwargs): + + """ + This is just an adapter to call the + plotting methods of the respective datatype + + Parameters + ---------- + data : an instance derived from :class:`~syncopy.datatype.base_data` + show_kwargs : :func:`~syncopy.datatype.methods.show.show` arguments + """ + + if not __plt__: + SPYWarning(pltErrMsg) + return + + data.multipanelplot(**show_kwargs) From 5b5a4cb8ab4db3a5a3a677e5cb5108b57a02cc7b Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 28 Mar 2022 18:12:24 +0200 Subject: [PATCH 132/166] NEW: Multipanelplots - both for line and image plots - given the show restrictions for cross spectral data, multipanelplot does not have a use case there atm On branch rework-plotting Your branch is up to date with 'origin/rework-plotting'. Changes to be committed: modified: syncopy/datatype/continuous_data.py modified: syncopy/plotting/_helpers.py new file: syncopy/plotting/_plotting.py deleted: syncopy/plotting/_singlepanelplot.py modified: syncopy/plotting/config.py new file: syncopy/plotting/mp_plotting.py modified: syncopy/plotting/sp_plotting.py --- syncopy/datatype/continuous_data.py | 10 +- syncopy/plotting/_helpers.py | 40 ++++- syncopy/plotting/_plotting.py | 153 ++++++++++++++++++ syncopy/plotting/config.py | 20 +-- .../{sp_plotting.py => mp_plotting.py} | 97 +++++++---- 5 files changed, 274 insertions(+), 46 deletions(-) create mode 100644 syncopy/plotting/_plotting.py rename syncopy/plotting/{sp_plotting.py => mp_plotting.py} (56%) diff --git a/syncopy/datatype/continuous_data.py b/syncopy/datatype/continuous_data.py index 71f621e37..61d6790b5 100644 --- a/syncopy/datatype/continuous_data.py +++ b/syncopy/datatype/continuous_data.py @@ -20,7 +20,7 @@ from syncopy.shared.parsers import scalar_parser, array_parser from syncopy.shared.errors import SPYValueError, SPYWarning from syncopy.shared.tools import best_match -from syncopy.plotting import sp_plotting +from syncopy.plotting import sp_plotting, mp_plotting __all__ = ["AnalogData", "SpectralData", "CrossSpectralData"] @@ -481,6 +481,10 @@ def singlepanelplot(self, shifted=True, **show_kwargs): sp_plotting.plot_AnalogData(self, shifted, **show_kwargs) + def multipanelplot(self, **show_kwargs): + + mp_plotting.plot_AnalogData(self, **show_kwargs) + class SpectralData(ContinuousData): """ @@ -625,6 +629,10 @@ def singlepanelplot(self, **show_kwargs): sp_plotting.plot_SpectralData(self, **show_kwargs) + def multipanelplot(self, **show_kwargs): + + mp_plotting.plot_SpectralData(self, **show_kwargs) + class CrossSpectralData(ContinuousData): """ diff --git a/syncopy/plotting/_helpers.py b/syncopy/plotting/_helpers.py index e3aab6852..29ec9b22d 100644 --- a/syncopy/plotting/_helpers.py +++ b/syncopy/plotting/_helpers.py @@ -119,7 +119,8 @@ def shift_multichan(data_y): # shift even further if next channel # dips below 0 offsets += np.abs(data_y.min(axis=0)[1:]) - offsets = np.r_[0, offsets] * 1.1 + offsets = np.cumsum(np.r_[0, offsets] * 1.1) + print(offsets) data_y += offsets return data_y @@ -138,3 +139,40 @@ def get_method(dataobject): if match: meth_str = match.group(1) return meth_str + + +def calc_multi_layout(nAx): + + """ + Given the total numbers of + axes `nAx` create the nrows, ncols + layout. In case of `nAx` being prime, + augment by 1 to rather leave an + empty plot than to have say a (17, 1) layout.. + """ + + # This works as well as long + # as `nAx` isn't prime :] + # so we have to augment by 1 if that's the case + if nAx % 2 != 0: + ncols = int(np.sqrt(nAx)) # this is max pltConfig["mMaxYaxes"] + nrows = ncols + while(ncols * nrows < nAx): + ncols += 1 + nrows = int(nAx / ncols) + # nAx was prime and too big + # for one plotting row + if ncols == nAx and nAx > 7: + nAx += 1 + # no elif to capture possibly incremented nAx + if nAx % 2 == 0 and nAx > 2: + ncols = int(np.sqrt(nAx)) # this is max pltConfig["mMaxYaxes"] + nrows = ncols + while(ncols * nrows < nAx): + nrows -= 1 + ncols = int(nAx / nrows) + # just two axes + elif nAx == 2: + nrows, ncols = 1, 2 + + return nrows, ncols diff --git a/syncopy/plotting/_plotting.py b/syncopy/plotting/_plotting.py new file mode 100644 index 000000000..a4155f2aa --- /dev/null +++ b/syncopy/plotting/_plotting.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +# +# Syncopy plotting backend +# + +from syncopy.plotting.config import pltConfig +from syncopy import __plt__ + +if __plt__: + import matplotlib.pyplot as ppl + + +# -- 2d-line plots -- + +def mk_line_figax(xlabel='time (s)', ylabel='signal (a.u.)'): + + """ + Create the figure and axis for a + standard 2d-line plot + """ + + fig, ax = ppl.subplots(figsize=pltConfig['sFigSize']) + # Hide the right and top spines + ax.spines['right'].set_visible(False) + ax.spines['top'].set_visible(False) + ax.tick_params(axis='both', which='major', + labelsize=pltConfig['sTickSize']) + + ax.set_xlabel(xlabel, fontsize=pltConfig['sLabelSize']) + ax.set_ylabel(ylabel, fontsize=pltConfig['sLabelSize']) + + return fig, ax + + +def mk_multi_line_figax(nrows, ncols, xlabel='time (s)', ylabel='signal (a.u.)'): + + """ + Create the figure and axes for a + multipanel 2d-line plot + """ + + # ncols and nrows get + # restricted via the plotting frontend + x_size = ncols * pltConfig['mXSize'] + y_size = nrows * pltConfig['mYSize'] + + fig, axs = ppl.subplots(nrows, ncols, figsize=(x_size, y_size), + sharex=True, sharey=True, squeeze=False) + + # Hide the right and top spines + # and remove all tick labels + for ax in axs.flatten(): + ax.spines['right'].set_visible(False) + ax.spines['top'].set_visible(False) + ax.tick_params(labelsize=0) + + # determine axis layout + y_left = axs[:, 0] + x_bottom = axs[-1, :] + + # write tick and axis labels only on outer axes to save space + for ax in y_left: + ax.tick_params(labelsize=pltConfig['mTickSize']) + ax.set_ylabel(ylabel, fontsize=pltConfig['mLabelSize']) + + for ax in x_bottom: + ax.tick_params(labelsize=pltConfig['mTickSize']) + ax.set_xlabel(xlabel, fontsize=pltConfig['mLabelSize']) + + return fig, axs + + +def plot_lines(ax, data_x, data_y, leg_fontsize=pltConfig['sLegendSize'], **pkwargs): + + if 'alpha' not in pkwargs: + ax.plot(data_x, data_y, alpha=0.9, **pkwargs) + else: + ax.plot(data_x, data_y, **pkwargs) + if 'label' in pkwargs: + ax.legend(ncol=2, loc='upper right', + fontsize=leg_fontsize) + # make room for the legend + mn, mx = ax.get_ylim() + ax.set_ylim((mn, 1.1 * mx)) + + +# -- image plots -- + +def mk_img_figax(xlabel='time (s)', ylabel='frequency (Hz)', title=''): + + """ + Create the figure and axes for an + image plot with `imshow` + """ + + fig, ax = ppl.subplots(figsize=pltConfig['sFigSize']) + + ax.tick_params(axis='both', which='major', + labelsize=pltConfig['sTickSize']) + ax.set_xlabel(xlabel, fontsize=pltConfig['sLabelSize']) + ax.set_ylabel(ylabel, fontsize=pltConfig['sLabelSize']) + + return fig, ax + + +def mk_multi_img_figax(nrows, ncols, xlabel='time (s)', ylabel='frequency (Hz)'): + + """ + Create the figure and axes for an + image plot with `imshow` for multiple + sub plots + """ + # ncols and nrows get + # restricted via the plotting frontend + x_size = ncols * pltConfig['mXSize'] + y_size = nrows * pltConfig['mYSize'] + + fig, axs = ppl.subplots(nrows, ncols, figsize=(x_size, y_size), + sharex=True, sharey=True, squeeze=False) + + # determine axis layout + y_left = axs[:, 0] + x_bottom = axs[-1, :] + + # write tick and axis labels only on outer axes to save space + for ax in y_left: + ax.tick_params(labelsize=pltConfig['mTickSize']) + ax.set_ylabel(ylabel, fontsize=pltConfig['mLabelSize']) + + for ax in x_bottom: + ax.tick_params(labelsize=pltConfig['mTickSize']) + ax.set_xlabel(xlabel, fontsize=pltConfig['mLabelSize']) + + return fig, axs + + +def plot_tfreq(ax, data_yx, times, freqs, title=''): + + """ + Plot time frequency data on a 2d grid, expects standard + row-column (freq-time) axis ordering. + + Needs frequencies (`freqs`) and sampling rate (`fs`) + for correct units. + """ + + # extent is defined in xy order + df = freqs[1] - freqs[0] + extent = [times[0], times[-1], + freqs[0] - df / 2, freqs[-1] - df / 2] + + ax.imshow(data_yx[::-1], aspect='auto', cmap=pltConfig['cmap'], + extent=extent) diff --git a/syncopy/plotting/config.py b/syncopy/plotting/config.py index 0e2802d80..4b1b6e012 100644 --- a/syncopy/plotting/config.py +++ b/syncopy/plotting/config.py @@ -9,21 +9,23 @@ import matplotlib as mpl mpl.style.use('seaborn-colorblind') # a hint of gray - mpl.rcParams['figure.facecolor'] = '#f5faf6' - mpl.rcParams['figure.edgecolor'] = '#f5faf6' - mpl.rcParams['axes.facecolor'] = '#f5faf6' + mpl.rcParams['figure.facecolor'] = '#fcfcfc' + mpl.rcParams['figure.edgecolor'] = '#fcfcfc' + mpl.rcParams['axes.facecolor'] = '#fcfcfc' # Global style settings for single-/multi-plots pltConfig = {"sTitleSize": 15, "sLabelSize": 16, "sTickSize": 12, "sLegendSize": 12, - "sFigSize": (6.4, 3.2), - "mTitleSize": 14, - "mLabelSize": 14, - "mTickSize": 10, - "mLegendSize": 12, - "mFigSize": (10, 6.8), + "sFigSize": (6.4, 4.8), + "mTitleSize": 12.5, + "mLabelSize": 12.5, + "mTickSize": 11, + "mLegendSize": 11, + "mXSize": 3.2, + "mYSize": 2.4, + "mMaxAxes": 35, "cmap": "magma"} # Global consistent error message if matplotlib is missing diff --git a/syncopy/plotting/sp_plotting.py b/syncopy/plotting/mp_plotting.py similarity index 56% rename from syncopy/plotting/sp_plotting.py rename to syncopy/plotting/mp_plotting.py index 0865f8365..06d606da2 100644 --- a/syncopy/plotting/sp_plotting.py +++ b/syncopy/plotting/mp_plotting.py @@ -10,16 +10,16 @@ # Syncopy imports from syncopy import __plt__ from syncopy.shared.errors import SPYWarning -from syncopy.plotting import _singlepanelplot as sp_plot +from syncopy.plotting import _plotting from syncopy.plotting import _helpers as plot_helpers -from syncopy.plotting.config import pltErrMsg +from syncopy.plotting.config import pltErrMsg, pltConfig def plot_AnalogData(data, shifted=True, **show_kwargs): """ - The probably simplest plot, a 2d-line - plot of selected channels + The probably simplest plot, 2d-line + plots of selected channels, one axis for each channel Parameters ---------- @@ -47,24 +47,37 @@ def plot_AnalogData(data, shifted=True, **show_kwargs): # multiple channels? labels = plot_helpers.parse_channel(data, show_kwargs) + nAx = 1 if isinstance(labels, str) else len(labels) - # plot multiple channels with offsets for - # better visibility - if shifted: - data_y = plot_helpers.shift_multichan(data_y) + if nAx < 2: + SPYWarning("Please select at least two channels for a multipanelplot!") + return + elif nAx > pltConfig['mMaxAxes']: + SPYWarning("Please select max. {pltConfig['mMaxAxes']} channels for a multipanelplot!") + return + else: + # determine axes layout, prefer columns over rows due to display aspect ratio + nrows, ncols = plot_helpers.calc_multi_layout(nAx) + + fig, axs = _plotting.mk_multi_line_figax(nrows, ncols) - fig, ax = sp_plot.mk_line_figax() + for chan_dat, ax, label in zip(data_y.T, axs.flatten(), labels): + _plotting.plot_lines(ax, data_x, chan_dat, label=label, leg_fontsize=pltConfig['mLegendSize']) + + # delete empty plot due to grid extension + # because of prime nAx -> can be maximally 1 plot + if ncols * nrows > nAx: + axs.flatten()[-1].remove() - sp_plot.plot_lines(ax, data_x, data_y, label=labels) fig.tight_layout() def plot_SpectralData(data, **show_kwargs): """ - Plot either a 2d-line plot in case of - singleton time axis or an image plot - for time-frequency spectra. + Plot either 2d-line plots in case of + singleton time axis or image plots + for time-frequency spectra, one for each channel. Parameters ---------- @@ -85,35 +98,50 @@ def plot_SpectralData(data, **show_kwargs): elif len(data.trials) == 1: trl = 0 + labels = plot_helpers.parse_channel(data, show_kwargs) + nAx = 1 if isinstance(labels, str) else len(labels) + + if nAx < 2: + SPYWarning("Please select at least two channels for a multipanelplot!") + return + elif nAx > pltConfig['mMaxAxes']: + SPYWarning("Please select max. {pltConfig['mMaxAxes']} channels for a multipanelplot!") + return + else: + # determine axes layout, prefer columns over rows due to display aspect ratio + nrows, ncols = plot_helpers.calc_multi_layout(nAx) + # how got the spectrum computed method = plot_helpers.get_method(data) - if method in ('wavelet', 'superlet', 'mtmconvol'): - # multiple channels? - label = plot_helpers.parse_channel(data, show_kwargs) - if not isinstance(label, str): - SPYWarning("Please select a single channel for plotting!") - return - # here we always need a new axes - fig, ax = sp_plot.mk_img_figax(title=f'{label}') + if method in ('wavelet', 'superlet', 'mtmconvol'): + fig, axs = _plotting.mk_multi_img_figax(nrows, ncols) time = plot_helpers.parse_toi(data, trl, show_kwargs) - # dimord is time x taper x freq x channel - # need freq x time for plotting - data_yx = data.show(**show_kwargs).T - sp_plot.plot_tfreq(ax, data_yx, time, data.freq) + # dimord is time x freq x channel + # need freq x time each for plotting + data_cyx = data.show(**show_kwargs).T + for data_yx, ax, label in zip(data_cyx, axs.flatten(), labels): + _plotting.plot_tfreq(ax, data_yx, time, data.freq) + ax.set_title(label, fontsize=pltConfig['mTitleSize']) + fig.tight_layout() + fig.subplots_adjust(wspace=0.05) + # just a line plot else: # get the data to plot data_x = plot_helpers.parse_foi(data, show_kwargs) data_y = np.log10(data.show(**show_kwargs)) - # multiple channels? - labels = plot_helpers.parse_channel(data, show_kwargs) + fig, axs = _plotting.mk_multi_line_figax(nrows, ncols, xlabel='frequency (Hz)', + ylabel='power (dB)') - fig, ax = sp_plot.mk_line_figax(xlabel='frequency (Hz)', - ylabel='power (dB)') + for chan_dat, ax, label in zip(data_y.T, axs.flatten(), labels): + _plotting.plot_lines(ax, data_x, chan_dat, label=label, leg_fontsize=pltConfig['mLegendSize']) - sp_plot.plot_lines(ax, data_x, data_y, label=labels) + # delete empty plot due to grid extension + # because of prime nAx -> can be maximally 1 plot + if ncols * nrows > nAx: + axs.flatten()[-1].remove() fig.tight_layout() @@ -159,10 +187,10 @@ def plot_CrossSpectralData(data, **show_kwargs): label = rf"channel{chi} - channel{chj}" data_x = plot_helpers.parse_foi(data, show_kwargs) elif method == 'corr': - xlabel = 'lag (s)' + xlabel = 'lag' ylabel = 'correlation' label = rf"channel{chi} - channel{chj}" - data_x = plot_helpers.parse_toi(data, trl, show_kwargs) + data_x = plot_helpers.parse_toi(data, show_kwargs) # that's all the methods we got so far else: raise NotImplementedError @@ -174,6 +202,5 @@ def plot_CrossSpectralData(data, **show_kwargs): # persisten axes allows for plotting different # channel combinations into the same figure if not hasattr(data, 'ax'): - data.fig, data.ax = sp_plot.mk_line_figax(xlabel, ylabel) - sp_plot.plot_lines(data.ax, data_x, data_y, label=label) - data.fig.tight_layout() + fig, data.ax = _plotting.mk_line_figax(xlabel, ylabel) + _plotting.plot_lines(data.ax, data_x, data_y, label=label) From 7b09e794e8c47c227d16933a0a6d08cab2ee27f0 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 28 Mar 2022 18:18:27 +0200 Subject: [PATCH 133/166] CHG: Add toplevel multipanelplot - added the spy.multipanelplot() adapter Changes to be committed: modified: syncopy/datatype/continuous_data.py modified: syncopy/plotting/spy_plotting.py --- syncopy/datatype/continuous_data.py | 3 +++ syncopy/plotting/spy_plotting.py | 21 ++++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/syncopy/datatype/continuous_data.py b/syncopy/datatype/continuous_data.py index 61d6790b5..02f0a3d06 100644 --- a/syncopy/datatype/continuous_data.py +++ b/syncopy/datatype/continuous_data.py @@ -400,6 +400,9 @@ def __init__(self, data=None, channel=None, samplerate=None, **kwargs): def singlepanelplot(self): raise NotImplementedError + def multipanelplot(self): + raise NotImplementedError + class AnalogData(ContinuousData): """Multi-channel, uniformly-sampled, analog (real float) data diff --git a/syncopy/plotting/spy_plotting.py b/syncopy/plotting/spy_plotting.py index f7d2cdccc..fd5bd5d50 100644 --- a/syncopy/plotting/spy_plotting.py +++ b/syncopy/plotting/spy_plotting.py @@ -7,7 +7,7 @@ from syncopy.plotting.config import pltErrMsg from syncopy.shared.errors import SPYWarning -__all__ = ['singlepanelplot'] +__all__ = ['singlepanelplot', 'multipanelplot'] def singlepanelplot(data, **show_kwargs): @@ -27,3 +27,22 @@ def singlepanelplot(data, **show_kwargs): return data.singlepanelplot(**show_kwargs) + + +def multipanelplot(data, **show_kwargs): + + """ + This is just an adapter to call the + plotting methods of the respective datatype + + Parameters + ---------- + data : an instance derived from :class:`~syncopy.datatype.base_data` + show_kwargs : :func:`~syncopy.datatype.methods.show.show` arguments + """ + + if not __plt__: + SPYWarning(pltErrMsg) + return + + data.multipanelplot(**show_kwargs) From fe04114dde558c16d82c886979d83e7e8d9b8569 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 28 Mar 2022 18:32:06 +0200 Subject: [PATCH 134/166] FIX: resurrect sp_plotting --- syncopy/plotting/sp_plotting.py | 177 ++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 syncopy/plotting/sp_plotting.py diff --git a/syncopy/plotting/sp_plotting.py b/syncopy/plotting/sp_plotting.py new file mode 100644 index 000000000..08a4431a1 --- /dev/null +++ b/syncopy/plotting/sp_plotting.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +# +# The singlepanel plotting functions for Syncopy +# data types +# + +# Builtin/3rd party package imports +import numpy as np + +# Syncopy imports +from syncopy import __plt__ +from syncopy.shared.errors import SPYWarning +from syncopy.plotting import _plotting +from syncopy.plotting import _helpers as plot_helpers +from syncopy.plotting.config import pltErrMsg, pltConfig + + +def plot_AnalogData(data, shifted=True, **show_kwargs): + + """ + The probably simplest plot, a 2d-line + plot of selected channels + + Parameters + ---------- + data : :class:`~syncopy.datatype.AnalogData` + show_kwargs : :func:`~syncopy.datatype.methods.show.show` arguments + """ + + if not __plt__: + SPYWarning(pltErrMsg) + return + + # right now we have to enforce + # single trial selection only + trl = show_kwargs.get('trials', None) + if not isinstance(trl, int) and len(data.trials) > 1: + SPYWarning("Please select a single trial for plotting!") + return + # only 1 trial so no explicit selection needed + elif len(data.trials) == 1: + trl = 0 + + # get the data to plot + data_x = plot_helpers.parse_toi(data, trl, show_kwargs) + data_y = data.show(**show_kwargs) + + # multiple channels? + labels = plot_helpers.parse_channel(data, show_kwargs) + + # plot multiple channels with offsets for + # better visibility + if shifted: + data_y = plot_helpers.shift_multichan(data_y) + + fig, ax = _plotting.mk_line_figax() + + _plotting.plot_lines(ax, data_x, data_y, label=labels) + + +def plot_SpectralData(data, **show_kwargs): + + """ + Plot either a 2d-line plot in case of + singleton time axis or an image plot + for time-frequency spectra. + + Parameters + ---------- + data : :class:`~syncopy.datatype.SpectralData` + show_kwargs : :func:`~syncopy.datatype.methods.show.show` arguments + """ + + if not __plt__: + SPYWarning(pltErrMsg) + return + + # right now we have to enforce + # single trial selection only + trl = show_kwargs.get('trials', None) + if not isinstance(trl, int) and len(data.trials) > 1: + SPYWarning("Please select a single trial for plotting!") + return + elif len(data.trials) == 1: + trl = 0 + + # how got the spectrum computed + method = plot_helpers.get_method(data) + if method in ('wavelet', 'superlet', 'mtmconvol'): + # multiple channels? + label = plot_helpers.parse_channel(data, show_kwargs) + if not isinstance(label, str): + SPYWarning("Please select a single channel for plotting!") + return + # here we always need a new axes + fig, ax = _plotting.mk_img_figax() + + time = plot_helpers.parse_toi(data, trl, show_kwargs) + # dimord is time x taper x freq x channel + # need freq x time for plotting + data_yx = data.show(**show_kwargs).T + _plotting.plot_tfreq(ax, data_yx, time, data.freq) + # just a line plot + else: + # get the data to plot + data_x = plot_helpers.parse_foi(data, show_kwargs) + data_y = np.log10(data.show(**show_kwargs)) + + # multiple channels? + labels = plot_helpers.parse_channel(data, show_kwargs) + + fig, ax = _plotting.mk_line_figax(xlabel='frequency (Hz)', + ylabel='power (dB)') + + _plotting.plot_lines(ax, data_x, data_y, label=labels) + ax.set_title(label, fontsize=pltConfig['sTitleSize']) + + +def plot_CrossSpectralData(data, **show_kwargs): + """ + Plot 2d-line plots for the different connectivity measures. + + Parameters + ---------- + data : :class:`~syncopy.datatype.CrossSpectralData` + show_kwargs : :func:`~syncopy.datatype.methods.show.show` arguments + """ + + if not __plt__: + SPYWarning(pltErrMsg) + return + + # right now we have to enforce + # single trial selection only + trl = show_kwargs.get('trials', None) + if not isinstance(trl, int) and len(data.trials) > 1: + SPYWarning("Please select a single trial for plotting!") + return + elif len(data.trials) == 1: + trl = 0 + + # what channel combination + if 'channel_i' not in show_kwargs or 'channel_j' not in show_kwargs: + SPYWarning("Please select a channel combination for plotting!") + return + chi, chj = show_kwargs['channel_i'], show_kwargs['channel_j'] + + # what data do we have? + method = plot_helpers.get_method(data) + if method == 'granger': + xlabel = 'frequency (Hz)' + ylabel = 'Granger causality' + label = rf"channel{chi} $\rightarrow$ channel{chj}" + data_x = plot_helpers.parse_foi(data, show_kwargs) + elif method == 'coh': + xlabel = 'frequency (Hz)' + ylabel = 'coherence' + label = rf"channel{chi} - channel{chj}" + data_x = plot_helpers.parse_foi(data, show_kwargs) + elif method == 'corr': + xlabel = 'lag' + ylabel = 'correlation' + label = rf"channel{chi} - channel{chj}" + data_x = plot_helpers.parse_toi(data, show_kwargs) + # that's all the methods we got so far + else: + raise NotImplementedError + + # get the data to plot + data_y = data.show(**show_kwargs) + + # create the axes and figure if needed + # persisten axes allows for plotting different + # channel combinations into the same figure + if not hasattr(data, 'ax'): + fig, data.ax = _plotting.mk_line_figax(xlabel, ylabel) + _plotting.plot_lines(data.ax, data_x, data_y, label=label) From 5f3bebb0bd4da33c121a7e6119b9e42f0cdfb14c Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 28 Mar 2022 18:34:47 +0200 Subject: [PATCH 135/166] FIX: remove obsolete file --- syncopy/plotting/_singlepanelplot.py | 85 ---------------------------- 1 file changed, 85 deletions(-) delete mode 100644 syncopy/plotting/_singlepanelplot.py diff --git a/syncopy/plotting/_singlepanelplot.py b/syncopy/plotting/_singlepanelplot.py deleted file mode 100644 index 1a1021c7a..000000000 --- a/syncopy/plotting/_singlepanelplot.py +++ /dev/null @@ -1,85 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Syncopy singlepanel plot backend -# - -from syncopy.plotting.config import pltConfig, pltErrMsg -from syncopy import __plt__ - -if __plt__: - import matplotlib.pyplot as ppl - - -# -- 2d-line plots -- - -def mk_line_figax(xlabel='time (s)', ylabel='signal (a.u.)'): - - """ - Create the figure and axes for a - standard 2d-line plot - """ - - fig, ax = ppl.subplots(figsize=pltConfig['sFigSize']) - # Hide the right and top spines - ax.spines['right'].set_visible(False) - ax.spines['top'].set_visible(False) - ax.tick_params(axis='both', which='major', - labelsize=pltConfig['sTickSize']) - - ax.set_xlabel(xlabel, fontsize=pltConfig['sLabelSize']) - ax.set_ylabel(ylabel, fontsize=pltConfig['sLabelSize']) - - return fig, ax - - -def plot_lines(ax, data_x, data_y, **pkwargs): - - if 'alpha' not in pkwargs: - ax.plot(data_x, data_y, alpha=0.9, **pkwargs) - else: - ax.plot(data_x, data_y, **pkwargs) - if 'label' in pkwargs: - ax.legend(ncol=2, loc='best', - fontsize=pltConfig['sLegendSize']) - # make room for the legend - mn, mx = ax.get_ylim() - ax.set_ylim((mn, mx + abs(.1 * mx))) - - -# -- image plots -- - -def mk_img_figax(xlabel='time (s)', ylabel='frequency (Hz)', title=''): - - """ - Create the figure and axes for an - image plot with `imshow` - """ - - fig, ax = ppl.subplots(figsize=pltConfig['sFigSize']) - - ax.tick_params(axis='both', which='major', - labelsize=pltConfig['sTickSize']) - ax.set_xlabel(xlabel, fontsize=pltConfig['sLabelSize']) - ax.set_ylabel(ylabel, fontsize=pltConfig['sLabelSize']) - ax.set_title(title, fontsize=pltConfig['sTitleSize']) - - return fig, ax - - -def plot_tfreq(ax, data_yx, times, freqs, **pkwargs): - - """ - Plot time frequency data on a 2d grid, expects standard - row-column (freq-time) axis ordering. - - Needs frequencies (`freqs`) and sampling rate (`fs`) - for correct units. - """ - - # extent is defined in xy order - df = freqs[1] - freqs[0] - extent = [times[0], times[-1], - freqs[0] - df / 2, freqs[-1] - df / 2] - - ax.imshow(data_yx, aspect='auto', cmap=pltConfig['cmap'], - extent=extent) From 0434c5203fe4ff40484a9bfdfd05b09beab0bf46 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Tue, 29 Mar 2022 11:43:12 +0200 Subject: [PATCH 136/166] CHG: Test also wilson HZH error - instead of only testing for accuracy of the direct factorization test also for the HZH reconstitution of the CSD Changes to be committed: modified: syncopy/nwanalysis/wilson_sf.py modified: syncopy/tests/backend/test_conn.py --- syncopy/nwanalysis/wilson_sf.py | 7 +++++++ syncopy/tests/backend/test_conn.py | 16 ++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/syncopy/nwanalysis/wilson_sf.py b/syncopy/nwanalysis/wilson_sf.py index dd69a0809..3251796c6 100644 --- a/syncopy/nwanalysis/wilson_sf.py +++ b/syncopy/nwanalysis/wilson_sf.py @@ -174,6 +174,13 @@ def _plusOperator(g): # --- End of Wilson's Algorithm --- +def max_rel_err(A, B): + + err = np.abs(A - B) + err = (err / np.abs(A)).max() + return err + + def regularize_csd(CSD, cond_max=1e6, eps_max=1e-3, nSteps=15): """ diff --git a/syncopy/tests/backend/test_conn.py b/syncopy/tests/backend/test_conn.py index 527dd491c..3c11b9232 100644 --- a/syncopy/tests/backend/test_conn.py +++ b/syncopy/tests/backend/test_conn.py @@ -6,7 +6,11 @@ from syncopy.tests import synth_data from syncopy.nwanalysis import csd from syncopy.nwanalysis import ST_compRoutines as stCR -from syncopy.nwanalysis.wilson_sf import wilson_sf, regularize_csd +from syncopy.nwanalysis.wilson_sf import ( + wilson_sf, + regularize_csd, + max_rel_err +) from syncopy.nwanalysis.granger import granger @@ -173,6 +177,12 @@ def test_wilson(): inbuild, we just need to check for convergence. """ + # -- test error testing routine + + A = np.random.randn(10, 10) + 1j * np.random.randn(10, 10) + + assert max_rel_err(A, A + A * 1e-16) < 1e-15 + # --- create test data --- fs = 5000 nChannels = 2 @@ -194,7 +204,7 @@ def test_wilson(): # --- factorize CSD with Wilson's algorithm --- - H, Sigma, conv = wilson_sf(CSDav, rtol=1e-9) + H, Sigma, conv = wilson_sf(CSDav, rtol=1e-12) # converged - \Psi \Psi^* \approx CSD, # with relative error <= rtol? @@ -202,6 +212,8 @@ def test_wilson(): # reconstitute CSDfac = H @ Sigma @ H.conj().transpose(0, 2, 1) + err = max_rel_err(CSDav, CSDfac) + assert err < 1e-12 fig, ax = ppl.subplots(figsize=(6, 4)) ax.set_xlabel('frequency (Hz)') From f0831660df2d52f1b95d90a7801d3abc3dd6c98d Mon Sep 17 00:00:00 2001 From: tensionhead Date: Tue, 29 Mar 2022 12:14:14 +0200 Subject: [PATCH 137/166] CHG: Wavelet spectra with new multipanelplot for the quickstart Changes to be committed: modified: doc/source/quickstart/wavelet_spec.png modified: syncopy/plotting/_plotting.py modified: syncopy/plotting/mp_plotting.py --- doc/source/quickstart/wavelet_spec.png | Bin 93344 -> 41488 bytes syncopy/plotting/_plotting.py | 6 +++--- syncopy/plotting/mp_plotting.py | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/source/quickstart/wavelet_spec.png b/doc/source/quickstart/wavelet_spec.png index 103beb2bc19730efcdcc54b9154ada5f4543bed4..092d911344a1da40c980839225038863d7a9dcc5 100644 GIT binary patch literal 41488 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�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 literal 93344 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�kJqBj!+qRB`m&>x@&0hGm@e# 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 JN)*ie{{w>Qm#zQ+ diff --git a/syncopy/plotting/_plotting.py b/syncopy/plotting/_plotting.py index a4155f2aa..bd94a7ed8 100644 --- a/syncopy/plotting/_plotting.py +++ b/syncopy/plotting/_plotting.py @@ -86,7 +86,7 @@ def plot_lines(ax, data_x, data_y, leg_fontsize=pltConfig['sLegendSize'], **pkwa # -- image plots -- -def mk_img_figax(xlabel='time (s)', ylabel='frequency (Hz)', title=''): +def mk_img_figax(xlabel='time (s)', ylabel='frequency (Hz)'): """ Create the figure and axes for an @@ -134,7 +134,7 @@ def mk_multi_img_figax(nrows, ncols, xlabel='time (s)', ylabel='frequency (Hz)') return fig, axs -def plot_tfreq(ax, data_yx, times, freqs, title=''): +def plot_tfreq(ax, data_yx, times, freqs, **pkwargs): """ Plot time frequency data on a 2d grid, expects standard @@ -150,4 +150,4 @@ def plot_tfreq(ax, data_yx, times, freqs, title=''): freqs[0] - df / 2, freqs[-1] - df / 2] ax.imshow(data_yx[::-1], aspect='auto', cmap=pltConfig['cmap'], - extent=extent) + extent=extent, **pkwargs) diff --git a/syncopy/plotting/mp_plotting.py b/syncopy/plotting/mp_plotting.py index 06d606da2..a24a27acc 100644 --- a/syncopy/plotting/mp_plotting.py +++ b/syncopy/plotting/mp_plotting.py @@ -120,8 +120,9 @@ def plot_SpectralData(data, **show_kwargs): # dimord is time x freq x channel # need freq x time each for plotting data_cyx = data.show(**show_kwargs).T + maxP = data_cyx.max() for data_yx, ax, label in zip(data_cyx, axs.flatten(), labels): - _plotting.plot_tfreq(ax, data_yx, time, data.freq) + _plotting.plot_tfreq(ax, data_yx, time, data.freq, vmax=maxP) ax.set_title(label, fontsize=pltConfig['mTitleSize']) fig.tight_layout() fig.subplots_adjust(wspace=0.05) From ed1b6e07d0383c0dacb908424f43b2eef1aa3759 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Thu, 31 Mar 2022 16:21:32 +0200 Subject: [PATCH 138/166] CHG: Another Wilson tweak - I forgot to take the transpose for the negative frequencies when initializing, with this change not only is the factorization still very exact, but now also the Transfer functions match the groundtruth (thx to Mukesh) very good (yet only to a factor of around 1-e3) Changes to be committed: modified: syncopy/nwanalysis/wilson_sf.py --- syncopy/nwanalysis/wilson_sf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/syncopy/nwanalysis/wilson_sf.py b/syncopy/nwanalysis/wilson_sf.py index 3251796c6..2aaff0d7e 100644 --- a/syncopy/nwanalysis/wilson_sf.py +++ b/syncopy/nwanalysis/wilson_sf.py @@ -57,7 +57,7 @@ def wilson_sf(CSD, nIter=100, rtol=1e-9, direct_inversion=True): Ident = np.eye(*CSD.shape[1:]) # attach negative frequencies - CSD = np.r_[CSD, CSD[nFreq:1:-1]] + CSD = np.r_[CSD, CSD[nFreq:1:-1].conj()] # nChannel x nChannel psi0 = _psi0_initial(CSD) @@ -65,7 +65,7 @@ def wilson_sf(CSD, nIter=100, rtol=1e-9, direct_inversion=True): # initial choice of psi, constant for all z(~f) psi = np.tile(psi0, (nFreq, 1, 1)) # attach negative frequencies - psi = np.r_[psi, psi[nFreq:1:-1]] + psi = np.r_[psi, psi[nFreq:1:-1].conj()] g = np.zeros(CSD.shape, dtype=np.complex64) converged = False From 8144bf676d5db64a4ecfd77aea45c3458368c11e Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 1 Apr 2022 10:23:48 +0200 Subject: [PATCH 139/166] CHG: Relax coherence tests - phase diffusion may lead to spurious coherence values Changes to be committed: modified: syncopy/tests/test_connectivity.py --- syncopy/tests/test_connectivity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/syncopy/tests/test_connectivity.py b/syncopy/tests/test_connectivity.py index 24a8917e7..1eefacb33 100644 --- a/syncopy/tests/test_connectivity.py +++ b/syncopy/tests/test_connectivity.py @@ -211,10 +211,10 @@ def test_coh_solution(self, **kwargs): # is low coherence null_idx = (res.freq < self.f1 - 5) | (res.freq > self.f1 + 5) null_idx *= (res.freq < self.f2 - 5) | (res.freq > self.f2 + 5) - assert np.all(res.data[0, null_idx, 0, 1] < 0.1) + assert np.all(res.data[0, null_idx, 0, 1] < 0.15) plot_coh(res, 0, 1, label="channel 0-1") - + def test_coh_selections(self): selections = mk_selection_dicts(self.nTrials, From 3f5c1b182498ecad6ab28520c31a85d40b4c5c65 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 1 Apr 2022 11:17:51 +0200 Subject: [PATCH 140/166] WIP: Rectify Changes to be committed: modified: syncopy/preproc/compRoutines.py modified: syncopy/preproc/preprocessing.py --- syncopy/preproc/compRoutines.py | 77 ++++++++++++++++++++++++++++++++ syncopy/preproc/preprocessing.py | 38 ++++++++++++---- 2 files changed, 106 insertions(+), 9 deletions(-) diff --git a/syncopy/preproc/compRoutines.py b/syncopy/preproc/compRoutines.py index 836bdadf3..400701af6 100644 --- a/syncopy/preproc/compRoutines.py +++ b/syncopy/preproc/compRoutines.py @@ -303,3 +303,80 @@ def process_metadata(self, data, out): out.samplerate = data.samplerate out.channel = np.array(data.channel[chanSec]) + + +@unwrap_io +def rectify_cF(dat, noCompute=False): + + """ + Provides straightforward rectification via `np.abs`. + + dat : (N, K) :class:`numpy.ndarray` + Uniformly sampled multi-channel time-series data + noCompute : bool + If `True`, do not perform actual calculation but + instead return expected shape and :class:`numpy.dtype` of output + array. + + Returns + ------- + rectified : (N, K) :class:`~numpy.ndarray` + The rectified signals + + Notes + ----- + This method is intended to be used as + :meth:`~syncopy.shared.computational_routine.ComputationalRoutine.computeFunction` + inside a :class:`~syncopy.shared.computational_routine.ComputationalRoutine`. + Thus, input parameters are presumed to be forwarded from a parent metafunction. + Consequently, this function does **not** perform any error checking and operates + under the assumption that all inputs have been externally validated and cross-checked. + + See also + -------- + `Scipy butterworth documentation `_ + + """ + + # operation does not change the shape + outShape = dat.shape + if noCompute: + return outShape, np.float32 + + return np.abs(dat) + + +class Rectify(ComputationalRoutine): + + """ + Compute class that performs rectification + of :class:`~syncopy.AnalogData` objects + + Sub-class of :class:`~syncopy.shared.computational_routine.ComputationalRoutine`, + see :doc:`/developer/compute_kernels` for technical details on Syncopy's compute + classes and metafunctions. + + See also + -------- + syncopy.preprocessing : parent metafunction + """ + + computeFunction = staticmethod(rectify_cF) + + # 1st argument,the data, gets omitted + valid_kws = list(signature(but_filtering_cF).parameters.keys())[1:] + + def process_metadata(self, data, out): + + # Some index gymnastics to get trial begin/end "samples" + if data._selection is not None: + chanSec = data.selection.channel + trl = data.selection.trialdefinition + else: + chanSec = slice(None) + trl = data.trialdefinition + + out.trialdefinition = trl + + out.samplerate = data.samplerate + out.channel = np.array(data.channel[chanSec]) diff --git a/syncopy/preproc/preprocessing.py b/syncopy/preproc/preprocessing.py index 9258ca9c6..5644ba660 100644 --- a/syncopy/preproc/preprocessing.py +++ b/syncopy/preproc/preprocessing.py @@ -18,7 +18,7 @@ check_passed_kwargs ) -from .compRoutines import But_Filtering, Sinc_Filtering +from .compRoutines import But_Filtering, Sinc_Filtering, Rectify availableFilters = ('but', 'firws') availableFilterTypes = ('lp', 'hp', 'bp', 'bs') @@ -37,10 +37,11 @@ def preprocessing(data, direction=None, window="hamming", polyremoval=None, + rectify=False, **kwargs ): """ - Filtering of time continuous raw data with IIR and FIR filters + Preprocessing of time continuous raw data with IIR and FIR filters data : `~syncopy.AnalogData` A non-empty Syncopy :class:`~syncopy.AnalogData` object @@ -68,6 +69,8 @@ def preprocessing(data, to filtering. A value of 0 corresponds to subtracting the mean ("de-meaning"), ``polyremoval = 1`` removes linear trends (subtracting the least squares fit of a linear polynomial). + rectify : bool, optional + Set to `True` to rectify (after filtering) Returns ------- @@ -118,6 +121,9 @@ def preprocessing(data, if polyremoval is not None: scalar_parser(polyremoval, varname="polyremoval", ntype="int_like", lims=[0, 1]) + if not isinstance(rectify, bool): + SPYValueError("either `True` or `False`", varname='rectify', actual=rectify) + # -- get trial info # if a subset selection is present @@ -220,16 +226,30 @@ def preprocessing(data, polyremoval=polyremoval, timeAxis=timeAxis) - # ------------------------------------ - # Call the chosen ComputationalRoutine - # ------------------------------------ + # ------------------------------------------- + # Call the chosen filter ComputationalRoutine + # ------------------------------------------- - out = AnalogData(dimord=data.dimord) + filtered = AnalogData(dimord=data.dimord) # Perform actual computation filterMethod.initialize(data, - out._stackingDim, + data._stackingDim, chan_per_worker=kwargs.get("chan_per_worker"), keeptrials=True) - filterMethod.compute(data, out, parallel=kwargs.get("parallel"), log_dict=log_dict) - + filterMethod.compute(data, filtered, parallel=kwargs.get("parallel"), log_dict=log_dict) + + # -- check for post processing flags -- + + if rectify: + + rectified = AnalogData(dimord=data.dimord) + rectCR = Rectify() + rectCR.initialize(filtered, + data._stackingDim, + chan_per_worker=kwargs.get("chan_per_worker"), + keeptrials=True) + rectCR.compute(filtered, rectified, + parallel=kwargs.get("parallel"), + log_dict=log_dict) + return out From b64b5a6770dcc6049eb2aa89899494f40375335e Mon Sep 17 00:00:00 2001 From: tensionhead Date: Tue, 5 Apr 2022 14:45:31 +0200 Subject: [PATCH 141/166] WIP: Attach rectified data to filtered data object - probably not the best idea.. Changes to be committed: modified: syncopy/preproc/preprocessing.py --- syncopy/preproc/preprocessing.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/syncopy/preproc/preprocessing.py b/syncopy/preproc/preprocessing.py index 5644ba660..f18e2d91e 100644 --- a/syncopy/preproc/preprocessing.py +++ b/syncopy/preproc/preprocessing.py @@ -251,5 +251,9 @@ def preprocessing(data, rectCR.compute(filtered, rectified, parallel=kwargs.get("parallel"), log_dict=log_dict) - - return out + # not sure if this is the best way, + # `rectified` then keep dangling in + # temporary syncopy files (./spy) folder + filtered.data = rectified.data + + return filtered From eb2beedfeee6e7ed1c554c4245c4c6a4182e3486 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Tue, 5 Apr 2022 15:03:27 +0200 Subject: [PATCH 142/166] NEW: Rectification after filtering - new flag `rectify` for the preprocessing frontend Changes to be committed: modified: syncopy/preproc/compRoutines.py modified: syncopy/preproc/preprocessing.py --- syncopy/preproc/compRoutines.py | 4 ++-- syncopy/preproc/preprocessing.py | 13 ++++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/syncopy/preproc/compRoutines.py b/syncopy/preproc/compRoutines.py index 08549adc4..076fdb325 100644 --- a/syncopy/preproc/compRoutines.py +++ b/syncopy/preproc/compRoutines.py @@ -304,7 +304,7 @@ def process_metadata(self, data, out): @unwrap_io -def rectify_cF(dat, noCompute=False): +def rectify_cF(dat, noCompute=False, chunkShape=None): """ Provides straightforward rectification via `np.abs`. @@ -367,7 +367,7 @@ class Rectify(ComputationalRoutine): def process_metadata(self, data, out): # Some index gymnastics to get trial begin/end "samples" - if data._selection is not None: + if data.selection is not None: chanSec = data.selection.channel trl = data.selection.trialdefinition else: diff --git a/syncopy/preproc/preprocessing.py b/syncopy/preproc/preprocessing.py index 18ebe8173..c6896bf8f 100644 --- a/syncopy/preproc/preprocessing.py +++ b/syncopy/preproc/preprocessing.py @@ -38,6 +38,7 @@ def preprocessing(data, window="hamming", polyremoval=None, rectify=False, + hilbert=False, **kwargs ): """ @@ -251,9 +252,11 @@ def preprocessing(data, rectCR.compute(filtered, rectified, parallel=kwargs.get("parallel"), log_dict=log_dict) - # not sure if this is the best way, - # `rectified` then keep dangling in - # temporary syncopy files (./spy) folder - filtered.data = rectified.data + return rectified - return filtered + elif hilbert: + pass + + # no post-processing + else: + return filtered From 3470bb8114cdfb798526509f09203c848201a8d1 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Tue, 5 Apr 2022 18:10:05 +0200 Subject: [PATCH 143/166] FIX: Ultimate Granger tweak - fixed a 1-off subtlety when mirroring towards negative frequencies - now we get machine precision matching with Mukesh's Matlab implementation Changes to be committed: modified: syncopy/nwanalysis/wilson_sf.py --- syncopy/nwanalysis/wilson_sf.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/syncopy/nwanalysis/wilson_sf.py b/syncopy/nwanalysis/wilson_sf.py index 2aaff0d7e..66257f482 100644 --- a/syncopy/nwanalysis/wilson_sf.py +++ b/syncopy/nwanalysis/wilson_sf.py @@ -57,7 +57,7 @@ def wilson_sf(CSD, nIter=100, rtol=1e-9, direct_inversion=True): Ident = np.eye(*CSD.shape[1:]) # attach negative frequencies - CSD = np.r_[CSD, CSD[nFreq:1:-1].conj()] + CSD = np.r_[CSD, CSD[nFreq - 2:0:-1].conj()] # nChannel x nChannel psi0 = _psi0_initial(CSD) @@ -65,16 +65,24 @@ def wilson_sf(CSD, nIter=100, rtol=1e-9, direct_inversion=True): # initial choice of psi, constant for all z(~f) psi = np.tile(psi0, (nFreq, 1, 1)) # attach negative frequencies - psi = np.r_[psi, psi[nFreq:1:-1].conj()] + psi = np.r_[psi, psi[nFreq - 2:0:-1].conj()] g = np.zeros(CSD.shape, dtype=np.complex64) converged = False + # use cholesky for performance + U = np.linalg.cholesky(CSD) for _ in range(nIter): if direct_inversion: psi_inv = np.linalg.inv(psi) + # the bracket of equation 3.1 - g = psi_inv @ CSD @ psi_inv.conj().transpose(0, 2, 1) + # g = psi_inv @ CSD @ psi_inv.conj().transpose(0, 2, 1) + + # equivalent using cholesky decomposition + g = psi_inv @ U + g = (g @ g.conj().transpose(0, 2, 1)) + else: for i in range(g.shape[0]): C = np.linalg.lstsq(psi[i], CSD[i], rcond=None)[0] @@ -153,12 +161,15 @@ def _plusOperator(g): # 'negative lags' from the ifft nLag = g.shape[0] // 2 + # the series expansion in beta_k - beta = np.fft.ifft(g, axis=0) + # is covariance like + beta = np.real(np.fft.ifft(g, axis=0)) # take half of the zero lag beta[0, ...] = 0.5 * beta[0, ...] - g0 = np.real(beta[0, ...].copy()) + g0 = beta[0, ...].copy() + # take half of Nyquist bin # Dhamala "NewEdits" 28.01.22 beta[nLag, ...] = 0.5 * beta[nLag, ...] From 24f79f32f0f6a4336efd66036b22d9cdb09d5410 Mon Sep 17 00:00:00 2001 From: KatharineShapcott <65502584+KatharineShapcott@users.noreply.github.com> Date: Wed, 6 Apr 2022 18:33:52 +0200 Subject: [PATCH 144/166] Now checking clear for bool type --- syncopy/datatype/methods/selectdata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncopy/datatype/methods/selectdata.py b/syncopy/datatype/methods/selectdata.py index c3ee4b2eb..9128a18b5 100644 --- a/syncopy/datatype/methods/selectdata.py +++ b/syncopy/datatype/methods/selectdata.py @@ -268,7 +268,7 @@ def selectdata(data, # Vet the only inputs not checked by `Selector` if not isinstance(inplace, bool): raise SPYTypeError(inplace, varname="inplace", expected="Boolean") - if not isinstance(inplace, bool): + if not isinstance(clear, bool): raise SPYTypeError(clear, varname="clear", expected="Boolean") # If provided, make sure output object is appropriate From 5055a05041f2516c9ded491cd117da56d78a1acf Mon Sep 17 00:00:00 2001 From: tensionhead Date: Thu, 7 Apr 2022 15:30:04 +0200 Subject: [PATCH 145/166] NEW: Hilbert transform for preprocessing - supports all output types of FT Changes to be committed: modified: syncopy/preproc/compRoutines.py modified: syncopy/preproc/preprocessing.py --- syncopy/preproc/compRoutines.py | 105 +++++++++++++++++++++++++++++-- syncopy/preproc/preprocessing.py | 30 ++++++++- 2 files changed, 128 insertions(+), 7 deletions(-) diff --git a/syncopy/preproc/compRoutines.py b/syncopy/preproc/compRoutines.py index 076fdb325..da74e4594 100644 --- a/syncopy/preproc/compRoutines.py +++ b/syncopy/preproc/compRoutines.py @@ -330,10 +330,6 @@ def rectify_cF(dat, noCompute=False, chunkShape=None): Consequently, this function does **not** perform any error checking and operates under the assumption that all inputs have been externally validated and cross-checked. - See also - -------- - `Scipy butterworth documentation `_ - """ # operation does not change the shape @@ -362,7 +358,106 @@ class Rectify(ComputationalRoutine): computeFunction = staticmethod(rectify_cF) # 1st argument,the data, gets omitted - valid_kws = list(signature(but_filtering_cF).parameters.keys())[1:] + valid_kws = list(signature(rectify_cF).parameters.keys())[1:] + + def process_metadata(self, data, out): + + # Some index gymnastics to get trial begin/end "samples" + if data.selection is not None: + chanSec = data.selection.channel + trl = data.selection.trialdefinition + else: + chanSec = slice(None) + trl = data.trialdefinition + + out.trialdefinition = trl + + out.samplerate = data.samplerate + out.channel = np.array(data.channel[chanSec]) + + +@unwrap_io +def hilbert_cF(dat, output='abs', timeAxis=0, noCompute=False, chunkShape=None): + + """ + Provides hilbert transformation with various outputs, band-pass filtering + beforehand highly recommended. + + dat : (N, K) :class:`numpy.ndarray` + Uniformly sampled multi-channel time-series data + output : {'abs', 'complex', 'real', 'imag', 'absreal', 'absimag', 'angle'} + The transformation after performing the complex hilbert transform. Choose + `'angle'` to get the phase. + timeAxis : int, optional + Index of running time axis in `dat` (0 or 1) + noCompute : bool + If `True`, do not perform actual calculation but + instead return expected shape and :class:`numpy.dtype` of output + array. + + Returns + ------- + rectified : (N, K) :class:`~numpy.ndarray` + The rectified signals + + Notes + ----- + This method is intended to be used as + :meth:`~syncopy.shared.computational_routine.ComputationalRoutine.computeFunction` + inside a :class:`~syncopy.shared.computational_routine.ComputationalRoutine`. + Thus, input parameters are presumed to be forwarded from a parent metafunction. + Consequently, this function does **not** perform any error checking and operates + under the assumption that all inputs have been externally validated and cross-checked. + + """ + + out_trafo = { + 'abs': lambda x: np.abs(x), + 'complex': lambda x: x, + 'real': lambda x: np.real(x), + 'imag': lambda x: np.imag(x), + 'absreal': lambda x: np.abs(np.real(x)), + 'absimag': lambda x: np.abs(np.imag(x)), + 'angle': lambda x: np.angle(x) + } + + # Re-arrange array if necessary and get dimensional information + if timeAxis != 0: + dat = dat.T # does not copy but creates view of `dat` + else: + dat = dat + + # operation does not change the shape + # but may change the number format + outShape = dat.shape + fmt = np.complex64 if output == 'complex' else np.float32 + if noCompute: + return outShape, fmt + + trafo = sci.hilbert(dat, axis=0) + + return out_trafo[output](trafo) + + +class Hilbert(ComputationalRoutine): + + """ + Compute class that performs Hilbert transforms + of :class:`~syncopy.AnalogData` objects + + Sub-class of :class:`~syncopy.shared.computational_routine.ComputationalRoutine`, + see :doc:`/developer/compute_kernels` for technical details on Syncopy's compute + classes and metafunctions. + + See also + -------- + syncopy.preprocessing : parent metafunction + """ + + computeFunction = staticmethod(hilbert_cF) + + # 1st argument,the data, gets omitted + valid_kws = list(signature(hilbert_cF).parameters.keys())[1:] def process_metadata(self, data, out): diff --git a/syncopy/preproc/preprocessing.py b/syncopy/preproc/preprocessing.py index c6896bf8f..fc5d5e97a 100644 --- a/syncopy/preproc/preprocessing.py +++ b/syncopy/preproc/preprocessing.py @@ -18,13 +18,15 @@ check_passed_kwargs ) -from .compRoutines import But_Filtering, Sinc_Filtering, Rectify +from .compRoutines import But_Filtering, Sinc_Filtering, Rectify, Hilbert availableFilters = ('but', 'firws') availableFilterTypes = ('lp', 'hp', 'bp', 'bs') availableDirections = ('twopass', 'onepass', 'onepass-minphase') availableWindows = ("hamming", "hann", "blackman") +hilbert_outputs = {'abs', 'complex', 'real', 'imag', 'absreal', 'absimag', 'angle'} + @unwrap_cfg @unwrap_select @@ -72,6 +74,9 @@ def preprocessing(data, least squares fit of a linear polynomial). rectify : bool, optional Set to `True` to rectify (after filtering) + hilbert : None or one of {'abs', 'complex', 'real', 'imag', 'absreal', 'absimag', 'angle'} + Choose one of the supported output types to perform + hilbert transformation after filtering. Set to `'angle'` to return the phase. Returns ------- @@ -148,6 +153,17 @@ def preprocessing(data, act = "non-equidistant sampling" raise SPYValueError(lgl, varname="data", actual=act) + # -- post processing + if rectify and hilbert: + lgl = "either rectification or hilbert transform" + raise SPYValueError(lgl, varname="rectify/hilbert", actual=(rectify, hilbert)) + + # `hilbert` acts both as a switch and a parameter to set the output (like in FT) + if hilbert: + if hilbert not in hilbert_outputs: + lgl = f"one of {hilbert_outputs}" + raise SPYValueError(lgl, varname="hilbert", actual=hilbert) + # -- Method calls # Prepare keyword dict for logging (use `lcls` to get actually provided @@ -255,7 +271,17 @@ def preprocessing(data, return rectified elif hilbert: - pass + + htrafo = AnalogData(dimord=data.dimord) + hilbertCR = Hilbert(output=hilbert, + timeAxis=timeAxis) + hilbertCR.initialize(filtered, data._stackingDim, + chan_per_worker=kwargs.get("chan_per_worker"), + keeptrials=True) + hilbertCR.compute(filtered, htrafo, + parallel=kwargs.get("parallel"), + log_dict=log_dict) + return htrafo # no post-processing else: From 4348c72566ec6b9d820652780d23176aafc3af26 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Thu, 7 Apr 2022 17:41:03 +0200 Subject: [PATCH 146/166] FIX: Removed unused imports + fixed typos - minor code cleanup On branch preproc-tests Changes to be committed: modified: syncopy/preproc/preprocessing.py modified: syncopy/shared/input_processors.py modified: syncopy/tests/helpers.py modified: syncopy/tests/test_connectivity.py modified: syncopy/tests/test_preproc.py --- syncopy/preproc/preprocessing.py | 8 ++++---- syncopy/shared/input_processors.py | 9 ++++----- syncopy/tests/helpers.py | 2 +- syncopy/tests/test_connectivity.py | 2 +- syncopy/tests/test_preproc.py | 6 +++--- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/syncopy/preproc/preprocessing.py b/syncopy/preproc/preprocessing.py index 40339bd30..e71159371 100644 --- a/syncopy/preproc/preprocessing.py +++ b/syncopy/preproc/preprocessing.py @@ -10,7 +10,7 @@ from syncopy import AnalogData from syncopy.shared.parsers import data_parser, scalar_parser, array_parser from syncopy.shared.tools import get_defaults -from syncopy.shared.errors import SPYValueError, SPYTypeError, SPYWarning, SPYInfo +from syncopy.shared.errors import SPYValueError, SPYInfo from syncopy.shared.kwarg_decorators import (unwrap_cfg, unwrap_select, detect_parallel_client) from syncopy.shared.input_processors import ( @@ -45,7 +45,7 @@ def preprocessing(data, data : `~syncopy.AnalogData` A non-empty Syncopy :class:`~syncopy.AnalogData` object filter_class : {'but', 'firws'} - Butterworth (IIR) or windowed sinc (FIR) + Butterworth (IIR) or windowed sinc (FIR) filter_type : {'lp', 'hp', 'bp', 'bs'}, optional Select type of filter, either low-pass `'lp'`, high-pass `'hp'`, band-pass `'bp'` or band-stop (Notch) `'bs'`. @@ -60,7 +60,7 @@ def preprocessing(data, Filter direction: `'twopass'` - zero-phase forward and reverse filter, IIR and FIR `'onepass'` - forward filter, introduces group delays for IIR, zerophase for FIR - `'onepass-minphase' - forward causal/minumum phase filter, FIR only + `'onepass-minphase' - forward causal/minimum phase filter, FIR only window : {"hamming", "hann", "blackman"}, optional The type of window to use for the FIR filter polyremoval : int or None, optional @@ -189,7 +189,7 @@ def preprocessing(data, if window not in availableWindows: lgl = "'" + "or '".join(opt + "' " for opt in availableWindows) - raise SPYValueError(legal=lgl, varname="window", actual=window) + raise SPYValueError(legal=lgl, varname="window", actual=window) # set filter specific defaults here if direction is None: diff --git a/syncopy/shared/input_processors.py b/syncopy/shared/input_processors.py index 56816e66b..d66889f08 100644 --- a/syncopy/shared/input_processors.py +++ b/syncopy/shared/input_processors.py @@ -69,9 +69,9 @@ def process_padding(pad_to_length, lenTrials): abs_pad = _nextpow2(int(lenTrials.max())) # no padding in case of equal length trials - elif pad_to_length is None: + elif pad_to_length is None: abs_pad = int(lenTrials.max()) - if lenTrials.min() != lenTrials.max(): + if lenTrials.min() != lenTrials.max(): msg = f"Unequal trial lengths present, padding all trials to {abs_pad} samples" SPYWarning(msg) @@ -363,11 +363,10 @@ def check_effective_parameters(CR, defaults, lcls, besides=None): def check_passed_kwargs(lcls, defaults, frontend_name): - - ''' + """ Catch additional kwargs passed to the frontends which have no effect - ''' + """ # unpack **kwargs of frontend call which # might contain arbitrary kws passed from the user diff --git a/syncopy/tests/helpers.py b/syncopy/tests/helpers.py index fade272e8..7ec983ba7 100644 --- a/syncopy/tests/helpers.py +++ b/syncopy/tests/helpers.py @@ -71,7 +71,7 @@ def run_polyremoval_test(method_call): assert 'Wrong type of `polyremoval`' in str(err) -def run_cfg_test(method_call, method, cfg, positivity=True): +def test_gr_polyremovalrun_cfg_test(method_call, method, cfg, positivity=True): cfg.method = method if method != 'granger': diff --git a/syncopy/tests/test_connectivity.py b/syncopy/tests/test_connectivity.py index b699c69a0..f6971c07b 100644 --- a/syncopy/tests/test_connectivity.py +++ b/syncopy/tests/test_connectivity.py @@ -20,7 +20,7 @@ from syncopy import connectivityanalysis as ca import syncopy.tests.synth_data as synth_data import syncopy.tests.helpers as helpers -from syncopy.shared.errors import SPYValueError, SPYTypeError +from syncopy.shared.errors import SPYValueError from syncopy.shared.tools import get_defaults # Decorator to decide whether or not to run dask-related tests diff --git a/syncopy/tests/test_preproc.py b/syncopy/tests/test_preproc.py index b60884640..4c310deb0 100644 --- a/syncopy/tests/test_preproc.py +++ b/syncopy/tests/test_preproc.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Test preprocessing +# Test preprocessing # # 3rd party imports @@ -20,7 +20,7 @@ import syncopy.preproc as preproc # submodule import syncopy.tests.helpers as helpers -from syncopy.shared.errors import SPYValueError, SPYTypeError +from syncopy.shared.errors import SPYValueError from syncopy.shared.tools import get_defaults, best_match # Decorator to decide whether or not to run dask-related tests @@ -97,7 +97,7 @@ def test_but_filter(self, **kwargs): foilim = [0, self.freq_kw[ftype]] elif ftype == 'hp': # toilim selections can screw up the - # frequency axis of freqanalysis/np.fft.rfftfreq :/ + # frequency axis of freqanalysis/np.fft.rfftfreq :/ foilim = [self.freq_kw[ftype], spec_f.freq[-1]] else: foilim = self.freq_kw[ftype] From 647418a500b9b291e59d83bdfe69fe4739d15a8a Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 8 Apr 2022 12:34:55 +0200 Subject: [PATCH 147/166] PR fixes Changes to be committed: modified: syncopy/nwanalysis/connectivity_analysis.py modified: syncopy/preproc/firws.py modified: syncopy/preproc/preprocessing.py modified: syncopy/tests/test_connectivity.py modified: syncopy/tests/test_preproc.py --- syncopy/nwanalysis/connectivity_analysis.py | 2 - syncopy/preproc/firws.py | 5 ++ syncopy/preproc/preprocessing.py | 14 +++-- syncopy/tests/test_connectivity.py | 66 +++++++++++---------- syncopy/tests/test_preproc.py | 20 +++---- 5 files changed, 57 insertions(+), 50 deletions(-) diff --git a/syncopy/nwanalysis/connectivity_analysis.py b/syncopy/nwanalysis/connectivity_analysis.py index 2c0403abb..808536dfe 100644 --- a/syncopy/nwanalysis/connectivity_analysis.py +++ b/syncopy/nwanalysis/connectivity_analysis.py @@ -162,14 +162,12 @@ def connectivityanalysis(data, method="coh", keeptrials=False, output="abs", # get sampleinfo and check for equidistancy if data.selection is not None: sinfo = data.selection.trialdefinition[:, :2] - trialList = data.selection.trials # user picked discrete set of time points if isinstance(data.selection.time[0], list): lgl = "equidistant time points (toi) or time slice (toilim)" actual = "non-equidistant set of time points" raise SPYValueError(legal=lgl, varname="select", actual=actual) else: - trialList = list(range(len(data.trials))) sinfo = data.sampleinfo lenTrials = np.diff(sinfo).squeeze() diff --git a/syncopy/preproc/firws.py b/syncopy/preproc/firws.py index da3681c89..355750bf2 100644 --- a/syncopy/preproc/firws.py +++ b/syncopy/preproc/firws.py @@ -61,6 +61,11 @@ def design_wsinc(window, order, f_c, filter_type='lp'): filter_type : {'lp', 'hp', 'bp, 'bs'}, optional Select type of filter, either low-pass `'lp'`, high-pass `'hp'`, band-pass `'bp'` or band-stop (Notch) `'bs'`. + + Returns + ------ + kernel : (order,) :class:`numpy.ndarray` + The windowed sinc as 1d array """ # order has to be even diff --git a/syncopy/preproc/preprocessing.py b/syncopy/preproc/preprocessing.py index 40339bd30..048d44ff0 100644 --- a/syncopy/preproc/preprocessing.py +++ b/syncopy/preproc/preprocessing.py @@ -106,6 +106,9 @@ def preprocessing(data, elif filter_type in ('bp', 'bs'): array_parser(freq, varname='freq', hasinf=False, hasnan=False, lims=[0, data.samplerate / 2], dims=(2,)) + if freq[0] == freq[1]: + lgl = "two different frequencies" + raise SPYValueError(lgl, varname='freq', actual=freq) freq = np.sort(freq) # -- here the defaults are filter specific and get set later -- @@ -124,22 +127,21 @@ def preprocessing(data, # get sampleinfo and check for equidistancy if data.selection is not None: sinfo = data.selection.trialdefinition[:, :2] - trialList = data.selection.trials # user picked discrete set of time points if isinstance(data.selection.time[0], list): lgl = "equidistant time points (toi) or time slice (toilim)" actual = "non-equidistant set of time points" raise SPYValueError(legal=lgl, varname="select", actual=actual) else: - trialList = list(range(len(data.trials))) sinfo = data.sampleinfo lenTrials = np.diff(sinfo).squeeze() # check for equidistant sampling as needed for filtering - if not all([np.allclose(np.diff(time), 1 / data.samplerate) for time in data.time]): - lgl = "equidistant sampling in time" - act = "non-equidistant sampling" - raise SPYValueError(lgl, varname="data", actual=act) + # FIXME: could be too slow, see #259 + # if not all([np.allclose(np.diff(time), 1 / data.samplerate) for time in data.time]): + # lgl = "equidistant sampling in time" + # act = "non-equidistant sampling" + # raise SPYValueError(lgl, varname="data", actual=act) # -- Method calls diff --git a/syncopy/tests/test_connectivity.py b/syncopy/tests/test_connectivity.py index b699c69a0..f2965dc06 100644 --- a/syncopy/tests/test_connectivity.py +++ b/syncopy/tests/test_connectivity.py @@ -17,7 +17,7 @@ import dask.distributed as dd from syncopy import AnalogData -from syncopy import connectivityanalysis as ca +from syncopy import connectivityanalysis as cafunc import syncopy.tests.synth_data as synth_data import syncopy.tests.helpers as helpers from syncopy.shared.errors import SPYValueError, SPYTypeError @@ -68,8 +68,8 @@ class TestGranger: def test_gr_solution(self, **kwargs): - Gcaus = ca(self.data, method='granger', - tapsmofrq=3, foi=None, **kwargs) + Gcaus = cafunc(self.data, method='granger', + tapsmofrq=3, foi=None, **kwargs) # check all channel combinations with coupling @@ -100,7 +100,7 @@ def test_gr_selections(self): for sel_dct in selections: - Gcaus = ca(self.data, method='granger', select=sel_dct) + Gcaus = cafunc(self.data, method='granger', select=sel_dct) # check here just for finiteness and positivity assert np.all(np.isfinite(Gcaus.data)) @@ -109,25 +109,26 @@ def test_gr_selections(self): def test_gr_foi(self): try: - ca(self.data, - method='granger', - foi=np.arange(0, 70) - ) + cafunc(self.data, + method='granger', + foi=np.arange(0, 70) + ) except SPYValueError as err: assert 'no foi specification' in str(err) try: - ca(self.data, - method='granger', - foilim=[0, 70] - ) + cafunc(self.data, + method='granger', + foilim=[0, 70] + ) except SPYValueError as err: assert 'no foi specification' in str(err) def test_gr_cfg(self): - call = lambda cfg: ca(self.data, cfg) - helpers.run_cfg_test(call, method='granger', cfg=get_defaults(ca)) + call = lambda cfg: cafunc(self.data, cfg) + helpers.run_cfg_test(call, method='granger', + cfg=get_defaults(cafunc)) @skip_without_acme @skip_low_mem @@ -189,12 +190,12 @@ class TestCoherence: def test_coh_solution(self, **kwargs): - res = ca(data=self.data, - method='coh', - foilim=[5, 60], - output='pow', - tapsmofrq=1.5, - **kwargs) + res = cafunc(data=self.data, + method='coh', + foilim=[5, 60], + output='pow', + tapsmofrq=1.5, + **kwargs) # coherence at the harmonic frequencies idx_f1 = np.argmin(res.freq < self.f1) @@ -224,7 +225,7 @@ def test_coh_selections(self): for sel_dct in selections: - result = ca(self.data, method='coh', select=sel_dct) + result = cafunc(self.data, method='coh', select=sel_dct) # check here just for finiteness and positivity assert np.all(np.isfinite(result.data)) @@ -232,17 +233,18 @@ def test_coh_selections(self): def test_coh_foi(self): - call = lambda foi, foilim: ca(self.data, - method='coh', - foi=foi, - foilim=foilim) + call = lambda foi, foilim: cafunc(self.data, + method='coh', + foi=foi, + foilim=foilim) helpers.run_foi_test(call, foilim=[0, 70]) def test_coh_cfg(self): - call = lambda cfg: ca(self.data, cfg) - helpers.run_cfg_test(call, method='coh', cfg=get_defaults(ca)) + call = lambda cfg: cafunc(self.data, cfg) + helpers.run_cfg_test(call, method='coh', + cfg=get_defaults(cafunc)) @skip_without_acme @skip_low_mem @@ -300,7 +302,7 @@ class TestCorrelation: def test_corr_solution(self, **kwargs): - corr = ca(data=self.data, method='corr', **kwargs) + corr = cafunc(data=self.data, method='corr', **kwargs) # test 0-lag autocorr is 1 for all channels assert np.all(corr.data[0, 0].diagonal() > .99) @@ -381,15 +383,17 @@ def test_corr_selections(self): for sel_dct in selections: - result = ca(self.data, method='corr', select=sel_dct) + result = cafunc(self.data, method='corr', select=sel_dct) # check here just for finiteness and positivity assert np.all(np.isfinite(result.data)) def test_corr_cfg(self): - call = lambda cfg: ca(self.data, cfg) - helpers.run_cfg_test(call, method='corr', positivity=False, cfg=get_defaults(ca)) + call = lambda cfg: cafunc(self.data, cfg) + helpers.run_cfg_test(call, method='corr', + positivity=False, + cfg=get_defaults(cafunc)) @skip_without_acme @skip_low_mem diff --git a/syncopy/tests/test_preproc.py b/syncopy/tests/test_preproc.py index b60884640..3756e2d10 100644 --- a/syncopy/tests/test_preproc.py +++ b/syncopy/tests/test_preproc.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Test preprocessing +# Test preprocessing # # 3rd party imports @@ -15,7 +15,7 @@ if __acme__: import dask.distributed as dd -from syncopy import preprocessing as pp +from syncopy import preprocessing as ppfunc from syncopy import AnalogData, freqanalysis import syncopy.preproc as preproc # submodule import syncopy.tests.helpers as helpers @@ -82,11 +82,11 @@ def test_but_filter(self, **kwargs): fig, ax = mk_spec_ax() for ftype in preproc.availableFilterTypes: - filtered = pp(self.data, - filter_class='but', - filter_type=ftype, - freq=self.freq_kw[ftype], - **kwargs) + filtered = ppfunc(self.data, + filter_class='but', + filter_type=ftype, + freq=self.freq_kw[ftype], + **kwargs) # check in frequency space spec_f = freqanalysis(filtered, tapsmofrq=3, keeptrials=False) @@ -175,7 +175,7 @@ def test_but_polyremoval(self): def test_but_cfg(self): - cfg = get_defaults(pp) + cfg = get_defaults(ppfunc) cfg.filter_class = 'but' cfg.order = 6 @@ -183,7 +183,7 @@ def test_but_cfg(self): cfg.freq = 30 cfg.filter_type = 'hp' - result = pp(self.data, cfg) + result = ppfunc(self.data, cfg) # check here just for finiteness assert np.all(np.isfinite(result.data)) @@ -231,5 +231,3 @@ def annotate_foilims(ax, flow, fhigh): if __name__ == '__main__': T1 = TestButterworth() - #T2 = TestCoherence() - #T3 = TestCorrelation() From 415bf2384ebaaec52161a54364f3233a6e43e3d1 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 8 Apr 2022 13:13:43 +0200 Subject: [PATCH 148/166] CHG: Code cosmetics - removed unnecessary import and white spaces Changes to be committed: modified: syncopy/tests/test_preproc.py --- syncopy/tests/test_preproc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/syncopy/tests/test_preproc.py b/syncopy/tests/test_preproc.py index 3756e2d10..c25137895 100644 --- a/syncopy/tests/test_preproc.py +++ b/syncopy/tests/test_preproc.py @@ -20,7 +20,7 @@ import syncopy.preproc as preproc # submodule import syncopy.tests.helpers as helpers -from syncopy.shared.errors import SPYValueError, SPYTypeError +from syncopy.shared.errors import SPYValueError from syncopy.shared.tools import get_defaults, best_match # Decorator to decide whether or not to run dask-related tests @@ -97,7 +97,7 @@ def test_but_filter(self, **kwargs): foilim = [0, self.freq_kw[ftype]] elif ftype == 'hp': # toilim selections can screw up the - # frequency axis of freqanalysis/np.fft.rfftfreq :/ + # frequency axis of freqanalysis/np.fft.rfftfreq :/ foilim = [self.freq_kw[ftype], spec_f.freq[-1]] else: foilim = self.freq_kw[ftype] From ea574546ef6f736e2610c368c250a2d843011b04 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 8 Apr 2022 14:37:56 +0200 Subject: [PATCH 149/166] All good things come in three Changes to be committed: modified: syncopy/tests/test_preproc.py --- syncopy/tests/test_preproc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncopy/tests/test_preproc.py b/syncopy/tests/test_preproc.py index c25137895..9d67edde4 100644 --- a/syncopy/tests/test_preproc.py +++ b/syncopy/tests/test_preproc.py @@ -52,7 +52,7 @@ class TestButterworth: data = AnalogData(trls, samplerate=fs) # for toi tests, -1s offset - time_span = [-.5, 3.1] + time_span = [-.5, 3.2] flow, fhigh = 0.3 * fNy, 0.4 * fNy freq_kw = {'lp': fhigh, 'hp': flow, 'bp': [flow, fhigh], 'bs': [flow, fhigh]} From 61367de910595dbbd92c7f33e32368e946bcd01b Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 8 Apr 2022 15:58:10 +0200 Subject: [PATCH 150/166] FIX: Exclude hilber and rectify from effective parameter checks Changes to be committed: modified: syncopy/preproc/preprocessing.py --- syncopy/preproc/preprocessing.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/syncopy/preproc/preprocessing.py b/syncopy/preproc/preprocessing.py index fc5d5e97a..14ec3a08a 100644 --- a/syncopy/preproc/preprocessing.py +++ b/syncopy/preproc/preprocessing.py @@ -198,7 +198,8 @@ def preprocessing(data, log_dict["order"] = order log_dict["direction"] = direction - check_effective_parameters(But_Filtering, defaults, lcls) + check_effective_parameters(But_Filtering, defaults, lcls, + besides=('hilbert', 'rectify')) filterMethod = But_Filtering(samplerate=data.samplerate, filter_type=filter_type, @@ -232,7 +233,7 @@ def preprocessing(data, log_dict["direction"] = direction check_effective_parameters(Sinc_Filtering, defaults, lcls, - besides=['filter_class']) + besides=['filter_class', 'hilbert', 'rectify']) filterMethod = Sinc_Filtering(samplerate=data.samplerate, filter_type=filter_type, From 43fc20d19fe57036680df42bdd5e6378da4a9664 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 8 Apr 2022 16:27:10 +0200 Subject: [PATCH 151/166] NEW: Test Hilbert and Rectification - after butterworth filtering --- syncopy/preproc/preprocessing.py | 2 +- syncopy/tests/test_preproc.py | 37 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/syncopy/preproc/preprocessing.py b/syncopy/preproc/preprocessing.py index bdd2ffe6c..1342b400a 100644 --- a/syncopy/preproc/preprocessing.py +++ b/syncopy/preproc/preprocessing.py @@ -215,7 +215,7 @@ def preprocessing(data, if window not in availableWindows: lgl = "'" + "or '".join(opt + "' " for opt in availableWindows) - raise SPYValueError(legal=lgl, varname="window", actual=window) + raise SPYValueError(legal=lgl, varname="window", actual=window) # set filter specific defaults here if direction is None: diff --git a/syncopy/tests/test_preproc.py b/syncopy/tests/test_preproc.py index 9d67edde4..12fe37a10 100644 --- a/syncopy/tests/test_preproc.py +++ b/syncopy/tests/test_preproc.py @@ -206,6 +206,43 @@ def test_but_parallel(self, testcluster=None): client.close() ppl.ion() + def test_but_hilbert_rect(self): + + call = lambda **kwargs: ppfunc(self.data, + freq=20, + filter_class='but', + filter_type='lp', + order=5, + direction='onepass', + **kwargs) + + # test rectification + filtered = call(rectify=False) + assert not np.all(filtered.trials[0] > 0) + rectified = call(rectify=True) + assert np.all(rectified.trials[0] > 0) + + # test simultaneous call to hilbert and rectification + try: + call(rectify=True, hilbert='abs') + except SPYValueError as err: + assert "either rectifi" in str(err) + assert "or hilbert" in str(err) + + # test hilbert outputs + for output in preproc.hilbert_outputs: + htrafo = call(hilbert=output) + if output == 'complex': + assert np.all(np.imag(htrafo.trials[0]) != 0) + else: + assert np.all(np.imag(htrafo.trials[0]) == 0) + + # test wrong hilbert parameter + try: + call(hilbert='absnot') + except SPYValueError as err: + assert "one of {'" in str(err) + def mk_spec_ax(): From 97912dfb44fe63b8590fc55dc034cce4adf4dc9f Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 8 Apr 2022 16:31:42 +0200 Subject: [PATCH 152/166] CHG: Wrapping try/except blocks in a boolean - we should also make sure, that such try/except blocks really do reach the exception.. Changes to be committed: modified: syncopy/tests/test_preproc.py --- syncopy/tests/test_preproc.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/syncopy/tests/test_preproc.py b/syncopy/tests/test_preproc.py index 12fe37a10..2aec7256b 100644 --- a/syncopy/tests/test_preproc.py +++ b/syncopy/tests/test_preproc.py @@ -138,26 +138,34 @@ def test_but_kwargs(self): 'order': 4} # only for firws if 'minphase' in direction: + failed = True try: self.test_but_filter(**kwargs) + failed = False except SPYValueError as err: assert "expected 'onepass'" in str(err) + assert failed for order in [-2, 10, 5.6]: kwargs = {'direction': 'twopass', 'order': order} if order < 1 and isinstance(order, int): + failed = True try: self.test_but_filter(**kwargs) + failed = False except SPYValueError as err: assert "value to be greater" in str(err) - + assert failed else: + failed = True try: self.test_but_filter(**kwargs) + failed = False except SPYValueError as err: assert "expected int_like" in str(err) + assert failed def test_but_selections(self): @@ -223,11 +231,14 @@ def test_but_hilbert_rect(self): assert np.all(rectified.trials[0] > 0) # test simultaneous call to hilbert and rectification + failed = True try: call(rectify=True, hilbert='abs') + failed = False except SPYValueError as err: assert "either rectifi" in str(err) assert "or hilbert" in str(err) + assert failed # test hilbert outputs for output in preproc.hilbert_outputs: @@ -238,10 +249,13 @@ def test_but_hilbert_rect(self): assert np.all(np.imag(htrafo.trials[0]) == 0) # test wrong hilbert parameter + failed = True try: call(hilbert='absnot') + failed = False except SPYValueError as err: assert "one of {'" in str(err) + assert failed def mk_spec_ax(): From b4b87564256d0cc8c8bc19497aa6ac5cae61f75a Mon Sep 17 00:00:00 2001 From: tensionhead Date: Fri, 8 Apr 2022 16:36:08 +0200 Subject: [PATCH 153/166] FIX: Captured a case where the exception was not triggered - test_but_kwargs now tests for all 3 cases for the order parameter Changes to be committed: modified: syncopy/tests/test_preproc.py --- syncopy/tests/test_preproc.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/syncopy/tests/test_preproc.py b/syncopy/tests/test_preproc.py index 2aec7256b..0d2eabe24 100644 --- a/syncopy/tests/test_preproc.py +++ b/syncopy/tests/test_preproc.py @@ -158,7 +158,7 @@ def test_but_kwargs(self): except SPYValueError as err: assert "value to be greater" in str(err) assert failed - else: + elif not isinstance(order, int): failed = True try: self.test_but_filter(**kwargs) @@ -166,6 +166,9 @@ def test_but_kwargs(self): except SPYValueError as err: assert "expected int_like" in str(err) assert failed + # valid order + else: + self.test_but_filter(**kwargs) def test_but_selections(self): From 6232355749dcc5e5ef56358f2c72240024f71c45 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 11 Apr 2022 10:18:52 +0200 Subject: [PATCH 154/166] CHG: Fix random number generators --- syncopy/plotting/_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncopy/plotting/_helpers.py b/syncopy/plotting/_helpers.py index e3aab6852..4f2d3f725 100644 --- a/syncopy/plotting/_helpers.py +++ b/syncopy/plotting/_helpers.py @@ -133,7 +133,7 @@ def get_method(dataobject): """ # get the method string in a capture group - pattern = re.compile('[\s\w\D]+method = (\w+)') + pattern = re.compile(r'[\s\w\D]+method = (\w+)') match = pattern.match(dataobject._log) if match: meth_str = match.group(1) From 1b0868edbe8848feff4841d4ea4610ea74b9d189 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 11 Apr 2022 10:20:01 +0200 Subject: [PATCH 155/166] Merge commit --- syncopy/tests/helpers.py | 3 +++ syncopy/tests/test_preproc.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/syncopy/tests/helpers.py b/syncopy/tests/helpers.py index 7ec983ba7..4a68a3a83 100644 --- a/syncopy/tests/helpers.py +++ b/syncopy/tests/helpers.py @@ -12,6 +12,9 @@ from syncopy.shared.errors import SPYValueError, SPYTypeError +# fix random generators +np.random.seed(40203) + def run_padding_test(method_call, pad_length): """ diff --git a/syncopy/tests/test_preproc.py b/syncopy/tests/test_preproc.py index 9d67edde4..557dbae3b 100644 --- a/syncopy/tests/test_preproc.py +++ b/syncopy/tests/test_preproc.py @@ -52,7 +52,7 @@ class TestButterworth: data = AnalogData(trls, samplerate=fs) # for toi tests, -1s offset - time_span = [-.5, 3.2] + time_span = [-.8, 4.2] flow, fhigh = 0.3 * fNy, 0.4 * fNy freq_kw = {'lp': fhigh, 'hp': flow, 'bp': [flow, fhigh], 'bs': [flow, fhigh]} @@ -165,7 +165,7 @@ def test_but_selections(self): nChannels=2, toi_min=self.time_span[0], toi_max=self.time_span[1], - min_len=2) + min_len=3.5) for sd in sel_dicts: self.test_but_filter(select=sd) From 4709fce3a75ef19ed46bfefe87b29fe89ce8d587 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 11 Apr 2022 10:33:51 +0200 Subject: [PATCH 156/166] CHG: move connectivity specific cfg test out of test helpers Changes to be committed: modified: syncopy/tests/helpers.py modified: syncopy/tests/test_connectivity.py --- syncopy/tests/helpers.py | 20 ------------------ syncopy/tests/test_connectivity.py | 34 ++++++++++++++++++++++++------ 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/syncopy/tests/helpers.py b/syncopy/tests/helpers.py index 4a68a3a83..bbea01c46 100644 --- a/syncopy/tests/helpers.py +++ b/syncopy/tests/helpers.py @@ -74,26 +74,6 @@ def run_polyremoval_test(method_call): assert 'Wrong type of `polyremoval`' in str(err) -def test_gr_polyremovalrun_cfg_test(method_call, method, cfg, positivity=True): - - cfg.method = method - if method != 'granger': - cfg.foilim = [0, 70] - # test general tapers with - # additional parameters - cfg.taper = 'kaiser' - cfg.taper_opt = {'beta': 2} - - cfg.output = 'abs' - - result = method_call(cfg) - - # check here just for finiteness and positivity - assert np.all(np.isfinite(result.data)) - if positivity: - assert np.all(result.data[0, ...] >= -1e-10) - - def run_foi_test(method_call, foilim, positivity=True): # only positive frequencies diff --git a/syncopy/tests/test_connectivity.py b/syncopy/tests/test_connectivity.py index 9aaccb08f..53e0366ef 100644 --- a/syncopy/tests/test_connectivity.py +++ b/syncopy/tests/test_connectivity.py @@ -127,8 +127,8 @@ def test_gr_foi(self): def test_gr_cfg(self): call = lambda cfg: cafunc(self.data, cfg) - helpers.run_cfg_test(call, method='granger', - cfg=get_defaults(cafunc)) + run_cfg_test(call, method='granger', + cfg=get_defaults(cafunc)) @skip_without_acme @skip_low_mem @@ -243,8 +243,8 @@ def test_coh_foi(self): def test_coh_cfg(self): call = lambda cfg: cafunc(self.data, cfg) - helpers.run_cfg_test(call, method='coh', - cfg=get_defaults(cafunc)) + run_cfg_test(call, method='coh', + cfg=get_defaults(cafunc)) @skip_without_acme @skip_low_mem @@ -391,9 +391,9 @@ def test_corr_selections(self): def test_corr_cfg(self): call = lambda cfg: cafunc(self.data, cfg) - helpers.run_cfg_test(call, method='corr', - positivity=False, - cfg=get_defaults(cafunc)) + run_cfg_test(call, method='corr', + positivity=False, + cfg=get_defaults(cafunc)) @skip_without_acme @skip_low_mem @@ -416,6 +416,26 @@ def test_corr_polyremoval(self): helpers.run_polyremoval_test(call) +def run_cfg_test(method_call, method, cfg, positivity=True): + + cfg.method = method + if method != 'granger': + cfg.foilim = [0, 70] + # test general tapers with + # additional parameters + cfg.taper = 'kaiser' + cfg.taper_opt = {'beta': 2} + + cfg.output = 'abs' + + result = method_call(cfg) + + # check here just for finiteness and positivity + assert np.all(np.isfinite(result.data)) + if positivity: + assert np.all(result.data[0, ...] >= -1e-10) + + def plot_Granger(G, i, j): ax = ppl.gca() From e64c8dd0f3ee5270c7bd798440ff2c7707b46dd2 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 11 Apr 2022 12:32:18 +0200 Subject: [PATCH 157/166] NEW: FIRWS and Hilbert + Recitification tests - mostly recycled Butterworth tests for firws Changes to be committed: modified: syncopy/preproc/compRoutines.py modified: syncopy/tests/test_preproc.py --- syncopy/preproc/compRoutines.py | 2 - syncopy/tests/test_preproc.py | 227 +++++++++++++++++++++++++++++++- 2 files changed, 224 insertions(+), 5 deletions(-) diff --git a/syncopy/preproc/compRoutines.py b/syncopy/preproc/compRoutines.py index da74e4594..b3046f458 100644 --- a/syncopy/preproc/compRoutines.py +++ b/syncopy/preproc/compRoutines.py @@ -157,8 +157,6 @@ def process_metadata(self, data, out): if data.selection is not None: chanSec = data.selection.channel trl = data.selection.trialdefinition - for row in range(trl.shape[0]): - trl[row, :2] = [row, row + 1] else: chanSec = slice(None) trl = data.trialdefinition diff --git a/syncopy/tests/test_preproc.py b/syncopy/tests/test_preproc.py index cb196c0d5..a1a67cf3a 100644 --- a/syncopy/tests/test_preproc.py +++ b/syncopy/tests/test_preproc.py @@ -73,7 +73,7 @@ def test_but_filter(self, **kwargs): 'order': 4} # the unfiltered data - spec = freqanalysis(self.data, tapsmofrq=3, keeptrials=False) + spec = freqanalysis(self.data, tapsmofrq=1, keeptrials=False) # total power in arbitrary units (for now) pow_tot = spec.show(channel=0).sum() nFreq = spec.freq.size @@ -89,7 +89,7 @@ def test_but_filter(self, **kwargs): **kwargs) # check in frequency space - spec_f = freqanalysis(filtered, tapsmofrq=3, keeptrials=False) + spec_f = freqanalysis(filtered, tapsmofrq=1, keeptrials=False) # get relevant frequency ranges # for integrated powers @@ -145,7 +145,9 @@ def test_but_kwargs(self): except SPYValueError as err: assert "expected 'onepass'" in str(err) assert failed - + else: + self.test_but_filter(**kwargs) + for order in [-2, 10, 5.6]: kwargs = {'direction': 'twopass', 'order': order} @@ -261,6 +263,224 @@ def test_but_hilbert_rect(self): assert failed +class TestFIRWS: + + nSamples = 1000 + nChannels = 4 + nTrials = 50 + fs = 200 + fNy = fs / 2 + + # -- use flat white noise as test data -- + + trls = [] + for _ in range(nTrials): + trl = np.random.randn(nSamples, nChannels) + trls.append(trl) + + data = AnalogData(trls, samplerate=fs) + # for toi tests, -1s offset + time_span = [-.8, 4.2] + flow, fhigh = 0.3 * fNy, 0.4 * fNy + freq_kw = {'lp': fhigh, 'hp': flow, + 'bp': [flow, fhigh], 'bs': [flow, fhigh]} + + def test_firws_filter(self, **kwargs): + + """ + We test for remaining power after filtering + for all available filter types. + Order parameter here means length of the filter, + 200 is safe to pass! + """ + # check if we run the default test + def_test = not len(kwargs) + + # write default parameters dict + if def_test: + kwargs = {'direction': 'twopass', + 'order': 200} + + # the unfiltered data + spec = freqanalysis(self.data, tapsmofrq=1, keeptrials=False) + # total power in arbitrary units (for now) + pow_tot = spec.show(channel=0).sum() + nFreq = spec.freq.size + + if def_test: + fig, ax = mk_spec_ax() + + for ftype in preproc.availableFilterTypes: + filtered = ppfunc(self.data, + filter_class='firws', + filter_type=ftype, + freq=self.freq_kw[ftype], + **kwargs) + # check in frequency space + spec_f = freqanalysis(filtered, tapsmofrq=1, keeptrials=False) + + # get relevant frequency ranges + # for integrated powers + if ftype == 'lp': + foilim = [0, self.freq_kw[ftype]] + elif ftype == 'hp': + # toilim selections can screw up the + # frequency axis of freqanalysis/np.fft.rfftfreq :/ + foilim = [self.freq_kw[ftype], spec_f.freq[-1]] + else: + foilim = self.freq_kw[ftype] + + # remaining power after filtering + pow_fil = spec_f.show(channel=0, foilim=foilim).sum() + _, idx = best_match(spec_f.freq, foilim, span=True) + # ratio of pass-band to total freqency band + ratio = len(idx) / nFreq + + # at least 80% of the ideal filter power + # should be still around + if ftype in ('lp', 'hp'): + assert 0.8 * ratio < pow_fil / pow_tot + # here we have two roll-offs, one at each side + elif ftype == 'bp': + assert 0.7 * ratio < pow_fil / pow_tot + # as well as here + elif ftype == 'bs': + assert 0.7 * ratio < (pow_tot - pow_fil) / pow_tot + if def_test: + plot_spec(ax, spec_f, label=ftype) + + # plotting + if def_test: + plot_spec(ax, spec, c='0.3', label='unfiltered') + annotate_foilims(ax, *self.freq_kw['bp']) + ax.set_title(f"Twopass FIRWS, order = {kwargs['order']}") + + def test_firws_kwargs(self): + + """ + Test order and direction parameter + """ + + for direction in preproc.availableDirections: + kwargs = {'direction': direction, + 'order': 200} + self.test_firws_filter(**kwargs) + for order in [-2, 220, 5.6]: + kwargs = {'direction': 'twopass', + 'order': order} + + if order < 1 and isinstance(order, int): + failed = True + try: + self.test_firws_filter(**kwargs) + failed = False + except SPYValueError as err: + assert "value to be greater" in str(err) + assert failed + elif not isinstance(order, int): + failed = True + try: + self.test_firws_filter(**kwargs) + failed = False + except SPYValueError as err: + assert "expected int_like" in str(err) + assert failed + # valid order + else: + self.test_firws_filter(**kwargs) + + def test_firws_selections(self): + + sel_dicts = helpers.mk_selection_dicts(nTrials=20, + nChannels=2, + toi_min=self.time_span[0], + toi_max=self.time_span[1], + min_len=3.5) + for sd in sel_dicts: + print(sd) + self.test_firws_filter(select=sd, order=200) + + def test_firws_polyremoval(self): + + helpers.run_polyremoval_test(self.test_firws_filter) + + def test_firws_cfg(self): + + cfg = get_defaults(ppfunc) + + cfg.filter_class = 'firws' + cfg.order = 200 + cfg.direction = 'twopass' + cfg.freq = 30 + cfg.filter_type = 'hp' + + result = ppfunc(self.data, cfg) + + # check here just for finiteness + assert np.all(np.isfinite(result.data)) + + @skip_without_acme + def test_firws_parallel(self, testcluster=None): + + ppl.ioff() + client = dd.Client(testcluster) + all_tests = [attr for attr in self.__dir__() + if (inspect.ismethod(getattr(self, attr)) and 'parallel' not in attr)] + + for test_name in all_tests: + test_method = getattr(self, test_name) + if 'firws_filter' in test_name: + # test parallelisation along channels + test_method(chan_per_worker=2) + else: + test_method() + client.close() + ppl.ion() + + def test_firws_hilbert_rect(self): + + call = lambda **kwargs: ppfunc(self.data, + freq=20, + filter_class='firws', + filter_type='lp', + order=200, + direction='onepass', + **kwargs) + + # test rectification + filtered = call(rectify=False) + assert not np.all(filtered.trials[0] > 0) + rectified = call(rectify=True) + assert np.all(rectified.trials[0] > 0) + + # test simultaneous call to hilbert and rectification + failed = True + try: + call(rectify=True, hilbert='abs') + failed = False + except SPYValueError as err: + assert "either rectifi" in str(err) + assert "or hilbert" in str(err) + assert failed + + # test hilbert outputs + for output in preproc.hilbert_outputs: + htrafo = call(hilbert=output) + if output == 'complex': + assert np.all(np.imag(htrafo.trials[0]) != 0) + else: + assert np.all(np.imag(htrafo.trials[0]) == 0) + + # test wrong hilbert parameter + failed = True + try: + call(hilbert='absnot') + failed = False + except SPYValueError as err: + assert "one of {'" in str(err) + assert failed + + def mk_spec_ax(): fig, ax = ppl.subplots() @@ -285,3 +505,4 @@ def annotate_foilims(ax, flow, fhigh): if __name__ == '__main__': T1 = TestButterworth() + T2 = TestFIRWS() From d130a378e7f5fd05177e7262cce7ff1292cbf3df Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 11 Apr 2022 14:41:39 +0200 Subject: [PATCH 158/166] CHG: Relax wilson convergence - locally an accuracy of 1e-12 was no problem, not sure what is happening but maybe this helps --- syncopy/tests/backend/test_conn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncopy/tests/backend/test_conn.py b/syncopy/tests/backend/test_conn.py index 3c11b9232..3e127b61d 100644 --- a/syncopy/tests/backend/test_conn.py +++ b/syncopy/tests/backend/test_conn.py @@ -204,7 +204,7 @@ def test_wilson(): # --- factorize CSD with Wilson's algorithm --- - H, Sigma, conv = wilson_sf(CSDav, rtol=1e-12) + H, Sigma, conv = wilson_sf(CSDav, rtol=1e-9) # converged - \Psi \Psi^* \approx CSD, # with relative error <= rtol? From d95c2e1df5d42cd188a0369b32696b578883ef29 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 11 Apr 2022 14:45:40 +0200 Subject: [PATCH 159/166] CHG: Relax wilson tests --- syncopy/tests/backend/test_conn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncopy/tests/backend/test_conn.py b/syncopy/tests/backend/test_conn.py index 3e127b61d..2f56e03d5 100644 --- a/syncopy/tests/backend/test_conn.py +++ b/syncopy/tests/backend/test_conn.py @@ -213,7 +213,7 @@ def test_wilson(): # reconstitute CSDfac = H @ Sigma @ H.conj().transpose(0, 2, 1) err = max_rel_err(CSDav, CSDfac) - assert err < 1e-12 + assert err < 1e-9 fig, ax = ppl.subplots(figsize=(6, 4)) ax.set_xlabel('frequency (Hz)') From 810d2f9ea06d3de35b1723558656b26e9077b647 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 11 Apr 2022 14:52:47 +0200 Subject: [PATCH 160/166] PR fix: removed commented code blocks --- syncopy/datatype/methods/show.py | 33 -------------------------------- 1 file changed, 33 deletions(-) diff --git a/syncopy/datatype/methods/show.py b/syncopy/datatype/methods/show.py index bd3328728..a62fd45b5 100644 --- a/syncopy/datatype/methods/show.py +++ b/syncopy/datatype/methods/show.py @@ -129,31 +129,6 @@ def show(data, squeeze=True, **kwargs): for trlno in data.selection.trials: idxList.append(data._preview_trial(trlno).idx) - # Perform some slicing/list-selection gymnastics: ensure that selections - # that result in contiguous slices are actually returned as such (e.g., - # `idxList = [(slice(1,2), [2]), (slice(2,3), [2])` -> `returnIdx = [slice(1,3), [2]]`) - - # COMMENT: Do we really need this? - # We still want a list returned with one trial per list item, also for consecutive (or - # the default 'all') trials! toi selections without gap only happen in those cases.. - # another mental burden of mixing time and trial indexing?! - # If not selecting consecutive trials, these gymnastics seem unnecessary as well?! - - # FIXME: remove this part if vetted in review to be really unnecessary - # singleIdx = [False] * len(idxList[0]) - # returnIdx = list(idxList[0]) - # for sk, selectors in enumerate(zip(*idxList)): - # print(selectors, np.unique(selectors), np.unique(selectors).size, len(selectors)) - # # toi and foi are lists/arrays and not slices like toilim/foilim - # # so they get implicitly concatenated by np.unique - # if np.unique(selectors).size == 1 or len(selectors) == 1: - # singleIdx[sk] = True - # else: - # if all(isinstance(sel, slice) for sel in selectors): - # gaps = [selectors[k + 1].start - selectors[k].stop for k in range(len(selectors) - 1)] - # if all(gap == 0 for gap in gaps): - # singleIdx[sk] = True - # returnIdx[sk] = slice(selectors[0].start, selectors[-1].stop) # Reset in-place subset selection data.selection = None @@ -164,11 +139,3 @@ def show(data, squeeze=True, **kwargs): # return multiple trials as list else: return [transform_out(data.data[idx]) for idx in idxList] - - # FIXME: remove for the same reason as above - # If possible slice underlying dataset only once, otherwise return a list - # of arrays corresponding to selected trials - # if all(si == True for si in singleIdx): - # return transform_out(data.data[tuple(returnIdx)]) - # else: - # return [transform_out(data.data[idx]) for idx in idxList] From 2a23102c5a29b6d53fa4af1b91e99e5187f7e0d9 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Mon, 11 Apr 2022 16:34:41 +0200 Subject: [PATCH 161/166] PR fixes Changes to be committed: modified: syncopy/preproc/preprocessing.py modified: syncopy/tests/test_preproc.py --- syncopy/preproc/preprocessing.py | 6 ++-- syncopy/tests/test_preproc.py | 57 ++++++-------------------------- 2 files changed, 15 insertions(+), 48 deletions(-) diff --git a/syncopy/preproc/preprocessing.py b/syncopy/preproc/preprocessing.py index eaf77c5cf..735f66456 100644 --- a/syncopy/preproc/preprocessing.py +++ b/syncopy/preproc/preprocessing.py @@ -261,7 +261,7 @@ def preprocessing(data, # -- check for post processing flags -- if rectify: - + log_dict['rectify'] = rectify rectified = AnalogData(dimord=data.dimord) rectCR = Rectify() rectCR.initialize(filtered, @@ -271,10 +271,11 @@ def preprocessing(data, rectCR.compute(filtered, rectified, parallel=kwargs.get("parallel"), log_dict=log_dict) + del filtered return rectified elif hilbert: - + log_dict['hilbert'] = hilbert htrafo = AnalogData(dimord=data.dimord) hilbertCR = Hilbert(output=hilbert, timeAxis=timeAxis) @@ -284,6 +285,7 @@ def preprocessing(data, hilbertCR.compute(filtered, htrafo, parallel=kwargs.get("parallel"), log_dict=log_dict) + del filtered return htrafo # no post-processing diff --git a/syncopy/tests/test_preproc.py b/syncopy/tests/test_preproc.py index b85065b76..e07b9b549 100644 --- a/syncopy/tests/test_preproc.py +++ b/syncopy/tests/test_preproc.py @@ -139,14 +139,9 @@ def test_but_kwargs(self): 'order': 4} # only for firws if 'minphase' in direction: - - failed = True - try: + with pytest.raises(SPYValueError) as err: self.test_but_filter(**kwargs) - failed = False - except SPYValueError as err: assert "expected 'onepass'" in str(err) - assert failed else: self.test_but_filter(**kwargs) @@ -155,21 +150,13 @@ def test_but_kwargs(self): 'order': order} if order < 1 and isinstance(order, int): - failed = True - try: + with pytest.raises(SPYValueError) as err: self.test_but_filter(**kwargs) - failed = False - except SPYValueError as err: assert "value to be greater" in str(err) - assert failed elif not isinstance(order, int): - failed = True - try: + with pytest.raises(SPYValueError) as err: self.test_but_filter(**kwargs) - failed = False - except SPYValueError as err: assert "expected int_like" in str(err) - assert failed # valid order else: self.test_but_filter(**kwargs) @@ -238,14 +225,10 @@ def test_but_hilbert_rect(self): assert np.all(rectified.trials[0] > 0) # test simultaneous call to hilbert and rectification - failed = True - try: + with pytest.raises(SPYValueError) as err: call(rectify=True, hilbert='abs') - failed = False - except SPYValueError as err: assert "either rectifi" in str(err) assert "or hilbert" in str(err) - assert failed # test hilbert outputs for output in preproc.hilbert_outputs: @@ -256,13 +239,9 @@ def test_but_hilbert_rect(self): assert np.all(np.imag(htrafo.trials[0]) == 0) # test wrong hilbert parameter - failed = True - try: + with pytest.raises(SPYValueError) as err: call(hilbert='absnot') - failed = False - except SPYValueError as err: assert "one of {'" in str(err) - assert failed class TestFIRWS: @@ -372,21 +351,15 @@ def test_firws_kwargs(self): 'order': order} if order < 1 and isinstance(order, int): - failed = True - try: + with pytest.raises(SPYValueError) as err: self.test_firws_filter(**kwargs) - failed = False - except SPYValueError as err: assert "value to be greater" in str(err) - assert failed + elif not isinstance(order, int): - failed = True - try: + with pytest.raises(SPYValueError) as err: self.test_firws_filter(**kwargs) - failed = False - except SPYValueError as err: assert "expected int_like" in str(err) - assert failed + # valid order else: self.test_firws_filter(**kwargs) @@ -456,14 +429,10 @@ def test_firws_hilbert_rect(self): assert np.all(rectified.trials[0] > 0) # test simultaneous call to hilbert and rectification - failed = True - try: + with pytest.raises(SPYValueError) as err: call(rectify=True, hilbert='abs') - failed = False - except SPYValueError as err: assert "either rectifi" in str(err) assert "or hilbert" in str(err) - assert failed # test hilbert outputs for output in preproc.hilbert_outputs: @@ -474,13 +443,9 @@ def test_firws_hilbert_rect(self): assert np.all(np.imag(htrafo.trials[0]) == 0) # test wrong hilbert parameter - failed = True - try: + with pytest.raises(SPYValueError) as err: call(hilbert='absnot') - failed = False - except SPYValueError as err: assert "one of {'" in str(err) - assert failed def mk_spec_ax(): From 78858605f5dd6188ef12fb5b2b40909eed6e1b12 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Tue, 12 Apr 2022 09:26:12 +0200 Subject: [PATCH 162/166] CHG: Fixed typos - capitalized Hilbert On branch preproc-hilbertrect Changes to be committed: modified: syncopy/preproc/compRoutines.py modified: syncopy/preproc/preprocessing.py --- syncopy/preproc/compRoutines.py | 6 +++--- syncopy/preproc/preprocessing.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/syncopy/preproc/compRoutines.py b/syncopy/preproc/compRoutines.py index 645b1a13a..02b5b41f4 100644 --- a/syncopy/preproc/compRoutines.py +++ b/syncopy/preproc/compRoutines.py @@ -268,7 +268,7 @@ def but_filtering_cF(dat, class But_Filtering(ComputationalRoutine): """ - Compute class that performs filtering with butterworth filters + Compute class that performs filtering with butterworth filters of :class:`~syncopy.AnalogData` objects Sub-class of :class:`~syncopy.shared.computational_routine.ComputationalRoutine`, @@ -378,13 +378,13 @@ def process_metadata(self, data, out): def hilbert_cF(dat, output='abs', timeAxis=0, noCompute=False, chunkShape=None): """ - Provides hilbert transformation with various outputs, band-pass filtering + Provides Hilbert transformation with various outputs, band-pass filtering beforehand highly recommended. dat : (N, K) :class:`numpy.ndarray` Uniformly sampled multi-channel time-series data output : {'abs', 'complex', 'real', 'imag', 'absreal', 'absimag', 'angle'} - The transformation after performing the complex hilbert transform. Choose + The transformation after performing the complex Hilbert transform. Choose `'angle'` to get the phase. timeAxis : int, optional Index of running time axis in `dat` (0 or 1) diff --git a/syncopy/preproc/preprocessing.py b/syncopy/preproc/preprocessing.py index eaf77c5cf..5799d1dab 100644 --- a/syncopy/preproc/preprocessing.py +++ b/syncopy/preproc/preprocessing.py @@ -76,7 +76,7 @@ def preprocessing(data, Set to `True` to rectify (after filtering) hilbert : None or one of {'abs', 'complex', 'real', 'imag', 'absreal', 'absimag', 'angle'} Choose one of the supported output types to perform - hilbert transformation after filtering. Set to `'angle'` to return the phase. + Hilbert transformation after filtering. Set to `'angle'` to return the phase. Returns ------- @@ -157,7 +157,7 @@ def preprocessing(data, # -- post processing if rectify and hilbert: - lgl = "either rectification or hilbert transform" + lgl = "either rectification or Hilbert transform" raise SPYValueError(lgl, varname="rectify/hilbert", actual=(rectify, hilbert)) # `hilbert` acts both as a switch and a parameter to set the output (like in FT) From 3bf4d8ea337548fe98631bd897c473594da61727 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Tue, 12 Apr 2022 15:56:34 +0200 Subject: [PATCH 163/166] CHG: Relax wilson convergence - even though we have extremely good precision with Mukesh' input now, the convergence for our own test data got worse :/ Changes to be committed: modified: syncopy/nwanalysis/wilson_sf.py modified: syncopy/tests/backend/test_conn.py --- syncopy/nwanalysis/wilson_sf.py | 7 +++---- syncopy/tests/backend/test_conn.py | 16 +++++++--------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/syncopy/nwanalysis/wilson_sf.py b/syncopy/nwanalysis/wilson_sf.py index 66257f482..6b49e3802 100644 --- a/syncopy/nwanalysis/wilson_sf.py +++ b/syncopy/nwanalysis/wilson_sf.py @@ -101,8 +101,7 @@ def wilson_sf(CSD, nIter=100, rtol=1e-9, direct_inversion=True): # max relative error CSDfac = psi @ psi.conj().transpose(0, 2, 1) - err = np.abs(CSD - CSDfac) - err = (err / np.abs(CSD)).max() + err = max_rel_err(CSD, CSDfac) # converged if err < rtol: converged = True @@ -129,8 +128,8 @@ def _psi0_initial(CSD): nSamples = CSD.shape[1] - # perform ifft to obtain gammas. - gamma = np.fft.ifft(CSD, axis=0) + # perform (i)fft to obtain gammas. + gamma = np.fft.fft(CSD, axis=0) gamma0 = gamma[0, ...] # Remove any asymmetry due to rounding error. diff --git a/syncopy/tests/backend/test_conn.py b/syncopy/tests/backend/test_conn.py index 2f56e03d5..fc2541341 100644 --- a/syncopy/tests/backend/test_conn.py +++ b/syncopy/tests/backend/test_conn.py @@ -184,11 +184,10 @@ def test_wilson(): assert max_rel_err(A, A + A * 1e-16) < 1e-15 # --- create test data --- - fs = 5000 + fs = 200 nChannels = 2 - nSamples = 5000 - nTrials = 50 - + nSamples = 1000 + nTrials = 150 CSDav = np.zeros((nSamples // 2 + 1, nChannels, nChannels), dtype=np.complex64) for _ in range(nTrials): @@ -204,8 +203,7 @@ def test_wilson(): # --- factorize CSD with Wilson's algorithm --- - H, Sigma, conv = wilson_sf(CSDav, rtol=1e-9) - + H, Sigma, conv = wilson_sf(CSDav, rtol=1e-6) # converged - \Psi \Psi^* \approx CSD, # with relative error <= rtol? assert conv @@ -213,7 +211,7 @@ def test_wilson(): # reconstitute CSDfac = H @ Sigma @ H.conj().transpose(0, 2, 1) err = max_rel_err(CSDav, CSDfac) - assert err < 1e-9 + assert err < 1e-6 fig, ax = ppl.subplots(figsize=(6, 4)) ax.set_xlabel('frequency (Hz)') @@ -265,8 +263,8 @@ def test_granger(): """ fs = 200 # Hz - nSamples = 2500 - nTrials = 25 + nSamples = 1500 + nTrials = 100 CSDav = np.zeros((nSamples // 2 + 1, 2, 2), dtype=np.complex64) for _ in range(nTrials): From 26ee2e64d1212c949b19ef57838e8dba684118a6 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Wed, 13 Apr 2022 09:23:24 +0200 Subject: [PATCH 164/166] CHG: Updated changelog - added recently included features On branch dev Changes to be committed: modified: CHANGELOG.md --- CHANGELOG.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 192b85e59..80bc03071 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] -Bugfix release +## [v0.21] - 2022-04-13 +Feature update and bugfixes. ### NEW +- Added preprocessing functionality - Added experimental loading functionality for NWB 2.0 files - Added experimental loading functionality for Matlab mat files - Added support for "scalar" selections, i.e., things like `selectdata(trials=0)` @@ -19,7 +20,7 @@ Bugfix release ### CHANGED - Renamed `_selection` class property to `selection` -- Made plotting routines matplotlib 3.5 compatible +- Reworked plotting framework and made it matplotlib 3.5 compatible - The output of `show` is now automatically squeezed (i.e., singleton dimensions are removed from the returned array). @@ -40,7 +41,7 @@ Bugfix release - Matched selector keywords and class attribute names, i.e., selecting channels is now done by using a `select` dictionary with key `'channel'` (not `'channels'` as before). See the documentation of `selectdata` for details. -- Retired travis CI tests since free test runs are exhausted. Migrated to GitHub +- Retired Travis CI tests since free test runs are exhausted. Migrated to GitHub actions (and re-included codecov) ### FIXED From 09754686f3efbe73eca8478eca177d8571833792 Mon Sep 17 00:00:00 2001 From: tensionhead Date: Wed, 13 Apr 2022 10:23:57 +0200 Subject: [PATCH 165/166] Update CHANGELOG.md --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80bc03071..bb7cfa67e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,12 +17,17 @@ Feature update and bugfixes. for running the testing pipeline is to execute a trimmed-down testing suite that does not probe all possible input permutations but focuses on the core functionality without sacrificing coverage. +- new meta-function `taper_opt` parameter to control arbitrary taper (e.g. kaiser) parameters ### CHANGED - Renamed `_selection` class property to `selection` - Reworked plotting framework and made it matplotlib 3.5 compatible - The output of `show` is now automatically squeezed (i.e., singleton dimensions are removed from the returned array). +- Enhanced online documentation, now also covering connectivity analysis +- Multi-tapering (`freqanalysis`, `connectivityanalysis`) now is switched on by only specifying + the `tapsmofrq` parameter, removed the need for the additional and redundant setting of `taper='dpss'` +- Granger-Geweke algorithm now matches the reference implementation (Dhamala 2008) with machine precision ### REMOVED - Do not parse scalars using `numbers.Number`, use `numpy.number` instead to @@ -49,6 +54,7 @@ Feature update and bugfixes. for `SpectralData` objects without time-axis, resulting in "empty" trials. This has been fixed (closes #207) - Repaired `array_parser` to adequately complain about mixed-type arrays (closes #211) +- `show` routine now consistently returns a list of trials if and only if multiple trials are selected ## [v0.20] - 2022-01-18 Major Release From 610d5e3740cb8bad0cc203c7ca94cdf046701db5 Mon Sep 17 00:00:00 2001 From: Stefan Fuertinger Date: Wed, 13 Apr 2022 14:14:20 +0200 Subject: [PATCH 166/166] CHG: Reformatted changelog - changed paragraph wrapping and capitalized sentence starters On branch dev Changes to be committed: modified: CHANGELOG.md --- CHANGELOG.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb7cfa67e..9487ef444 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,8 @@ Feature update and bugfixes. for running the testing pipeline is to execute a trimmed-down testing suite that does not probe all possible input permutations but focuses on the core functionality without sacrificing coverage. -- new meta-function `taper_opt` parameter to control arbitrary taper (e.g. kaiser) parameters +- New meta-function `taper_opt` parameter to control arbitrary taper (e.g. kaiser) + parameters ### CHANGED - Renamed `_selection` class property to `selection` @@ -25,9 +26,11 @@ Feature update and bugfixes. - The output of `show` is now automatically squeezed (i.e., singleton dimensions are removed from the returned array). - Enhanced online documentation, now also covering connectivity analysis -- Multi-tapering (`freqanalysis`, `connectivityanalysis`) now is switched on by only specifying - the `tapsmofrq` parameter, removed the need for the additional and redundant setting of `taper='dpss'` -- Granger-Geweke algorithm now matches the reference implementation (Dhamala 2008) with machine precision +- Multi-tapering (`freqanalysis`, `connectivityanalysis`) now is switched on by + only specifying the `tapsmofrq` parameter, removed the need for the additional + and redundant setting of `taper='dpss'` +- Granger-Geweke algorithm now matches the reference implementation (Dhamala 2008) + with machine precision ### REMOVED - Do not parse scalars using `numbers.Number`, use `numpy.number` instead to @@ -54,7 +57,8 @@ Feature update and bugfixes. for `SpectralData` objects without time-axis, resulting in "empty" trials. This has been fixed (closes #207) - Repaired `array_parser` to adequately complain about mixed-type arrays (closes #211) -- `show` routine now consistently returns a list of trials if and only if multiple trials are selected +- The `show` routine now consistently returns a list of trials if and only if + multiple trials are selected ## [v0.20] - 2022-01-18 Major Release