├── setup.py ├── brpylib ├── __init__.py ├── brMiscFxns.py └── brpylib.py ├── .gitignore ├── Python Offline Utilities IFU.pdf ├── README.md ├── pyproject.toml └── examples ├── save_subset_nsx.py └── extract_continuous_data.ipynb /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /brpylib/__init__.py: -------------------------------------------------------------------------------- 1 | from .brpylib import NevFile, NsxFile, brpylib_ver 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__ 3 | .vscode 4 | *.swp 5 | .DS_Store 6 | *.egg-info/ 7 | .idea 8 | -------------------------------------------------------------------------------- /Python Offline Utilities IFU.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackrockNeurotech/Python-Utilities/HEAD/Python Offline Utilities IFU.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python-Utilities 2 | A collection of scripts for loading and manipulating Blackrock Microsystems datafiles. 3 | 4 | See the included instructions for use to see how to install and use this utility. 5 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "brpylib" 7 | description = "Blackrock Neurotech Python utilities" 8 | readme = "README.md" 9 | authors = [{ name = "Blackrock Neurotech", email = "support@blackrockneuro.com" }] 10 | 11 | dependencies = [ 12 | "numpy", 13 | ] 14 | dynamic = ["version"] 15 | 16 | [project.optional-dependencies] 17 | dev = [ 18 | "matplotlib", 19 | "qtpy", 20 | "pyside6_essentials; python_version>='3.6'", 21 | "pyside2; python_version<'3.6'", 22 | "jupyterlab" 23 | ] 24 | test = [ 25 | "pytest", 26 | ] 27 | 28 | [project.urls] 29 | Repository = "https://github.com/BlackrockNeurotech/Python-Utilities" 30 | Homepage = "https://blackrockneurotech.com/research/support/#manuals-and-software-downloads" 31 | 32 | [tool.setuptools.dynamic] 33 | version = {attr = "brpylib.brpylib.brpylib_ver"} 34 | 35 | -------------------------------------------------------------------------------- /examples/save_subset_nsx.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Example of how to extract data from a Blackrock Nsx data file and save new subset Nsx data files 4 | current version: 1.1.1 --- 07/22/2016 5 | 6 | @author: Mitch Frankel - Blackrock Microsystems 7 | """ 8 | 9 | """ 10 | Version History: 11 | v1.0.0 - 07/08/2016 - initial release - requires brpylib v1.1.0 or higher 12 | v1.1.0 - 07/12/2016 - addition of version checking for brpylib starting with v1.2.0 13 | v1.1.1 - 07/22/2016 - minor modifications to use close() functionality of NsxFile class 14 | """ 15 | 16 | # Imports 17 | from brpylib import NsxFile, brpylib_ver 18 | 19 | # Version control 20 | brpylib_ver_req = "1.2.1" 21 | if brpylib_ver.split('.') < brpylib_ver_req.split('.'): 22 | raise Exception("requires brpylib " + brpylib_ver_req + " or higher, please use latest version") 23 | 24 | # Inits 25 | datafile = 'D:/Dropbox/BlackrockDB/software/sampledata/The Most Perfect Data in the WWWorld/' \ 26 | 'sampleData.ns6' 27 | 28 | # Open file and extract headers 29 | brns_file = NsxFile(datafile) 30 | 31 | # save a subset of data based on elec_ids 32 | brns_file.savesubsetnsx(elec_ids=[1, 2, 5, 15, 20, 200], file_suffix='elec_subset') 33 | 34 | # save a subset of data based on file sizing (100 Mb) 35 | brns_file.savesubsetnsx(file_size=(1024**2) * 100, file_suffix='size_subset') 36 | 37 | # save a subset of data based on file timing 38 | brns_file.savesubsetnsx(file_time_s=30, file_suffix='time_subset') 39 | 40 | # save a subset of data based on elec_ids and timing 41 | brns_file.savesubsetnsx(elec_ids=[1, 2, 5, 15, 20, 200], file_time_s=30, file_suffix='elecAndTime_subset') 42 | 43 | # Close the original datafile 44 | brns_file.close() 45 | -------------------------------------------------------------------------------- /brpylib/brMiscFxns.py: -------------------------------------------------------------------------------- 1 | """ 2 | Random functions that may be useful elsewhere (or necessary) 3 | current version: 1.2.0 --- 08/04/2016 4 | 5 | @author: Mitch Frankel - Blackrock Microsystems 6 | 7 | Version History: 8 | v1.0.0 - 07/05/2016 - initial release 9 | v1.1.0 - 07/12/2016 - minor editing changes to print statements and addition of version control 10 | v1.2.0 - 08/04/2016 - minor modifications to allow use of Python 2.6+ 11 | """ 12 | from os import getcwd, path 13 | 14 | try: 15 | from qtpy.QtWidgets import QApplication, QFileDialog 16 | 17 | HAS_QT = True 18 | except ModuleNotFoundError: 19 | HAS_QT = False 20 | 21 | # Version control 22 | brmiscfxns_ver = "1.2.0" 23 | 24 | # Patch for use with Python 2.6+ 25 | try: 26 | input = raw_input 27 | except NameError: 28 | pass 29 | 30 | 31 | def openfilecheck(open_mode, file_name="", file_ext="", file_type=""): 32 | """ 33 | :param open_mode: {str} method to open the file (e.g., 'rb' for binary read only) 34 | :param file_name: [optional] {str} full path of file to open 35 | :param file_ext: [optional] {str} file extension (e.g., '.nev') 36 | :param file_type: [optional] {str} file type for use when browsing for file (e.g., 'Blackrock NEV Files') 37 | :return: {file} opened file 38 | """ 39 | 40 | while True: 41 | if not file_name: # no file name passed 42 | if not HAS_QT: 43 | raise ModuleNotFoundError( 44 | "Qt required for file dialog. Install PySide + qtpy or provide file_name." 45 | ) 46 | 47 | # Ask user to specify a file path or browse 48 | file_name = input( 49 | "Enter complete " + file_ext + " file path or hit enter to browse: " 50 | ) 51 | 52 | if not file_name: 53 | if "app" not in locals(): 54 | app = QApplication([]) 55 | if not file_ext: 56 | file_type = "All Files" 57 | file_name = QFileDialog.getOpenFileName( 58 | QFileDialog(), 59 | "Select File", 60 | getcwd(), 61 | file_type + " (*" + file_ext + ")", 62 | ) 63 | file_name = file_name[0] 64 | 65 | # Ensure file exists (really needed for users type entering) 66 | if path.isfile(file_name): 67 | # Ensure given file matches file_ext 68 | if file_ext: 69 | _, fext = path.splitext(file_name) 70 | 71 | # check for * in extension 72 | if file_ext[-1] == "*": 73 | test_extension = file_ext[:-1] 74 | else: 75 | test_extension = file_ext 76 | 77 | if fext[0 : len(test_extension)] != test_extension: 78 | file_name = "" 79 | print( 80 | "\n*** File given is not a " 81 | + file_ext 82 | + " file, try again ***\n" 83 | ) 84 | continue 85 | break 86 | else: 87 | file_name = "" 88 | print("\n*** File given does not exist, try again ***\n") 89 | 90 | print("\n" + file_name.split("/")[-1] + " opened") 91 | return open(file_name, open_mode) 92 | 93 | 94 | def checkequal(iterator): 95 | try: 96 | iterator = iter(iterator) 97 | first = next(iterator) 98 | return all(first == rest for rest in iterator) 99 | except StopIteration: 100 | return True 101 | -------------------------------------------------------------------------------- /examples/extract_continuous_data.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "attachments": {}, 5 | "cell_type": "markdown", 6 | "metadata": {}, 7 | "source": [ 8 | "Example of how to extract and plot continuous data saved in Blackrock nsX data files\n", 9 | "current version: 1.1.1 --- 07/22/2016\n", 10 | "\n", 11 | "@author: Mitch Frankel - Blackrock Microsystems" 12 | ] 13 | }, 14 | { 15 | "attachments": {}, 16 | "cell_type": "markdown", 17 | "metadata": {}, 18 | "source": [ 19 | "```\n", 20 | "Version History:\n", 21 | "v1.0.0 - 07/05/2016 - initial release - requires brpylib v1.0.0 or higher\n", 22 | "v1.1.0 - 07/12/2016 - addition of version checking for brpylib starting with v1.2.0\n", 23 | " minor code cleanup for readability\n", 24 | "v1.1.1 - 07/22/2016 - now uses 'samp_per_sec' as returned by NsxFile.getdata()\n", 25 | " minor modifications to use close() functionality of NsxFile class\n", 26 | "v2.0.0 - 05/11/2023 - Refactored as Jupyter notebook and updated to brpylib 2.0.2 - Chadwick Boulay\n", 27 | "```" 28 | ] 29 | }, 30 | { 31 | "attachments": {}, 32 | "cell_type": "markdown", 33 | "metadata": {}, 34 | "source": [ 35 | "The next cell is tagged as 'parameters'. These parameters can be modified using `papermill`. [See here](https://papermill.readthedocs.io/en/latest/usage-parameterize.html) for more info." 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": 1, 41 | "metadata": { 42 | "tags": [ 43 | "parameters" 44 | ] 45 | }, 46 | "outputs": [], 47 | "source": [ 48 | "datafile = \"\"\n", 49 | "start_time_s = 1.0\n", 50 | "plot_chan = 5" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": 2, 56 | "metadata": {}, 57 | "outputs": [], 58 | "source": [ 59 | "from pathlib import Path\n", 60 | "\n", 61 | "import numpy as np\n", 62 | "import matplotlib.pyplot as plt\n", 63 | "\n", 64 | "import brpylib" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": 3, 70 | "metadata": {}, 71 | "outputs": [], 72 | "source": [ 73 | "if int(brpylib.brpylib_ver.split(\".\")[0]) < 2:\n", 74 | " raise Exception(f\"Old library version unsupported: {brpylib.brpylib_ver}\")" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": 4, 80 | "metadata": {}, 81 | "outputs": [], 82 | "source": [ 83 | "if datafile:\n", 84 | " datapath = Path(datafile)\n", 85 | "else:\n", 86 | " datapath = Path.home() / \"sampledata\" / \"array_Sc2.ns6\"\n", 87 | "if not datapath.exists():\n", 88 | " raise NameError(f\"Datafile {str(datapath)} does not exist.\")\n" 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": 5, 94 | "metadata": {}, 95 | "outputs": [], 96 | "source": [ 97 | "elec_ids = list(range(1, 97)) # 'all' is default for all (1-indexed)\n", 98 | "data_time_s = 2.0 # 'all' is default for all\n", 99 | "downsample = 1 # 1 is default" 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": 20, 105 | "metadata": {}, 106 | "outputs": [ 107 | { 108 | "name": "stdout", 109 | "output_type": "stream", 110 | "text": [ 111 | "\n", 112 | "array_Sc2.ns6 opened\n", 113 | "\n", 114 | "array_Sc2.ns6 closed\n" 115 | ] 116 | } 117 | ], 118 | "source": [ 119 | "# Open file and extract headers\n", 120 | "nsx_file = brpylib.NsxFile(str(datapath))\n", 121 | "\n", 122 | "# Extract data - note: data will be returned based on *SORTED* elec_ids, see cont_data['elec_ids']\n", 123 | "cont_data = nsx_file.getdata(elec_ids, start_time_s, data_time_s, downsample, full_timestamps=True)\n", 124 | "\n", 125 | "# Close the nsx file now that all data is out\n", 126 | "nsx_file.close()" 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": 24, 132 | "metadata": {}, 133 | "outputs": [], 134 | "source": [ 135 | "# Plot the data channel\n", 136 | "seg_id = 0\n", 137 | "ch_idx = cont_data[\"elec_ids\"].index(plot_chan)\n", 138 | "t = cont_data[\"data_headers\"][seg_id][\"Timestamp\"] / cont_data[\"samp_per_s\"]" 139 | ] 140 | }, 141 | { 142 | "cell_type": "code", 143 | "execution_count": 27, 144 | "metadata": {}, 145 | "outputs": [ 146 | { 147 | "data": { 148 | "image/png": "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", 149 | "text/plain": [ 150 | "
" 151 | ] 152 | }, 153 | "metadata": {}, 154 | "output_type": "display_data" 155 | } 156 | ], 157 | "source": [ 158 | "plt.plot(t, cont_data[\"data\"][seg_id][ch_idx])\n", 159 | "plt.axis([t[0], t[-1], min(cont_data[\"data\"][seg_id][ch_idx]), max(cont_data[\"data\"][seg_id][ch_idx])])\n", 160 | "plt.locator_params(axis=\"y\", nbins=20)\n", 161 | "plt.xlabel(\"Time (s)\")\n", 162 | "# plt.ylabel(\"Output (\" + nsx_file.extended_headers[hdr_idx]['Units'] + \")\")\n", 163 | "# plt.title(nsx_file.extended_headers[hdr_idx]['ElectrodeLabel'])\n", 164 | "plt.show()" 165 | ] 166 | } 167 | ], 168 | "metadata": { 169 | "kernelspec": { 170 | "display_name": "brn_nsx", 171 | "language": "python", 172 | "name": "python3" 173 | }, 174 | "language_info": { 175 | "codemirror_mode": { 176 | "name": "ipython", 177 | "version": 3 178 | }, 179 | "file_extension": ".py", 180 | "mimetype": "text/x-python", 181 | "name": "python", 182 | "nbconvert_exporter": "python", 183 | "pygments_lexer": "ipython3", 184 | "version": "3.9.13" 185 | }, 186 | "orig_nbformat": 4 187 | }, 188 | "nbformat": 4, 189 | "nbformat_minor": 2 190 | } 191 | -------------------------------------------------------------------------------- /brpylib/brpylib.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Collection of classes used for reading headers and data from Blackrock files 4 | current version: 2.0.1 --- 11/12/2021 5 | 6 | @author: Mitch Frankel - Blackrock Microsystems 7 | Stephen Hou - v1.4.0 edits 8 | David Kluger - v2.0.0 overhaul 9 | 10 | Version History: 11 | v1.0.0 - 07/05/2016 - initial release - requires brMiscFxns v1.0.0 12 | v1.1.0 - 07/08/2016 - inclusion of NsxFile.savesubsetnsx() for saving subset of Nsx data to disk4 13 | v1.1.1 - 07/09/2016 - update to NsxFile.savesubsetnsx() for option (not)overwriting subset files if already exist 14 | bug fixes in NsxFile class as reported from beta user 15 | v1.2.0 - 07/12/2016 - bug fixes in NsxFile.savesubsetnsx() 16 | added version control and checking for brMiscFxns 17 | requires brMiscFxns v1.1.0 18 | v1.3.0 - 07/22/2016 - added 'samp_per_s' to NsxFile.getdata() output 19 | added close() method to NsxFile and NevFile objects 20 | NsxFile.getdata() now pre-allocates output['data'] as zeros - speed and safety 21 | v1.3.1 - 08/02/2016 - bug fixes to NsxFile.getdata() for usability with Python 2.7 as reported from beta user 22 | patch for use with multiple NSP sync (overwriting of initial null data from initial data packet) 23 | __future__ import for use with Python 2.7 (division) 24 | minor modifications to allow use of Python 2.6+ 25 | v1.3.2 - 08/12/2016 - bug fixes to NsXFile.getdata() 26 | v1.4.0 - 06/22/2017 - inclusion of wave_read parameter to NevFile.getdata() for including/excluding waveform data 27 | v2.0.0 - 04/27/2021 - numpy-based architecture rebuild of NevFile.getdata() 28 | v2.0.1 - 11/12/2021 - fixed indexing error in NevFile.getdata() 29 | Added numpy architecture to NsxFile.getdata() 30 | v2.0.2 - 03/21/2023 - added logic to NsxFile.getdata() for where PTP timestamps are applied to every continuous sample 31 | v2.0.3 - 05/11/2023 - Fixed bug with memmap and file.seek 32 | """ 33 | 34 | 35 | from __future__ import division # for those using Python 2.6+ 36 | 37 | from collections import namedtuple 38 | from datetime import datetime 39 | from math import ceil 40 | from os import path as ospath 41 | from struct import calcsize, pack, unpack, unpack_from 42 | 43 | import numpy as np 44 | 45 | from .brMiscFxns import brmiscfxns_ver, openfilecheck 46 | 47 | # Version control set/check 48 | brpylib_ver = "2.0.3" 49 | brmiscfxns_ver_req = "1.2.0" 50 | if brmiscfxns_ver.split(".") < brmiscfxns_ver_req.split("."): 51 | raise Exception( 52 | "brpylib requires brMiscFxns " 53 | + brmiscfxns_ver_req 54 | + " or higher, please use latest version" 55 | ) 56 | 57 | # Patch for use with Python 2.6+ 58 | try: 59 | input = raw_input 60 | except NameError: 61 | pass 62 | 63 | # Define global variables to remove magic numbers 64 | # 65 | WARNING_SLEEP_TIME = 5 66 | DATA_PAGING_SIZE = 1024**3 67 | DATA_FILE_SIZE_MIN = 1024**2 * 10 68 | STRING_TERMINUS = "\x00" 69 | UNDEFINED = 0 70 | ELEC_ID_DEF = "all" 71 | START_TIME_DEF = 0 72 | DATA_TIME_DEF = "all" 73 | DOWNSAMPLE_DEF = 1 74 | START_OFFSET_MIN = 0 75 | STOP_OFFSET_MIN = 0 76 | 77 | UV_PER_BIT_21 = 0.25 78 | WAVEFORM_SAMPLES_21 = 48 79 | NSX_BASIC_HEADER_BYTES_22 = 314 80 | NSX_EXT_HEADER_BYTES_22 = 66 81 | DATA_BYTE_SIZE = 2 82 | TIMESTAMP_NULL_21 = 0 83 | MAX_SAMP_PER_S = 30000 84 | 85 | NO_FILTER = 0 86 | BUTTER_FILTER = 1 87 | SERIAL_MODE = 0 88 | 89 | RB2D_MARKER = 1 90 | RB2D_BLOB = 2 91 | RB3D_MARKER = 3 92 | BOUNDARY_2D = 4 93 | MARKER_SIZE = 5 94 | 95 | DIGITAL_PACKET_ID = 0 96 | NEURAL_PACKET_ID_MIN = 1 97 | NEURAL_PACKET_ID_MAX = 16384 98 | COMMENT_PACKET_ID = 65535 99 | VIDEO_SYNC_PACKET_ID = 65534 100 | TRACKING_PACKET_ID = 65533 101 | BUTTON_PACKET_ID = 65532 102 | CONFIGURATION_PACKET_ID = 65531 103 | 104 | PARALLEL_REASON = 1 105 | PERIODIC_REASON = 64 106 | SERIAL_REASON = 129 107 | LOWER_BYTE_MASK = 255 108 | FIRST_BIT_MASK = 1 109 | SECOND_BIT_MASK = 2 110 | 111 | CLASSIFIER_MIN = 1 112 | CLASSIFIER_MAX = 16 113 | CLASSIFIER_NOISE = 255 114 | 115 | CHARSET_ANSI = 0 116 | CHARSET_UTF = 1 117 | CHARSET_ROI = 255 118 | 119 | COMM_RGBA = 0 120 | COMM_TIME = 1 121 | 122 | BUTTON_PRESS = 1 123 | BUTTON_RESET = 2 124 | 125 | CHG_NORMAL = 0 126 | CHG_CRITICAL = 1 127 | 128 | ENTER_EVENT = 1 129 | EXIT_EVENT = 2 130 | # 131 | 132 | # Define a named tuple that has information about header/packet fields 133 | FieldDef = namedtuple("FieldDef", ["name", "formatStr", "formatFnc"]) 134 | 135 | 136 | # 137 | def processheaders(curr_file, packet_fields): 138 | """ 139 | :param curr_file: {file} the current BR datafile to be processed 140 | :param packet_fields : {named tuple} the specific binary fields for the given header 141 | :return: a fully unpacked and formatted tuple set of header information 142 | 143 | Read a packet from a binary data file and return a list of fields 144 | The amount and format of data read will be specified by the 145 | packet_fields container 146 | """ 147 | 148 | # This is a lot in one line. First I pull out all the format strings from 149 | # the basic_header_fields named tuple, then concatenate them into a string 150 | # with '<' at the front (for little endian format) 151 | packet_format_str = "<" + "".join([fmt for name, fmt, fun in packet_fields]) 152 | 153 | # Calculate how many bytes to read based on the format strings of the header fields 154 | bytes_in_packet = calcsize(packet_format_str) 155 | packet_binary = curr_file.read(bytes_in_packet) 156 | 157 | # unpack the binary data from the header based on the format strings of each field. 158 | # This returns a list of data, but it's not always correctly formatted (eg, FileSpec 159 | # is read as ints 2 and 3 but I want it as '2.3' 160 | packet_unpacked = unpack(packet_format_str, packet_binary) 161 | 162 | # Create a iterator from the data list. This allows a formatting function 163 | # to use more than one item from the list if needed, and the next formatting 164 | # function can pick up on the correct item in the list 165 | data_iter = iter(packet_unpacked) 166 | 167 | # create an empty dictionary from the name field of the packet_fields. 168 | # The loop below will fill in the values with formatted data by calling 169 | # each field's formatting function 170 | packet_formatted = dict.fromkeys([name for name, fmt, fun in packet_fields]) 171 | for name, fmt, fun in packet_fields: 172 | packet_formatted[name] = fun(data_iter) 173 | 174 | return packet_formatted 175 | 176 | 177 | def format_filespec(header_list): 178 | return str(next(header_list)) + "." + str(next(header_list)) # eg 2.3 179 | 180 | 181 | def format_timeorigin(header_list): 182 | year = next(header_list) 183 | month = next(header_list) 184 | _ = next(header_list) 185 | day = next(header_list) 186 | hour = next(header_list) 187 | minute = next(header_list) 188 | second = next(header_list) 189 | millisecond = next(header_list) 190 | return datetime(year, month, day, hour, minute, second, millisecond * 1000) 191 | 192 | 193 | def format_stripstring(header_list): 194 | string = bytes.decode(next(header_list), "latin-1") 195 | return string.split(STRING_TERMINUS, 1)[0] 196 | 197 | 198 | def format_none(header_list): 199 | return next(header_list) 200 | 201 | 202 | def format_freq(header_list): 203 | return str(float(next(header_list)) / 1000) + " Hz" 204 | 205 | 206 | def format_filter(header_list): 207 | filter_type = next(header_list) 208 | if filter_type == NO_FILTER: 209 | return "none" 210 | elif filter_type == BUTTER_FILTER: 211 | return "butterworth" 212 | 213 | 214 | def format_charstring(header_list): 215 | return int(next(header_list)) 216 | 217 | 218 | def format_digconfig(header_list): 219 | config = next(header_list) & FIRST_BIT_MASK 220 | if config: 221 | return "active" 222 | else: 223 | return "ignored" 224 | 225 | 226 | def format_anaconfig(header_list): 227 | config = next(header_list) 228 | if config & FIRST_BIT_MASK: 229 | return "low_to_high" 230 | if config & SECOND_BIT_MASK: 231 | return "high_to_low" 232 | else: 233 | return "none" 234 | 235 | 236 | def format_digmode(header_list): 237 | dig_mode = next(header_list) 238 | if dig_mode == SERIAL_MODE: 239 | return "serial" 240 | else: 241 | return "parallel" 242 | 243 | 244 | def format_trackobjtype(header_list): 245 | trackobj_type = next(header_list) 246 | if trackobj_type == UNDEFINED: 247 | return "undefined" 248 | elif trackobj_type == RB2D_MARKER: 249 | return "2D RB markers" 250 | elif trackobj_type == RB2D_BLOB: 251 | return "2D RB blob" 252 | elif trackobj_type == RB3D_MARKER: 253 | return "3D RB markers" 254 | elif trackobj_type == BOUNDARY_2D: 255 | return "2D boundary" 256 | elif trackobj_type == MARKER_SIZE: 257 | return "marker size" 258 | else: 259 | return "error" 260 | 261 | 262 | def getdigfactor(ext_headers, idx): 263 | max_analog = ext_headers[idx]["MaxAnalogValue"] 264 | min_analog = ext_headers[idx]["MinAnalogValue"] 265 | max_digital = ext_headers[idx]["MaxDigitalValue"] 266 | min_digital = ext_headers[idx]["MinDigitalValue"] 267 | return float(max_analog - min_analog) / float(max_digital - min_digital) 268 | 269 | 270 | # 271 | 272 | 273 | # 274 | nev_header_dict = { 275 | "basic": [ 276 | FieldDef("FileTypeID", "8s", format_stripstring), # 8 bytes - 8 char array 277 | FieldDef("FileSpec", "2B", format_filespec), # 2 bytes - 2 unsigned char 278 | FieldDef("AddFlags", "H", format_none), # 2 bytes - uint16 279 | FieldDef("BytesInHeader", "I", format_none), # 4 bytes - uint32 280 | FieldDef("BytesInDataPackets", "I", format_none), # 4 bytes - uint32 281 | FieldDef("TimeStampResolution", "I", format_none), # 4 bytes - uint32 282 | FieldDef("SampleTimeResolution", "I", format_none), # 4 bytes - uint32 283 | FieldDef("TimeOrigin", "8H", format_timeorigin), # 16 bytes - 8 x uint16 284 | FieldDef( 285 | "CreatingApplication", "32s", format_stripstring 286 | ), # 32 bytes - 32 char array 287 | FieldDef("Comment", "256s", format_stripstring), # 256 bytes - 256 char array 288 | FieldDef("NumExtendedHeaders", "I", format_none), 289 | ], # 4 bytes - uint32 290 | "ARRAYNME": FieldDef( 291 | "ArrayName", "24s", format_stripstring 292 | ), # 24 bytes - 24 char array 293 | "ECOMMENT": FieldDef( 294 | "ExtraComment", "24s", format_stripstring 295 | ), # 24 bytes - 24 char array 296 | "CCOMMENT": FieldDef( 297 | "ContComment", "24s", format_stripstring 298 | ), # 24 bytes - 24 char array 299 | "MAPFILE": FieldDef( 300 | "MapFile", "24s", format_stripstring 301 | ), # 24 bytes - 24 char array 302 | "NEUEVWAV": [ 303 | FieldDef("ElectrodeID", "H", format_none), # 2 bytes - uint16 304 | FieldDef( 305 | "PhysicalConnector", "B", format_charstring 306 | ), # 1 byte - 1 unsigned char 307 | FieldDef("ConnectorPin", "B", format_charstring), # 1 byte - 1 unsigned char 308 | FieldDef("DigitizationFactor", "H", format_none), # 2 bytes - uint16 309 | FieldDef("EnergyThreshold", "H", format_none), # 2 bytes - uint16 310 | FieldDef("HighThreshold", "h", format_none), # 2 bytes - int16 311 | FieldDef("LowThreshold", "h", format_none), # 2 bytes - int16 312 | FieldDef( 313 | "NumSortedUnits", "B", format_charstring 314 | ), # 1 byte - 1 unsigned char 315 | FieldDef( 316 | "BytesPerWaveform", "B", format_charstring 317 | ), # 1 byte - 1 unsigned char 318 | FieldDef("SpikeWidthSamples", "H", format_none), # 2 bytes - uint16 319 | FieldDef("EmptyBytes", "8s", format_none), 320 | ], # 8 bytes - empty 321 | "NEUEVLBL": [ 322 | FieldDef("ElectrodeID", "H", format_none), # 2 bytes - uint16 323 | FieldDef("Label", "16s", format_stripstring), # 16 bytes - 16 char array 324 | FieldDef("EmptyBytes", "6s", format_none), 325 | ], # 6 bytes - empty 326 | "NEUEVFLT": [ 327 | FieldDef("ElectrodeID", "H", format_none), # 2 bytes - uint16 328 | FieldDef("HighFreqCorner", "I", format_freq), # 4 bytes - uint32 329 | FieldDef("HighFreqOrder", "I", format_none), # 4 bytes - uint32 330 | FieldDef("HighFreqType", "H", format_filter), # 2 bytes - uint16 331 | FieldDef("LowFreqCorner", "I", format_freq), # 4 bytes - uint32 332 | FieldDef("LowFreqOrder", "I", format_none), # 4 bytes - uint32 333 | FieldDef("LowFreqType", "H", format_filter), # 2 bytes - uint16 334 | FieldDef("EmptyBytes", "2s", format_none), 335 | ], # 2 bytes - empty 336 | "DIGLABEL": [ 337 | FieldDef("Label", "16s", format_stripstring), # 16 bytes - 16 char array 338 | FieldDef("Mode", "?", format_digmode), # 1 byte - boolean 339 | FieldDef("EmptyBytes", "7s", format_none), 340 | ], # 7 bytes - empty 341 | "NSASEXEV": [ 342 | FieldDef("Frequency", "H", format_none), # 2 bytes - uint16 343 | FieldDef( 344 | "DigitalInputConfig", "B", format_digconfig 345 | ), # 1 byte - 1 unsigned char 346 | FieldDef( 347 | "AnalogCh1Config", "B", format_anaconfig 348 | ), # 1 byte - 1 unsigned char 349 | FieldDef("AnalogCh1DetectVal", "h", format_none), # 2 bytes - int16 350 | FieldDef( 351 | "AnalogCh2Config", "B", format_anaconfig 352 | ), # 1 byte - 1 unsigned char 353 | FieldDef("AnalogCh2DetectVal", "h", format_none), # 2 bytes - int16 354 | FieldDef( 355 | "AnalogCh3Config", "B", format_anaconfig 356 | ), # 1 byte - 1 unsigned char 357 | FieldDef("AnalogCh3DetectVal", "h", format_none), # 2 bytes - int16 358 | FieldDef( 359 | "AnalogCh4Config", "B", format_anaconfig 360 | ), # 1 byte - 1 unsigned char 361 | FieldDef("AnalogCh4DetectVal", "h", format_none), # 2 bytes - int16 362 | FieldDef( 363 | "AnalogCh5Config", "B", format_anaconfig 364 | ), # 1 byte - 1 unsigned char 365 | FieldDef("AnalogCh5DetectVal", "h", format_none), # 2 bytes - int16 366 | FieldDef("EmptyBytes", "6s", format_none), 367 | ], # 2 bytes - empty 368 | "VIDEOSYN": [ 369 | FieldDef("VideoSourceID", "H", format_none), # 2 bytes - uint16 370 | FieldDef("VideoSource", "16s", format_stripstring), # 16 bytes - 16 char array 371 | FieldDef("FrameRate", "f", format_none), # 4 bytes - single float 372 | FieldDef("EmptyBytes", "2s", format_none), 373 | ], # 2 bytes - empty 374 | "TRACKOBJ": [ 375 | FieldDef("TrackableType", "H", format_trackobjtype), # 2 bytes - uint16 376 | FieldDef("TrackableID", "I", format_none), # 4 bytes - uint32 377 | # FieldDef('PointCount', 'H', format_none), # 2 bytes - uint16 378 | FieldDef("VideoSource", "16s", format_stripstring), # 16 bytes - 16 char array 379 | FieldDef("EmptyBytes", "2s", format_none), 380 | ], # 2 bytes - empty 381 | } 382 | 383 | nsx_header_dict = { 384 | "basic_21": [ 385 | FieldDef("Label", "16s", format_stripstring), # 16 bytes - 16 char array 386 | FieldDef("Period", "I", format_none), # 4 bytes - uint32 387 | FieldDef("ChannelCount", "I", format_none), 388 | ], # 4 bytes - uint32 389 | "basic": [ 390 | FieldDef("FileSpec", "2B", format_filespec), # 2 bytes - 2 unsigned char 391 | FieldDef("BytesInHeader", "I", format_none), # 4 bytes - uint32 392 | FieldDef("Label", "16s", format_stripstring), # 16 bytes - 16 char array 393 | FieldDef("Comment", "256s", format_stripstring), # 256 bytes - 256 char array 394 | FieldDef("Period", "I", format_none), # 4 bytes - uint32 395 | FieldDef("TimeStampResolution", "I", format_none), # 4 bytes - uint32 396 | FieldDef("TimeOrigin", "8H", format_timeorigin), # 16 bytes - 8 uint16 397 | FieldDef("ChannelCount", "I", format_none), 398 | ], # 4 bytes - uint32 399 | "extended": [ 400 | FieldDef("Type", "2s", format_stripstring), # 2 bytes - 2 char array 401 | FieldDef("ElectrodeID", "H", format_none), # 2 bytes - uint16 402 | FieldDef( 403 | "ElectrodeLabel", "16s", format_stripstring 404 | ), # 16 bytes - 16 char array 405 | FieldDef("PhysicalConnector", "B", format_none), # 1 byte - uint8 406 | FieldDef("ConnectorPin", "B", format_none), # 1 byte - uint8 407 | FieldDef("MinDigitalValue", "h", format_none), # 2 bytes - int16 408 | FieldDef("MaxDigitalValue", "h", format_none), # 2 bytes - int16 409 | FieldDef("MinAnalogValue", "h", format_none), # 2 bytes - int16 410 | FieldDef("MaxAnalogValue", "h", format_none), # 2 bytes - int16 411 | FieldDef("Units", "16s", format_stripstring), # 16 bytes - 16 char array 412 | FieldDef("HighFreqCorner", "I", format_freq), # 4 bytes - uint32 413 | FieldDef("HighFreqOrder", "I", format_none), # 4 bytes - uint32 414 | FieldDef("HighFreqType", "H", format_filter), # 2 bytes - uint16 415 | FieldDef("LowFreqCorner", "I", format_freq), # 4 bytes - uint32 416 | FieldDef("LowFreqOrder", "I", format_none), # 4 bytes - uint32 417 | FieldDef("LowFreqType", "H", format_filter), 418 | ], # 2 bytes - uint16 419 | "data": [ 420 | FieldDef("Header", "B", format_none), # 1 byte - uint8 421 | FieldDef("Timestamp", "I", format_none), # 4 bytes - uint32 422 | FieldDef("NumDataPoints", "I", format_none), 423 | ], # 4 bytes - uint32] 424 | } 425 | # 426 | 427 | 428 | # 429 | def check_elecid(elec_ids): 430 | if type(elec_ids) is str and elec_ids != ELEC_ID_DEF: 431 | print( 432 | "\n*** WARNING: Electrode IDs must be 'all', a single integer, or a list of integers." 433 | ) 434 | print(" Setting elec_ids to 'all'") 435 | elec_ids = ELEC_ID_DEF 436 | if elec_ids != ELEC_ID_DEF and type(elec_ids) is not list: 437 | if type(elec_ids) == range: 438 | elec_ids = list(elec_ids) 439 | elif type(elec_ids) == int: 440 | elec_ids = [elec_ids] 441 | return elec_ids 442 | 443 | 444 | def check_starttime(start_time_s): 445 | if not isinstance(start_time_s, (int, float)) or ( 446 | isinstance(start_time_s, (int, float)) and start_time_s < START_TIME_DEF 447 | ): 448 | print("\n*** WARNING: Start time is not valid, setting start_time_s to 0") 449 | start_time_s = START_TIME_DEF 450 | return start_time_s 451 | 452 | 453 | def check_datatime(data_time_s): 454 | if (type(data_time_s) is str and data_time_s != DATA_TIME_DEF) or ( 455 | isinstance(data_time_s, (int, float)) and data_time_s < 0 456 | ): 457 | print("\n*** WARNING: Data time is not valid, setting data_time_s to 'all'") 458 | data_time_s = DATA_TIME_DEF 459 | return data_time_s 460 | 461 | 462 | def check_downsample(downsample): 463 | if not isinstance(downsample, int) or downsample < DOWNSAMPLE_DEF: 464 | print( 465 | "\n*** WARNING: downsample must be an integer value greater than 0. " 466 | " Setting downsample to 1 (no downsampling)" 467 | ) 468 | downsample = DOWNSAMPLE_DEF 469 | if downsample > 1: 470 | print( 471 | "\n*** WARNING: downsample will be deprecated in a future version." 472 | " Set downsample to 1 (default) to match future behavior." 473 | "\n*** WARNING: downsample does not perform anti-aliasing." 474 | ) 475 | return downsample 476 | 477 | 478 | def check_dataelecid(elec_ids, all_elec_ids): 479 | unique_elec_ids = set(elec_ids) 480 | all_elec_ids = set(all_elec_ids) 481 | 482 | # if some electrodes asked for don't exist, reset list with those that do, or throw error and return 483 | if not unique_elec_ids.issubset(all_elec_ids): 484 | if not unique_elec_ids & all_elec_ids: 485 | print("\nNone of the elec_ids passed exist in the data, returning None") 486 | return None 487 | else: 488 | print( 489 | "\n*** WARNING: Channels " 490 | + str(sorted(list(unique_elec_ids - all_elec_ids))) 491 | + " do not exist in the data" 492 | ) 493 | unique_elec_ids = unique_elec_ids & all_elec_ids 494 | 495 | return sorted(list(unique_elec_ids)) 496 | 497 | 498 | def check_filesize(file_size): 499 | if file_size < DATA_FILE_SIZE_MIN: 500 | print("\n file_size must be larger than 10 Mb, setting file_size=10 Mb") 501 | return DATA_FILE_SIZE_MIN 502 | else: 503 | return int(file_size) 504 | 505 | 506 | # 507 | 508 | 509 | class NevFile: 510 | """ 511 | attributes and methods for all BR event data files. Initialization opens the file and extracts the 512 | basic header information. 513 | """ 514 | 515 | def __init__(self, datafile=""): 516 | self.datafile = datafile 517 | self.basic_header = {} 518 | self.extended_headers = [] 519 | 520 | # Run openfilecheck and open the file passed or allow user to browse to one 521 | self.datafile = openfilecheck( 522 | "rb", 523 | file_name=self.datafile, 524 | file_ext=".nev", 525 | file_type="Blackrock NEV Files", 526 | ) 527 | 528 | # extract basic header information 529 | self.basic_header = processheaders(self.datafile, nev_header_dict["basic"]) 530 | 531 | # Extract extended headers 532 | for i in range(self.basic_header["NumExtendedHeaders"]): 533 | self.extended_headers.append({}) 534 | header_string = bytes.decode( 535 | unpack("<8s", self.datafile.read(8))[0], "latin-1" 536 | ) 537 | self.extended_headers[i]["PacketID"] = header_string.split( 538 | STRING_TERMINUS, 1 539 | )[0] 540 | self.extended_headers[i].update( 541 | processheaders( 542 | self.datafile, nev_header_dict[self.extended_headers[i]["PacketID"]] 543 | ) 544 | ) 545 | 546 | # Must set this for file spec 2.1 and 2.2 547 | if ( 548 | header_string == "NEUEVWAV" 549 | and float(self.basic_header["FileSpec"]) < 2.3 550 | ): 551 | self.extended_headers[i]["SpikeWidthSamples"] = WAVEFORM_SAMPLES_21 552 | 553 | def getdata(self, elec_ids="all", wave_read="read"): 554 | """ 555 | This function is used to return a set of data from the NEV datafile. 556 | 557 | :param elec_ids: [optional] {list} User selection of elec_ids to extract specific spike waveforms (e.g., [13]) 558 | :param wave_read: [optional] {STR} 'read' or 'no_read' - whether to read waveforms or not 559 | :return: output: {Dictionary} with one or more of the following dictionaries (all include TimeStamps) 560 | dig_events: Reason, Data, [for file spec 2.2 and below, AnalogData and AnalogDataUnits] 561 | spike_events: Units='nV', ChannelID, NEUEVWAV_HeaderIndices, Classification, Waveforms 562 | comments: CharSet, Flag, Data, Comment 563 | video_sync_events: VideoFileNum, VideoFrameNum, VideoElapsedTime_ms, VideoSourceID 564 | tracking_events: ParentID, NodeID, NodeCount, TrackingPoints 565 | button_trigger_events: TriggerType 566 | configuration_events: ConfigChangeType 567 | 568 | Note: For digital and neural data - TimeStamps, Classification, and Data can be lists of lists when more 569 | than one digital type or spike event exists for a channel 570 | """ 571 | 572 | # Initialize output dictionary and reset position in file (if read before, may not be here anymore) 573 | output = dict() 574 | 575 | # Safety checks 576 | elec_ids = check_elecid(elec_ids) 577 | 578 | ###### 579 | # extract raw data 580 | self.datafile.seek(0, 2) 581 | lData = self.datafile.tell() 582 | nPackets = int( 583 | (lData - self.basic_header["BytesInHeader"]) 584 | / self.basic_header["BytesInDataPackets"] 585 | ) 586 | self.datafile.seek(self.basic_header["BytesInHeader"], 0) 587 | rawdata = self.datafile.read() 588 | # rawdataArray = np.reshape(np.fromstring(rawdata,'B'),(nPackets,self.basic_header['BytesInDataPackets'])) 589 | 590 | # Find all timestamps and PacketIDs 591 | if self.basic_header["FileTypeID"] == "BREVENTS": 592 | tsBytes = 8 593 | ts = np.ndarray( 594 | (nPackets,), 595 | " 0: 626 | ChannelID = PacketID 627 | if type(elec_ids) is list: 628 | elecindices = [ 629 | idx 630 | for idx, element in enumerate(ChannelID[neuralPackets]) 631 | if element in elec_ids 632 | ] 633 | neuralPackets = [neuralPackets[index] for index in elecindices] 634 | 635 | spikeUnit = np.ndarray( 636 | (nPackets,), 637 | " 0: 666 | insertionReason = np.ndarray( 667 | (nPackets,), 668 | " 0: 691 | charSet = np.ndarray( 692 | (nPackets,), 693 | " 0: 711 | charSetList[ANSIPackets] = "ANSI" 712 | UTFPackets = [ 713 | idx for idx, element in enumerate(charSet) if element == CHARSET_UTF 714 | ] 715 | if len(UTFPackets) > 0: 716 | charSetList[UTFPackets] = "UTF " 717 | 718 | # need to transfer comments from neuromotive. identify region of interest (ROI) events... 719 | ROIPackets = [ 720 | idx for idx, element in enumerate(charSet) if element == CHARSET_ROI 721 | ] 722 | 723 | lcomment = self.basic_header["BytesInDataPackets"] - tsBytes - 10 724 | comments = np.chararray( 725 | (nPackets, lcomment), 726 | 1, 727 | False, 728 | rawdata, 729 | tsBytes + 8, 730 | (self.basic_header["BytesInDataPackets"], 1), 731 | ) 732 | 733 | # extract only the "true" comments, distinct from ROI packets 734 | trueComments = np.setdiff1d( 735 | list(range(0, len(commentPackets) - 1)), ROIPackets 736 | ) 737 | trueCommentsidx = np.asarray(commentPackets)[trueComments] 738 | textComments = comments[trueCommentsidx] 739 | textComments[:, -1] = "$" 740 | stringarray = textComments.tostring() 741 | stringvector = stringarray.decode("latin-1") 742 | stringvector = stringvector[0:-1] 743 | validstrings = stringvector.replace("\x00", "") 744 | commentsFinal = validstrings.split("$") 745 | 746 | # Remove the ROI comments from the list 747 | subsetInds = list( 748 | set(list(range(0, len(charSetList) - 1))) - set(ROIPackets) 749 | ) 750 | 751 | output["comments"] = { 752 | "TimeStamps": list(ts[trueCommentsidx]), 753 | "TimeStampsStarted": list(tsStarted[trueCommentsidx]), 754 | "Data": commentsFinal, 755 | "CharSet": list(charSetList[subsetInds]), 756 | } 757 | 758 | # parsing and outputing ROI events 759 | if len(ROIPackets) > 0: 760 | nmPackets = np.asarray(ROIPackets) 761 | nmCommentsidx = np.asarray(commentPackets)[ROIPackets] 762 | nmcomments = comments[nmCommentsidx] 763 | nmcomments[:, -1] = ":" 764 | nmstringarray = nmcomments.tostring() 765 | nmstringvector = nmstringarray.decode("latin-1") 766 | nmstringvector = nmstringvector[0:-1] 767 | nmvalidstrings = nmstringvector.replace("\x00", "") 768 | nmcommentsFinal = nmvalidstrings.split(":") 769 | ROIfields = [l.split(":") for l in ":".join(nmcommentsFinal).split(":")] 770 | ROIfieldsRS = np.reshape(ROIfields, (len(ROIPackets), 5)) 771 | output["tracking_events"] = { 772 | "TimeStamps": list(ts[nmCommentsidx]), 773 | "ROIName": list(ROIfieldsRS[:, 0]), 774 | "ROINumber": list(ROIfieldsRS[:, 1]), 775 | "Event": list(ROIfieldsRS[:, 2]), 776 | "Frame": list(ROIfieldsRS[:, 3]), 777 | } 778 | 779 | # NeuroMotive video syncronization packets 780 | vidsyncPackets = [ 781 | idx 782 | for idx, element in enumerate(PacketID) 783 | if element == VIDEO_SYNC_PACKET_ID 784 | ] 785 | if len(vidsyncPackets) > 0: 786 | fileNumber = np.ndarray( 787 | (nPackets,), 788 | " 0: 827 | trackerObjs = [ 828 | sub["VideoSource"] 829 | for sub in self.extended_headers 830 | if sub["PacketID"] == "TRACKOBJ" 831 | ] 832 | trackerIDs = [ 833 | sub["TrackableID"] 834 | for sub in self.extended_headers 835 | if sub["PacketID"] == "TRACKOBJ" 836 | ] 837 | output["tracking"] = { 838 | "TrackerIDs": trackerIDs, 839 | "TrackerTypes": [ 840 | sub["TrackableType"] 841 | for sub in self.extended_headers 842 | if sub["PacketID"] == "TRACKOBJ" 843 | ], 844 | } 845 | parentID = np.ndarray( 846 | (nPackets,), 847 | " 0: 937 | trigType = np.ndarray( 938 | (nPackets,), 939 | " 0: 956 | changeType = np.ndarray( 957 | (nPackets,), 958 | "= 3.x with PTP timestamping. 1093 | :return: output: {Dictionary} of: data_headers: {list} dictionaries of all data headers, 1 per segment 1094 | [seg_id]["Timestamp"]: timestamps of each sample in segment 1095 | if full_timestamps, else timestamp of first sample in segment 1096 | [seg_id]["NumDataPoints"]: number of samples in segment 1097 | [seg_id]["data_time_s"]: duration in segment 1098 | elec_ids: {list} elec_ids that were extracted (sorted) 1099 | start_time_s: {float} starting time for data extraction 1100 | data_time_s: {float} length of time of data returned 1101 | downsample: {int} data downsampling factor 1102 | samp_per_s: {float} output data samples per second 1103 | data: {numpy array} continuous data in a 2D elec x samps numpy array 1104 | (or samps x elec if elec_rows is False). 1105 | """ 1106 | # Safety checks 1107 | start_time_s = check_starttime(start_time_s) 1108 | data_time_s = check_datatime(data_time_s) 1109 | downsample = check_downsample(downsample) 1110 | elec_ids = check_elecid(elec_ids) 1111 | if zeropad and self.basic_header["TimeStampResolution"] == 1e9: 1112 | print("zeropad does not work with ptp-timestamped data. Ignoring zeropad argument.\n") 1113 | zeropad = False 1114 | if force_srate and self.basic_header["TimeStampResolution"] != 1e9: 1115 | print("force_srate only works with ptp timestamps in filespec >= 3.x. Ignoring force_srate argument.\n") 1116 | force_srate = False 1117 | 1118 | # initialize parameters 1119 | output = dict() 1120 | output["start_time_s"] = float(start_time_s) 1121 | output["data_time_s"] = data_time_s 1122 | output["downsample"] = downsample 1123 | output["elec_ids"] = [] 1124 | output["data_headers"] = [] # List of dicts with fields Timestamp, NumDataPoints, data_time_s, BoH, BoD 1125 | output["data"] = [] # List of ndarrays 1126 | output["samp_per_s"] = self.basic_header["SampleResolution"] / self.basic_header["Period"] 1127 | 1128 | # Pull some useful variables from the basic_header 1129 | data_pt_size = self.basic_header["ChannelCount"] * DATA_BYTE_SIZE 1130 | clk_per_samp = self.basic_header["Period"] * self.basic_header["TimeStampResolution"] / self.basic_header["SampleResolution"] 1131 | filespec_maj, filespec_min = tuple([int(_) for _ in self.basic_header["FileSpec"].split(".")][:2]) 1132 | 1133 | # Timestamp is 64-bit for filespec >= 3.0 1134 | ts_type, ts_size = (" 2 else ("= 3: 1191 | # Starty by assuming that these files are from firmware >= 7.6 thus we have 1 sample per packet. 1192 | npackets = int((eof - eoh) / np.dtype(ptp_dt).itemsize) 1193 | struct_arr = np.memmap(self.datafile, dtype=ptp_dt, shape=npackets, offset=eoh, mode="r") 1194 | self.datafile.seek(eoh, 0) # Reset to end-of-header in case memmap moved the pointer. 1195 | samp_per_pkt = np.all(struct_arr["num_data_points"] == 1) # Confirm 1 sample per packet 1196 | 1197 | if not samp_per_pkt: 1198 | # Multiple samples per packet; 1 packet == 1 uninterrupted segment. 1199 | while 0 < self.datafile.tell() < ospath.getsize(self.datafile.name): 1200 | # boh = self.datafile.tell() # Beginning of segment header 1201 | self.datafile.seek(1, 1) # Skip the reserved 0x01 1202 | timestamp = unpack(ts_type, self.datafile.read(ts_size))[0] 1203 | num_data_pts = unpack(" expected_loc: 1226 | # Moved it too far (probably to end of file); move manually from beginning to expected. 1227 | self.datafile.seek(expected_loc, 0) 1228 | else: 1229 | # 1 sample per packet. Reuse struct_arr. 1230 | seg_thresh_clk = 2 * clk_per_samp 1231 | seg_starts = np.hstack((0, 1 + np.argwhere(np.diff(struct_arr["timestamps"]) > seg_thresh_clk).flatten())) 1232 | for seg_ix, seg_start_idx in enumerate(seg_starts): 1233 | seg_stop_idx = seg_starts[seg_ix + 1] if seg_ix < (len(seg_starts) - 1) else (len(struct_arr) - 1) 1234 | offset = eoh + seg_start_idx * struct_arr.dtype.itemsize 1235 | num_data_pts = seg_stop_idx - seg_start_idx 1236 | seg_struct_arr = np.memmap(self.datafile, dtype=ptp_dt, shape=num_data_pts, offset=offset, mode="r") 1237 | output["data_headers"].append({ 1238 | "Timestamp": seg_struct_arr["timestamps"], 1239 | "NumDataPoints": num_data_pts, 1240 | "data_time_s": num_data_pts / output["samp_per_s"] 1241 | }) 1242 | output["data"].append(seg_struct_arr["samples"]) 1243 | 1244 | ## Post-processing ## 1245 | 1246 | # Drop segments that are not within the requested time window 1247 | ts_0 = output["data_headers"][0]["Timestamp"][0] 1248 | start_time_ts = start_time_s * self.basic_header["TimeStampResolution"] 1249 | test_start_ts = ts_0 + start_time_ts 1250 | test_stop_ts = np.inf # Will update below 1251 | if start_time_s != START_TIME_DEF: 1252 | # Keep segments with at least one sample on-or-after test_start_ts 1253 | b_keep = [_["Timestamp"][-1] >= test_start_ts for _ in output["data_headers"]] 1254 | output["data_headers"] = [_ for _, b in zip(output["data_headers"], b_keep) if b] 1255 | output["data"] = [_ for _, b in zip(output["data"], b_keep) if b] 1256 | if data_time_s != DATA_TIME_DEF: 1257 | # Keep segments with at least one sample on-or-before test_stop_ts 1258 | data_time_ts = data_time_s * self.basic_header["TimeStampResolution"] 1259 | test_stop_ts = test_start_ts + data_time_ts 1260 | b_keep = [_["Timestamp"][0] <= test_stop_ts for _ in output["data_headers"]] 1261 | output["data_headers"] = [_ for _, b in zip(output["data_headers"], b_keep) if b] 1262 | output["data"] = [_ for _, b in zip(output["data"], b_keep) if b] 1263 | 1264 | # Post-process segments for start_time_s, data_time_s, zeropad 1265 | for ix, data_header in enumerate(output["data_headers"]): 1266 | data = output["data"][ix] 1267 | # start_time_s and data_time_s 1268 | b_keep = np.ones((data.shape[0],), dtype=bool) 1269 | if start_time_s > START_TIME_DEF and data_header["Timestamp"][0] < test_start_ts: 1270 | # if segment begins before test_start_ts, slice it to begin at test_start_ts. 1271 | b_keep &= data_header["Timestamp"] >= test_start_ts 1272 | if data_time_s != DATA_TIME_DEF and data_header["Timestamp"][-1] > test_stop_ts: 1273 | # if segment finishes after start_time_s + data_time_s, slice it to finish at start_time_s + data_time_s 1274 | b_keep &= data_header["Timestamp"] <= test_stop_ts 1275 | if np.any(~b_keep): 1276 | data_header["Timestamp"] = data_header["Timestamp"][b_keep] 1277 | data = data[b_keep] 1278 | 1279 | # zeropad: Prepend the data with zeros so its first timestamp is nsp_time=0. 1280 | if ix == 0 and zeropad and data_header["Timestamp"][0] != 0: 1281 | # Calculate how many samples we need. 1282 | padsize = ceil(data_header["Timestamp"][0] / self.basic_header["Period"]) 1283 | pad_dat = np.zeros((padsize, data.shape[1]), dtype=data.dtype) 1284 | # Stack pad_dat in front of output["data"][ix]. Slow! Might run out of memory! 1285 | try: 1286 | data = np.vstack((pad_dat, data)) 1287 | except MemoryError as err: 1288 | err.args += ( 1289 | " Output data size requested is larger than available memory. Use the parameters\n" 1290 | " for getdata(), e.g., 'elec_ids', to request a subset of the data or use\n" 1291 | " NsxFile.savesubsetnsx() to create subsets of the main nsx file\n", 1292 | ) 1293 | raise 1294 | pad_ts = data_header["Timestamp"][0] - (clk_per_samp * np.arange(1, padsize + 1)).astype(np.int64)[::-1] 1295 | data_header["Timestamp"] = np.hstack((pad_ts, data_header["Timestamp"])) 1296 | 1297 | # force_srate: Force the returned arrays to have exactly the expected number of samples per elapsed ptp time. 1298 | if force_srate: 1299 | # Dur of segment in ts-clks (nanoseconds) 1300 | seg_clks = data_header["Timestamp"][-1] - data_header["Timestamp"][0] + np.uint64(clk_per_samp) 1301 | # Number of samples in segment 1302 | npoints = data.shape[0] 1303 | # Expected number of samples based on duration. 1304 | n_expected = seg_clks / clk_per_samp 1305 | # How many are we missing? -ve number means we have too many. 1306 | n_insert = int(np.round(n_expected - npoints)) 1307 | # identify where in the segments the data should be added/removed 1308 | insert_inds = np.linspace(0, npoints, num=abs(n_insert) + 1, endpoint=False, dtype=int)[1:] 1309 | if n_insert > 0: 1310 | # Create samples for the middle of the N largest gaps then insert. 1311 | insert_vals = (data[insert_inds] + data[insert_inds + 1]) / 2 1312 | data = np.insert(data, insert_inds, insert_vals, axis=0) 1313 | elif n_insert < 0: 1314 | data = np.delete(data, insert_inds, axis=0) 1315 | 1316 | # Replace data_header["Timestamp"] with ideal timestamps 1317 | data_header["Timestamp"] = data_header["Timestamp"][0] + (clk_per_samp * np.arange(data.shape[0])).astype(np.int64) 1318 | 1319 | if downsample > 1: 1320 | data = data[::downsample] 1321 | 1322 | data_header["NumDataPoints"] = data.shape[0] 1323 | data_header["data_time_s"] = data_header["NumDataPoints"] / output["samp_per_s"] 1324 | 1325 | if elec_rows: 1326 | data = data.T 1327 | 1328 | output["data"][ix] = data 1329 | 1330 | if not full_timestamps: 1331 | data_header["Timestamp"] = data_header["Timestamp"][0] 1332 | 1333 | return output 1334 | 1335 | def savesubsetnsx( 1336 | self, elec_ids="all", file_size=None, file_time_s=None, file_suffix="" 1337 | ): 1338 | """ 1339 | This function is used to save a subset of data based on electrode IDs, file sizing, or file data time. If 1340 | both file_time_s and file_size are passed, it will default to file_time_s and determine sizing accordingly. 1341 | 1342 | :param elec_ids: [optional] {list} List of elec_ids to extract (e.g., [13]) 1343 | :param file_size: [optional] {int} Byte size of each subset file to save (e.g., 1024**3 = 1 Gb). If nothing 1344 | is passed, file_size will be all data points. 1345 | :param file_time_s: [optional] {float} Time length of data for each subset file, in seconds (e.g. 60.0). If 1346 | nothing is passed, file_size will be used as default. 1347 | :param file_suffix: [optional] {str} Suffix to append to NSx datafile name for subset files. If nothing is 1348 | passed, default will be "_subset". 1349 | :return: None - None of the electrodes requested exist in the data 1350 | SUCCESS - All file subsets extracted and saved 1351 | """ 1352 | 1353 | # Initializations 1354 | elec_id_indices = [] 1355 | file_num = 1 1356 | pausing = False 1357 | datafile_datapt_size = self.basic_header["ChannelCount"] * DATA_BYTE_SIZE 1358 | self.datafile.seek(0, 0) 1359 | 1360 | # Run electrode id checks and set num_elecs 1361 | elec_ids = check_elecid(elec_ids) 1362 | if self.basic_header["FileSpec"] == "2.1": 1363 | all_elec_ids = self.basic_header["ChannelID"] 1364 | else: 1365 | all_elec_ids = [x["ElectrodeID"] for x in self.extended_headers] 1366 | 1367 | if elec_ids == ELEC_ID_DEF: 1368 | elec_ids = all_elec_ids 1369 | else: 1370 | elec_ids = check_dataelecid(elec_ids, all_elec_ids) 1371 | if not elec_ids: 1372 | return None 1373 | else: 1374 | elec_id_indices = [all_elec_ids.index(x) for x in elec_ids] 1375 | 1376 | num_elecs = len(elec_ids) 1377 | 1378 | # If file_size or file_time_s passed, check it and set file_sizing accordingly 1379 | if file_time_s: 1380 | if file_time_s and file_size: 1381 | print( 1382 | "\nWARNING: Only one of file_size or file_time_s can be passed, defaulting to file_time_s." 1383 | ) 1384 | file_size = int( 1385 | num_elecs 1386 | * DATA_BYTE_SIZE 1387 | * file_time_s 1388 | * self.basic_header["TimeStampResolution"] 1389 | / self.basic_header["Period"] 1390 | ) 1391 | if self.basic_header["FileSpec"] == "2.1": 1392 | file_size += 32 + 4 * num_elecs 1393 | else: 1394 | file_size += ( 1395 | NSX_BASIC_HEADER_BYTES_22 + NSX_EXT_HEADER_BYTES_22 * num_elecs + 5 1396 | ) 1397 | print( 1398 | "\nBased on timing request, file size will be {0:d} Mb".format( 1399 | int(file_size / 1024**2) 1400 | ) 1401 | ) 1402 | elif file_size: 1403 | file_size = check_filesize(file_size) 1404 | 1405 | # Create and open subset file as writable binary, if it already exists ask user for overwrite permission 1406 | file_name, file_ext = ospath.splitext(self.datafile.name) 1407 | if file_suffix: 1408 | file_name += "_" + file_suffix 1409 | else: 1410 | file_name += "_subset" 1411 | 1412 | if ospath.isfile(file_name + "_000" + file_ext): 1413 | if "y" != input( 1414 | "\nFile '" 1415 | + file_name.split("/")[-1] 1416 | + "_xxx" 1417 | + file_ext 1418 | + "' already exists, overwrite [y/n]: " 1419 | ): 1420 | print("\nExiting, no overwrite, returning None") 1421 | return None 1422 | else: 1423 | print("\n*** Overwriting existing subset files ***") 1424 | 1425 | subset_file = open(file_name + "_000" + file_ext, "wb") 1426 | print("\nWriting subset file: " + ospath.split(subset_file.name)[1]) 1427 | 1428 | # For file spec 2.1: 1429 | # 1) copy the first 28 bytes from the datafile (these are unchanged) 1430 | # 2) write subset channel count and channel ID to file 1431 | # 3) skip ahead in datafile the number of bytes in datafile ChannelCount(4) plus ChannelID (4*ChannelCount) 1432 | if self.basic_header["FileSpec"] == "2.1": 1433 | subset_file.write(self.datafile.read(28)) 1434 | subset_file.write(np.array(num_elecs).astype(np.uint32).tobytes()) 1435 | subset_file.write(np.array(elec_ids).astype(np.uint32).tobytes()) 1436 | self.datafile.seek(4 + 4 * self.basic_header["ChannelCount"], 1) 1437 | 1438 | # For file spec 2.2 and above 1439 | # 1) copy the first 10 bytes from the datafile (unchanged) 1440 | # 2) write subset bytes-in-headers and skip 4 bytes in datafile, noting position of this for update later 1441 | # 3) copy the next 296 bytes from datafile (unchanged) 1442 | # 4) write subset channel-count value and skip 4 bytes in datafile 1443 | # 5) append extended headers based on the channel ID. Must read the first 4 bytes, determine if correct 1444 | # Channel ID, repack first 4 bytes, write to disk, then copy remaining 62 (66-4) bytes 1445 | else: 1446 | subset_file.write(self.datafile.read(10)) 1447 | bytes_in_headers = ( 1448 | NSX_BASIC_HEADER_BYTES_22 + NSX_EXT_HEADER_BYTES_22 * num_elecs 1449 | ) 1450 | num_pts_header_pos = bytes_in_headers + 5 1451 | subset_file.write(np.array(bytes_in_headers).astype(np.uint32).tobytes()) 1452 | self.datafile.seek(4, 1) 1453 | subset_file.write(self.datafile.read(296)) 1454 | subset_file.write(np.array(num_elecs).astype(np.uint32).tobytes()) 1455 | self.datafile.seek(4, 1) 1456 | 1457 | for i in range(len(self.extended_headers)): 1458 | h_type = self.datafile.read(2) 1459 | chan_id = self.datafile.read(2) 1460 | if unpack("