From e2fa1f6415d90f6eb64c6d3daf3f3c6c935f3a9e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Mar 2026 19:46:17 +0000 Subject: [PATCH 1/3] Fix downloadDataset path and add MATLAB-compatible ndi.dataset.dir / ndi.session.dir Three changes to match MATLAB behavior: 1. ndi.cloud.downloadDataset now creates target/{cloudDatasetId} subdirectory, matching the MATLAB implementation where the actual download path includes the cloud dataset ID. 2. Convert ndi/dataset.py to a package (ndi/dataset/) and expose ndi.dataset.dir as an alias for Dataset, so ndi.dataset.dir(path) works as a constructor just like in MATLAB. 3. Add ndi.session.dir = DirSession so ndi.session.dir(ref, path) works as a constructor matching MATLAB's ndi.session.dir. https://claude.ai/code/session_01SPLxWdyCaZ5VH8EUMneuJ3 --- src/ndi/cloud/orchestration.py | 4 +++- src/ndi/dataset/__init__.py | 22 +++++++++++++++++++++ src/ndi/{dataset.py => dataset/_dataset.py} | 10 +++++----- src/ndi/session/__init__.py | 9 +++++++++ 4 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 src/ndi/dataset/__init__.py rename src/ndi/{dataset.py => dataset/_dataset.py} (98%) diff --git a/src/ndi/cloud/orchestration.py b/src/ndi/cloud/orchestration.py index 509163d..d59c90b 100644 --- a/src/ndi/cloud/orchestration.py +++ b/src/ndi/cloud/orchestration.py @@ -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 diff --git a/src/ndi/dataset/__init__.py b/src/ndi/dataset/__init__.py new file mode 100644 index 0000000..1ce7a82 --- /dev/null +++ b/src/ndi/dataset/__init__.py @@ -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", +] diff --git a/src/ndi/dataset.py b/src/ndi/dataset/_dataset.py similarity index 98% rename from src/ndi/dataset.py rename to src/ndi/dataset/_dataset.py index 6d18894..ce41064 100644 --- a/src/ndi/dataset.py +++ b/src/ndi/dataset/_dataset.py @@ -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__) @@ -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 @@ -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 = [] diff --git a/src/ndi/session/__init__.py b/src/ndi/session/__init__.py index 2a09348..0f7afbd 100644 --- a/src/ndi/session/__init__.py +++ b/src/ndi/session/__init__.py @@ -4,6 +4,10 @@ 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 @@ -11,10 +15,15 @@ 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", ] From 569b4c594f97d03405bfd30c2ca3572d984d8899 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Mar 2026 19:54:16 +0000 Subject: [PATCH 2/3] Add MATLAB-compatible callables to ndi.file and ndi.file.type Apply the same porting guide pattern used for ndi.dataset.dir and ndi.session.dir to the ndi.file namespace: - ndi.file.navigator(...) -> FileNavigator constructor - ndi.file.navigator_epochdir(...) -> EpochDirNavigator constructor - ndi.file.pfilemirror(...) -> pfilemirror function - ndi.file.type.mfdaq_epoch_channel(...) -> MFDAQEpochChannel constructor All existing import paths (from ndi.file.navigator import FileNavigator, etc.) continue to work unchanged. ndi.common has no MATLAB equivalent namespace so no changes needed. Updated MATLAB_MAPPING.md to document the new callable aliases. https://claude.ai/code/session_01SPLxWdyCaZ5VH8EUMneuJ3 --- MATLAB_MAPPING.md | 11 ++++++----- src/ndi/file/__init__.py | 27 ++++++++++++++++++++++----- src/ndi/file/type/__init__.py | 9 ++++++++- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/MATLAB_MAPPING.md b/MATLAB_MAPPING.md index 4d1ad68..f29a85c 100644 --- a/MATLAB_MAPPING.md +++ b/MATLAB_MAPPING.md @@ -64,10 +64,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` | @@ -76,10 +77,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 diff --git a/src/ndi/file/__init__.py b/src/ndi/file/__init__.py index f6728ee..f7c9c17 100644 --- a/src/ndi/file/__init__.py +++ b/src/ndi/file/__init__.py @@ -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", ] diff --git a/src/ndi/file/type/__init__.py b/src/ndi/file/type/__init__.py index 3a92ac4..7c7c7ed 100644 --- a/src/ndi/file/type/__init__.py +++ b/src/ndi/file/type/__init__.py @@ -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"] From 96d8b554fae4e6da174b0e738a6edc0848fcac1d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Mar 2026 20:02:11 +0000 Subject: [PATCH 3/3] Add MATLAB-compatible ndi.common functions: getLogger, getCache, getDatabaseHierarchy, assertDIDInstalled Port the full ndi.common MATLAB namespace to Python: - getLogger(name) - renamed from get_logger to match MATLAB; old name kept for backwards compatibility - getCache() - returns singleton ndi.Cache instance, matching MATLAB's persistent cache pattern - getDatabaseHierarchy() - reads document definitions from ndi_common and returns cached type hierarchy dict - assertDIDInstalled() - raises ImportError if the did package is missing Also export ndi.common as a submodule from ndi/__init__.py so ndi.common.X is accessible after `import ndi`. Updated MATLAB_MAPPING.md with the new ndi.common section. https://claude.ai/code/session_01SPLxWdyCaZ5VH8EUMneuJ3 --- MATLAB_MAPPING.md | 10 +++ src/ndi/__init__.py | 4 +- src/ndi/common/__init__.py | 133 +++++++++++++++++++++++++++++++++++-- 3 files changed, 140 insertions(+), 7 deletions(-) diff --git a/MATLAB_MAPPING.md b/MATLAB_MAPPING.md index f29a85c..4990ae0 100644 --- a/MATLAB_MAPPING.md +++ b/MATLAB_MAPPING.md @@ -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 | diff --git a/src/ndi/__init__.py b/src/ndi/__init__.py index 585a4ef..75d3c90 100644 --- a/src/ndi/__init__.py +++ b/src/ndi/__init__.py @@ -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 @@ -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 diff --git a/src/ndi/common/__init__.py b/src/ndi/common/__init__.py index b5e0889..dce5b24 100644 --- a/src/ndi/common/__init__.py +++ b/src/ndi/common/__init__.py @@ -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: @@ -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'). @@ -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", +]