Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions MATLAB_MAPPING.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ Complete reference mapping every MATLAB NDI function/class to its Python equival
| `ndi.validate` | `ndi.validate` | `ndi.validate` |
| `ndi.documentservice` | `ndi.DocumentService` | `ndi.documentservice` |

## Common Utilities

| MATLAB | Python | Module |
|--------|--------|--------|
| `ndi.common.PathConstants` | `ndi.common.PathConstants` | `ndi.common` |
| `ndi.common.getLogger` | `ndi.common.getLogger()` | `ndi.common` |
| `ndi.common.getCache` | `ndi.common.getCache()` | `ndi.common` |
| `ndi.common.getDatabaseHierarchy` | `ndi.common.getDatabaseHierarchy()` | `ndi.common` |
| `ndi.common.assertDIDInstalled` | `ndi.common.assertDIDInstalled()` | `ndi.common` |

## Time Synchronization

| MATLAB | Python | Module |
Expand Down Expand Up @@ -64,10 +74,11 @@ Complete reference mapping every MATLAB NDI function/class to its Python equival
| MATLAB | Python | Module |
|--------|--------|--------|
| `ndi.session` | `ndi.Session` | `ndi.session` |
| `ndi.session.dir` | `ndi.DirSession` | `ndi.session.dir` |
| `ndi.session.dir` | `ndi.session.dir` / `ndi.DirSession` | `ndi.session.dir` |
| `ndi.session.sessiontable` | `ndi.session.SessionTable` | `ndi.session.sessiontable` |
| MockSession (conceptual) | `ndi.session.MockSession` | `ndi.session.mock` |
| `ndi.dataset` | `ndi.Dataset` | `ndi.dataset` (`.cloud_client` property for on-demand file fetching) |
| `ndi.dataset.dir` | `ndi.dataset.dir` / `ndi.Dataset` | `ndi.dataset` |
| `ndi.subject` | `ndi.Subject` | `ndi.subject` |
| `ndi.neuron` | `ndi.Neuron` | `ndi.neuron` |
| `ndi.element_timeseries` | `ndi.ElementTimeseries` | `ndi.element_timeseries` |
Expand All @@ -76,10 +87,10 @@ Complete reference mapping every MATLAB NDI function/class to its Python equival

| MATLAB | Python | Module |
|--------|--------|--------|
| `ndi.file.navigator` | `ndi.file.FileNavigator` | `ndi.file.navigator` |
| `ndi.file.navigator_epochdir` | `ndi.file.navigator.EpochDirNavigator` | `ndi.file.navigator.epochdir` |
| `ndi.file.type.mfdaq_epoch_channel` | `ndi.file.type.MFDAQEpochChannel` | `ndi.file.type.mfdaq_epoch_channel` |
| `ndi.file.pfilemirror` | `ndi.file.PFileMirror` | `ndi.file.pfilemirror` |
| `ndi.file.navigator` | `ndi.file.navigator` / `ndi.file.FileNavigator` | `ndi.file.navigator` |
| `ndi.file.navigator_epochdir` | `ndi.file.navigator_epochdir` / `ndi.file.EpochDirNavigator` | `ndi.file.navigator.epochdir` |
| `ndi.file.type.mfdaq_epoch_channel` | `ndi.file.type.mfdaq_epoch_channel` / `ndi.file.type.MFDAQEpochChannel` | `ndi.file.type.mfdaq_epoch_channel` |
| `ndi.file.pfilemirror` | `ndi.file.pfilemirror()` | `ndi.file.pfilemirror` |

## App Framework

Expand Down
4 changes: 2 additions & 2 deletions src/ndi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
# Import epoch module (Phase 6)
# Import Phase 10: Cloud API client
# Import Phase 11: Schema validation
from . import calc, cloud, daq, epoch, file, session, time, util, validate, validators
from . import calc, cloud, common, daq, epoch, file, session, time, util, validate, validators

# Import Phase 9: App framework and calculators
from .app import App
Expand All @@ -41,7 +41,7 @@
# Import session and cache modules (Phase 7)
from .cache import Cache
from .calculator import Calculator
from .common import PathConstants, get_logger, timestamp
from .common import PathConstants, get_logger, getLogger, timestamp
from .database import Database, open_database
from .dataset import Dataset
from .document import Document
Expand Down
4 changes: 3 additions & 1 deletion src/ndi/cloud/orchestration.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ def downloadDataset(
)
from .internal import createRemoteDatasetDoc

target = Path(target_folder)
# MATLAB compatibility: the actual download directory is
# target_folder / cloud_dataset_id, matching MATLAB behaviour.
target = Path(target_folder) / cloud_dataset_id
target.mkdir(parents=True, exist_ok=True)

# Verify dataset exists
Expand Down
133 changes: 128 additions & 5 deletions src/ndi/common/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
"""
ndi.common - common utilities for NDI
ndi.common - common utilities for NDI.

Provides path constants, timestamps, and other shared utilities.
MATLAB equivalent: +ndi/+common

Provides path constants, cache, logger, and other shared utilities.

MATLAB functions:
ndi.common.PathConstants
ndi.common.assertDIDInstalled
ndi.common.getCache
ndi.common.getDatabaseHierarchy
ndi.common.getLogger
"""

from __future__ import annotations

import os
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
from typing import TYPE_CHECKING, Any


class PathConstants:
Expand Down Expand Up @@ -127,9 +138,11 @@ def timestamp() -> str:
return now.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"


def get_logger(name: str = "ndi"):
def getLogger(name: str = "ndi"):
"""Get a logger for NDI components.

MATLAB equivalent: ndi.common.getLogger

Args:
name: Logger name (default: 'ndi').

Expand All @@ -141,4 +154,114 @@ def get_logger(name: str = "ndi"):
return logging.getLogger(name)


__all__ = ["PathConstants", "timestamp", "get_logger"]
# Keep old name for backwards compatibility
get_logger = getLogger


# ---------------------------------------------------------------------------
# Singleton cache — mirrors MATLAB ndi.common.getCache
# ---------------------------------------------------------------------------

_cache_singleton: Any = None


def getCache() -> Any:
"""Return the global NDI cache singleton.

MATLAB equivalent: ndi.common.getCache

Returns a shared :class:`ndi.cache.Cache` instance, creating it on first
call. Subsequent calls return the same object.

Returns:
The global :class:`~ndi.cache.Cache` instance.
"""
global _cache_singleton # noqa: PLW0603
if _cache_singleton is None:
from ..cache import Cache

_cache_singleton = Cache()
return _cache_singleton


# ---------------------------------------------------------------------------
# Database hierarchy — mirrors MATLAB ndi.common.getDatabaseHierarchy
# ---------------------------------------------------------------------------

_database_hierarchy_singleton: Any = None


def getDatabaseHierarchy() -> dict[str, Any]:
"""Return the database document type hierarchy.

MATLAB equivalent: ndi.common.getDatabaseHierarchy

Reads the document definitions from ``ndi_common/database_documents``
and builds a mapping of document types to their superclasses and fields.
The result is cached after the first call.

Returns:
Dict mapping document type names to their definition metadata.
"""
global _database_hierarchy_singleton # noqa: PLW0603
if _database_hierarchy_singleton is not None:
return _database_hierarchy_singleton

import json

hierarchy: dict[str, Any] = {}
doc_path = PathConstants.DOCUMENT_PATH
if doc_path.is_dir():
for json_file in sorted(doc_path.rglob("*.json")):
try:
data = json.loads(json_file.read_text())
# Each definition has a "document_class" with "definition"
# containing the type name and superclasses.
doc_class = data.get("document_class", {})
def_info = doc_class.get("definition", "")
if def_info:
# Use the definition URL/path stem as the type name
type_name = Path(def_info).stem
hierarchy[type_name] = {
"definition": def_info,
"class_version": doc_class.get("class_version", 1),
"superclasses": doc_class.get("superclasses", []),
"file": str(json_file),
}
except (json.JSONDecodeError, KeyError):
continue

_database_hierarchy_singleton = hierarchy
return _database_hierarchy_singleton


# ---------------------------------------------------------------------------
# DID install check — mirrors MATLAB ndi.common.assertDIDInstalled
# ---------------------------------------------------------------------------


def assertDIDInstalled() -> None:
"""Assert that the DID (Document Interface Database) package is installed.

MATLAB equivalent: ndi.common.assertDIDInstalled

Raises:
ImportError: If the ``did`` package is not installed.
"""
try:
import did # noqa: F401
except ImportError:
raise ImportError(
"The 'did' package is required but not installed. " "Install it with: pip install did"
) from None


__all__ = [
"PathConstants",
"timestamp",
"getLogger",
"get_logger",
"getCache",
"getDatabaseHierarchy",
"assertDIDInstalled",
]
22 changes: 22 additions & 0 deletions src/ndi/dataset/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""
ndi.dataset - Multi-session dataset container.

A Dataset manages multiple sessions, either linked (by reference) or
ingested (copied into the dataset's own database). Datasets have their
own session for storing dataset-level documents and metadata.

MATLAB equivalents:
ndi.dataset -> ndi.dataset.Dataset (or ndi.Dataset)
ndi.dataset.dir -> ndi.dataset.dir (constructor for directory-based datasets)
"""

from ._dataset import Dataset

# MATLAB compatibility: ``ndi.dataset.dir(path)`` creates a directory-based
# dataset, mirroring the MATLAB constructor ``ndi.dataset.dir``.
dir = Dataset

__all__ = [
"Dataset",
"dir",
]
10 changes: 5 additions & 5 deletions src/ndi/dataset.py → src/ndi/dataset/_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
from pathlib import Path
from typing import Any

from .document import Document
from .ido import Ido
from .query import Query
from ..document import Document
from ..ido import Ido
from ..query import Query

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -53,7 +53,7 @@ def __init__(
path: Directory path for the dataset
reference: Human-readable reference name
"""
from .session.dir import DirSession
from ..session.dir import DirSession

self._path = Path(path)
self._reference = reference or self._path.name
Expand Down Expand Up @@ -432,7 +432,7 @@ def _recreate_linked_session(self, props: dict[str, Any]) -> Any | None:
creator = props.get("session_creator", "")

if creator == "DirSession":
from .session.dir import DirSession
from ..session.dir import DirSession

# Get creator args
args = []
Expand Down
27 changes: 22 additions & 5 deletions src/ndi/file/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,39 @@
This module provides classes for navigating and organizing data files
into epochs for neuroscience experiments.

Classes:
FileNavigator: Base class for finding and organizing epoch files
EpochDir: Directory-based epoch navigation
MATLAB equivalents:
ndi.file.navigator -> ndi.file.navigator (constructor)
ndi.file.navigator_epochdir -> ndi.file.navigator_epochdir (constructor)
ndi.file.pfilemirror -> ndi.file.pfilemirror (function)
ndi.file.type.mfdaq_epoch_channel -> ndi.file.type.mfdaq_epoch_channel

Example:
>>> from ndi.file import FileNavigator
>>> nav = FileNavigator(session, '*.rhd')
>>> nav = ndi.file.navigator(session, '*.rhd')
>>> epochfiles = nav.getepochfiles(1)
"""

from . import type as filetype
from .navigator import FileNavigator
from .navigator.epochdir import EpochDirNavigator
from .pfilemirror import pfilemirror as _pfilemirror_func

# MATLAB compatibility: ``ndi.file.navigator(session, patterns)`` creates a
# FileNavigator, mirroring the MATLAB constructor ``ndi.file.navigator``.
navigator = FileNavigator

# MATLAB compatibility: ``ndi.file.navigator_epochdir(session, patterns)``
# creates an EpochDirNavigator, mirroring ``ndi.file.navigator_epochdir``.
navigator_epochdir = EpochDirNavigator

# MATLAB compatibility: ``ndi.file.pfilemirror(src, dest)`` calls the
# directory-mirror utility, mirroring ``ndi.file.pfilemirror``.
pfilemirror = _pfilemirror_func

__all__ = [
"FileNavigator",
"EpochDirNavigator",
"filetype",
"navigator",
"navigator_epochdir",
"pfilemirror",
]
9 changes: 8 additions & 1 deletion src/ndi/file/type/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@
ndi.file.type - File type classes for NDI.

Contains dataclasses describing channel metadata for various file types.

MATLAB equivalents:
ndi.file.type.mfdaq_epoch_channel -> ndi.file.type.mfdaq_epoch_channel
"""

from .mfdaq_epoch_channel import MFDAQEpochChannel

__all__ = ["MFDAQEpochChannel"]
# MATLAB compatibility: ``ndi.file.type.mfdaq_epoch_channel(...)`` creates an
# MFDAQEpochChannel, mirroring the MATLAB constructor.
mfdaq_epoch_channel = MFDAQEpochChannel

__all__ = ["MFDAQEpochChannel", "mfdaq_epoch_channel"]
9 changes: 9 additions & 0 deletions src/ndi/session/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,26 @@
This module provides session classes for managing NDI experiments:
- Session: Abstract base class for session management
- DirSession: Directory-based session implementation

MATLAB equivalents:
ndi.session -> ndi.session.Session (or ndi.Session)
ndi.session.dir -> ndi.session.dir (constructor for directory-based sessions)
"""

from .dir import DirSession
from .mock import MockSession
from .session_base import Session, empty_id
from .sessiontable import SessionTable

# MATLAB compatibility: ``ndi.session.dir(path)`` creates a directory-based
# session, mirroring the MATLAB constructor ``ndi.session.dir``.
dir = DirSession

__all__ = [
"Session",
"DirSession",
"MockSession",
"SessionTable",
"empty_id",
"dir",
]
Loading