<h1 style="text-align:center">
pysnirf2
`pip install snirf`
</h1>


[](https://badge.fury.io/py/pysnirf2)
[](https://zenodo.org/badge/latestdoi/426339949)
Dynamically generated Python library for reading, writing, and validating [Shared Near Infrared Spectroscopy Format (SNIRF) files](https://github.com/fNIRS/snirf).
Developed and maintained by the [Boston University Neurophotonics Center](https://www.bu.edu/neurophotonics/).
## Documentation
[Documentation](https://github.com/BUNPC/pysnirf2/tree/main/docs) is generated from source using [lazydocs](https://github.com/ml-tooling/lazydocs)
## Installation
`pip install snirf`
pysnirf2 requires Python > 3.6.
# Features
The library generated via metaprogramming, but the resulting classes explicitly implement each and every specified SNIRF field so as to provide an extensible object-oriented foundation for SNIRF applications.
## Open a SNIRF file
`Snirf(<path>, <mode>)` opens a SNIRF file at `<path>` _or creates a new one if it doesn't exist._ Use mode 'w' to create a new file, 'r' to read a file, and 'r+' to edit an existing file.
```python
from snirf import Snirf
snirf = Snirf(r'some\path\subj1_run01.snirf', 'r+')
```
## Create a SNIRF file object
`Snirf()` with no arguments creates a temporary file which can be written later using `save()`.
```python
snirf = Snirf()
```
## Closing a SNIRF file
A `Snirf` instance wraps a file on disk. It should be closed when you're done reading from it or saving.
```python
snirf.close()
```
Use a `with` statement to ensure that the file is closed when you're done with it:
```python
with Snirf(r'some\path\subj1_run01.snirf', 'r+') as snirf:
# Read/write
snirf.save()
```
## Copy a SNIRF file object
Any `Snirf` object can be copied to a new instance in memory, after which the original can be closed.
```python
snirf2 = snirf.copy()
snirf.close()
# snirf2 is free for manipulation
```
## View or retrieve a file's contents
```python
>>> snirf
Snirf at /
filename:
C:\Users\you\some\path\subj1_run01.snirf
formatVersion: v1.0
nirs: <iterable of 2 <class 'pysnirf2.NirsElement'>>
```
```python
>>> snirf.nirs[0].probe
Probe at /nirs1/probe
correlationTimeDelayWidths: [0.]
correlationTimeDelays: [0.]
detectorLabels: ['D1' 'D2']
detectorPos2D: [[30. 0.]
[ 0. 30.]]
detectorPos3D: [[30. 0. 0.]
[ 0. 30. 0.]]
filename:
C:\Users\you\some\path\subj1_run01.snirf
frequencies: [1.]
landmarkLabels: None
landmarkPos2D: None
landmarkPos3D: None
location: /nirs/probe
momentOrders: None
sourceLabels: ['S1']
sourcePos2D: [[0. 0.]]
sourcePos3D: [[0.]
[0.]
[0.]]
timeDelayWidths: [0.]
timeDelays: [0.]
useLocalIndex: None
wavelengths: [690. 830.]
wavelengthsEmission: None
```
## Edit a SNIRF file
Assign a new value to a field
```python
>>> snirf.nirs[0].metaDataTags.SubjectID = 'subj1'
>>> snirf.nirs[0].metaDataTags.SubjectID
'subj1'
```
```python
>>> snirf.nirs[0].probe.detectorPos3D[0, :] = [90, 90, 90]
>>> snirf.nirs[0].probe.detectorPos3D
array([[90., 90., 90.],
[ 0., 30., 0.]])
```
> Note: assignment via slicing is not possible in `dynamic_loading` mode.
## Indexed groups
Indexed groups are defined by the SNIRF file format as groups of the same type which are indexed via their name + a 1-based index, i.e. `data1`, `data2`, ... or `stim1`, `stim2`, `stim3`, ...
pysnirf2 provides an iterable interface for these groups using Pythonic 0-based indexing, i.e. `data[0]`, `data[1]`, ... or `stim[0]`, `stim[1]]`, `stim[2]`, ...
```python
>>> snirf.nirs[0].stim
<iterable of 0 <class 'pysnirf2.StimElement'>>
>>> len(nirs[0].stim)
0
```
To add an indexed group, use the `appendGroup()` method of any `IndexedGroup` class. Indexed groups are created automatically. `nirs` is an indexed group.
```python
>>> snirf.nirs[0].stim.appendGroup()
>>> len(nirs[0].stim)
1
>>> snirf.nirs[0].stim[0]
StimElement at /nirs/stim2
data: None
dataLabels: None
filename:
C:\Users\you\some\path\subj1_run01.snirf
name: None
```
To remove an indexed group
```python
del snirf.nirs[0].stim[0]
```
## Save a SNIRF file
Overwrite the open file
```python
snirf.save()
```
Save As in a new location
```python
snirf.save(r'some\new\path\subj1_run01_edited.snirf')
```
The `save()` function can be called for any group or indexed group:
```python
snirf.nirs[0].metaDataTags.save('subj1_run01_edited_metadata_only.snirf')
```
## Dynamic loading mode
For larger files, it may be useful to load data dynamically: data will only be loaded on access, and only changed datasets will be written on `save()`. When creating a new `Snirf` instance, set `dynamic_loading` to `True` (Default `False`).
```python
snirf = Snirf(r'some\path\subj1_run01.snirf', 'r+', dynamic_loading=True)
```
> Note: in dynamic loading mode, array data cannot be modified with indices like in the example above:
> ```python
> >>> snirf = Snirf(TESTPATH, 'r+', dynamic_loading=True)
> >>> snirf.nirs[0].probe.detectorPos3D
>
> array([[30., 0., 0.],
> [ 0., 30., 0.]])
>
> >>> snirf.nirs[0].probe.detectorPos3D[0, :] = [90, 90, 90]
> >>> snirf.nirs[0].probe.detectorPos3D
>
> array([[30., 0., 0.],
> [ 0., 30., 0.]])
> ```
> To modify an array in `dynamic_loading` mode, assign it, modify it, and assign it back to the Snirf object.
> ```python
> >>> detectorPos3D = snirf.nirs[0].probe.detectorPos3D
> >>> detectorPos3D[0, :] = [90, 90, 90]
> >>> snirf.nirs[0].probe.detectorPos3D = detectorPos3D
>
> array([[90., 90., 90.],
> [ 0., 30., 0.]])
# Validating a SNIRF file
pysnirf2 features functions for validating SNIRF files against the specification and generating detailed error reports.
## Validate a Snirf object you have created
```python
result = snirf.validate()
```
## Validate a SNIRF file on disk
To validate a SNIRF file on disk
```python
from snirf import validateSnirf
result = validateSnirf(r'some\path\subj1_run01.snirf')
assert result, 'Invalid SNIRF file!\n' + result.display() # Crash and display issues if the file is invalid.
```
## Validation results
The validation functions return a [`ValidationResult`](https://github.com/BUNPC/pysnirf2/blob/main/docs/pysnirf2.md#class-validationresult) instance which contains details about the SNIRF file.
To view the validation result:
```python
>>> result.display(severity=3) # Display all fatal errors
<pysnirf2.pysnirf2.ValidationResult object at 0x000001C0CCF05A00>
/nirs1/data1/measurementList103/dataType FATAL REQUIRED_DATASET_MISSING
/nirs1/data1/measurementList103/dataTypeIndex FATAL REQUIRED_DATASET_MISSING
/nirs1/data1 FATAL INVALID_MEASUREMENTLIST
Found 668 OK (hidden)
Found 635 INFO (hidden)
Found 204 WARNING (hidden)
Found 3 FATAL
File is INVALID
```
To look at a particular result:
```python
>>> result.errors[2]
<pysnirf2.pysnirf2.ValidationIssue object at 0x000001C0CB502F70>
location: /nirs1/data1
severity: 3 FATAL
name: 8 INVALID_MEASUREMENTLIST
message: The number of measurementList elements does not match the second dimension of dataTimeSeries
```
The full list of validation results `result.issues` can be explored programatically.
# Code generation
The interface and validator are generated via metacode that downloads and parses [the latest SNIRF specification](https://raw.githubusercontent.com/fNIRS/snirf/master/snirf_specification.md).
See [\gen](https://github.com/BUNPC/pysnirf2/tree/main/gen) for details.
Raw data
{
"_id": null,
"home_page": "https://github.com/BUNPC/pysnirf2",
"name": "pysnirf2",
"maintainer": "",
"docs_url": null,
"requires_python": ">=3.6.0",
"maintainer_email": "",
"keywords": "",
"author": "",
"author_email": "sstucker@bu.edu",
"download_url": "https://files.pythonhosted.org/packages/1b/d3/b0a2947ce101ab675126f3e79c44ac007cdf0362e5ed3c7fe9d523e5dc85/pysnirf2-0.7.3.tar.gz",
"platform": null,
"description": "\n<h1 style=\"text-align:center\">\n\npysnirf2\n\n`pip install snirf`\n\n</h1>\n\n\n\n\n[](https://badge.fury.io/py/pysnirf2)\n[](https://zenodo.org/badge/latestdoi/426339949)\n\nDynamically generated Python library for reading, writing, and validating [Shared Near Infrared Spectroscopy Format (SNIRF) files](https://github.com/fNIRS/snirf).\n\nDeveloped and maintained by the [Boston University Neurophotonics Center](https://www.bu.edu/neurophotonics/).\n\n## Documentation\n\n[Documentation](https://github.com/BUNPC/pysnirf2/tree/main/docs) is generated from source using [lazydocs](https://github.com/ml-tooling/lazydocs)\n\n## Installation\n`pip install snirf`\n\npysnirf2 requires Python > 3.6.\n\n# Features\n\nThe library generated via metaprogramming, but the resulting classes explicitly implement each and every specified SNIRF field so as to provide an extensible object-oriented foundation for SNIRF applications.\n\n## Open a SNIRF file\n`Snirf(<path>, <mode>)` opens a SNIRF file at `<path>` _or creates a new one if it doesn't exist._ Use mode 'w' to create a new file, 'r' to read a file, and 'r+' to edit an existing file.\n```python\nfrom snirf import Snirf\nsnirf = Snirf(r'some\\path\\subj1_run01.snirf', 'r+')\n```\n## Create a SNIRF file object\n`Snirf()` with no arguments creates a temporary file which can be written later using `save()`.\n```python\nsnirf = Snirf()\n```\n\n## Closing a SNIRF file\nA `Snirf` instance wraps a file on disk. It should be closed when you're done reading from it or saving.\n```python\nsnirf.close()\n```\nUse a `with` statement to ensure that the file is closed when you're done with it:\n```python\nwith Snirf(r'some\\path\\subj1_run01.snirf', 'r+') as snirf:\n # Read/write\n snirf.save()\n```\n\n## Copy a SNIRF file object\nAny `Snirf` object can be copied to a new instance in memory, after which the original can be closed.\n```python\nsnirf2 = snirf.copy()\nsnirf.close()\n# snirf2 is free for manipulation\n```\n\n## View or retrieve a file's contents\n```python\n>>> snirf\n\nSnirf at /\nfilename: \nC:\\Users\\you\\some\\path\\subj1_run01.snirf\nformatVersion: v1.0\nnirs: <iterable of 2 <class 'pysnirf2.NirsElement'>>\n```\n```python\n>>> snirf.nirs[0].probe\n\nProbe at /nirs1/probe\ncorrelationTimeDelayWidths: [0.]\ncorrelationTimeDelays: [0.]\ndetectorLabels: ['D1' 'D2']\ndetectorPos2D: [[30. 0.]\n [ 0. 30.]]\ndetectorPos3D: [[30. 0. 0.]\n [ 0. 30. 0.]]\nfilename: \nC:\\Users\\you\\some\\path\\subj1_run01.snirf\nfrequencies: [1.]\nlandmarkLabels: None\nlandmarkPos2D: None\nlandmarkPos3D: None\nlocation: /nirs/probe\nmomentOrders: None\nsourceLabels: ['S1']\nsourcePos2D: [[0. 0.]]\nsourcePos3D: [[0.]\n [0.]\n [0.]]\ntimeDelayWidths: [0.]\ntimeDelays: [0.]\nuseLocalIndex: None\nwavelengths: [690. 830.]\nwavelengthsEmission: None\n```\n## Edit a SNIRF file\nAssign a new value to a field\n```python\n>>> snirf.nirs[0].metaDataTags.SubjectID = 'subj1'\n>>> snirf.nirs[0].metaDataTags.SubjectID\n\n'subj1'\n```\n```python\n>>> snirf.nirs[0].probe.detectorPos3D[0, :] = [90, 90, 90]\n>>> snirf.nirs[0].probe.detectorPos3D\n\narray([[90., 90., 90.],\n [ 0., 30., 0.]])\n```\n> Note: assignment via slicing is not possible in `dynamic_loading` mode. \n## Indexed groups\nIndexed groups are defined by the SNIRF file format as groups of the same type which are indexed via their name + a 1-based index, i.e. `data1`, `data2`, ... or `stim1`, `stim2`, `stim3`, ...\n\npysnirf2 provides an iterable interface for these groups using Pythonic 0-based indexing, i.e. `data[0]`, `data[1]`, ... or `stim[0]`, `stim[1]]`, `stim[2]`, ...\n\n```python\n>>> snirf.nirs[0].stim\n\n\n<iterable of 0 <class 'pysnirf2.StimElement'>>\n\n>>> len(nirs[0].stim)\n\n0\n```\nTo add an indexed group, use the `appendGroup()` method of any `IndexedGroup` class. Indexed groups are created automatically. `nirs` is an indexed group.\n```python\n>>> snirf.nirs[0].stim.appendGroup()\n>>> len(nirs[0].stim)\n\n1\n\n>>> snirf.nirs[0].stim[0]\n\nStimElement at /nirs/stim2\ndata: None\ndataLabels: None\nfilename: \nC:\\Users\\you\\some\\path\\subj1_run01.snirf\nname: None\n```\nTo remove an indexed group\n```python\ndel snirf.nirs[0].stim[0]\n```\n## Save a SNIRF file\nOverwrite the open file\n```python\nsnirf.save()\n```\nSave As in a new location\n```python\nsnirf.save(r'some\\new\\path\\subj1_run01_edited.snirf')\n```\nThe `save()` function can be called for any group or indexed group:\n```python\nsnirf.nirs[0].metaDataTags.save('subj1_run01_edited_metadata_only.snirf')\n```\n## Dynamic loading mode\nFor larger files, it may be useful to load data dynamically: data will only be loaded on access, and only changed datasets will be written on `save()`. When creating a new `Snirf` instance, set `dynamic_loading` to `True` (Default `False`).\n```python\nsnirf = Snirf(r'some\\path\\subj1_run01.snirf', 'r+', dynamic_loading=True)\n```\n> Note: in dynamic loading mode, array data cannot be modified with indices like in the example above:\n> ```python\n> >>> snirf = Snirf(TESTPATH, 'r+', dynamic_loading=True)\n> >>> snirf.nirs[0].probe.detectorPos3D\n> \n> array([[30., 0., 0.],\n> [ 0., 30., 0.]])\n> \n> >>> snirf.nirs[0].probe.detectorPos3D[0, :] = [90, 90, 90]\n> >>> snirf.nirs[0].probe.detectorPos3D\n> \n> array([[30., 0., 0.],\n> [ 0., 30., 0.]])\n> ```\n> To modify an array in `dynamic_loading` mode, assign it, modify it, and assign it back to the Snirf object.\n> ```python\n> >>> detectorPos3D = snirf.nirs[0].probe.detectorPos3D\n> >>> detectorPos3D[0, :] = [90, 90, 90]\n> >>> snirf.nirs[0].probe.detectorPos3D = detectorPos3D\n> \n> array([[90., 90., 90.],\n> [ 0., 30., 0.]])\n\n# Validating a SNIRF file\npysnirf2 features functions for validating SNIRF files against the specification and generating detailed error reports.\n## Validate a Snirf object you have created\n```python\nresult = snirf.validate()\n```\n## Validate a SNIRF file on disk\nTo validate a SNIRF file on disk\n```python\nfrom snirf import validateSnirf\nresult = validateSnirf(r'some\\path\\subj1_run01.snirf')\nassert result, 'Invalid SNIRF file!\\n' + result.display() # Crash and display issues if the file is invalid.\n```\n## Validation results\nThe validation functions return a [`ValidationResult`](https://github.com/BUNPC/pysnirf2/blob/main/docs/pysnirf2.md#class-validationresult) instance which contains details about the SNIRF file.\nTo view the validation result:\n```python\n>>> result.display(severity=3) # Display all fatal errors\n\n<pysnirf2.pysnirf2.ValidationResult object at 0x000001C0CCF05A00>\n/nirs1/data1/measurementList103/dataType FATAL REQUIRED_DATASET_MISSING\n/nirs1/data1/measurementList103/dataTypeIndex FATAL REQUIRED_DATASET_MISSING\n/nirs1/data1 FATAL INVALID_MEASUREMENTLIST \n\nFound 668 OK (hidden)\nFound 635 INFO (hidden)\nFound 204 WARNING (hidden)\nFound 3 FATAL \n\nFile is INVALID\n```\nTo look at a particular result:\n```python\n>>> result.errors[2]\n\n<pysnirf2.pysnirf2.ValidationIssue object at 0x000001C0CB502F70>\nlocation: /nirs1/data1\nseverity: 3 FATAL \nname: 8 INVALID_MEASUREMENTLIST\nmessage: The number of measurementList elements does not match the second dimension of dataTimeSeries\n```\nThe full list of validation results `result.issues` can be explored programatically.\n# Code generation\n\nThe interface and validator are generated via metacode that downloads and parses [the latest SNIRF specification](https://raw.githubusercontent.com/fNIRS/snirf/master/snirf_specification.md). \n\nSee [\\gen](https://github.com/BUNPC/pysnirf2/tree/main/gen) for details.\n\n\n\n\n\n\n",
"bugtrack_url": null,
"license": "GPLv3",
"summary": "Interface and validator for SNIRF files",
"version": "0.7.3",
"split_keywords": [],
"urls": [
{
"comment_text": "",
"digests": {
"md5": "bc10d7b1e047237ca43793085528f7f7",
"sha256": "338a081630c9f189228dfc4558e838156218127c9f828c8790858182ac7dd85a"
},
"downloads": -1,
"filename": "pysnirf2-0.7.3-py2.py3-none-any.whl",
"has_sig": false,
"md5_digest": "bc10d7b1e047237ca43793085528f7f7",
"packagetype": "bdist_wheel",
"python_version": "py2.py3",
"requires_python": ">=3.6.0",
"size": 55516,
"upload_time": "2022-08-03T21:19:55",
"upload_time_iso_8601": "2022-08-03T21:19:55.894570Z",
"url": "https://files.pythonhosted.org/packages/8e/73/cd2850f18baf099870b253b36102e1915cbf5aa473f8f426b29c512751ed/pysnirf2-0.7.3-py2.py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"md5": "65d9a1f1dcae41d9b816976b1e320caf",
"sha256": "68d51d921af12f3ae2ab544443cc2c91cbdf3e2e001901b58860b49a1acd4632"
},
"downloads": -1,
"filename": "pysnirf2-0.7.3.tar.gz",
"has_sig": false,
"md5_digest": "65d9a1f1dcae41d9b816976b1e320caf",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.6.0",
"size": 58045,
"upload_time": "2022-08-03T21:19:57",
"upload_time_iso_8601": "2022-08-03T21:19:57.661391Z",
"url": "https://files.pythonhosted.org/packages/1b/d3/b0a2947ce101ab675126f3e79c44ac007cdf0362e5ed3c7fe9d523e5dc85/pysnirf2-0.7.3.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2022-08-03 21:19:57",
"github": true,
"gitlab": false,
"bitbucket": false,
"github_user": "BUNPC",
"github_project": "pysnirf2",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"requirements": [
{
"name": "h5py",
"specs": [
[
"==",
"3.1.0"
]
]
},
{
"name": "numpy",
"specs": [
[
"==",
"1.19.5"
]
]
},
{
"name": "setuptools",
"specs": [
[
"==",
"40.8.0"
]
]
},
{
"name": "pip",
"specs": [
[
"==",
"21.3.1"
]
]
},
{
"name": "termcolor",
"specs": [
[
"==",
"1.1.0"
]
]
},
{
"name": "colorama",
"specs": [
[
"==",
"0.4.4"
]
]
}
],
"lcname": "pysnirf2"
}