"""
NWB (Neurodata Without Borders) export utilities.
This module provides functions to export MyoGen simulation data to NWB format
using Neo's NWBIO. NWB is a standardized data format for neurophysiology that
enables data sharing and interoperability with other neuroscience tools.
NWB enables:
- Data sharing via the DANDI Archive (https://dandiarchive.org/)
- Interoperability with the NWB ecosystem of tools
- Schema validation for data integrity
- Rich metadata for experimental context
For more information:
- NWB documentation: https://pynwb.readthedocs.io/
- NWB Overview: https://nwb-overview.readthedocs.io/
"""
from __future__ import annotations
import uuid
from datetime import datetime
from pathlib import Path
from neo import Block
from .decorators import beartowertype
# Check for optional NWB dependencies
try:
from neo.io import NWBIO as _BaseNWBIO
from itertools import chain
HAS_NWB = True
class NWBIO(_BaseNWBIO):
"""Custom NWBIO that preserves signal names instead of renaming them."""
def _write_segment(self, nwbfile, segment, electrodes):
"""Override to preserve original signal names."""
for signal in chain(segment.analogsignals, segment.irregularlysampledsignals):
if signal.segment is not segment:
raise TypeError(f"signal.segment must be segment and is {signal.segment}")
# Preserve original name - don't rename like base class does
if not signal.name:
signal.name = f"{segment.name}_signal"
self._write_signal(self._nwbfile, signal, electrodes)
for i, train in enumerate(segment.spiketrains):
if train.segment is not segment:
raise TypeError(f"train.segment must be segment and is {train.segment}")
if not train.name:
train.name = f"{segment.name}_spiketrain{i}"
self._write_spiketrain(self._nwbfile, train)
for event in segment.events:
if event.segment is not segment:
raise TypeError(f"event.segment must be segment and is {event.segment}")
if not event.name:
event.name = f"{segment.name}_event"
self._write_event(self._nwbfile, event)
for i, epoch in enumerate(segment.epochs):
if not epoch.name:
epoch.name = f"{segment.name}_epoch{i}"
self._write_epoch(self._nwbfile, epoch)
except ImportError:
HAS_NWB = False
NWBIO = None
def _check_nwb_available() -> None:
"""Check if NWB dependencies are available."""
if not HAS_NWB:
raise ImportError(
"NWB export requires optional dependencies. "
"Install with: pip install myogen[nwb]"
)
[docs]
@beartowertype
def export_to_nwb(
block: Block,
filepath: str | Path,
session_description: str = "MyoGen simulation",
identifier: str | None = None,
session_start_time: datetime | None = None,
experimenter: str | list[str] | None = None,
institution: str | None = None,
lab: str | None = None,
experiment_description: str | None = None,
keywords: list[str] | None = None,
# Subject metadata (important for DANDI compliance)
subject_id: str | None = None,
species: str = "Homo sapiens",
age: str | None = None,
sex: str | None = None,
subject_description: str | None = None,
**kwargs,
) -> Path:
"""
Export a Neo Block to NWB format.
This function uses Neo's NWBIO to write simulation data to an NWB file.
The Block should contain AnalogSignals with grid annotations (created
via create_grid_signal) for electrode array data.
Parameters
----------
block : Block
Neo Block containing simulation data. Can be spike trains, EMG,
or MUAP data from MyoGen simulations.
filepath : str or Path
Output file path. Should end with '.nwb'.
session_description : str, default="MyoGen simulation"
Description of the simulation session.
identifier : str, optional
Unique identifier for this NWB file. If None, a UUID is generated.
session_start_time : datetime, optional
Start time of the session. If None, current time is used.
experimenter : str or list[str], optional
Name(s) of experimenter(s).
institution : str, optional
Institution where the simulation was performed.
lab : str, optional
Lab where the simulation was performed.
experiment_description : str, optional
Description of the experiment/simulation.
keywords : list[str], optional
Keywords describing the data.
subject_id : str, optional
Subject identifier (recommended for DANDI).
species : str, default="Homo sapiens"
Species of the subject. Use Latin binomial (e.g., "Homo sapiens",
"Mus musculus"). For simulations, defaults to human.
age : str, optional
Age of subject in ISO 8601 duration format (e.g., "P30Y" for 30 years).
sex : str, optional
Sex of subject. One of: "M", "F", "U" (unknown), "O" (other).
subject_description : str, optional
Description of the subject.
**kwargs
Additional keyword arguments passed to NWBIO.
Returns
-------
Path
Path to the created NWB file.
Examples
--------
>>> from myogen.utils.nwb import export_to_nwb
>>>
>>> # Export spike trains to NWB
>>> export_to_nwb(
... spike_train__Block,
... "simulation_spikes.nwb",
... session_description="Motor neuron pool simulation",
... institution="My University",
... )
>>>
>>> # Export surface EMG to NWB
>>> export_to_nwb(
... surface_emg__Block,
... "simulation_emg.nwb",
... session_description="Surface EMG simulation",
... experimenter="John Doe",
... )
Notes
-----
For electrode array data (surface EMG, MUAPs), the grid structure is
preserved via electrode_positions in annotations, which map to NWB's
electrode table.
See Also
--------
create_grid_signal : Create grid-annotated AnalogSignals
validate_nwb : Validate NWB file with NWBInspector
"""
_check_nwb_available()
filepath = Path(filepath)
if not filepath.suffix == ".nwb":
filepath = filepath.with_suffix(".nwb")
# Generate defaults
if identifier is None:
identifier = str(uuid.uuid4())
if session_start_time is None:
session_start_time = datetime.now()
if keywords is None:
keywords = ["MyoGen", "simulation", "EMG", "motor unit"]
# Build metadata dict
nwb_metadata = {
"session_description": session_description,
"identifier": identifier,
"session_start_time": session_start_time,
}
if experimenter is not None:
nwb_metadata["experimenter"] = (
[experimenter] if isinstance(experimenter, str) else experimenter
)
if institution is not None:
nwb_metadata["institution"] = institution
if lab is not None:
nwb_metadata["lab"] = lab
if experiment_description is not None:
nwb_metadata["experiment_description"] = experiment_description
if keywords:
nwb_metadata["keywords"] = keywords
# Merge with any additional kwargs
nwb_metadata.update(kwargs)
# Write to NWB
writer = NWBIO(str(filepath), mode="w", **nwb_metadata)
writer.write(block)
# Add subject metadata if provided (important for DANDI compliance)
# Neo's NWBIO doesn't support subject directly, so we add it post-hoc
if subject_id is not None or subject_description is not None:
from pynwb import NWBHDF5IO
from pynwb.file import Subject
with NWBHDF5IO(str(filepath), mode="r+") as io:
nwbfile = io.read()
nwbfile.subject = Subject(
subject_id=subject_id or "simulation",
species=species,
age=age,
sex=sex,
description=subject_description or "Simulated subject",
)
io.write(nwbfile)
return filepath
[docs]
@beartowertype
def export_simulation_to_nwb(
filepath: str | Path,
spike_train__Block: Block | None = None,
surface_emg__Block: Block | None = None,
surface_muap__Block: Block | None = None,
intramuscular_emg__Block: Block | None = None,
intramuscular_muap__Block: Block | None = None,
session_description: str = "MyoGen neuromuscular simulation",
identifier: str | None = None,
session_start_time: datetime | None = None,
**kwargs,
) -> Path:
"""
Export all simulation data to a single NWB file.
This is a convenience function that combines multiple Neo Blocks
(spike trains, EMG, MUAPs) into a single NWB file.
Parameters
----------
filepath : str or Path
Output file path.
spike_train__Block : SPIKE_TRAIN__Block, optional
Block containing spike train data.
surface_emg__Block : SURFACE_EMG__Block, optional
Block containing surface EMG data.
surface_muap__Block : SURFACE_MUAP__Block, optional
Block containing surface MUAP templates.
intramuscular_emg__Block : INTRAMUSCULAR_EMG__Block, optional
Block containing intramuscular EMG data.
intramuscular_muap__Block : INTRAMUSCULAR_MUAP__Block, optional
Block containing intramuscular MUAP templates.
session_description : str, default="MyoGen neuromuscular simulation"
Description of the simulation session.
identifier : str, optional
Unique identifier for this NWB file.
session_start_time : datetime, optional
Start time of the session.
**kwargs
Additional metadata passed to export_to_nwb.
Returns
-------
Path
Path to the created NWB file.
Examples
--------
>>> from myogen.utils.nwb import export_simulation_to_nwb
>>>
>>> # Export complete simulation
>>> export_simulation_to_nwb(
... "full_simulation.nwb",
... spike_train__Block=simulation.get_spike_train__Block(),
... surface_emg__Block=surface_emg.surface_emg__Block,
... session_description="Biceps brachii simulation",
... institution="University",
... )
"""
_check_nwb_available()
# Combine all blocks into one
combined_block = Block(name="MyoGen_Simulation")
# Add annotations to identify data types
if spike_train__Block is not None:
for segment in spike_train__Block.segments:
segment.annotate(myogen_data_type="spike_train")
combined_block.segments.append(segment)
if surface_emg__Block is not None:
for group in surface_emg__Block.groups:
group.annotate(myogen_data_type="surface_emg")
combined_block.groups.append(group)
if surface_muap__Block is not None:
for group in surface_muap__Block.groups:
group.annotate(myogen_data_type="surface_muap")
combined_block.groups.append(group)
if intramuscular_emg__Block is not None:
for segment in intramuscular_emg__Block.segments:
segment.annotate(myogen_data_type="intramuscular_emg")
combined_block.segments.append(segment)
if intramuscular_muap__Block is not None:
for segment in intramuscular_muap__Block.segments:
segment.annotate(myogen_data_type="intramuscular_muap")
combined_block.segments.append(segment)
return export_to_nwb(
combined_block,
filepath,
session_description=session_description,
identifier=identifier,
session_start_time=session_start_time,
**kwargs,
)
[docs]
@beartowertype
def validate_nwb(filepath: str | Path, verbose: bool = True) -> bool:
"""
Validate an NWB file using NWBInspector.
Parameters
----------
filepath : str or Path
Path to the NWB file to validate.
verbose : bool, default=True
If True, print validation results.
Returns
-------
bool
True if validation passed with no errors, False otherwise.
Examples
--------
>>> from myogen.utils.nwb import validate_nwb
>>>
>>> is_valid = validate_nwb("simulation.nwb")
>>> if is_valid:
... print("File is valid!")
"""
try:
from nwbinspector import inspect_nwbfile
from nwbinspector.inspector_tools import format_messages
except ImportError:
if verbose:
print(
"NWBInspector not installed. Install with: pip install nwbinspector"
)
return True # Can't validate without inspector
filepath = Path(filepath)
if not filepath.exists():
raise FileNotFoundError(f"NWB file not found: {filepath}")
# Run inspection
messages = list(inspect_nwbfile(nwbfile_path=str(filepath)))
# Filter by severity
errors = [m for m in messages if m.importance.name == "CRITICAL"]
warnings = [m for m in messages if m.importance.name in ("ERROR", "WARNING")]
if verbose:
if not messages:
print(f"✓ {filepath.name}: No issues found")
else:
print(f"\n{filepath.name} validation results:")
print(format_messages(messages))
return len(errors) == 0
__all__ = [
"export_to_nwb",
"export_simulation_to_nwb",
"validate_nwb",
]