Metadata-Version: 2.1
Name: pysnirf2
Version: 0.4.3
Summary: Interface and validator for SNIRF files
Home-page: https://github.com/BUNPC/pysnirf2
Author-email: sstucker@bu.edu
License: GPLv3
Platform: UNKNOWN
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Requires-Python: >=3.6.0
Description-Content-Type: text/markdown
Requires-Dist: h5py (>=3.1.0)
Requires-Dist: numpy
Requires-Dist: setuptools
Requires-Dist: pip
Requires-Dist: termcolor
Requires-Dist: colorama


![testing](https://github.com/BUNPC/pysnirf2/actions/workflows/test.yml/badge.svg)
![lazydocs](https://github.com/BUNPC/pysnirf2/actions/workflows/lazydocs.yml/badge.svg)
[![PyPI version](https://badge.fury.io/py/pysnirf2.svg)](https://badge.fury.io/py/pysnirf2)

# pysnirf2

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 pysnirf2`

pysnirf2 requires Python > 3.6

# Features

## Open a SNIRF file
`Snirf(<path>)` opens a SNIRF file at `<path>` _or creates a new one if it doesn't exist._
```python
from pysnirf2 import Snirf
>>> snirf = Snirf(r'some\path\subj1_run01.snirf')
```
## 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') as snirf:
>>>      # Read/write
>>>      snirf.save()
```

## 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', dynamic_loading=True)
```
> Note: in dynamic loading mode, array data cannot be modified with indices like in the example above:
> ```python
> >>> snirf = Snirf(TESTPATH, 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 pysnirf2 import validateSnirf
>> result = validateSnirf(r'some\path\subj1_run01.snirf')
```
## 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.
```python
>> assert valid
```
To view the validation result:
```python
>> result.display(severity=3)
<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.






