# ndx-ecg Extension for NWB
This extension is developed to extend NWB data standards to incorporate ECG recordings. `CardiacSeries`, the main neurodata-type in this extension, in fact extends the base type of NWB TimeSeries and can be stored into three specific data interfaces of `ECG`, `HeartRate` and `AuxiliaryAnalysis`. Also, the `ECGRecordingGroup` is another neurodata-type in this module which extends `LabMetaData` which itself extends the NWBContainer and stores descriptive meta-data recording channels information along with the electrodes implementation (`ECGChannels` and `ECGElectrodes` respectively as extensions of DynamicTable) and a link to another extended neurodata-type -`ECGRecDevice`- which extends the type Device.
<div align="center">
<img src="https://github.com/Defense-Circuits-Lab/ndx_ecg/assets/63550467/4efaddd3-ced3-47d3-9b53-c82647e8c450" width="1000">
</div>
## Installation
Simply clone the repo and navigate to the root directory, then:
```
pip install .
```
## Test
A roundTrip test is runnable through ```pytest``` from the root. The test script can be found here:
```
\src\pynwb\tests
```
## An example use-case
The following is an example use case of ```ndx-ecg``` with explanatory comments. First, we build up an ```nwbfile``` and define an endpoint recording device:
```python
from datetime import datetime
from uuid import uuid4
import numpy as np
from dateutil.tz import tzlocal
from pynwb import NWBHDF5IO, NWBFile
from hdmf.common import DynamicTable
from ndx_ecg import (
CardiacSeries,
ECG,
HeartRate,
AuxiliaryAnalysis,
ECGRecordingGroup,
ECGRecDevice,
ECGElectrodes,
ECGChannels
)
nwbfile = NWBFile(
session_description='ECG test-rec session',
identifier=str(uuid4()),
session_start_time=datetime.now(tzlocal()),
experimenter='experimenter',
lab='DCL',
institution='UKW',
experiment_description='',
session_id='',
)
# define an endpoint main recording device
main_device = nwbfile.create_device(
name='endpoint_recording_device',
description='description_of_the_ERD', # ERD: Endpoint recording device
manufacturer='manufacturer_of_the_ERD'
)
```
Then, we define instances of `ECGElectrodes` and `ECGChannels`, to represent the meta-data on the recording electrodes and also the recording channels:
```python
'''
creating an ECG electrodes table
as a DynamicTable
'''
ecg_electrodes_table = ECGElectrodes(
description='descriptive meta-data on ECG recording electrodes'
)
# add electrodes
ecg_electrodes_table.add_row(
electrode_name='el_0',
electrode_location='right upper-chest',
electrode_info='descriptive info on el_0'
)
ecg_electrodes_table.add_row(
electrode_name='el_1',
electrode_location='left lower-chest',
electrode_info='descriptive info on el_1'
)
ecg_electrodes_table.add_row(
electrode_name='reference',
electrode_location='top of the head',
electrode_info='descriptive info on reference'
)
# adding the object of DynamicTable
nwbfile.add_acquisition(ecg_electrodes_table) # storage point for DT
'''
creating an ECG recording channels table
as a DynamicTable
'''
ecg_channels_table = ECGChannels(
description='descriptive meta-data on ECG recording channels'
)
# add channels
ecg_channels_table.add_row(
channel_name='ch_0',
channel_type='single',
involved_electrodes='el_0',
channel_info='channel info on ch_0'
)
ecg_channels_table.add_row(
channel_name='ch_1',
channel_type='differential',
involved_electrodes='el_0 and el_1',
channel_info='channel info on ch_1'
)
# adding the object of DynamicTable
nwbfile.add_acquisition(ecg_channels_table) # storage point for DT
```
Now, we can define an instance of ```ECGRecDevice```:
```python
# define an ECGRecDevice-type device for ecg recording
ecg_device = ECGRecDevice(
name='recording_device',
description='description_of_the_ECGRD',
manufacturer='manufacturer_of_the_ECGRD',
filtering='notch-60Hz-analog',
gain='100',
offset='0',
synchronization='taken care of via ...',
endpoint_recording_device=main_device
)
# adding the object of ECGRecDevice
nwbfile.add_device(ecg_device)
```
And also an instance of ```ECGChannelsGroup```:
```python
ecg_recording_group = ECGRecordingGroup(
name='recording_group',
group_description='a group to store electrodes and channels table, and linking to ECGRecDevice.',
electrodes=ecg_electrodes_table,
channels=ecg_channels_table,
recording_device=ecg_device
)
# adding the object of ECGChannelsGroup
nwbfile.add_lab_meta_data(ecg_recording_group) # storage point for custom LMD
#
```
Now, we have all the required standard arguments to genearate instances of `CardiacSeries` and stroing them in our three different NWBDataInterfaces:
```python
# storing the ECG data
dum_data_ecg = np.random.randn(20, 2)
dum_time_ecg = np.linspace(0, 10, len(dum_data_ecg))
ecg_cardiac_series = CardiacSeries(
name='ecg_raw_CS',
data=dum_data_ecg,
timestamps=dum_time_ecg,
unit='mV',
recording_group=ecg_recording_group
)
ecg_raw = ECG(
cardiac_series=[ecg_cardiac_series],
processing_description='raw acquisition'
)
```
Here, we built an instance of our `CradiacSeries` to store a dummy raw ECG acquisition into a specified `ECG` interface, and we store it as an acquisition into the `nwbfile`:
```python
# adding the raw acquisition of ECG to the nwb_file inside an 'ECG' container
nwbfile.add_acquisition(ecg_raw)
```
In the following, we have taken the similar approach but this time storing dummy data as processed data, into specific interfaces of `HeartRate` and `AuxiliaryAnalysis`, then storing it into a -to be defined- `ecg_module`:
```python
# storing the HeartRate data
dum_data_hr = np.random.randn(10, 2)
dum_time_hr = np.linspace(0, 10, len(dum_data_hr))
hr_cardiac_series = CardiacSeries(
name='heart_rate_CS',
data=dum_data_hr,
timestamps=dum_time_hr,
unit='bpm',
recording_group=ecg_recording_group
)
# defining an ecg_module to store the processed cardiac data and analysis
ecg_module = nwbfile.create_processing_module(
name='cardio_module',
description='a module to store processed cardiac data'
)
hr = HeartRate(
cardiac_series=[hr_cardiac_series],
processing_description='processed heartRate of the animal'
)
# adding the heart rate data to the nwb_file inside an 'HeartRate' container
ecg_module.add(hr)
# storing the Auxiliary data
# An example could be the concept of ceiling that is being used in the literature published by DCL@UKW
dum_data_ceil = np.random.randn(10, 2)
dum_time_ceil = np.linspace(0, 10, len(dum_data_ceil))
ceil_cardiac_series = CardiacSeries(
name='heart_rate_ceil_CS',
data=dum_data_ceil,
timestamps=dum_time_ceil,
unit='bpm',
recording_group=ecg_recording_group
)
ceil = AuxiliaryAnalysis(
cardiac_series=[ceil_cardiac_series],
processing_description='processed auxiliary analysis'
)
# adding the 'ceiling' auxiliary analysis to the nwb_file inside an 'AuxiliaryAnalysis' container
ecg_module.add(ceil)
# storing the processed heart rate: as an NWBDataInterface with the new assigned name instead of default
# An example could be the concept of HR2ceiling that is being used in the literature published by DCL@UKW
dum_data_hr2ceil = np.random.randn(10, 2)
dum_time_hr2ceil = np.linspace(0, 10, len(dum_data_hr2ceil))
hr2ceil_cardiac_series = CardiacSeries(
name='heart_rate_to_ceil_CS',
data=dum_data_hr2ceil,
timestamps=dum_time_hr2ceil,
unit='bpm',
recording_group=ecg_recording_group
)
hr2ceil = HeartRate(
name='HR2Ceil',
cardiac_series=[hr2ceil_cardiac_series],
processing_description='processed heartRate to ceiling'
)
# adding the 'HR2ceiling' processed HR to the nwb_file inside an 'HeartRate' container
ecg_module.add(hr2ceil)
```
Now, the `nwbfile` is ready to be written on the disk and read back.
Raw data
{
"_id": null,
"home_page": "",
"name": "ndx-ecg",
"maintainer": "",
"docs_url": null,
"requires_python": "",
"maintainer_email": "",
"keywords": "NeurodataWithoutBorders,NWB,nwb-extension,ndx-extension,ndx-ecg",
"author": "Hamidreza Alimohammadi ([AT]DefenseCircuitsLab)",
"author_email": "alimohammadi.hamidreza@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/80/b7/6961d18b52323dde23c2f3d465d42512a0632c308d719322dc3444916769/ndx-ecg-0.1.0.tar.gz",
"platform": null,
"description": "# ndx-ecg Extension for NWB\r\n\r\nThis extension is developed to extend NWB data standards to incorporate ECG recordings. `CardiacSeries`, the main neurodata-type in this extension, in fact extends the base type of NWB TimeSeries and can be stored into three specific data interfaces of `ECG`, `HeartRate` and `AuxiliaryAnalysis`. Also, the `ECGRecordingGroup` is another neurodata-type in this module which extends `LabMetaData` which itself extends the NWBContainer and stores descriptive meta-data recording channels information along with the electrodes implementation (`ECGChannels` and `ECGElectrodes` respectively as extensions of DynamicTable) and a link to another extended neurodata-type -`ECGRecDevice`- which extends the type Device.\r\n\r\n<div align=\"center\">\r\n<img src=\"https://github.com/Defense-Circuits-Lab/ndx_ecg/assets/63550467/4efaddd3-ced3-47d3-9b53-c82647e8c450\" width=\"1000\">\r\n</div>\r\n\r\n## Installation\r\nSimply clone the repo and navigate to the root directory, then:\r\n```\r\npip install .\r\n```\r\n## Test\r\nA roundTrip test is runnable through ```pytest``` from the root. The test script can be found here:\r\n```\r\n\\src\\pynwb\\tests\r\n```\r\n## An example use-case\r\nThe following is an example use case of ```ndx-ecg``` with explanatory comments. First, we build up an ```nwbfile``` and define an endpoint recording device:\r\n```python\r\nfrom datetime import datetime\r\nfrom uuid import uuid4\r\nimport numpy as np\r\nfrom dateutil.tz import tzlocal\r\nfrom pynwb import NWBHDF5IO, NWBFile\r\nfrom hdmf.common import DynamicTable\r\n\r\nfrom ndx_ecg import (\r\n CardiacSeries,\r\n ECG,\r\n HeartRate,\r\n AuxiliaryAnalysis,\r\n ECGRecordingGroup,\r\n ECGRecDevice,\r\n ECGElectrodes,\r\n ECGChannels\r\n)\r\n\r\nnwbfile = NWBFile(\r\n session_description='ECG test-rec session',\r\n identifier=str(uuid4()),\r\n session_start_time=datetime.now(tzlocal()),\r\n experimenter='experimenter',\r\n lab='DCL',\r\n institution='UKW',\r\n experiment_description='',\r\n session_id='',\r\n)\r\n# define an endpoint main recording device\r\nmain_device = nwbfile.create_device(\r\n name='endpoint_recording_device',\r\n description='description_of_the_ERD', # ERD: Endpoint recording device\r\n manufacturer='manufacturer_of_the_ERD'\r\n)\r\n```\r\nThen, we define instances of `ECGElectrodes` and `ECGChannels`, to represent the meta-data on the recording electrodes and also the recording channels:\r\n```python\r\n'''\r\ncreating an ECG electrodes table\r\nas a DynamicTable\r\n'''\r\necg_electrodes_table = ECGElectrodes(\r\n description='descriptive meta-data on ECG recording electrodes'\r\n)\r\n\r\n# add electrodes\r\necg_electrodes_table.add_row(\r\n electrode_name='el_0',\r\n electrode_location='right upper-chest',\r\n electrode_info='descriptive info on el_0'\r\n)\r\necg_electrodes_table.add_row(\r\n electrode_name='el_1',\r\n electrode_location='left lower-chest',\r\n electrode_info='descriptive info on el_1'\r\n)\r\necg_electrodes_table.add_row(\r\n electrode_name='reference',\r\n electrode_location='top of the head',\r\n electrode_info='descriptive info on reference'\r\n)\r\n# adding the object of DynamicTable\r\nnwbfile.add_acquisition(ecg_electrodes_table) # storage point for DT\r\n\r\n'''\r\ncreating an ECG recording channels table\r\nas a DynamicTable\r\n'''\r\necg_channels_table = ECGChannels(\r\n description='descriptive meta-data on ECG recording channels'\r\n)\r\n\r\n# add channels\r\necg_channels_table.add_row(\r\n channel_name='ch_0',\r\n channel_type='single',\r\n involved_electrodes='el_0',\r\n channel_info='channel info on ch_0'\r\n)\r\necg_channels_table.add_row(\r\n channel_name='ch_1',\r\n channel_type='differential',\r\n involved_electrodes='el_0 and el_1',\r\n channel_info='channel info on ch_1'\r\n)\r\n# adding the object of DynamicTable\r\nnwbfile.add_acquisition(ecg_channels_table) # storage point for DT\r\n```\r\nNow, we can define an instance of ```ECGRecDevice```:\r\n```python\r\n# define an ECGRecDevice-type device for ecg recording\r\necg_device = ECGRecDevice(\r\n name='recording_device',\r\n description='description_of_the_ECGRD',\r\n manufacturer='manufacturer_of_the_ECGRD',\r\n filtering='notch-60Hz-analog',\r\n gain='100',\r\n offset='0',\r\n synchronization='taken care of via ...',\r\n endpoint_recording_device=main_device\r\n)\r\n# adding the object of ECGRecDevice\r\nnwbfile.add_device(ecg_device)\r\n```\r\nAnd also an instance of ```ECGChannelsGroup```:\r\n```python\r\necg_recording_group = ECGRecordingGroup(\r\n name='recording_group',\r\n group_description='a group to store electrodes and channels table, and linking to ECGRecDevice.',\r\n electrodes=ecg_electrodes_table,\r\n channels=ecg_channels_table,\r\n recording_device=ecg_device\r\n)\r\n# adding the object of ECGChannelsGroup\r\nnwbfile.add_lab_meta_data(ecg_recording_group) # storage point for custom LMD\r\n#\r\n```\r\nNow, we have all the required standard arguments to genearate instances of `CardiacSeries` and stroing them in our three different NWBDataInterfaces:\r\n```python\r\n# storing the ECG data\r\ndum_data_ecg = np.random.randn(20, 2)\r\ndum_time_ecg = np.linspace(0, 10, len(dum_data_ecg))\r\necg_cardiac_series = CardiacSeries(\r\n name='ecg_raw_CS',\r\n data=dum_data_ecg,\r\n timestamps=dum_time_ecg,\r\n unit='mV',\r\n recording_group=ecg_recording_group\r\n)\r\n\r\necg_raw = ECG(\r\n cardiac_series=[ecg_cardiac_series],\r\n processing_description='raw acquisition'\r\n)\r\n```\r\nHere, we built an instance of our `CradiacSeries` to store a dummy raw ECG acquisition into a specified `ECG` interface, and we store it as an acquisition into the `nwbfile`:\r\n```python\r\n# adding the raw acquisition of ECG to the nwb_file inside an 'ECG' container\r\nnwbfile.add_acquisition(ecg_raw)\r\n```\r\nIn the following, we have taken the similar approach but this time storing dummy data as processed data, into specific interfaces of `HeartRate` and `AuxiliaryAnalysis`, then storing it into a -to be defined- `ecg_module`:\r\n```python\r\n# storing the HeartRate data\r\ndum_data_hr = np.random.randn(10, 2)\r\ndum_time_hr = np.linspace(0, 10, len(dum_data_hr))\r\nhr_cardiac_series = CardiacSeries(\r\n name='heart_rate_CS',\r\n data=dum_data_hr,\r\n timestamps=dum_time_hr,\r\n unit='bpm',\r\n recording_group=ecg_recording_group\r\n)\r\n\r\n# defining an ecg_module to store the processed cardiac data and analysis\r\necg_module = nwbfile.create_processing_module(\r\n name='cardio_module',\r\n description='a module to store processed cardiac data'\r\n)\r\n\r\nhr = HeartRate(\r\n cardiac_series=[hr_cardiac_series],\r\n processing_description='processed heartRate of the animal'\r\n)\r\n# adding the heart rate data to the nwb_file inside an 'HeartRate' container\r\necg_module.add(hr)\r\n\r\n# storing the Auxiliary data\r\n# An example could be the concept of ceiling that is being used in the literature published by DCL@UKW\r\ndum_data_ceil = np.random.randn(10, 2)\r\ndum_time_ceil = np.linspace(0, 10, len(dum_data_ceil))\r\nceil_cardiac_series = CardiacSeries(\r\n name='heart_rate_ceil_CS',\r\n data=dum_data_ceil,\r\n timestamps=dum_time_ceil,\r\n unit='bpm',\r\n recording_group=ecg_recording_group\r\n)\r\n\r\nceil = AuxiliaryAnalysis(\r\n cardiac_series=[ceil_cardiac_series],\r\n processing_description='processed auxiliary analysis'\r\n)\r\n# adding the 'ceiling' auxiliary analysis to the nwb_file inside an 'AuxiliaryAnalysis' container\r\necg_module.add(ceil)\r\n\r\n# storing the processed heart rate: as an NWBDataInterface with the new assigned name instead of default\r\n# An example could be the concept of HR2ceiling that is being used in the literature published by DCL@UKW\r\ndum_data_hr2ceil = np.random.randn(10, 2)\r\ndum_time_hr2ceil = np.linspace(0, 10, len(dum_data_hr2ceil))\r\nhr2ceil_cardiac_series = CardiacSeries(\r\n name='heart_rate_to_ceil_CS',\r\n data=dum_data_hr2ceil,\r\n timestamps=dum_time_hr2ceil,\r\n unit='bpm',\r\n recording_group=ecg_recording_group\r\n)\r\n\r\nhr2ceil = HeartRate(\r\n name='HR2Ceil',\r\n cardiac_series=[hr2ceil_cardiac_series],\r\n processing_description='processed heartRate to ceiling'\r\n)\r\n# adding the 'HR2ceiling' processed HR to the nwb_file inside an 'HeartRate' container\r\necg_module.add(hr2ceil)\r\n\r\n```\r\nNow, the `nwbfile` is ready to be written on the disk and read back. \r\n\r\n",
"bugtrack_url": null,
"license": "BSD",
"summary": "This extension is developed to extend NWB data standards to incorporate ECG recordings.",
"version": "0.1.0",
"project_urls": null,
"split_keywords": [
"neurodatawithoutborders",
"nwb",
"nwb-extension",
"ndx-extension",
"ndx-ecg"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "80b76961d18b52323dde23c2f3d465d42512a0632c308d719322dc3444916769",
"md5": "7a4b962517986d66782dcb9543af3828",
"sha256": "8a327b993725aaf20363c15033714fe7a05c287b36e0c6e0738afe44f1758a67"
},
"downloads": -1,
"filename": "ndx-ecg-0.1.0.tar.gz",
"has_sig": false,
"md5_digest": "7a4b962517986d66782dcb9543af3828",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 45238,
"upload_time": "2023-11-13T15:27:48",
"upload_time_iso_8601": "2023-11-13T15:27:48.748342Z",
"url": "https://files.pythonhosted.org/packages/80/b7/6961d18b52323dde23c2f3d465d42512a0632c308d719322dc3444916769/ndx-ecg-0.1.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2023-11-13 15:27:48",
"github": false,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"lcname": "ndx-ecg"
}