diff --git a/.cursorrules b/.cursorrules index 1736f73..dc1b2f4 100644 --- a/.cursorrules +++ b/.cursorrules @@ -1,7 +1,7 @@ # NDI Cursor Rules ## 1. Project Context -This is the NDI (Neuroscience Data Interface) Python port. It is a "Lead-Follow" project where the MATLAB codebase is the Source of Truth. +This is the NDI (Neuroscience ndi_gui_Data Interface) Python port. It is a "Lead-Follow" project where the MATLAB codebase is the Source of Truth. ## 2. Core Instructions Before proposing or writing any code, you MUST read and adhere to the following project-specific files: diff --git a/.github/workflows/test-symmetry.yml b/.github/workflows/test-symmetry.yml new file mode 100644 index 0000000..c0bace4 --- /dev/null +++ b/.github/workflows/test-symmetry.yml @@ -0,0 +1,122 @@ +name: Test Cross-Language Symmetry + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + symmetry: + name: MATLAB ↔ Python symmetry tests + runs-on: ubuntu-latest + + steps: + # ── Checkout both repos ──────────────────────────────────────────── + - name: Check out NDI-python + uses: actions/checkout@v4 + with: + path: NDI-python + + - name: Check out NDI-matlab + uses: actions/checkout@v4 + with: + repository: VH-Lab/NDI-matlab + path: NDI-matlab + + # ── Runtime setup ────────────────────────────────────────────────── + - name: Start virtual display server + run: | + sudo apt-get install -y xvfb + Xvfb :99 & + echo "DISPLAY=:99" >> $GITHUB_ENV + + - name: Set up MATLAB + uses: matlab-actions/setup-matlab@v2 + with: + release: latest + cache: true + products: | + Communications_Toolbox + Statistics_and_Machine_Learning_Toolbox + Signal_Processing_Toolbox + + - name: Install MatBox + uses: ehennestad/matbox-actions/install-matbox@v1 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install NDI-python + working-directory: NDI-python + run: | + python -m pip install --upgrade pip + python ndi_install.py --dev --no-validate --verbose + + # ── Stage 1: MATLAB makeArtifacts ────────────────────────────────── + - name: "Stage 1: MATLAB makeArtifacts" + uses: matlab-actions/run-command@v2 + with: + command: | + cd("NDI-matlab"); + addpath(genpath("src")); + addpath(genpath("tests")); + addpath(genpath("tools")); + matbox.installRequirements(fullfile(pwd, "tests")); + + import matlab.unittest.TestSuite + import matlab.unittest.TestRunner + + suite = TestSuite.fromPackage("ndi.symmetry.makeArtifacts", "IncludingSubpackages", true); + fprintf("\n=== MATLAB makeArtifacts: %d test(s) ===\n", numel(suite)); + assert(~isempty(suite), "No MATLAB makeArtifacts tests found.") + + runner = TestRunner.withTextOutput("Verbosity", "Detailed"); + results = runner.run(suite); + + fprintf("\n=== MATLAB makeArtifacts: %d passed, %d failed ===\n", ... + nnz([results.Passed]), nnz([results.Failed])); + assert(all(~[results.Failed]), "MATLAB makeArtifacts failed") + + # ── Stage 2: Python makeArtifacts + readArtifacts ────────────────── + - name: "Stage 2: Python symmetry tests (make + read)" + working-directory: NDI-python + run: | + echo "=== Running Python makeArtifacts ===" + pytest tests/symmetry/make_artifacts/ -v --tb=short + + echo "" + echo "=== Running Python readArtifacts (reads both MATLAB and Python artifacts) ===" + pytest tests/symmetry/read_artifacts/ -v --tb=short + + # ── Stage 3: MATLAB readArtifacts ────────────────────────────────── + - name: "Stage 3: MATLAB readArtifacts (reads Python artifacts)" + uses: matlab-actions/run-command@v2 + with: + command: | + cd("NDI-matlab"); + addpath(genpath("src")); + addpath(genpath("tests")); + addpath(genpath("tools")); + matbox.installRequirements(fullfile(pwd, "tests")); + + import matlab.unittest.TestSuite + import matlab.unittest.TestRunner + + suite = TestSuite.fromPackage("ndi.symmetry.readArtifacts", "IncludingSubpackages", true); + fprintf("\n=== MATLAB readArtifacts: %d test(s) ===\n", numel(suite)); + assert(~isempty(suite), "No MATLAB readArtifacts tests found.") + + runner = TestRunner.withTextOutput("Verbosity", "Detailed"); + results = runner.run(suite); + + fprintf("\n=== MATLAB readArtifacts: %d passed, %d failed ===\n", ... + nnz([results.Passed]), nnz([results.Failed])); + assert(all(~[results.Failed]), "MATLAB readArtifacts failed") diff --git a/AGENTS.md b/AGENTS.md index 2dc139e..193a57f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## 1. Role & Mission -You are an AI Developer for the NDI (Neuroscience Data Interface) project. Your mission is to maintain 1:1 functional and semantic parity between the mature MATLAB (Source of Truth) codebase and the Python port. +You are an AI Developer for the NDI (Neuroscience ndi_gui_Data Interface) project. Your mission is to maintain 1:1 functional and semantic parity between the mature MATLAB (Source of Truth) codebase and the Python port. ## 2. The Mandatory Knowledge Base @@ -34,5 +34,5 @@ Every sub-package contains a file named `ndi_matlab_python_bridge.yaml`. ## 5. Directory Mapping Reference -- **MATLAB Source:** `VH-Lab/NDI-matlab` (GitHub) +- **MATLAB Source:** `VH-ndi_gui_Lab/NDI-matlab` (GitHub) - **Python Target:** `src/ndi/[namespace]/` (Mirrors MATLAB `+namespace/`) diff --git a/DID_PYTHON_UPDATE_INSTRUCTIONS.md b/DID_PYTHON_UPDATE_INSTRUCTIONS.md deleted file mode 100644 index 51f8e3e..0000000 --- a/DID_PYTHON_UPDATE_INSTRUCTIONS.md +++ /dev/null @@ -1,285 +0,0 @@ -# Instructions: Update DID-python to Match DID-matlab SQLite Behavior - -## Background - -DID-python (`pip show did`, installed at `/usr/local/lib/python3.11/dist-packages/did/`) is a Python port of [VH-Lab/DID-matlab](https://github.com/VH-Lab/DID-matlab). The SQLite implementation has two critical gaps versus the MATLAB original: - -1. **`_do_add_doc` does not populate `fields`/`doc_data` tables** — it only stores JSON blobs in `docs`. The MATLAB version (`did.implementations.sqlitedb.do_add_doc`) uses `did.implementations.doc2sql` to flatten documents and populate both tables. - -2. **`search` uses brute-force JSON deserialization** instead of SQL queries against `doc_data`. The MATLAB version builds SQL queries from query structs and runs them directly. - -This means databases created by Python cannot be searched by MATLAB, and vice versa (MATLAB's search relies on `doc_data`). - -## Reference Files - -**DID-matlab** (authoritative, on `main` branch of https://github.com/VH-Lab/DID-matlab): -- `src/did/+did/+implementations/sqlitedb.m` — SQLite implementation (uses mksqlite) -- `src/did/+did/+implementations/doc2sql.m` — Document-to-SQL flattener -- `src/did/+did/database.m` — Base class with `search_doc_ids`, `get_sql_query_str`, `query_struct_to_sql_str` -- `src/did/+did/query.m` — Query class with `to_searchstructure` resolving `isa`/`depends_on` -- `src/did/+did/+datastructures/fieldsearch.m` — In-memory field search (used by non-SQL backends) - -**DID-python** (to be updated): -- `/usr/local/lib/python3.11/dist-packages/did/implementations/sqlitedb.py` -- `/usr/local/lib/python3.11/dist-packages/did/implementations/doc2sql.py` -- `/usr/local/lib/python3.11/dist-packages/did/database.py` -- `/usr/local/lib/python3.11/dist-packages/did/query.py` -- `/usr/local/lib/python3.11/dist-packages/did/datastructures.py` - -**NDI-python** (consumer — has workarounds to remove after DID is fixed): -- `/home/user/NDI-python/src/ndi/database.py` — `SQLiteDriver` class with `_populate_doc_data` workaround - -## Task 1: Populate `fields`/`doc_data` in `_do_add_doc` - -### What MATLAB Does - -In `sqlitedb.m`, `do_add_doc` (line ~256-404): - -1. Calls `did.implementations.doc2sql(document_obj)` to produce a struct array of "meta-tables" -2. The first meta-table is always `"meta"` with columns: `doc_id`, `class`, `superclass`, `datestamp`, `creation`, `deletion`, `depends_on` -3. Subsequent meta-tables are named after top-level document fields (e.g., `"base"`, `"element"`, `"daqsystem"`) with sub-field columns -4. For each column (skipping `doc_id`), it calls `get_field_idx(group_name, field_name)` to look up or create a `fields` row -5. Inserts all `(doc_idx, field_idx, value)` triples into `doc_data` in bulk - -### How `doc2sql` Flattens Documents - -Given a document with properties: -```json -{ - "document_class": {"class_name": "element", "superclasses": [{"definition": "$NDIDOCUMENTPATH/base.json"}]}, - "base": {"id": "abc", "name": "elec1", "session_id": "sess1", "datestamp": "2024-01-01"}, - "element": {"type": "probe", "reference": 1}, - "depends_on": [{"name": "subject_id", "value": "xyz"}] -} -``` - -`doc2sql` produces: - -**Meta table** (`name="meta"`): -| column name | value | -|---|---| -| doc_id | `"abc"` | -| class | `"element"` | -| superclass | `"base"` (extracted from definitions, stripped path and `.json`) | -| datestamp | `"2024-01-01"` | -| creation | `""` | -| deletion | `""` | -| depends_on | `"subject_id,xyz;"` (formatted as `name,value;name,value;...`) | - -**Base table** (`name="base"`): -| column name | value | -|---|---| -| doc_id | `"abc"` | -| id | `"abc"` | -| name | `"elec1"` | -| session_id | `"sess1"` | -| datestamp | `"2024-01-01"` | - -**Element table** (`name="element"`): -| column name | value | -|---|---| -| doc_id | `"abc"` | -| type | `"probe"` | -| reference | `1` | - -### How `get_field_idx` Works - -The field name stored in the `fields` table uses the format `{group_name}.{field_name}` — e.g., `"meta.class"`, `"base.name"`, `"element.type"`. - -Triple-underscores in column names from `doc2sql` are converted back to dots: `"___"` → `"."`. - -The `fields` table columns are: -- `class`: the group/table name (e.g., `"meta"`, `"base"`, `"element"`) -- `field_name`: the dot-separated path (e.g., `"meta.class"`, `"base.name"`) -- `json_name`: dots replaced with `___` (e.g., `"meta___class"`) -- `field_idx`: auto-increment primary key - -A `fields_cache` (in-memory dict) avoids repeated DB lookups. - -### What to Change in DID-python - -In `sqlitedb.py`, method `_do_add_doc` (currently lines 145-180): - -After the `docs` INSERT (line 168) and before the `branch_docs` INSERT (line 173), add logic to: - -1. Call a Python equivalent of `doc2sql` to flatten the document -2. For each (field_name, value) pair, look up or create a `field_idx` in the `fields` table -3. Bulk-insert all `(doc_idx, field_idx, value)` triples into `doc_data` - -You can either: -- **Option A**: Update the existing `doc2sql.py` to match MATLAB's `doc2sql.m` and call it from `_do_add_doc` -- **Option B**: Implement the flattening inline in `_do_add_doc` (simpler) - -**Critical**: The field names in the `fields` table MUST match what MATLAB's `query_struct_to_sql_str` expects. Specifically, the search queries use these field names: -- `"meta.class"` — the document class name (from `document_class.class_name`) -- `"meta.superclass"` — comma-separated superclass names (extracted from `document_class.superclasses[].definition`, with path and `.json` stripped) -- `"meta.depends_on"` — semicolon-separated `name,value;` pairs -- `"meta.datestamp"` — from `base.datestamp` -- `"{group}.{field}"` — for all other top-level groups and their sub-fields (e.g., `"base.name"`, `"element.type"`) - -### Superclass Extraction - -MATLAB extracts superclass names from the `definition` field of each superclass entry: -```matlab -superclass = getField(doc_props, 'document_class.superclasses'); -if isstruct(superclass) - superclass = regexprep({superclass.definition},{'.+/','\..+$'},''); - superclass = strjoin(unique(superclass), ', '); -end -``` - -This takes `"$NDIDOCUMENTPATH/daq/daqsystem.json"` → strips path → strips extension → `"daqsystem"`. - -Multiple superclasses are joined as `"base, daqsystem"` (comma-space separated, alphabetically sorted/unique). - -### Depends_on Serialization - -MATLAB serializes `depends_on` as: -```matlab -allData = [{dependsOn.name}; {dependsOn.value}]; -dependsOn = sprintf('%s,%s;',allData{:}); -``` - -This produces: `"filenavigator_id,abc123;daqreader_id,def456;"`. - -The search query for `depends_on` uses `LIKE "%name,value;%"`. - -### Nested Struct Fields (via `getMetaTableFrom`) - -For each top-level field group (excluding `depends_on`, `document_class`, `files`), MATLAB creates a "meta-table" with: -- A `doc_id` column -- One column per sub-field, with nested structs flattened using `___` separator - -Example: if `element` has `{"type": "probe", "details": {"count": 3}}`, the columns would be: -- `type` → `"probe"` (stored as field `element.type`) -- `details___count` → `3` (stored as field `element.details.count` after `___` → `.` conversion) - -## Task 2: Update `query.to_search_structure` to Resolve `isa` and `depends_on` - -### What MATLAB Does - -In `query.m`, the `to_searchstructure` method (line ~173-227) resolves `isa` and `depends_on` into lower-level operations BEFORE passing to search: - -**`isa` resolution** — converts to `OR(hasanysubfield_contains_string, contains_string)`: -```matlab -if strcmpi('isa', operation) - findinsubfield = struct('field','document_class.superclasses',... - 'operation','hasanysubfield_contains_string',... - 'param1','definition', 'param2', classname); - findinmainfield = struct('field','document_class.definition', ... - 'operation','contains_string', 'param1', classname, 'param2', ''); - ss.field = ''; - ss.operation = 'or'; - ss.param1 = findinsubfield; - ss.param2 = findinmainfield; -end -``` - -**`depends_on` resolution** — converts to `hasanysubfield_exact_string`: -```matlab -if strcmpi('depends_on', operation) - param1 = {'name','value'}; - param2 = {name_param, value_param}; - if strcmp(param2{1},'*') % wildcard: ignore name - param1 = param1(2); - param2 = param2(2); - end - ss = struct('field','depends_on','operation','hasanysubfield_exact_string'); - ss.param1 = param1; - ss.param2 = param2; -end -``` - -### What DID-python Currently Does - -In `query.py` line 52-55: -```python -def to_search_structure(self): - # A full implementation would recursively resolve 'isa', 'depends_on', etc. - # This is a simplified version for now. - return self.search_structure -``` - -It passes `isa` and `depends_on` through unchanged, relying on `field_search` in `datastructures.py` to handle them directly. This works for brute-force search but NOT for SQL-based search. - -### What to Change - -Update `query.py`'s `to_search_structure` to resolve `isa` and `depends_on` into lower-level operations, matching MATLAB's logic. This is needed so the SQL query builder can translate them. - -However, note that `datastructures.py`'s `field_search` also handles `isa` and `depends_on` directly (for non-SQL backends), so you should NOT break that path. The cleanest approach: resolve in `to_search_structure` and let `field_search` handle the resolved operations. - -## Task 3: Add SQL-Based Search (Optional but Recommended) - -### What MATLAB Does - -MATLAB's `did.database` base class (on `main` branch) has a SQL-based `do_search` that: - -1. Calls `search_doc_ids(query_struct, branch_id)` which recursively: - - For AND queries (struct arrays): intersects results from each sub-query - - For OR queries: unions results from each sub-query - - For leaf queries: calls `get_sql_query_str` → runs SQL → returns doc_ids - -2. `get_sql_query_str` builds: -```sql -SELECT DISTINCT docs.doc_id -FROM docs, branch_docs, doc_data, fields -WHERE docs.doc_idx = doc_data.doc_idx - AND docs.doc_idx = branch_docs.doc_idx - AND branch_docs.branch_id = "a" - AND fields.field_idx = doc_data.field_idx - AND {per-operation clause} -``` - -3. `query_struct_to_sql_str` maps operations to SQL: - - `exact_string` → `fields.field_name="X" AND doc_data.value = "Y"` - - `contains_string` → `fields.field_name="X" AND doc_data.value LIKE "%Y%"` - - `regexp` → `fields.field_name="X" AND regex(doc_data.value, "Y") NOT NULL` - - Note: MATLAB uses mksqlite's built-in `regex()` function. Python's sqlite3 does NOT have regex by default — you'd need `connection.create_function("regexp", 2, ...)`. - - `isa` → `(fields.field_name="meta.class" AND doc_data.value = "X") OR (fields.field_name="meta.superclass" AND regex(doc_data.value, "(^|, )X(,|$)") NOT NULL)` - - `depends_on` → `fields.field_name="meta.depends_on" AND doc_data.value LIKE "%name,value;%"` - - `hasfield` → `fields.field_name="X" OR fields.field_name LIKE "X.%"` - - Numeric comparisons: `doc_data.value < Y`, etc. - - `NOT` prefix: adds `NOT` before the value check - -### What to Change - -You can either: -- **Option A (Recommended for parity)**: Override `search` in `SQLiteDB` to build SQL queries against `doc_data`, matching MATLAB's `query_struct_to_sql_str` logic -- **Option B (Minimum viable)**: Keep the brute-force `field_search` approach in `database.py` but ensure `doc_data` is populated (Task 1) so MATLAB can search Python-created DBs - -Option A gives full symmetry. Option B gives cross-language write compatibility but not search performance parity. - -## Task 4: Remove NDI-python Workarounds - -After DID-python is updated, remove the workaround code from NDI-python's `database.py`: - -1. Remove `SQLiteDriver._flatten_document()` (static method) -2. Remove `SQLiteDriver._get_or_create_field_idx()` -3. Remove `SQLiteDriver._populate_doc_data()` -4. Remove `SQLiteDriver._populate_doc_data_with_cursor()` -5. Remove the `_populate_doc_data` calls in `add()`, `bulk_add()`, and `update()` -6. Remove the `doc_data` cleanup in `update()` - -The `bulk_add` method bypasses DID-python's `_do_add_doc` for performance — after DID is fixed, either: -- Route `bulk_add` through DID's `add_docs` (simpler, slightly slower), or -- Keep the direct SQL but call DID's flattening logic instead of NDI's - -## Testing - -1. Create a fresh database with Python, add documents, verify `fields` and `doc_data` tables are populated correctly -2. Search the Python-created database with MATLAB — verify `isa`, `depends_on`, `exact_string` queries all work -3. Create a database with MATLAB, search it with Python — verify all query types work -4. Run the existing symmetry tests in `/home/user/NDI-python/tests/symmetry/` - -## Key Gotcha: Field Name Format - -The most critical detail for cross-language compatibility is that **field names in the `fields` table must match between MATLAB and Python**. MATLAB uses `doc2sql` which produces: - -- `meta.class` (not `document_class.class_name`) -- `meta.superclass` (not `document_class.superclasses`) -- `meta.depends_on` (not `depends_on`) -- `meta.datestamp` (not `base.datestamp`) -- `base.id`, `base.name`, etc. -- `element.type`, `element.reference`, etc. - -Python's current workaround in NDI uses raw dotted paths like `document_class.class_name` and `depends_on(0).name` — these do NOT match what MATLAB expects. The Python `doc2sql` must match MATLAB's `doc2sql` field naming exactly. diff --git a/docs/developer_notes/PYTHON_PORTING_GUIDE.md b/docs/developer_notes/PYTHON_PORTING_GUIDE.md index 9183c1d..2a90bd3 100644 --- a/docs/developer_notes/PYTHON_PORTING_GUIDE.md +++ b/docs/developer_notes/PYTHON_PORTING_GUIDE.md @@ -24,7 +24,7 @@ To port or update a function, agents must follow these steps: 2. **Sync the Interface:** If the function is missing or outdated, update the YAML entry first based on the MATLAB `.m` file. 3. **Record the Sync Hash:** Store the short git hash of the MATLAB `.m` file being ported in the `matlab_last_sync_hash` field. Obtain it with: `git log -1 --format="%h" -- `. This allows future comparison to detect upstream MATLAB changes. 4. **Implement:** Write the Python code to satisfy the `input_arguments` and `output_arguments` defined in the YAML. -5. **Log & Notify:** Record the sync date in the YAML's `decision_log` (e.g., `"Synchronized with MATLAB main as of 2026-03-12."`). Document any intentional divergences. Explicitly tell the user what changes were made to the bridge file so they can review the contract. +5. **Log & Notify:** Record the sync date in the YAML's `decision_log` (e.g., `"Synchronized with MATLAB main as of 2026-03-12."`). ndi_document any intentional divergences. Explicitly tell the user what changes were made to the bridge file so they can review the contract. ## 4. Input Validation: Pydantic is Mandatory diff --git a/docs/developer_notes/class_naming_convention.md b/docs/developer_notes/class_naming_convention.md new file mode 100644 index 0000000..5fbd01a --- /dev/null +++ b/docs/developer_notes/class_naming_convention.md @@ -0,0 +1,116 @@ +# Class Naming Convention: MATLAB-Python Mechanical Mapping + +- **Status:** Active +- **Scope:** All NDI-Python and related projects (DID-Python, etc.) +- **Goal:** Eliminate lookup tables and manual mapping between MATLAB and Python class names. + +## The Rule + +Python class names are derived mechanically from the fully-qualified MATLAB class name using two substitutions: + +1. Replace every `.` (dot) in the MATLAB name with `_` (single underscore) +2. Replace every `_` (underscore) in the MATLAB name with `__` (double underscore) + +The mapping is **invertible** — given a Python class name you can always recover the MATLAB name: + +1. Replace every `__` with `_` +2. Replace every (remaining) `_` with `.` + +## Examples + +| MATLAB class | Python class | +|-------------------------------------|-------------------------------------------| +| `ndi.session.dir` | `ndi_session_dir` | +| `ndi.probe.timeseries.mfdaq` | `ndi_probe_timeseries_mfdaq` | +| `ndi.daq.system` | `ndi_daq_system` | +| `ndi.epoch.epochprobemap_daqsystem` | `ndi_epoch_epochprobemap__daqsystem` | +| `ndi.time.timereference_struct` | `ndi_time_timereference__struct` | +| `ndi.calc.tuning_fit` | `ndi_calc_tuning__fit` | +| `ndi.element` | `ndi_element` | +| `ndi.document` | `ndi_document` | + +Note how `epochprobemap_daqsystem` (which has an underscore in MATLAB) becomes +`epochprobemap__daqsystem` with a double underscore in Python. This distinguishes +it from a hypothetical `ndi.epoch.epochprobemap.daqsystem` (with a dot), which +would be `ndi_epoch_epochprobemap_daqsystem` (single underscore). + +## Why This Convention + +### Problem: Fragile CamelCase mapping + +The previous approach used PEP 8 CamelCase names (`DirSession`, `ProbeTimeseriesMFDAQ`, +`EpochProbeMapDAQSystem`) and maintained explicit mapping tables (YAML bridge files, +per-class `matlab_class` attributes, module-level aliases). This was fragile because: + +- Capitalization conventions are ambiguous (is it `MFDAQReader` or `MfdaqReader`?) +- Every new class required updating multiple mapping tables +- No mechanical way to go from a MATLAB name to the Python name or vice versa + +### Solution: Mechanical underscore mapping + +With this convention: + +- **No lookup tables needed.** The class name IS the mapping. +- **No ambiguity.** The conversion is deterministic in both directions. +- **No fragility.** Adding a new class requires zero mapping infrastructure. +- **Cross-project safety.** The full MATLAB namespace prefix (`ndi_`, `did_`, `vhlab_`, etc.) + is included in the name, so classes from different projects never collide. + +### Trade-off: PEP 8 + +This convention departs from PEP 8's recommendation of CamelCase for class names. +This is a deliberate choice — MATLAB interop and mapping simplicity take priority +over Python style conventions in this project. (NumPy similarly departs from PEP 8 +for consistency with mathematical conventions.) + +## Scope + +This convention applies to **classes that have MATLAB equivalents**. Python-only +classes (exceptions, enums, internal dataclasses, test utilities) may use standard +Python naming conventions. Examples of classes that keep standard names: + +- `CacheEntry` (internal data structure, no MATLAB equivalent) +- `SQLiteDriver` (internal backend, no MATLAB equivalent) +- `DocExistsAction` (Python enum) +- `ValidationResult` (Python-specific) +- `CloudError`, `CloudAuthError` (Python exceptions) +- `ChannelType`, `ChannelInfo` (Python-specific enums/dataclasses) + +## Conversion Functions + +For programmatic use, the conversion can be expressed as: + +```python +def matlab_to_python(matlab_name: str) -> str: + """Convert 'ndi.session.dir' -> 'ndi_session_dir'.""" + return matlab_name.replace("_", "__").replace(".", "_") + +def python_to_matlab(python_name: str) -> str: + """Convert 'ndi_session_dir' -> 'ndi.session.dir'.""" + return python_name.replace("__", "\x00").replace("_", ".").replace("\x00", "_") +``` + +## Module-Level Aliases + +For convenience, `__init__.py` files may provide aliases so that the Python +import path mirrors the MATLAB access path. For example: + +```python +# In ndi/session/__init__.py +from .dir import ndi_session_dir + +# Alias so that ndi.session.dir(...) works like MATLAB +dir = ndi_session_dir +``` + +These aliases are optional conveniences, not the primary naming mechanism. + +## Instructions for AI Agents + +1. When creating a new class that has a MATLAB equivalent, derive the Python + class name mechanically using the rules above. +2. Do NOT invent CamelCase names or maintain separate mapping tables. +3. External library classes (e.g., `did.query.Query`, `requests.Session`) must + NOT be renamed — only NDI-Python classes follow this convention. +4. The `ndi_matlab_python_bridge.yaml` files record the mapping for documentation + purposes but are NOT the source of truth — the class name itself is. diff --git a/docs/developer_notes/symmetry_tests.md b/docs/developer_notes/symmetry_tests.md index eb5600d..a416f6a 100644 --- a/docs/developer_notes/symmetry_tests.md +++ b/docs/developer_notes/symmetry_tests.md @@ -119,7 +119,7 @@ Pick the NDI domain being tested (e.g., `session`, `document`, `probe`). import json, shutil from pathlib import Path from tests.symmetry.conftest import PYTHON_ARTIFACTS -from ndi.session.dir import DirSession +from ndi.session.dir import ndi_session_dir ARTIFACT_DIR = PYTHON_ARTIFACTS / "" / "" / "" @@ -127,7 +127,7 @@ class TestMyFeature: @pytest.fixture(autouse=True) def _setup(self, tmp_path): # Build session in tmp_path ... - self.session = DirSession("exp1", tmp_path / "exp1") + self.session = ndi_session_dir("exp1", tmp_path / "exp1") def test_my_feature_artifacts(self): if ARTIFACT_DIR.exists(): @@ -147,7 +147,7 @@ Follow the INSTRUCTIONS.md in the MATLAB `+makeArtifacts` folder. ```python import json, pytest from tests.symmetry.conftest import SOURCE_TYPES, SYMMETRY_BASE -from ndi.session.dir import DirSession +from ndi.session.dir import ndi_session_dir @pytest.fixture(params=SOURCE_TYPES) def source_type(request): @@ -158,7 +158,7 @@ class TestMyFeature: artifact_dir = SYMMETRY_BASE / source_type / "" / ... if not artifact_dir.exists(): pytest.skip(f"No artifacts from {source_type}") - session = DirSession("exp1", artifact_dir) + session = ndi_session_dir("exp1", artifact_dir) # Assertions ... ``` diff --git a/docs/getting-started.md b/docs/getting-started.md index a505a00..f2f4ba7 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -5,12 +5,12 @@ ### Prerequisites - Python 3.10 or later -- Git (for installing VH-Lab dependencies) +- Git (for installing VH-ndi_gui_Lab dependencies) ### Install from source ```bash -git clone https://github.com/Waltham-Data-Science/NDI-python.git +git clone https://github.com/Waltham-ndi_gui_Data-Science/NDI-python.git cd NDI-python python -m venv venv source venv/bin/activate # Linux/macOS (venv\Scripts\activate on Windows) @@ -25,10 +25,10 @@ Run `python -m ndi check` at any time to verify your installation. ### Creating Documents ```python -from ndi import Document +from ndi import ndi_document # Create a document from a schema -doc = Document('base') +doc = ndi_document('base') print(f"ID: {doc.id}") print(f"Type: {doc.document_properties['document_class']['class_name']}") ``` @@ -36,46 +36,46 @@ print(f"Type: {doc.document_properties['document_class']['class_name']}") ### Querying ```python -from ndi import Query +from ndi import ndi_query # Exact match -q = Query('base.name') == 'my_experiment' +q = ndi_query('base.name') == 'my_experiment' # Type query -q_type = Query('').isa('subject') +q_type = ndi_query('').isa('subject') # Combine with logical operators q_combined = q & q_type # Dependency query -q_dep = Query('').depends_on('element_id', 'some-uuid') +q_dep = ndi_query('').depends_on('element_id', 'some-uuid') ``` ### Sessions ```python -from ndi import DirSession, Document, Query +from ndi import ndi_session_dir, ndi_document, ndi_query # Create a directory-backed session -session = DirSession('my_experiment', '/path/to/data') +session = ndi_session_dir('my_experiment', '/path/to/data') # Add documents -doc = Document('base') +doc = ndi_document('base') session.database_add(doc) # Search -results = session.database_search(Query.all()) +results = session.database_search(ndi_query.all()) ``` -### Testing with MockSession +### Testing with ndi_session_mock ```python -from ndi.session import MockSession -from ndi import Document, Query +from ndi.session import ndi_session_mock +from ndi import ndi_document, ndi_query -with MockSession('test') as session: - session.database_add(Document('base')) - results = session.database_search(Query('').isa('base')) +with ndi_session_mock('test') as session: + session.database_add(ndi_document('base')) + results = session.database_search(ndi_query('').isa('base')) assert len(results) == 1 ``` diff --git a/docs/index.md b/docs/index.md index 71aca9b..530c419 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,14 +1,14 @@ # NDI-Python -**Neuroscience Data Interface** - Python implementation. +**Neuroscience ndi_gui_Data Interface** - Python implementation. NDI provides a unified framework for managing, querying, and analyzing neuroscience experimental data across different acquisition systems. ## Overview -NDI-Python is a complete port of the [MATLAB NDI](https://github.com/VH-Lab/NDI-matlab) codebase, providing: +NDI-Python is a complete port of the [MATLAB NDI](https://github.com/VH-ndi_gui_Lab/NDI-matlab) codebase, providing: -- **Document-based metadata** with JSON schema validation +- **ndi_document-based metadata** with JSON schema validation - **SQLite database** backend for persistent storage and rich queries - **Time synchronization** across heterogeneous recording devices - **DAQ readers** for Intan, Blackrock, CED Spike2, and SpikeGadgets @@ -19,26 +19,26 @@ NDI-Python is a complete port of the [MATLAB NDI](https://github.com/VH-Lab/NDI- ## Quick Start ```python -from ndi import Document, Query, DirSession -from ndi.session import MockSession +from ndi import ndi_document, ndi_query, ndi_session_dir +from ndi.session import ndi_session_mock # Create and query documents -with MockSession('demo') as session: - doc = Document('base') +with ndi_session_mock('demo') as session: + doc = ndi_document('base') session.database_add(doc) - results = session.database_search(Query.all()) + results = session.database_search(ndi_query.all()) print(f"Found {len(results)} documents") ``` ## Installation ```bash -git clone https://github.com/Waltham-Data-Science/NDI-python.git +git clone https://github.com/Waltham-ndi_gui_Data-Science/NDI-python.git cd NDI-python ``` -See the [Getting Started guide](getting-started.md) or the [README](https://github.com/Waltham-Data-Science/NDI-python#readme) for full installation instructions including VH-Lab dependency setup. +See the [Getting Started guide](getting-started.md) or the [README](https://github.com/Waltham-ndi_gui_Data-Science/NDI-python#readme) for full installation instructions including VH-ndi_gui_Lab dependency setup. ## MATLAB Migration -If you're migrating from MATLAB NDI, see [MATLAB_MAPPING.md](https://github.com/Waltham-Data-Science/NDI-python/blob/main/MATLAB_MAPPING.md) for a complete function-by-function reference. +If you're migrating from MATLAB NDI, see [MATLAB_MAPPING.md](https://github.com/Waltham-ndi_gui_Data-Science/NDI-python/blob/main/MATLAB_MAPPING.md) for a complete function-by-function reference. diff --git a/docs/matlab-migration.md b/docs/matlab-migration.md index 9f82bef..7ab6fd6 100644 --- a/docs/matlab-migration.md +++ b/docs/matlab-migration.md @@ -2,11 +2,11 @@ This page summarizes key differences when migrating from MATLAB NDI to Python NDI. -For the complete function-by-function mapping, see [MATLAB_MAPPING.md](https://github.com/Waltham-Data-Science/NDI-python/blob/main/MATLAB_MAPPING.md). +For the complete function-by-function mapping, see [MATLAB_MAPPING.md](https://github.com/Waltham-ndi_gui_Data-Science/NDI-python/blob/main/MATLAB_MAPPING.md). ## Key API Differences -### Query Syntax +### ndi_query Syntax **MATLAB:** ```matlab @@ -17,8 +17,8 @@ q_combined = ndi.query(q, '&', q_type); **Python:** ```python -q = Query('base.name') == 'my_experiment' -q_type = Query('').isa('subject') +q = ndi_query('base.name') == 'my_experiment' +q_type = ndi_query('').isa('subject') q_combined = q & q_type ``` @@ -54,6 +54,6 @@ doc = ndi.document('base'); **Python:** ```python -session = DirSession('my_experiment', '/path/to/data') -doc = Document('base') +session = ndi_session_dir('my_experiment', '/path/to/data') +doc = ndi_document('base') ``` diff --git a/src/ndi/__init__.py b/src/ndi/__init__.py index e9c4c95..3128ebb 100644 --- a/src/ndi/__init__.py +++ b/src/ndi/__init__.py @@ -1,29 +1,29 @@ """ -NDI - Neuroscience Data Interface +NDI - Neuroscience ndi_gui_Data Interface Python implementation of NDI for managing neuroscience experimental data. This package provides: -- Document management with JSON schemas -- Database operations for storing and querying documents +- ndi_document management with JSON schemas +- ndi_database operations for storing and querying documents - Time synchronization across data sources -- Data acquisition system abstraction +- ndi_gui_Data acquisition system abstraction Example: - from ndi import Document, Query, Ido, Database + from ndi import ndi_document, ndi_query, ndi_ido, ndi_database # Create a new document - doc = Document('base', **{'base.name': 'my_experiment'}) + doc = ndi_document('base', **{'base.name': 'my_experiment'}) # Create a query - q = Query('base.name') == 'my_experiment' + q = ndi_query('base.name') == 'my_experiment' # Generate unique IDs - ido = Ido() + ido = ndi_ido() print(ido.id) # Open a database - db = Database('/path/to/session') + db = ndi_database('/path/to/session') db.add(doc) """ @@ -34,31 +34,31 @@ # Import Phase 11: Schema validation from . import calc, cloud, common, daq, epoch, file, session, time, util, validate, validators -# Import Phase 9: App framework and calculators -from .app import App -from .app.appdoc import AppDoc, DocExistsAction +# Import Phase 9: ndi_app framework and calculators +from .app import ndi_app +from .app.appdoc import DocExistsAction, ndi_app_appdoc # Import session and cache modules (Phase 7) -from .cache import Cache -from .calculator import Calculator -from .common import PathConstants, getLogger, timestamp -from .database import Database, open_database -from .dataset import Dataset, DatasetDir -from .document import Document -from .documentservice import DocumentService -from .element import Element -from .element_timeseries import ElementTimeseries -from .ido import Ido -from .neuron import Neuron -from .probe import Probe -from .query import Query -from .session import DirSession, Session, empty_id +from .cache import ndi_cache +from .calculator import ndi_calculator +from .common import getLogger, ndi_common_PathConstants, timestamp +from .database import ndi_database, open_database +from .dataset import ndi_dataset, ndi_dataset_dir +from .document import ndi_document +from .documentservice import ndi_documentservice +from .element import ndi_element +from .element_timeseries import ndi_element_timeseries +from .ido import ndi_ido +from .neuron import ndi_neuron +from .probe import ndi_probe +from .query import ndi_query +from .session import empty_id, ndi_session, ndi_session_dir # Import Phase 8 classes -from .subject import Subject +from .subject import ndi_subject __version__ = "0.1.0" -__author__ = "VH-Lab" +__author__ = "VH-ndi_gui_Lab" def version() -> tuple: @@ -74,7 +74,7 @@ def version() -> tuple: import subprocess from pathlib import Path as _Path - url = "https://github.com/Waltham-Data-Science/NDI-python" + url = "https://github.com/Waltham-ndi_gui_Data-Science/NDI-python" # Try git describe from the repo root repo = _Path(__file__).resolve().parent.parent.parent try: @@ -93,35 +93,35 @@ def version() -> tuple: __all__ = [ - "Ido", - "Query", - "Document", - "Database", + "ndi_ido", + "ndi_query", + "ndi_document", + "ndi_database", "open_database", - "PathConstants", + "ndi_common_PathConstants", "timestamp", "getLogger", "time", "daq", "file", "epoch", - "Element", - "Probe", - "DocumentService", - "Cache", + "ndi_element", + "ndi_probe", + "ndi_documentservice", + "ndi_cache", "session", - "Session", - "DirSession", + "ndi_session", + "ndi_session_dir", "empty_id", - "Subject", - "ElementTimeseries", - "Neuron", - "Dataset", - "DatasetDir", - "App", - "AppDoc", + "ndi_subject", + "ndi_element_timeseries", + "ndi_neuron", + "ndi_dataset", + "ndi_dataset_dir", + "ndi_app", + "ndi_app_appdoc", "DocExistsAction", - "Calculator", + "ndi_calculator", "calc", "cloud", "util", diff --git a/src/ndi/app/__init__.py b/src/ndi/app/__init__.py index 691a4f7..1455b5c 100644 --- a/src/ndi/app/__init__.py +++ b/src/ndi/app/__init__.py @@ -1,7 +1,7 @@ """ ndi.app - Base class for NDI applications. -An App operates on a Session and can create/find/store documents +An ndi_app operates on a ndi_session and can create/find/store documents that record the app's name, version, and execution environment. MATLAB equivalent: src/ndi/+ndi/app.m @@ -16,40 +16,40 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, Optional, Tuple -from ..documentservice import DocumentService +from ..documentservice import ndi_documentservice if TYPE_CHECKING: - from ..document import Document - from ..query import Query - from ..session.session_base import Session + from ..document import ndi_document + from ..query import ndi_query + from ..session.session_base import ndi_session -class App(DocumentService): +class ndi_app(ndi_documentservice): """ Base class for NDI applications. - An App is attached to a Session and can create documents that + An ndi_app is attached to a ndi_session and can create documents that record metadata about the app (name, version, URL, OS info). Attributes: - session: The ndi.Session this app operates on + session: The ndi.ndi_session this app operates on name: The name of the application Example: - >>> app = App(session, 'my_analysis') + >>> app = ndi_app(session, 'my_analysis') >>> doc = app.newdocument() """ def __init__( self, - session: Session | None = None, + session: ndi_session | None = None, name: str = "generic", ): self._session = session self._name = name @property - def session(self) -> Session | None: + def session(self) -> ndi_session | None: """Get the session.""" return self._session @@ -110,25 +110,25 @@ def version_url(self) -> tuple[str, str]: if result_url.returncode == 0: url = result_url.stdout.strip() else: - url = "https://github.com/Waltham-Data-Science/NDI-python" + url = "https://github.com/Waltham-ndi_gui_Data-Science/NDI-python" except Exception: version = "$Format:%H$" - url = "https://github.com/Waltham-Data-Science/NDI-python" + url = "https://github.com/Waltham-ndi_gui_Data-Science/NDI-python" return version, url - def newdocument(self) -> Document: + def newdocument(self) -> ndi_document: """ Create a new 'app' document with environment metadata. Returns: - Document of type 'app' + ndi_document of type 'app' """ - from ..document import Document + from ..document import ndi_document version, url = self.version_url() - doc = Document( + doc = ndi_document( "app", **{ "app.name": self._name, @@ -146,19 +146,19 @@ def newdocument(self) -> Document: return doc - def searchquery(self) -> Query: + def searchquery(self) -> ndi_query: """ Return a query to find this app's documents. Matches on app.name == varappname(). Returns: - Compound Query object + Compound ndi_query object """ - from ..query import Query + from ..query import ndi_query - q = Query("").isa("app") & (Query("app.name") == self.varappname()) + q = ndi_query("").isa("app") & (ndi_query("app.name") == self.varappname()) return q def __repr__(self) -> str: - return f"App('{self._name}')" + return f"ndi_app('{self._name}')" diff --git a/src/ndi/app/appdoc.py b/src/ndi/app/appdoc.py index bc50f3a..af4584c 100644 --- a/src/ndi/app/appdoc.py +++ b/src/ndi/app/appdoc.py @@ -13,7 +13,7 @@ from typing import TYPE_CHECKING, Any if TYPE_CHECKING: - from ..document import Document + from ..document import ndi_document class DocExistsAction(str, Enum): @@ -25,7 +25,7 @@ class DocExistsAction(str, Enum): REPLACE_IF_DIFFERENT = "ReplaceIfDifferent" -class AppDoc: +class ndi_app_appdoc: """ Mixin for application document management. @@ -36,7 +36,7 @@ class AppDoc: Attributes: doc_types: List of internal names for document types doc_document_types: List of NDI document schema types - doc_session: Session for database access + doc_session: ndi_session for database access """ def __init__( @@ -56,7 +56,7 @@ def add_appdoc( doc_exists_action: DocExistsAction = DocExistsAction.ERROR, *args, **kwargs, - ) -> list[Document]: + ) -> list[ndi_document]: """ Create and store an app document. @@ -77,7 +77,7 @@ def add_appdoc( if existing: if doc_exists_action == DocExistsAction.ERROR: - raise RuntimeError(f"Document of type '{appdoc_type}' already exists") + raise RuntimeError(f"ndi_document of type '{appdoc_type}' already exists") elif doc_exists_action == DocExistsAction.NO_ACTION: return existing elif doc_exists_action == DocExistsAction.REPLACE_IF_DIFFERENT: @@ -115,9 +115,9 @@ def struct2doc( appdoc_struct: dict, *args, **kwargs, - ) -> Document | None: + ) -> ndi_document | None: """ - Convert a parameter dict to an ndi.Document. + Convert a parameter dict to an ndi.ndi_document. Base class returns None. Subclasses must override. """ @@ -126,10 +126,10 @@ def struct2doc( def doc2struct( self, appdoc_type: str, - doc: Document, + doc: ndi_document, ) -> dict: """ - Extract parameter dict from a Document. + Extract parameter dict from a ndi_document. Base class reads the property_list_name from the document. """ @@ -147,7 +147,7 @@ def find_appdoc( appdoc_type: str, *args, **kwargs, - ) -> list[Document]: + ) -> list[ndi_document]: """ Find existing app documents in the database. diff --git a/src/ndi/app/markgarbage.py b/src/ndi/app/markgarbage.py index ccd92dc..a5a3492 100644 --- a/src/ndi/app/markgarbage.py +++ b/src/ndi/app/markgarbage.py @@ -1,7 +1,7 @@ """ ndi.app.markgarbage - Mark valid/invalid time intervals. -Provides the MarkGarbage app for identifying and storing valid +Provides the ndi_app_markgarbage app for identifying and storing valid time intervals within recording epochs. MATLAB equivalent: src/ndi/+ndi/+app/markgarbage.m @@ -11,27 +11,27 @@ from typing import TYPE_CHECKING, Any -from . import App +from . import ndi_app if TYPE_CHECKING: - from ..document import Document - from ..session.session_base import Session + from ..document import ndi_document + from ..session.session_base import ndi_session -class MarkGarbage(App): +class ndi_app_markgarbage(ndi_app): """ - App for marking valid/invalid time intervals in recordings. + ndi_app for marking valid/invalid time intervals in recordings. Allows users to identify and store which portions of a recording are valid (not "garbage") for analysis. Example: - >>> app = MarkGarbage(session) + >>> app = ndi_app_markgarbage(session) >>> app.markvalidinterval(epochset, 0.5, timeref, 10.0, timeref) >>> intervals, docs = app.loadvalidinterval(epochset) """ - def __init__(self, session: Session | None = None): + def __init__(self, session: ndi_session | None = None): super().__init__(session=session, name="ndi_app_markgarbage") def markvalidinterval( @@ -48,7 +48,7 @@ def markvalidinterval( MATLAB equivalent: ndi.app.markgarbage/markvalidinterval Args: - epochset_obj: EpochSet or Element to mark + epochset_obj: ndi_epoch_epochset or ndi_element to mark t0: Start time of valid interval timeref_t0: Time reference for t0 t1: End time of valid interval @@ -76,7 +76,7 @@ def savevalidinterval( MATLAB equivalent: ndi.app.markgarbage/savevalidinterval Args: - epochset_obj: EpochSet or Element + epochset_obj: ndi_epoch_epochset or ndi_element interval_struct: Dict with t0, timeref_t0, t1, timeref_t1 Returns: @@ -85,9 +85,9 @@ def savevalidinterval( if self._session is None: raise RuntimeError("No session configured") - from ..document import Document + from ..document import ndi_document - doc = Document("apps/markgarbage/valid_interval", valid_interval=interval_struct) + doc = ndi_document("apps/markgarbage/valid_interval", valid_interval=interval_struct) doc = doc.set_session_id(self._session.id()) if hasattr(epochset_obj, "id"): doc = doc.set_dependency_value( @@ -105,29 +105,31 @@ def clearvalidinterval(self, epochset_obj: Any) -> None: MATLAB equivalent: ndi.app.markgarbage/clearvalidinterval Args: - epochset_obj: EpochSet or Element + epochset_obj: ndi_epoch_epochset or ndi_element """ if self._session is None: return - from ..query import Query + from ..query import ndi_query - q = Query("").isa("valid_interval") + q = ndi_query("").isa("valid_interval") if hasattr(epochset_obj, "id"): - q = q & Query("").depends_on("element_id", epochset_obj.id) + q = q & ndi_query("").depends_on("element_id", epochset_obj.id) docs = self._session.database_search(q) for doc in docs: self._session.database_remove(doc) - def loadvalidinterval(self, epochset_obj: Any) -> tuple[list[dict[str, Any]], list[Document]]: + def loadvalidinterval( + self, epochset_obj: Any + ) -> tuple[list[dict[str, Any]], list[ndi_document]]: """ Load stored valid intervals. MATLAB equivalent: ndi.app.markgarbage/loadvalidinterval Args: - epochset_obj: EpochSet or Element + epochset_obj: ndi_epoch_epochset or ndi_element Returns: Tuple of (intervals, docs) where intervals is a list of @@ -136,11 +138,11 @@ def loadvalidinterval(self, epochset_obj: Any) -> tuple[list[dict[str, Any]], li if self._session is None: return [], [] - from ..query import Query + from ..query import ndi_query - q = Query("").isa("valid_interval") + q = ndi_query("").isa("valid_interval") if hasattr(epochset_obj, "id"): - q = q & Query("").depends_on("element_id", epochset_obj.id) + q = q & ndi_query("").depends_on("element_id", epochset_obj.id) docs = self._session.database_search(q) intervals = [] @@ -167,7 +169,7 @@ def identifyvalidintervals( MATLAB equivalent: ndi.app.markgarbage/identifyvalidintervals Args: - epochset_obj: EpochSet or Element + epochset_obj: ndi_epoch_epochset or ndi_element timeref: Time reference for the query interval t0: Start time of query interval t1: End time of query interval @@ -180,4 +182,4 @@ def identifyvalidintervals( ) def __repr__(self) -> str: - return f"MarkGarbage(session={self._session is not None})" + return f"ndi_app_markgarbage(session={self._session is not None})" diff --git a/src/ndi/app/ndi_matlab_python_bridge.yaml b/src/ndi/app/ndi_matlab_python_bridge.yaml index 11e6852..9f04298 100644 --- a/src/ndi/app/ndi_matlab_python_bridge.yaml +++ b/src/ndi/app/ndi_matlab_python_bridge.yaml @@ -18,13 +18,13 @@ classes: type: class matlab_path: "src/ndi/+ndi/app.m" python_path: "ndi/app/__init__.py" - python_class: "App" + python_class: "ndi_app" inherits: "ndi.documentservice" properties: - name: session type_matlab: "ndi.session" - type_python: "Session | None" + type_python: "ndi_session | None" access: "protected set, public get" decision_log: "Exact match." @@ -40,7 +40,7 @@ classes: input_arguments: - name: session type_matlab: "ndi.session (optional, via varargin)" - type_python: "Session | None" + type_python: "ndi_session | None" default: "None" - name: name type_matlab: "char (optional, via varargin)" @@ -48,7 +48,7 @@ classes: default: "'generic'" output_arguments: - name: app_obj - type_python: "App" + type_python: "ndi_app" decision_log: > MATLAB uses varargin for optional session and name. Python uses explicit keyword arguments with defaults. @@ -77,7 +77,7 @@ classes: input_arguments: [] output_arguments: - name: doc - type_python: "Document" + type_python: "ndi_document" decision_log: > Exact match. Creates 'app' document with name, version, url, os, os_version, interpreter, interpreter_version. @@ -87,7 +87,7 @@ classes: input_arguments: [] output_arguments: - name: q - type_python: "Query" + type_python: "ndi_query" decision_log: > Exact match. Returns query for isa('app') & app.name matches. @@ -98,7 +98,7 @@ classes: type: class matlab_path: "src/ndi/+ndi/+app/appdoc.m" python_path: "ndi/app/appdoc.py" - python_class: "AppDoc" + python_class: "ndi_app_appdoc" properties: - name: doc_types @@ -132,7 +132,7 @@ classes: default: "None" output_arguments: - name: appdoc_obj - type_python: "AppDoc" + type_python: "ndi_app_appdoc" decision_log: "Exact match." - name: add_appdoc @@ -151,7 +151,7 @@ classes: output_arguments: - name: doc type_matlab: "cell array of ndi.document" - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: > Exact match. MATLAB accepts struct/char/document/empty for appdoc_struct; Python accepts dict or None. MATLAB returns @@ -168,7 +168,7 @@ classes: type_python: "dict" output_arguments: - name: doc - type_python: "Document | None" + type_python: "ndi_document | None" decision_log: > Exact match. Base class returns empty/None. Subclasses must override. @@ -180,7 +180,7 @@ classes: type_python: "str" - name: doc type_matlab: "ndi.document" - type_python: "Document" + type_python: "ndi_document" output_arguments: - name: appdoc_struct type_python: "dict" @@ -228,7 +228,7 @@ classes: output_arguments: - name: doc type_matlab: "cell array of ndi.document" - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: > Exact match. Base class returns empty/[]. @@ -275,7 +275,7 @@ classes: type: class matlab_path: "src/ndi/+ndi/+app/markgarbage.m" python_path: "ndi/app/markgarbage.py" - python_class: "MarkGarbage" + python_class: "ndi_app_markgarbage" inherits: "ndi.app" methods: @@ -284,11 +284,11 @@ classes: input_arguments: - name: session type_matlab: "ndi.session (optional, via varargin)" - type_python: "Session | None" + type_python: "ndi_session | None" default: "None" output_arguments: - name: obj - type_python: "MarkGarbage" + type_python: "ndi_app_markgarbage" decision_log: "Exact match. Name is 'ndi_app_markgarbage'." - name: markvalidinterval @@ -351,7 +351,7 @@ classes: type_python: "list[dict[str, Any]]" - name: mydoc type_matlab: "cell array of ndi.document" - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: > DISCREPANCY FIXED: MATLAB returns [vi, mydoc] 2-tuple. Python was returning only intervals list. Updated Python @@ -386,7 +386,7 @@ classes: type: class matlab_path: "src/ndi/+ndi/+app/spikeextractor.m" python_path: "ndi/app/spikeextractor.py" - python_class: "SpikeExtractor" + python_class: "ndi_app_spikeextractor" inherits: "ndi.app & ndi.app.appdoc" methods: @@ -395,18 +395,18 @@ classes: input_arguments: - name: session type_matlab: "ndi.session (optional, via varargin)" - type_python: "Session | None" + type_python: "ndi_session | None" default: "None" output_arguments: - name: obj - type_python: "SpikeExtractor" + type_python: "ndi_app_spikeextractor" decision_log: "Exact match." - name: makefilterstruct input_arguments: - name: extraction_doc type_matlab: "ndi.document" - type_python: "Document" + type_python: "ndi_document" - name: sample_rate type_matlab: "double" type_python: "float" @@ -455,7 +455,7 @@ classes: default: "None" output_arguments: [] decision_log: > - MATLAB returns void. Python had return type list[Document] + MATLAB returns void. Python had return type list[ndi_document] which was incorrect. Fixed to return None. - name: struct2doc @@ -468,7 +468,7 @@ classes: type_python: "dict" output_arguments: - name: doc - type_python: "Document" + type_python: "ndi_document" decision_log: "Exact match." - name: isvalid_appdoc_struct @@ -495,7 +495,7 @@ classes: type_python: "str" output_arguments: - name: doc - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: "Exact match." - name: loaddata_appdoc @@ -517,7 +517,7 @@ classes: type: class matlab_path: "src/ndi/+ndi/+app/spikesorter.m" python_path: "ndi/app/spikesorter.py" - python_class: "SpikeSorter" + python_class: "ndi_app_spikesorter" inherits: "ndi.app & ndi.app.appdoc" methods: @@ -526,11 +526,11 @@ classes: input_arguments: - name: session type_matlab: "ndi.session (optional, via varargin)" - type_python: "Session | None" + type_python: "ndi_session | None" default: "None" output_arguments: - name: obj - type_python: "SpikeSorter" + type_python: "ndi_app_spikesorter" decision_log: "Exact match." - name: check_sorting_parameters @@ -564,9 +564,9 @@ classes: - name: epochinfo type_python: "list[dict]" - name: extraction_params_doc - type_python: "Document" + type_python: "ndi_document" - name: waveform_docs - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: > DISCREPANCY FIXED: Method was missing from Python. Added stub. Returns 6-tuple matching MATLAB. @@ -590,7 +590,7 @@ classes: default: "False" output_arguments: - name: spike_cluster_doc - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: "Exact match." - name: clusters2neurons @@ -623,7 +623,7 @@ classes: type_python: "dict" output_arguments: - name: doc - type_python: "Document" + type_python: "ndi_document" decision_log: "Exact match." - name: isvalid_appdoc_struct @@ -647,7 +647,7 @@ classes: type_python: "str" output_arguments: - name: doc - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: "Exact match." - name: loaddata_appdoc @@ -668,7 +668,7 @@ classes: type: class matlab_path: "src/ndi/+ndi/+app/oridirtuning.m" python_path: "ndi/app/oridirtuning.py" - python_class: "OriDirTuning" + python_class: "ndi_app_oridirtuning" inherits: "ndi.app & ndi.app.appdoc" methods: @@ -677,11 +677,11 @@ classes: input_arguments: - name: session type_matlab: "ndi.session (optional, via varargin)" - type_python: "Session | None" + type_python: "ndi_session | None" default: "None" output_arguments: - name: obj - type_python: "OriDirTuning" + type_python: "ndi_app_oridirtuning" decision_log: "Exact match." - name: calculate_all_tuning_curves @@ -695,7 +695,7 @@ classes: default: "'Replace'" output_arguments: - name: tuning_doc - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: "Exact match." - name: calculate_tuning_curve @@ -705,14 +705,14 @@ classes: type_python: "Any" - name: ndi_response_doc type_matlab: "ndi.document" - type_python: "Document" + type_python: "ndi_document" - name: do_add type_matlab: "logical" type_python: "bool" default: "True" output_arguments: - name: tuning_doc - type_python: "Document | None" + type_python: "ndi_document | None" decision_log: > DISCREPANCY FIXED: Method was missing from Python. Added stub. @@ -728,14 +728,14 @@ classes: default: "'Replace'" output_arguments: - name: oriprops - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: "Exact match." - name: calculate_oridir_indexes input_arguments: - name: tuning_doc type_matlab: "ndi.document" - type_python: "Document" + type_python: "ndi_document" - name: do_add type_matlab: "logical" type_python: "bool" @@ -746,7 +746,7 @@ classes: default: "False" output_arguments: - name: oriprops - type_python: "Document | None" + type_python: "ndi_document | None" decision_log: > DISCREPANCY FIXED: Method was missing from Python. Added stub. @@ -755,7 +755,7 @@ classes: input_arguments: - name: response_doc type_matlab: "ndi.document" - type_python: "Document" + type_python: "ndi_document" output_arguments: - name: b type_python: "bool" @@ -765,7 +765,7 @@ classes: input_arguments: - name: oriprops_doc type_matlab: "ndi.document" - type_python: "Document" + type_python: "ndi_document" output_arguments: [] decision_log: > DISCREPANCY FIXED: Method was missing from Python. @@ -780,7 +780,7 @@ classes: type_python: "dict" output_arguments: - name: doc - type_python: "Document" + type_python: "ndi_document" decision_log: "Exact match." - name: find_appdoc @@ -789,7 +789,7 @@ classes: type_python: "str" output_arguments: - name: doc - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: "Exact match." - name: isvalid_appdoc_struct diff --git a/src/ndi/app/oridirtuning.py b/src/ndi/app/oridirtuning.py index 7a24dfc..677a9d9 100644 --- a/src/ndi/app/oridirtuning.py +++ b/src/ndi/app/oridirtuning.py @@ -11,17 +11,17 @@ from typing import TYPE_CHECKING, Any -from . import App -from .appdoc import AppDoc +from . import ndi_app +from .appdoc import ndi_app_appdoc if TYPE_CHECKING: - from ..document import Document - from ..session.session_base import Session + from ..document import ndi_document + from ..session.session_base import ndi_session -class OriDirTuning(App, AppDoc): +class ndi_app_oridirtuning(ndi_app, ndi_app_appdoc): """ - App for orientation/direction tuning analysis. + ndi_app for orientation/direction tuning analysis. Computes orientation and direction selectivity measures from stimulus tuning curves, including: @@ -35,14 +35,14 @@ class OriDirTuning(App, AppDoc): - tuning_curve: Stimulus tuning curves Example: - >>> odt = OriDirTuning(session) + >>> odt = ndi_app_oridirtuning(session) >>> odt.calculate_all_tuning_curves(element_obj) >>> odt.calculate_all_oridir_indexes(element_obj) """ - def __init__(self, session: Session | None = None): - App.__init__(self, session=session, name="ndi_app_oridirtuning") - AppDoc.__init__( + def __init__(self, session: ndi_session | None = None): + ndi_app.__init__(self, session=session, name="ndi_app_oridirtuning") + ndi_app_appdoc.__init__( self, doc_types=["orientation_direction_tuning", "tuning_curve"], doc_document_types=[ @@ -55,7 +55,7 @@ def calculate_all_tuning_curves( self, ndi_element_obj: Any, docexistsaction: str = "Replace", - ) -> list[Document]: + ) -> list[ndi_document]: """ Calculate tuning curves for all stimulus responses. @@ -73,9 +73,9 @@ def calculate_all_tuning_curves( def calculate_tuning_curve( self, ndi_element_obj: Any, - ndi_response_doc: Document, + ndi_response_doc: ndi_document, do_add: bool = True, - ) -> Document | None: + ) -> ndi_document | None: """ Calculate a single tuning curve from a response document. @@ -97,7 +97,7 @@ def calculate_all_oridir_indexes( self, ndi_element_obj: Any, docexistsaction: str = "Replace", - ) -> list[Document]: + ) -> list[ndi_document]: """ Calculate orientation/direction indices for all responses. @@ -114,10 +114,10 @@ def calculate_all_oridir_indexes( def calculate_oridir_indexes( self, - tuning_doc: Document, + tuning_doc: ndi_document, do_add: bool = True, do_plot: bool = False, - ) -> Document | None: + ) -> ndi_document | None: """ Calculate orientation/direction indices from a tuning curve. @@ -134,7 +134,7 @@ def calculate_oridir_indexes( raise NotImplementedError("Index calculation requires circular statistics.") @staticmethod - def is_oridir_stimulus_response(response_doc: Document) -> bool: + def is_oridir_stimulus_response(response_doc: ndi_document) -> bool: """ Check if a response document contains orientation/direction data. @@ -153,7 +153,7 @@ def is_oridir_stimulus_response(response_doc: Document) -> bool: except AttributeError: return False - def plot_oridir_response(self, oriprops_doc: Document) -> None: + def plot_oridir_response(self, oriprops_doc: ndi_document) -> None: """ Plot orientation/direction response. @@ -168,20 +168,20 @@ def plot_oridir_response(self, oriprops_doc: Document) -> None: """ raise NotImplementedError("Plotting requires matplotlib. Use matplotlib directly.") - def struct2doc(self, appdoc_type: str, appdoc_struct: dict, **kwargs) -> Document: - from ..document import Document + def struct2doc(self, appdoc_type: str, appdoc_struct: dict, **kwargs) -> ndi_document: + from ..document import ndi_document - return Document( + return ndi_document( self.doc_document_types[self.doc_types.index(appdoc_type)], **{appdoc_type: appdoc_struct}, ) - def find_appdoc(self, appdoc_type: str, **kwargs) -> list[Document]: + def find_appdoc(self, appdoc_type: str, **kwargs) -> list[ndi_document]: if self._session is None: return [] - from ..query import Query + from ..query import ndi_query - return self._session.database_search(Query("").isa(appdoc_type)) + return self._session.database_search(ndi_query("").isa(appdoc_type)) def isvalid_appdoc_struct(self, appdoc_type: str, appdoc_struct: dict) -> tuple[bool, str]: """ @@ -195,4 +195,4 @@ def isvalid_appdoc_struct(self, appdoc_type: str, appdoc_struct: dict) -> tuple[ return True, "" def __repr__(self) -> str: - return f"OriDirTuning(session={self._session is not None})" + return f"ndi_app_oridirtuning(session={self._session is not None})" diff --git a/src/ndi/app/spikeextractor.py b/src/ndi/app/spikeextractor.py index 473ab4f..00f8d0e 100644 --- a/src/ndi/app/spikeextractor.py +++ b/src/ndi/app/spikeextractor.py @@ -1,7 +1,7 @@ """ ndi.app.spikeextractor - Spike extraction from timeseries data. -Provides the SpikeExtractor app for detecting and extracting spike +Provides the ndi_app_spikeextractor app for detecting and extracting spike waveforms from continuous electrophysiology recordings. MATLAB equivalent: src/ndi/+ndi/+app/spikeextractor.m @@ -13,17 +13,17 @@ import numpy as np -from . import App -from .appdoc import AppDoc +from . import ndi_app +from .appdoc import ndi_app_appdoc if TYPE_CHECKING: - from ..document import Document - from ..session.session_base import Session + from ..document import ndi_document + from ..session.session_base import ndi_session -class SpikeExtractor(App, AppDoc): +class ndi_app_spikeextractor(ndi_app, ndi_app_appdoc): """ - App for extracting spike waveforms from timeseries data. + ndi_app for extracting spike waveforms from timeseries data. Detects threshold crossings in filtered neural data and extracts spike waveforms around each detected event. @@ -34,13 +34,13 @@ class SpikeExtractor(App, AppDoc): - spikewaves: Extracted waveform binary data Example: - >>> extractor = SpikeExtractor(session) + >>> extractor = ndi_app_spikeextractor(session) >>> extractor.extract(timeseries_obj, epoch=1) """ - def __init__(self, session: Session | None = None): - App.__init__(self, session=session, name="ndi_app_spikeextractor") - AppDoc.__init__( + def __init__(self, session: ndi_session | None = None): + ndi_app.__init__(self, session=session, name="ndi_app_spikeextractor") + ndi_app_appdoc.__init__( self, doc_types=["extraction_parameters", "extraction_parameters_modification", "spikewaves"], doc_document_types=[ @@ -52,7 +52,7 @@ def __init__(self, session: Session | None = None): def makefilterstruct( self, - extraction_doc: Document, + extraction_doc: ndi_document, sample_rate: float, ) -> dict[str, Any]: """ @@ -103,7 +103,7 @@ def extract( Args: ndi_timeseries_obj: Timeseries element or probe - epoch: Epoch number/id or None for all epochs + epoch: ndi_epoch_epoch number/id or None for all epochs extraction_name: Name of extraction parameters to use redo: If True, re-extract even if results exist t0_t1: Optional time bounds [t0, t1] @@ -140,20 +140,20 @@ def default_extraction_parameters() -> dict[str, Any]: }, } - def struct2doc(self, appdoc_type: str, appdoc_struct: dict, **kwargs) -> Document: - from ..document import Document + def struct2doc(self, appdoc_type: str, appdoc_struct: dict, **kwargs) -> ndi_document: + from ..document import ndi_document - return Document( + return ndi_document( self.doc_document_types[self.doc_types.index(appdoc_type)], **{appdoc_type: appdoc_struct}, ) - def find_appdoc(self, appdoc_type: str, **kwargs) -> list[Document]: + def find_appdoc(self, appdoc_type: str, **kwargs) -> list[ndi_document]: if self._session is None: return [] - from ..query import Query + from ..query import ndi_query - q = Query("").isa(appdoc_type) + q = ndi_query("").isa(appdoc_type) return self._session.database_search(q) def isvalid_appdoc_struct(self, appdoc_type: str, appdoc_struct: dict) -> tuple[bool, str]: @@ -180,4 +180,4 @@ def loaddata_appdoc(self, appdoc_type: str, *args, **kwargs) -> Any: return None def __repr__(self) -> str: - return f"SpikeExtractor(session={self._session is not None})" + return f"ndi_app_spikeextractor(session={self._session is not None})" diff --git a/src/ndi/app/spikesorter.py b/src/ndi/app/spikesorter.py index 9d0f9b6..ee1d32b 100644 --- a/src/ndi/app/spikesorter.py +++ b/src/ndi/app/spikesorter.py @@ -1,7 +1,7 @@ """ ndi.app.spikesorter - Spike sorting and clustering. -Provides the SpikeSorter app for clustering extracted spike waveforms +Provides the ndi_app_spikesorter app for clustering extracted spike waveforms into putative single-neuron units. MATLAB equivalent: src/ndi/+ndi/+app/spikesorter.m @@ -13,19 +13,19 @@ import numpy as np -from . import App -from .appdoc import AppDoc +from . import ndi_app +from .appdoc import ndi_app_appdoc if TYPE_CHECKING: - from ..document import Document - from ..session.session_base import Session + from ..document import ndi_document + from ..session.session_base import ndi_session -class SpikeSorter(App, AppDoc): +class ndi_app_spikesorter(ndi_app, ndi_app_appdoc): """ - App for clustering spikes into neuron units. + ndi_app for clustering spikes into neuron units. - Takes extracted spike waveforms (from SpikeExtractor) and + Takes extracted spike waveforms (from ndi_app_spikeextractor) and clusters them using PCA and K-means or similar algorithms. Doc types: @@ -33,13 +33,13 @@ class SpikeSorter(App, AppDoc): - spike_clusters: Cluster assignments and statistics Example: - >>> sorter = SpikeSorter(session) + >>> sorter = ndi_app_spikesorter(session) >>> sorter.spike_sort(timeseries_obj, 'default', 'default') """ - def __init__(self, session: Session | None = None): - App.__init__(self, session=session, name="ndi_app_spikesorter") - AppDoc.__init__( + def __init__(self, session: ndi_session | None = None): + ndi_app.__init__(self, session=session, name="ndi_app_spikesorter") + ndi_app_appdoc.__init__( self, doc_types=["sorting_parameters", "spike_clusters"], doc_document_types=[ @@ -107,7 +107,7 @@ def spike_sort( extraction_name: str = "default", sorting_parameters_name: str = "default", redo: bool = False, - ) -> list[Document]: + ) -> list[ndi_document]: """ Perform spike sorting on extracted spikes. @@ -135,7 +135,7 @@ def clusters2neurons( redo: bool = False, ) -> None: """ - Create Neuron elements from cluster assignments. + Create ndi_neuron elements from cluster assignments. MATLAB equivalent: ndi.app.spikesorter/clusters2neurons @@ -149,20 +149,20 @@ def clusters2neurons( "Full neuron creation from clusters requires additional infrastructure." ) - def struct2doc(self, appdoc_type: str, appdoc_struct: dict, **kwargs) -> Document: - from ..document import Document + def struct2doc(self, appdoc_type: str, appdoc_struct: dict, **kwargs) -> ndi_document: + from ..document import ndi_document - return Document( + return ndi_document( self.doc_document_types[self.doc_types.index(appdoc_type)], **{appdoc_type: appdoc_struct}, ) - def find_appdoc(self, appdoc_type: str, **kwargs) -> list[Document]: + def find_appdoc(self, appdoc_type: str, **kwargs) -> list[ndi_document]: if self._session is None: return [] - from ..query import Query + from ..query import ndi_query - return self._session.database_search(Query("").isa(appdoc_type)) + return self._session.database_search(ndi_query("").isa(appdoc_type)) def isvalid_appdoc_struct(self, appdoc_type: str, appdoc_struct: dict) -> tuple[bool, str]: """ @@ -188,4 +188,4 @@ def loaddata_appdoc(self, appdoc_type: str, *args, **kwargs) -> Any: return None def __repr__(self) -> str: - return f"SpikeSorter(session={self._session is not None})" + return f"ndi_app_spikesorter(session={self._session is not None})" diff --git a/src/ndi/app/stimulus/__init__.py b/src/ndi/app/stimulus/__init__.py index dc45ecf..98be986 100644 --- a/src/ndi/app/stimulus/__init__.py +++ b/src/ndi/app/stimulus/__init__.py @@ -5,7 +5,7 @@ stimulus-response tuning curves. """ -from .decoder import StimulusDecoder -from .tuning_response import TuningResponse +from .decoder import ndi_app_stimulus_decoder +from .tuning_response import ndi_app_stimulus_tuning__response -__all__ = ["StimulusDecoder", "TuningResponse"] +__all__ = ["ndi_app_stimulus_decoder", "ndi_app_stimulus_tuning__response"] diff --git a/src/ndi/app/stimulus/decoder.py b/src/ndi/app/stimulus/decoder.py index bf5180d..26ba32a 100644 --- a/src/ndi/app/stimulus/decoder.py +++ b/src/ndi/app/stimulus/decoder.py @@ -11,34 +11,34 @@ from typing import TYPE_CHECKING, Any -from .. import App +from .. import ndi_app if TYPE_CHECKING: - from ...document import Document - from ...session.session_base import Session + from ...document import ndi_document + from ...session.session_base import ndi_session -class StimulusDecoder(App): +class ndi_app_stimulus_decoder(ndi_app): """ - App for decoding stimulus presentations. + ndi_app for decoding stimulus presentations. Reads raw stimulus data from stimulus probes/elements and converts it into structured stimulus_presentation documents with timing and parameter information. Example: - >>> decoder = StimulusDecoder(session) + >>> decoder = ndi_app_stimulus_decoder(session) >>> newdocs, existingdocs = decoder.parse_stimuli(stim_element) """ - def __init__(self, session: Session | None = None): + def __init__(self, session: ndi_session | None = None): super().__init__(session=session, name="ndi_app_stimulus_decoder") def parse_stimuli( self, ndi_element_stim: Any, reset: bool = False, - ) -> tuple[list[Document], list[Document]]: + ) -> tuple[list[ndi_document], list[ndi_document]]: """ Parse stimulus presentations from a stimulus element. @@ -63,7 +63,7 @@ def parse_stimuli( def load_presentation_time( self, - stimulus_presentation_doc: Document, + stimulus_presentation_doc: ndi_document, ) -> dict[str, Any] | None: """ Load presentation timing from a stimulus_presentation document. @@ -85,14 +85,14 @@ def _clear_presentations(self, ndi_element_stim: Any) -> None: """Clear existing stimulus presentation documents.""" if self._session is None: return - from ...query import Query + from ...query import ndi_query - q = Query("").isa("stimulus_presentation") + q = ndi_query("").isa("stimulus_presentation") if hasattr(ndi_element_stim, "id"): - q = q & Query("").depends_on("stimulus_element_id", ndi_element_stim.id) + q = q & ndi_query("").depends_on("stimulus_element_id", ndi_element_stim.id) docs = self._session.database_search(q) for doc in docs: self._session.database_remove(doc) def __repr__(self) -> str: - return f"StimulusDecoder(session={self._session is not None})" + return f"ndi_app_stimulus_decoder(session={self._session is not None})" diff --git a/src/ndi/app/stimulus/ndi_matlab_python_bridge.yaml b/src/ndi/app/stimulus/ndi_matlab_python_bridge.yaml index c08ce6d..c31ab4b 100644 --- a/src/ndi/app/stimulus/ndi_matlab_python_bridge.yaml +++ b/src/ndi/app/stimulus/ndi_matlab_python_bridge.yaml @@ -18,7 +18,7 @@ classes: type: class matlab_path: "src/ndi/+ndi/+app/+stimulus/decoder.m" python_path: "ndi/app/stimulus/decoder.py" - python_class: "StimulusDecoder" + python_class: "ndi_app_stimulus_decoder" inherits: "ndi.app" methods: @@ -27,11 +27,11 @@ classes: input_arguments: - name: session type_matlab: "ndi.session (optional, via varargin)" - type_python: "Session | None" + type_python: "ndi_session | None" default: "None" output_arguments: - name: obj - type_python: "StimulusDecoder" + type_python: "ndi_app_stimulus_decoder" decision_log: "Exact match." - name: parse_stimuli @@ -46,10 +46,10 @@ classes: output_arguments: - name: newdocs type_matlab: "cell array of ndi.document" - type_python: "list[Document]" + type_python: "list[ndi_document]" - name: existingdocs type_matlab: "cell array of ndi.document" - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: > DISCREPANCY FIXED: MATLAB returns [newdocs, existingdocs] 2-tuple. Python was returning single list. Updated Python @@ -59,7 +59,7 @@ classes: input_arguments: - name: stimulus_presentation_doc type_matlab: "ndi.document" - type_python: "Document" + type_python: "ndi_document" output_arguments: - name: presentation_time type_matlab: "struct" @@ -73,7 +73,7 @@ classes: type: class matlab_path: "src/ndi/+ndi/+app/+stimulus/tuning_response.m" python_path: "ndi/app/stimulus/tuning_response.py" - python_class: "TuningResponse" + python_class: "ndi_app_stimulus_tuning__response" inherits: "ndi.app" methods: @@ -82,11 +82,11 @@ classes: input_arguments: - name: session type_matlab: "ndi.session (optional, via varargin)" - type_python: "Session | None" + type_python: "ndi_session | None" default: "None" output_arguments: - name: obj - type_python: "TuningResponse" + type_python: "ndi_app_stimulus_tuning__response" decision_log: "Exact match." - name: stimulus_responses @@ -107,7 +107,7 @@ classes: default: "False" output_arguments: - name: rdocs - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: "Exact match." - name: compute_stimulus_response_scalar @@ -120,13 +120,13 @@ classes: type_python: "Any" - name: stim_doc type_matlab: "ndi.document" - type_python: "Document" + type_python: "ndi_document" - name: control_doc type_matlab: "ndi.document" - type_python: "Document | None" + type_python: "ndi_document | None" output_arguments: - name: response_doc - type_python: "Document | None" + type_python: "ndi_document | None" decision_log: > DISCREPANCY FIXED: Method was missing from Python. Added stub. @@ -135,7 +135,7 @@ classes: input_arguments: - name: stim_response_doc type_matlab: "ndi.document" - type_python: "Document" + type_python: "ndi_document" - name: independent_label type_matlab: "char (Name-Value via varargin)" type_python: "str" @@ -146,7 +146,7 @@ classes: default: "'angle'" output_arguments: - name: tuning_doc - type_python: "Document | None" + type_python: "ndi_document | None" decision_log: "Exact match." - name: label_control_stimuli @@ -160,19 +160,19 @@ classes: default: "False" output_arguments: - name: cs_doc - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: "Exact match." - name: control_stimulus input_arguments: - name: stim_doc type_matlab: "ndi.document" - type_python: "Document" + type_python: "ndi_document" output_arguments: - name: cs_ids type_python: "list[int]" - name: cs_doc - type_python: "Document | None" + type_python: "ndi_document | None" decision_log: > DISCREPANCY FIXED: Method was missing from Python. Added stub. Returns 2-tuple. @@ -191,9 +191,9 @@ classes: default: "'mean'" output_arguments: - name: tc_doc - type_python: "list[Document]" + type_python: "list[ndi_document]" - name: srs_doc - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: > DISCREPANCY FIXED: MATLAB returns [tc_doc, srs_doc] 2-tuple. Python was returning single list. Updated to return tuple. @@ -202,7 +202,7 @@ classes: input_arguments: - name: stim_response_doc type_matlab: "ndi.document" - type_python: "Document" + type_python: "ndi_document" - name: param_to_vary type_matlab: "char" type_python: "str" @@ -214,7 +214,7 @@ classes: type_python: "list[str]" output_arguments: - name: tuning_docs - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: > DISCREPANCY FIXED: Method was missing from Python. Added stub. diff --git a/src/ndi/app/stimulus/tuning_response.py b/src/ndi/app/stimulus/tuning_response.py index 3f6ed28..696ad31 100644 --- a/src/ndi/app/stimulus/tuning_response.py +++ b/src/ndi/app/stimulus/tuning_response.py @@ -11,28 +11,28 @@ from typing import TYPE_CHECKING, Any -from .. import App +from .. import ndi_app if TYPE_CHECKING: - from ...document import Document - from ...session.session_base import Session + from ...document import ndi_document + from ...session.session_base import ndi_session -class TuningResponse(App): +class ndi_app_stimulus_tuning__response(ndi_app): """ - App for computing stimulus-response relationships. + ndi_app for computing stimulus-response relationships. Computes scalar response measures (mean firing rate, F1 component, etc.) of neural elements to each stimulus in a set, then organizes these into tuning curves. Example: - >>> tr = TuningResponse(session) + >>> tr = ndi_app_stimulus_tuning__response(session) >>> docs = tr.stimulus_responses(stim_element, timeseries_obj) >>> tuning = tr.tuning_curve(response_doc) """ - def __init__(self, session: Session | None = None): + def __init__(self, session: ndi_session | None = None): super().__init__(session=session, name="ndi_app_tuning_response") def stimulus_responses( @@ -41,7 +41,7 @@ def stimulus_responses( ndi_timeseries_obj: Any, reset: bool = False, do_mean_only: bool = False, - ) -> list[Document]: + ) -> list[ndi_document]: """ Compute responses to a stimulus set. @@ -65,9 +65,9 @@ def compute_stimulus_response_scalar( self, ndi_stim_obj: Any, ndi_timeseries_obj: Any, - stim_doc: Document, - control_doc: Document | None = None, - ) -> Document | None: + stim_doc: ndi_document, + control_doc: ndi_document | None = None, + ) -> ndi_document | None: """ Compute scalar response for a single stimulus presentation. @@ -88,10 +88,10 @@ def compute_stimulus_response_scalar( def tuning_curve( self, - stim_response_doc: Document, + stim_response_doc: ndi_document, independent_label: str = "angle", independent_parameter: str = "angle", - ) -> Document | None: + ) -> ndi_document | None: """ Create a tuning curve from stimulus responses. @@ -111,7 +111,7 @@ def label_control_stimuli( self, stimulus_element_obj: Any, reset: bool = False, - ) -> list[Document]: + ) -> list[ndi_document]: """ Label control stimuli in a stimulus set. @@ -128,8 +128,8 @@ def label_control_stimuli( def control_stimulus( self, - stim_doc: Document, - ) -> tuple[list[int], Document | None]: + stim_doc: ndi_document, + ) -> tuple[list[int], ndi_document | None]: """ Determine control stimulus IDs for a stimulus presentation. @@ -150,7 +150,7 @@ def find_tuningcurve_document( ndi_element_obj: Any, epochid: str, response_type: str = "mean", - ) -> tuple[list[Document], list[Document]]: + ) -> tuple[list[ndi_document], list[ndi_document]]: """ Find existing tuning curve documents. @@ -158,7 +158,7 @@ def find_tuningcurve_document( Args: ndi_element_obj: Neural element - epochid: Epoch ID + epochid: ndi_epoch_epoch ID response_type: Response type (mean, f1, etc.) Returns: @@ -168,14 +168,14 @@ def find_tuningcurve_document( if self._session is None: return [], [] - from ...query import Query + from ...query import ndi_query - q = Query("").isa("stimulus_tuningcurve") & Query("").depends_on( + q = ndi_query("").isa("stimulus_tuningcurve") & ndi_query("").depends_on( "element_id", ndi_element_obj.id ) tc_docs = self._session.database_search(q) - q_srs = Query("").isa("stimulus_response_scalar") & Query("").depends_on( + q_srs = ndi_query("").isa("stimulus_response_scalar") & ndi_query("").depends_on( "element_id", ndi_element_obj.id ) srs_docs = self._session.database_search(q_srs) @@ -184,11 +184,11 @@ def find_tuningcurve_document( def make_1d_tuning( self, - stim_response_doc: Document, + stim_response_doc: ndi_document, param_to_vary: str, param_to_vary_label: str, param_to_fix: list[str], - ) -> list[Document]: + ) -> list[ndi_document]: """ Create 1D tuning curves from a multi-dimensional parameter space. @@ -208,4 +208,4 @@ def make_1d_tuning( ) def __repr__(self) -> str: - return f"TuningResponse(session={self._session is not None})" + return f"ndi_app_stimulus_tuning__response(session={self._session is not None})" diff --git a/src/ndi/cache.py b/src/ndi/cache.py index 9e66b1d..1199ec8 100644 --- a/src/ndi/cache.py +++ b/src/ndi/cache.py @@ -1,7 +1,7 @@ """ -ndi.cache - Cache class for NDI. +ndi.cache - ndi_cache class for NDI. -This module provides the Cache class for session-level caching +This module provides the ndi_cache class for session-level caching of computed results with memory limits and eviction policies. """ @@ -25,9 +25,9 @@ class CacheEntry: data: Any -class Cache: +class ndi_cache: """ - Cache class for NDI session caching. + ndi_cache class for NDI session caching. Provides key-type based caching with memory limits and configurable eviction policies (FIFO, LIFO, or error). @@ -37,7 +37,7 @@ class Cache: replacement_rule: Eviction policy ('fifo', 'lifo', 'error') Example: - >>> cache = Cache(max_memory=1e9) + >>> cache = ndi_cache(max_memory=1e9) >>> cache.add('element_123', 'epochtable', my_data) >>> entry = cache.lookup('element_123', 'epochtable') >>> if entry: @@ -52,7 +52,7 @@ def __init__( replacement_rule: str = "fifo", ): """ - Create a new Cache. + Create a new ndi_cache. Args: max_memory: Maximum memory in bytes (default 10GB) @@ -72,7 +72,7 @@ def replacement_rule(self) -> str: """Get current replacement rule.""" return self._replacement_rule - def set_replacement_rule(self, rule: str) -> Cache: + def set_replacement_rule(self, rule: str) -> ndi_cache: """ Set the replacement rule for cache eviction. @@ -99,7 +99,7 @@ def add( type: str, data: Any, priority: float = 0, - ) -> Cache: + ) -> ndi_cache: """ Add data to the cache. @@ -121,7 +121,8 @@ def add( if data_bytes > self._max_memory: raise MemoryError( - f"Data ({data_bytes} bytes) exceeds cache max_memory " f"({self._max_memory} bytes)" + f"ndi_gui_Data ({data_bytes} bytes) exceeds cache max_memory " + f"({self._max_memory} bytes)" ) # Create new entry @@ -138,7 +139,7 @@ def add( total_memory = self.bytes() + data_bytes if total_memory > self._max_memory: if self._replacement_rule == "error": - raise MemoryError("Cache is full and replacement_rule is 'error'") + raise MemoryError("ndi_cache is full and replacement_rule is 'error'") free_needed = total_memory - self._max_memory indices_to_remove, safe_to_add = self._evaluate_items_for_removal( @@ -157,7 +158,7 @@ def remove( self, key_or_index: Any, type: str | None = None, - ) -> Cache: + ) -> ndi_cache: """ Remove data from the cache. @@ -195,7 +196,7 @@ def _remove_indices(self, indices: list[int]) -> None: if 0 <= i < len(self._table): del self._table[i] - def clear(self) -> Cache: + def clear(self) -> ndi_cache: """ Clear all entries from the cache. @@ -318,7 +319,7 @@ def __len__(self) -> int: def __repr__(self) -> str: """String representation.""" return ( - f"Cache(entries={len(self._table)}, " + f"ndi_cache(entries={len(self._table)}, " f"bytes={self.bytes()}, " f"max_memory={self._max_memory})" ) diff --git a/src/ndi/calc/__init__.py b/src/ndi/calc/__init__.py index 3bd872b..bdb70e0 100644 --- a/src/ndi/calc/__init__.py +++ b/src/ndi/calc/__init__.py @@ -4,10 +4,10 @@ Contains concrete calculator classes organized by domain. Submodules: - example: Example/demo calculators (SimpleCalc) + example: Example/demo calculators (ndi_calc_example_simple) """ from . import example, stimulus -from .tuning_fit import TuningFit +from .tuning_fit import ndi_calc_tuning__fit -__all__ = ["example", "stimulus", "TuningFit"] +__all__ = ["example", "stimulus", "ndi_calc_tuning__fit"] diff --git a/src/ndi/calc/example/__init__.py b/src/ndi/calc/example/__init__.py index a647471..60bb70e 100644 --- a/src/ndi/calc/example/__init__.py +++ b/src/ndi/calc/example/__init__.py @@ -1,9 +1,9 @@ """ ndi.calc.example - Example calculator implementations. -Contains demo calculators that show how to build on ndi.Calculator. +Contains demo calculators that show how to build on ndi.ndi_calculator. """ -from .simple import SimpleCalc +from .simple import ndi_calc_example_simple -__all__ = ["SimpleCalc"] +__all__ = ["ndi_calc_example_simple"] diff --git a/src/ndi/calc/example/ndi_matlab_python_bridge.yaml b/src/ndi/calc/example/ndi_matlab_python_bridge.yaml index f283c6d..c328298 100644 --- a/src/ndi/calc/example/ndi_matlab_python_bridge.yaml +++ b/src/ndi/calc/example/ndi_matlab_python_bridge.yaml @@ -18,7 +18,7 @@ classes: type: class matlab_path: "src/ndi/+ndi/+calc/+example/simple.m" python_path: "ndi/calc/example/simple.py" - python_class: "SimpleCalc" + python_class: "ndi_calc_example_simple" inherits: "ndi.calculator" methods: @@ -27,11 +27,11 @@ classes: input_arguments: - name: session type_matlab: "ndi.session" - type_python: "Session | None" + type_python: "ndi_session | None" default: "None" output_arguments: - name: obj - type_python: "SimpleCalc" + type_python: "ndi_calc_example_simple" decision_log: "Exact match." - name: calculate @@ -41,7 +41,7 @@ classes: type_python: "dict" output_arguments: - name: doc - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: "Exact match." - name: default_search_for_input_parameters diff --git a/src/ndi/calc/example/simple.py b/src/ndi/calc/example/simple.py index 3791e4c..56aa4f5 100644 --- a/src/ndi/calc/example/simple.py +++ b/src/ndi/calc/example/simple.py @@ -1,7 +1,7 @@ """ ndi.calc.example.simple - Simple demonstration calculator. -A minimal calculator that demonstrates the ndi.Calculator pattern. +A minimal calculator that demonstrates the ndi.ndi_calculator pattern. It takes an 'answer' input parameter and stores it in a simple_calc document. @@ -12,14 +12,14 @@ from typing import TYPE_CHECKING -from ...calculator import Calculator +from ...calculator import ndi_calculator if TYPE_CHECKING: - from ...document import Document - from ...session.session_base import Session + from ...document import ndi_document + from ...session.session_base import ndi_session -class SimpleCalc(Calculator): +class ndi_calc_example_simple(ndi_calculator): """ Simple demonstration calculator. @@ -28,18 +28,18 @@ class SimpleCalc(Calculator): and as a template for new calculators. Example: - >>> calc = SimpleCalc(session) + >>> calc = ndi_calc_example_simple(session) >>> docs = calc.run(DocExistsAction.REPLACE) """ - def __init__(self, session: Session | None = None): + def __init__(self, session: ndi_session | None = None): super().__init__( session=session, document_type="simple_calc", path_to_doc_type="apps/calculators/simple_calc", ) - def calculate(self, parameters: dict) -> list[Document]: + def calculate(self, parameters: dict) -> list[ndi_document]: """ Perform the simple calculation. @@ -50,9 +50,9 @@ def calculate(self, parameters: dict) -> list[Document]: parameters: Dict with 'input_parameters' and 'depends_on' Returns: - List containing one simple_calc Document + List containing one simple_calc ndi_document """ - from ...document import Document + from ...document import ndi_document input_params = parameters.get("input_parameters", {}) @@ -63,7 +63,7 @@ def calculate(self, parameters: dict) -> list[Document]: } # Create document using full schema path - doc = Document( + doc = ndi_document( "apps/calculators/simple_calc", **{"simple_calc": simple_calc_data}, ) @@ -93,7 +93,7 @@ def default_search_for_input_parameters(self) -> dict: Returns: Parameter specification dict """ - from ...query import Query + from ...query import ndi_query return { "input_parameters": {"answer": 5}, @@ -101,10 +101,10 @@ def default_search_for_input_parameters(self) -> dict: "query": [ { "name": "document_id", - "query": Query("").isa("base"), + "query": ndi_query("").isa("base"), }, ], } def __repr__(self) -> str: - return f"SimpleCalc(session={self._session is not None})" + return f"ndi_calc_example_simple(session={self._session is not None})" diff --git a/src/ndi/calc/ndi_matlab_python_bridge.yaml b/src/ndi/calc/ndi_matlab_python_bridge.yaml index 420e3ba..dbca74a 100644 --- a/src/ndi/calc/ndi_matlab_python_bridge.yaml +++ b/src/ndi/calc/ndi_matlab_python_bridge.yaml @@ -18,7 +18,7 @@ classes: type: class matlab_path: "src/ndi/+ndi/+calc/tuning_fit.m" python_path: "ndi/calc/tuning_fit.py" - python_class: "TuningFit" + python_class: "ndi_calc_tuning__fit" inherits: "ndi.calculator" methods: @@ -27,7 +27,7 @@ classes: input_arguments: - name: session type_matlab: "ndi.session" - type_python: "Session | None" + type_python: "ndi_session | None" default: "None" - name: name type_matlab: "char" @@ -39,7 +39,7 @@ classes: default: "''" output_arguments: - name: obj - type_python: "TuningFit" + type_python: "ndi_calc_tuning__fit" decision_log: > MATLAB constructor takes (session, name, doc_type). Python takes (session, document_type, path_to_doc_type). diff --git a/src/ndi/calc/stimulus/__init__.py b/src/ndi/calc/stimulus/__init__.py index cc2fcfd..e984e5c 100644 --- a/src/ndi/calc/stimulus/__init__.py +++ b/src/ndi/calc/stimulus/__init__.py @@ -4,6 +4,6 @@ Contains calculators for stimulus tuning analysis. """ -from .tuningcurve import TuningCurveCalc +from .tuningcurve import ndi_calc_stimulus_tuningcurve -__all__ = ["TuningCurveCalc"] +__all__ = ["ndi_calc_stimulus_tuningcurve"] diff --git a/src/ndi/calc/stimulus/ndi_matlab_python_bridge.yaml b/src/ndi/calc/stimulus/ndi_matlab_python_bridge.yaml index b57f525..a45aa8c 100644 --- a/src/ndi/calc/stimulus/ndi_matlab_python_bridge.yaml +++ b/src/ndi/calc/stimulus/ndi_matlab_python_bridge.yaml @@ -18,7 +18,7 @@ classes: type: class matlab_path: "src/ndi/+ndi/+calc/+stimulus/tuningcurve.m" python_path: "ndi/calc/stimulus/tuningcurve.py" - python_class: "TuningCurveCalc" + python_class: "ndi_calc_stimulus_tuningcurve" inherits: "ndi.calculator" methods: @@ -27,11 +27,11 @@ classes: input_arguments: - name: session type_matlab: "ndi.session" - type_python: "Session | None" + type_python: "ndi_session | None" default: "None" output_arguments: - name: obj - type_python: "TuningCurveCalc" + type_python: "ndi_calc_stimulus_tuningcurve" decision_log: "Exact match." - name: calculate @@ -41,7 +41,7 @@ classes: type_python: "dict" output_arguments: - name: doc - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: "Exact match." - name: default_search_for_input_parameters @@ -74,7 +74,7 @@ classes: type_python: "bool" decision_log: > DISCREPANCY NOTED: Method exists in MATLAB but is not - overridden in Python TuningCurveCalc. Base class default + overridden in Python ndi_calc_stimulus_tuningcurve. Base class default (always True) is used, which is acceptable for now. - name: generate_mock_docs @@ -111,7 +111,7 @@ classes: type_python: "str" - name: stim_response_doc type_matlab: "ndi.document" - type_python: "Document" + type_python: "ndi_document" - name: property type_matlab: "char" type_python: "str" @@ -130,7 +130,7 @@ classes: input_arguments: - name: stim_response_doc type_matlab: "ndi.document" - type_python: "Document" + type_python: "ndi_document" - name: property type_matlab: "char" type_python: "str" @@ -148,7 +148,7 @@ classes: input_arguments: - name: stim_response_doc type_matlab: "ndi.document" - type_python: "Document" + type_python: "ndi_document" - name: property type_matlab: "char" type_python: "str" diff --git a/src/ndi/calc/stimulus/tuningcurve.py b/src/ndi/calc/stimulus/tuningcurve.py index 16c04ac..bf18d5e 100644 --- a/src/ndi/calc/stimulus/tuningcurve.py +++ b/src/ndi/calc/stimulus/tuningcurve.py @@ -2,7 +2,7 @@ ndi.calc.stimulus.tuningcurve - Tuning curve calculator. Computes tuning curves from stimulus_response_scalar documents -using the Calculator pipeline. +using the ndi_calculator pipeline. MATLAB equivalent: src/ndi/+ndi/+calc/+stimulus/tuningcurve.m """ @@ -14,33 +14,33 @@ import numpy as np -from ...calculator import Calculator +from ...calculator import ndi_calculator if TYPE_CHECKING: - from ...document import Document - from ...session.session_base import Session + from ...document import ndi_document + from ...session.session_base import ndi_session -class TuningCurveCalc(Calculator): +class ndi_calc_stimulus_tuningcurve(ndi_calculator): """ - Calculator for stimulus tuning curves. + ndi_calculator for stimulus tuning curves. Takes stimulus_response_scalar documents as input and produces tuningcurve_calc documents containing organized tuning curve data. Example: - >>> calc = TuningCurveCalc(session) + >>> calc = ndi_calc_stimulus_tuningcurve(session) >>> docs = calc.run(DocExistsAction.REPLACE) """ - def __init__(self, session: Session | None = None): + def __init__(self, session: ndi_session | None = None): super().__init__( session=session, document_type="tuningcurve_calc", path_to_doc_type="apps/calculators/tuningcurve_calc", ) - def calculate(self, parameters: dict) -> list[Document]: + def calculate(self, parameters: dict) -> list[ndi_document]: """ Calculate tuning curves from stimulus responses. @@ -54,7 +54,7 @@ def calculate(self, parameters: dict) -> list[Document]: Returns: List of tuningcurve_calc documents """ - from ...document import Document + from ...document import ndi_document input_params = parameters.get("input_parameters", {}) @@ -65,7 +65,7 @@ def calculate(self, parameters: dict) -> list[Document]: "response_stderr": [], } - doc = Document( + doc = ndi_document( "apps/calculators/tuningcurve_calc", **{"tuningcurve_calc": tuningcurve_data}, ) @@ -85,7 +85,7 @@ def calculate(self, parameters: dict) -> list[Document]: def default_search_for_input_parameters(self) -> dict: """Return default search parameters for tuning curves.""" - from ...query import Query + from ...query import ndi_query return { "input_parameters": { @@ -98,7 +98,7 @@ def default_search_for_input_parameters(self) -> dict: "query": [ { "name": "document_id", - "query": Query("").isa("stimulus_response_scalar"), + "query": ndi_query("").isa("stimulus_response_scalar"), }, ], } @@ -125,20 +125,20 @@ def default_parameters_query( Returns: List of query spec dicts ``[{name, query}]``. """ - from ...query import Query + from ...query import ndi_query q_default = super().default_parameters_query(parameters_specification) if q_default: return q_default - q1 = Query("").isa("stimulus_response_scalar") - q2 = Query.from_search( + q1 = ndi_query("").isa("stimulus_response_scalar") + q2 = ndi_query.from_search( "stimulus_response_scalar.response_type", "contains_string", "mean", "", ) - q3 = Query.from_search( + q3 = ndi_query.from_search( "stimulus_response_scalar.response_type", "contains_string", "F1", @@ -151,7 +151,7 @@ def default_parameters_query( def best_value( self, algorithm: str, - stim_response_doc: Document, + stim_response_doc: ndi_document, prop: str, ) -> tuple[int, float, Any]: """Find the stimulus with the "best" response. @@ -160,7 +160,7 @@ def best_value( Args: algorithm: Algorithm name (e.g. ``'empirical_maximum'``). - stim_response_doc: A stimulus_response_scalar Document. + stim_response_doc: A stimulus_response_scalar ndi_document. prop: Stimulus property name to examine. Returns: @@ -178,7 +178,7 @@ def best_value( def best_value_empirical( self, - stim_response_doc: Document, + stim_response_doc: ndi_document, prop: str, ) -> tuple[int, float, Any]: """Find the stimulus with the largest empirical mean response. @@ -190,7 +190,7 @@ def best_value_empirical( with the highest value. Args: - stim_response_doc: A stimulus_response_scalar Document. + stim_response_doc: A stimulus_response_scalar ndi_document. prop: Stimulus property name. Returns: @@ -267,7 +267,7 @@ def best_value_empirical( def property_value_array( self, - stim_response_doc: Document, + stim_response_doc: ndi_document, prop: str, ) -> list[Any]: """Find all unique values of a stimulus property. @@ -275,7 +275,7 @@ def property_value_array( MATLAB equivalent: tuningcurve.property_value_array Args: - stim_response_doc: A stimulus_response_scalar Document. + stim_response_doc: A stimulus_response_scalar ndi_document. prop: Stimulus property name. Returns: @@ -442,16 +442,16 @@ def generate_mock_docs( def _get_stim_presentation_doc( self, - stim_response_doc: Document, - ) -> Document: + stim_response_doc: ndi_document, + ) -> ndi_document: """Retrieve the stimulus_presentation doc linked to a response doc.""" - from ...query import Query + from ...query import ndi_query if self._session is None: - raise RuntimeError("Session is required for stimulus lookup") + raise RuntimeError("ndi_session is required for stimulus lookup") dep_id = stim_response_doc.dependency_value("stimulus_presentation_id") - results = self._session.database_search(Query("base.id") == dep_id) + results = self._session.database_search(ndi_query("base.id") == dep_id) if len(results) != 1: doc_id = stim_response_doc.document_properties.get("base", {}).get("id", "") raise RuntimeError( @@ -460,7 +460,7 @@ def _get_stim_presentation_doc( return results[0] def __repr__(self) -> str: - return f"TuningCurveCalc(session={self._session is not None})" + return f"ndi_calc_stimulus_tuningcurve(session={self._session is not None})" def _is_nan(value: Any) -> bool: diff --git a/src/ndi/calc/tuning_fit.py b/src/ndi/calc/tuning_fit.py index 5aef793..63177fb 100644 --- a/src/ndi/calc/tuning_fit.py +++ b/src/ndi/calc/tuning_fit.py @@ -1,8 +1,8 @@ """ ndi.calc.tuning_fit - Abstract base class for tuning curve fitting calculators. -Extends Calculator with a mock document generation framework that -subclasses (e.g., OriDirTuning) use for self-testing. +Extends ndi_calculator with a mock document generation framework that +subclasses (e.g., ndi_app_oridirtuning) use for self-testing. MATLAB equivalent: src/ndi/+ndi/+calc/tuning_fit.m """ @@ -14,13 +14,13 @@ import numpy as np -from ..calculator import Calculator +from ..calculator import ndi_calculator if TYPE_CHECKING: - from ..session.session_base import Session + from ..session.session_base import ndi_session -class TuningFit(Calculator): +class ndi_calc_tuning__fit(ndi_calculator): """ Abstract base for stimulus tuning curve fitting calculators. @@ -29,11 +29,11 @@ class TuningFit(Calculator): - ``generate_mock_docs()`` — creates synthetic stimulus-response data for self-testing at two noise levels (highSNR / lowSNR) - Subclasses must implement both ``calculate()`` (from Calculator) + Subclasses must implement both ``calculate()`` (from ndi_calculator) and ``generate_mock_parameters()``. Example: - >>> class MyFit(TuningFit): + >>> class MyFit(ndi_calc_tuning__fit): ... def calculate(self, parameters): ... ... def generate_mock_parameters(self, scope, index): ... >>> fit = MyFit(session, 'my_fit', 'apps/calculators/my_fit') @@ -47,7 +47,7 @@ class TuningFit(Calculator): def __init__( self, - session: Session | None = None, + session: ndi_session | None = None, document_type: str = "", path_to_doc_type: str = "", ): @@ -197,4 +197,4 @@ def generate_mock_docs( def __repr__(self) -> str: doc_type = self.doc_document_types[0] if self.doc_document_types else "none" - return f"TuningFit(type={doc_type!r}, " f"session={self._session is not None})" + return f"ndi_calc_tuning__fit(type={doc_type!r}, " f"session={self._session is not None})" diff --git a/src/ndi/calculator.py b/src/ndi/calculator.py index 4ddb460..8fb7bea 100644 --- a/src/ndi/calculator.py +++ b/src/ndi/calculator.py @@ -1,7 +1,7 @@ """ ndi.calculator - Base class for NDI calculators. -A Calculator is an App that can discover input parameters, +A ndi_calculator is an ndi_app that can discover input parameters, check for existing results, and run computations that produce Documents stored in the session database. @@ -14,21 +14,21 @@ import logging from typing import TYPE_CHECKING -from .app import App -from .app.appdoc import AppDoc, DocExistsAction +from .app import ndi_app +from .app.appdoc import DocExistsAction, ndi_app_appdoc if TYPE_CHECKING: - from .document import Document - from .session.session_base import Session + from .document import ndi_document + from .session.session_base import ndi_session logger = logging.getLogger(__name__) -class Calculator(App, AppDoc): +class ndi_calculator(ndi_app, ndi_app_appdoc): """ Base class for NDI calculators. - A Calculator discovers inputs from the database, checks for + A ndi_calculator discovers inputs from the database, checks for existing results, runs computations, and stores output documents. Subclasses override `calculate()` to implement specific analyses. @@ -46,26 +46,26 @@ class Calculator(App, AppDoc): def __init__( self, - session: Session | None = None, + session: ndi_session | None = None, document_type: str = "", path_to_doc_type: str = "", ): """ - Create a new Calculator. + Create a new ndi_calculator. Args: - session: Session for database access + session: ndi_session for database access document_type: NDI document type for calculator outputs path_to_doc_type: Path to the document type schema """ - # Initialize App (session + name from class) + # Initialize ndi_app (session + name from class) name = type(self).__name__ - App.__init__(self, session=session, name=name) + ndi_app.__init__(self, session=session, name=name) - # Initialize AppDoc + # Initialize ndi_app_appdoc doc_types = [document_type] if document_type else [] doc_document_types = [path_to_doc_type or document_type] if document_type else [] - AppDoc.__init__( + ndi_app_appdoc.__init__( self, doc_types=doc_types, doc_document_types=doc_document_types, @@ -80,7 +80,7 @@ def run( self, doc_exists_action: DocExistsAction = DocExistsAction.ERROR, parameters: dict | None = None, - ) -> list[Document]: + ) -> list[ndi_document]: """ Run the calculator pipeline. @@ -106,8 +106,8 @@ def run( len(all_parameters), ) - docs: list[Document] = [] - docs_to_add: list[Document] = [] + docs: list[ndi_document] = [] + docs_to_add: list[ndi_document] = [] for i, params in enumerate(all_parameters): logger.debug("Processing parameter set %d of %d", i + 1, len(all_parameters)) @@ -120,7 +120,7 @@ def run( if existing: if doc_exists_action == DocExistsAction.ERROR: raise RuntimeError( - f"Calculator document already exists for parameter set {i + 1}" + f"ndi_calculator document already exists for parameter set {i + 1}" ) elif doc_exists_action == DocExistsAction.NO_ACTION: docs.extend(existing) @@ -168,7 +168,7 @@ def run( try: self._session.database_add(app_doc) except Exception: - pass # App doc may already exist + pass # ndi_app doc may already exist for doc in docs_to_add: try: @@ -179,7 +179,7 @@ def run( logger.debug("Concluding calculator %s", type(self).__name__) return docs - def calculate(self, parameters: dict) -> list[Document]: + def calculate(self, parameters: dict) -> list[ndi_document]: """ Perform the calculation. @@ -328,7 +328,7 @@ def default_parameters_query( def search_for_calculator_docs( self, parameters: dict, - ) -> list[Document]: + ) -> list[ndi_document]: """ Find existing calculator documents matching the given parameters. @@ -344,11 +344,11 @@ def search_for_calculator_docs( if self._session is None or not self.doc_types: return [] - from .query import Query + from .query import ndi_query # Build query for calculator class name (not schema path) doc_type = self.doc_types[0] - q = Query("").isa(doc_type) + q = ndi_query("").isa(doc_type) # Add dependency constraints depends_on = parameters.get("depends_on", []) @@ -356,7 +356,7 @@ def search_for_calculator_docs( dep_name = dep.get("name", "") dep_value = dep.get("value", "") if dep_value: - q = q & Query("").depends_on(dep_name, dep_value) + q = q & ndi_query("").depends_on(dep_name, dep_value) # Search database candidates = self._session.database_search(q) @@ -409,7 +409,7 @@ def is_valid_dependency_input( Args: name: Dependency name - value: Document ID + value: ndi_document ID Returns: True if valid @@ -420,7 +420,7 @@ def is_valid_dependency_input( # Internal Helpers # ========================================================================= - def _extract_input_parameters(self, doc: Document) -> dict | None: + def _extract_input_parameters(self, doc: ndi_document) -> dict | None: """Extract input_parameters from a calculator document.""" props = doc.document_properties doc_type = self.doc_types[0] if self.doc_types else "" @@ -429,7 +429,7 @@ def _extract_input_parameters(self, doc: Document) -> dict | None: return section.get("input_parameters") return None - def _remove_docs(self, docs: list[Document]) -> None: + def _remove_docs(self, docs: list[ndi_document]) -> None: """Remove documents from the database.""" if self._session is None: return @@ -441,4 +441,4 @@ def _remove_docs(self, docs: list[Document]) -> None: def __repr__(self) -> str: doc_type = self.doc_document_types[0] if self.doc_document_types else "none" - return f"Calculator({self._name}, type={doc_type})" + return f"ndi_calculator({self._name}, type={doc_type})" diff --git a/src/ndi/check.py b/src/ndi/check.py index d7dbfd0..847d557 100644 --- a/src/ndi/check.py +++ b/src/ndi/check.py @@ -49,9 +49,9 @@ def check(name: str, ok: bool, detail: str = "") -> None: # ndi_common data folder try: - from ndi.common import PathConstants + from ndi.common import ndi_common_PathConstants - folder = PathConstants.COMMON_FOLDER + folder = ndi_common_PathConstants.COMMON_FOLDER if folder.is_dir(): check("ndi_common data folder", True, str(folder)) else: @@ -93,15 +93,15 @@ def check(name: str, ok: bool, detail: str = "") -> None: # Smoke test try: - from ndi.document import Document + from ndi.document import ndi_document - doc = Document("base") + doc = ndi_document("base") if doc.id: - check("Smoke test: Document('base')", True, f"id={doc.id[:12]}...") + check("Smoke test: ndi_document('base')", True, f"id={doc.id[:12]}...") else: - check("Smoke test: Document('base')", False, "created but has no ID") + check("Smoke test: ndi_document('base')", False, "created but has no ID") except Exception as e: - check("Smoke test: Document('base')", False, str(e)) + check("Smoke test: ndi_document('base')", False, str(e)) return results, passed, total diff --git a/src/ndi/class_registry.py b/src/ndi/class_registry.py new file mode 100644 index 0000000..bde135c --- /dev/null +++ b/src/ndi/class_registry.py @@ -0,0 +1,94 @@ +"""Unified NDI class registry for cross-language document compatibility. + +Maps NDI class identifier strings (e.g. ``"ndi.probe.timeseries.mfdaq"``) +to their Python implementation classes. This registry is used in two +directions: + +* **Object → ndi_document**: each Python class exposes an ``NDI_*_CLASS`` + constant or ``ndi_element_class()`` method that returns its + identifier, which gets written into the document. +* **ndi_document → Object**: :func:`get_class` looks up the Python class + for a given identifier string so the object can be reconstructed. + +The identifiers intentionally mirror the MATLAB class hierarchy to +ensure cross-language database compatibility. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + pass + +# Lazily populated on first access to avoid circular imports. +_REGISTRY: dict[str, type] | None = None + + +def _build_registry() -> dict[str, type]: + """Build the class registry by importing all known NDI classes.""" + from .daq.reader.mfdaq.blackrock import ndi_daq_reader_mfdaq_blackrock + from .daq.reader.mfdaq.cedspike2 import ndi_daq_reader_mfdaq_cedspike2 + from .daq.reader.mfdaq.intan import ndi_daq_reader_mfdaq_intan + from .daq.reader.mfdaq.spikegadgets import ndi_daq_reader_mfdaq_spikegadgets + from .daq.system import ndi_daq_system + from .element import ndi_element + from .file.navigator import ndi_file_navigator + from .probe import ndi_probe + from .probe.timeseries import ndi_probe_timeseries + from .probe.timeseries_mfdaq import ndi_probe_timeseries_mfdaq + from .probe.timeseries_stimulator import ndi_probe_timeseries_stimulator + + registry: dict[str, type] = {} + + # Elements / probes (keyed by ndi_element_class() return value) + for cls in ( + ndi_element, + ndi_probe, + ndi_probe_timeseries, + ndi_probe_timeseries_mfdaq, + ndi_probe_timeseries_stimulator, + ): + registry[cls().ndi_element_class()] = cls + + # DAQ readers (keyed by NDI_DAQREADER_CLASS constant) + for cls in ( + ndi_daq_reader_mfdaq_intan, + ndi_daq_reader_mfdaq_blackrock, + ndi_daq_reader_mfdaq_cedspike2, + ndi_daq_reader_mfdaq_spikegadgets, + ): + registry[cls.NDI_DAQREADER_CLASS] = cls + + # DAQ system + registry[ndi_daq_system.NDI_DAQSYSTEM_CLASS] = ndi_daq_system + + # File navigator + registry[ndi_file_navigator.NDI_FILENAVIGATOR_CLASS] = ndi_file_navigator + + return registry + + +def get_class(ndi_class_name: str) -> type | None: + """Look up the Python class for an NDI class identifier. + + Args: + ndi_class_name: The NDI class identifier string, + e.g. ``"ndi.probe.timeseries.mfdaq"`` or + ``"ndi.daq.reader.mfdaq.intan"``. + + Returns: + The corresponding Python class, or ``None`` if not found. + """ + global _REGISTRY # noqa: PLW0603 + if _REGISTRY is None: + _REGISTRY = _build_registry() + return _REGISTRY.get(ndi_class_name) + + +def get_all() -> dict[str, type]: + """Return a copy of the full registry (mainly for debugging).""" + global _REGISTRY # noqa: PLW0603 + if _REGISTRY is None: + _REGISTRY = _build_registry() + return dict(_REGISTRY) diff --git a/src/ndi/cloud/admin/crossref.py b/src/ndi/cloud/admin/crossref.py index 591201e..85754d3 100644 --- a/src/ndi/cloud/admin/crossref.py +++ b/src/ndi/cloud/admin/crossref.py @@ -12,7 +12,7 @@ from dataclasses import dataclass from datetime import datetime, timezone from typing import Any -from xml.etree.ElementTree import Element, SubElement, tostring +from xml.etree.ElementTree import SubElement, ndi_element, tostring @dataclass(frozen=True) @@ -46,7 +46,7 @@ def createDoiBatchSubmission( Returns: XML string suitable for Crossref deposit. """ - root = Element("doi_batch") + root = ndi_element("doi_batch") root.set("version", "5.3.1") root.set("xmlns", "http://www.crossref.org/schema/5.3.1") @@ -74,7 +74,7 @@ def createDoiBatchSubmission( title = SubElement(titles, "title") title.text = CONSTANTS.DATABASE_TITLE - # Dataset entry + # ndi_dataset entry dataset = SubElement(database, "dataset") dataset.set("dataset_type", "record") @@ -95,7 +95,7 @@ def createDoiBatchSubmission( # Titles ds_titles = SubElement(dataset, "titles") ds_title = SubElement(ds_titles, "title") - ds_title.text = dataset_metadata.get("name", "Untitled Dataset") + ds_title.text = dataset_metadata.get("name", "Untitled ndi_dataset") # Description desc = dataset_metadata.get("description", "") diff --git a/src/ndi/cloud/api/__init__.py b/src/ndi/cloud/api/__init__.py index 33f6104..7ec3d94 100644 --- a/src/ndi/cloud/api/__init__.py +++ b/src/ndi/cloud/api/__init__.py @@ -2,8 +2,8 @@ ndi.cloud.api - REST API endpoint modules for NDI Cloud. Submodules: - datasets — Dataset CRUD, publish, branch - documents — Document CRUD, bulk operations + datasets — ndi_dataset CRUD, publish, branch + documents — ndi_document CRUD, bulk operations files — Presigned URL retrieval, file upload users — User creation, profile compute — Compute session management diff --git a/src/ndi/cloud/api/datasets.py b/src/ndi/cloud/api/datasets.py index dc7af5f..aa233c9 100644 --- a/src/ndi/cloud/api/datasets.py +++ b/src/ndi/cloud/api/datasets.py @@ -1,5 +1,5 @@ """ -ndi.cloud.api.datasets - Dataset CRUD, publish, and branch operations. +ndi.cloud.api.datasets - ndi_dataset CRUD, publish, and branch operations. All functions accept an optional ``client`` keyword argument. When omitted, a client is created automatically from environment variables diff --git a/src/ndi/cloud/api/documents.py b/src/ndi/cloud/api/documents.py index 0912fd3..867ca4e 100644 --- a/src/ndi/cloud/api/documents.py +++ b/src/ndi/cloud/api/documents.py @@ -1,5 +1,5 @@ """ -ndi.cloud.api.documents - Document CRUD and bulk operations. +ndi.cloud.api.documents - ndi_document CRUD and bulk operations. All functions accept an optional ``client`` keyword argument. When omitted, a client is created automatically from environment variables. @@ -21,16 +21,16 @@ def _coerce_search_structure(search_structure: Any) -> Any: - """Convert Query objects to JSON-serializable dicts. + """Convert ndi_query objects to JSON-serializable dicts. - Accepts :class:`~ndi.query.Query` objects, raw dicts/lists, or lists + Accepts :class:`~ndi.query.ndi_query` objects, raw dicts/lists, or lists containing a mix of both. Returns a JSON-serializable value suitable for the cloud ``searchstructure`` POST body. """ - # Single Query object + # Single ndi_query object if hasattr(search_structure, "to_search_structure"): return search_structure.to_search_structure() - # List that may contain Query objects + # List that may contain ndi_query objects if isinstance(search_structure, list): return [ q.to_search_structure() if hasattr(q, "to_search_structure") else q @@ -263,14 +263,14 @@ def ndiquery( *, client: _Client = None, ) -> dict[str, Any]: - """Query documents across datasets via the NDI query API. + """ndi_query documents across datasets via the NDI query API. MATLAB equivalent: +cloud/+api/+documents/ndiquery.m Args: scope: One of ``'public'``, ``'private'``, ``'all'``. - search_structure: Query object, search structure dict, or list. - Accepts :class:`~ndi.query.Query` objects (auto-converted), + search_structure: ndi_query object, search structure dict, or list. + Accepts :class:`~ndi.query.ndi_query` objects (auto-converted), raw dicts, or lists of either. page: Page number (1-based). page_size: Results per page. diff --git a/src/ndi/cloud/download.py b/src/ndi/cloud/download.py index cab4055..7e0e7fc 100644 --- a/src/ndi/cloud/download.py +++ b/src/ndi/cloud/download.py @@ -342,7 +342,7 @@ def downloadFilesForDocument( Args: dataset_id: Cloud dataset ID. - document: Document dict (must include ``file_uid``). + document: ndi_document dict (must include ``file_uid``). target_dir: Directory to save downloaded files. client: Authenticated cloud client (auto-created if omitted). @@ -416,16 +416,16 @@ def downloadDatasetFiles( def jsons2documents( doc_jsons: list[dict[str, Any]], ) -> list[Any]: - """Convert a list of raw JSON dicts into ndi.Document objects. + """Convert a list of raw JSON dicts into ndi.ndi_document objects. MATLAB equivalent: downloadDataset.m conversion step. """ - from ndi.document import Document + from ndi.document import ndi_document documents = [] for dj in doc_jsons: try: - documents.append(Document(dj)) + documents.append(ndi_document(dj)) except Exception: pass return documents @@ -449,7 +449,7 @@ def datasetDocuments( saves each document as a JSON file in *json_dir*. Args: - dataset_info: Dataset dict as returned by ``getDataset``, must + dataset_info: ndi_dataset dict as returned by ``getDataset``, must include ``documents`` (list of document IDs) and ``_id``. mode: ``'local'`` — files are expected on disk, set ingest/delete flags. ``'hybrid'`` — leave files in cloud, set ndic:// URIs. @@ -485,7 +485,7 @@ def datasetDocuments( out_file = json_path / f"{document_id}.json" if out_file.exists(): if verbose: - print(f" Document {i + 1} already exists. Skipping...") + print(f" ndi_document {i + 1} already exists. Skipping...") continue try: @@ -559,14 +559,14 @@ def downloadGenericFiles( # Resolve cloud dataset ID cloud_dataset_id, _ = getCloudDatasetIdForLocalDataset(ndi_dataset, client=client) if not cloud_dataset_id: - return False, "Dataset is not linked to an NDI cloud dataset", report + return False, "ndi_dataset is not linked to an NDI cloud dataset", report # Get documents from the local database - from ndi.query import Query + from ndi.query import ndi_query all_docs = [] for doc_id in ndi_document_ids: - q = Query("base.id", "exact_string", doc_id, "") + q = ndi_query("base.id", "exact_string", doc_id, "") results = ( ndi_dataset.database_search(q) if hasattr(ndi_dataset, "database_search") else [] ) @@ -687,7 +687,7 @@ def setFileInfo( MATLAB equivalent: ``ndi.cloud.download.internal.setFileInfo`` Args: - doc_struct: Document properties dict. + doc_struct: ndi_document properties dict. mode: ``'local'`` — set delete_original and ingest to 1 and update file locations to local paths. ``'hybrid'`` — set delete_original and ingest to 0 (leave files in cloud). @@ -761,7 +761,7 @@ def setFileInfo( def structsToNdiDocuments( ndi_document_structs: list[dict[str, Any]], ) -> list[Any]: - """Convert downloaded NDI document structures to ndi.Document objects. + """Convert downloaded NDI document structures to ndi.ndi_document objects. MATLAB equivalent: ``ndi.cloud.download.internal.structsToNdiDocuments`` @@ -772,7 +772,7 @@ def structsToNdiDocuments( ndi_document_structs: List of document property dicts. Returns: - List of :class:`ndi.Document` objects. + List of :class:`ndi.ndi_document` objects. """ return jsons2documents(ndi_document_structs) diff --git a/src/ndi/cloud/filehandler.py b/src/ndi/cloud/filehandler.py index 85b5b79..929617e 100644 --- a/src/ndi/cloud/filehandler.py +++ b/src/ndi/cloud/filehandler.py @@ -62,7 +62,7 @@ def updateFileInfoForRemoteFiles(doc_props: dict, cloud_dataset_id: str) -> None and ``locations`` fields. Args: - doc_props: Document properties dict (as from JSON). + doc_props: ndi_document properties dict (as from JSON). cloud_dataset_id: The cloud dataset ID to embed in URIs. """ files = doc_props.get("files") @@ -209,7 +209,7 @@ def updateFileInfoForLocalFiles( and sets ``delete_original=1``, ``ingest=1``. Args: - doc_props: Document properties dict (as from JSON). + doc_props: ndi_document properties dict (as from JSON). file_directory: Directory where local files are stored. """ import os diff --git a/src/ndi/cloud/internal.py b/src/ndi/cloud/internal.py index eaa6751..4a6c7cc 100644 --- a/src/ndi/cloud/internal.py +++ b/src/ndi/cloud/internal.py @@ -48,7 +48,7 @@ def getCloudDatasetIdForLocalDataset( that links this dataset to a cloud dataset. Args: - dataset: A local :class:`~ndi.dataset.Dataset` instance. + dataset: A local :class:`~ndi.dataset.ndi_dataset` instance. client: Authenticated cloud client (auto-created if omitted). Returns: @@ -57,9 +57,9 @@ def getCloudDatasetIdForLocalDataset( """ try: db = dataset.database - from ndi.query import Query + from ndi.query import ndi_query - q = Query("").isa("dataset_remote") + q = ndi_query("").isa("dataset_remote") results = db.search(q) if results: doc = results[0] @@ -85,11 +85,11 @@ def createRemoteDatasetDoc( dataset: Local dataset to add the document to. Returns: - The created Document instance. + The created ndi_document instance. """ - from ndi.document import Document + from ndi.document import ndi_document - doc = Document("dataset_remote") + doc = ndi_document("dataset_remote") doc._set_nested_property("dataset_remote.dataset_id", cloud_dataset_id) return doc @@ -102,10 +102,10 @@ def listLocalDocuments(dataset: Any) -> tuple[list[Any], list[str]]: Returns: Tuple of (documents, document_ids). """ - from ndi.query import Query + from ndi.query import ndi_query try: - docs = dataset.session.database_search(Query("").isa("base")) + docs = dataset.session.database_search(ndi_query("").isa("base")) except Exception: docs = [] diff --git a/src/ndi/cloud/ndi_matlab_python_bridge.yaml b/src/ndi/cloud/ndi_matlab_python_bridge.yaml index a5f1ad5..6d270c4 100644 --- a/src/ndi/cloud/ndi_matlab_python_bridge.yaml +++ b/src/ndi/cloud/ndi_matlab_python_bridge.yaml @@ -25,7 +25,7 @@ infrastructure: python_path: "ndi/cloud/client.py" decision_log: > Python-only HTTP client. MATLAB uses webread/webwrite/call.m; - Python wraps requests.Session for all REST calls. + Python wraps requests.ndi_session for all REST calls. - name: APIResponse type: class @@ -1043,7 +1043,7 @@ not_yet_ported: status: not_applicable decision_log: > MATLAB-specific HTTP configuration. Python uses CloudClient - with requests.Session which handles auth headers internally. + with requests.ndi_session which handles auth headers internally. - name: createCloudMetadataStruct matlab_path: "+ndi/+cloud/+utility/createCloudMetadataStruct.m" diff --git a/src/ndi/cloud/orchestration.py b/src/ndi/cloud/orchestration.py index 08e0a6d..3b681ce 100644 --- a/src/ndi/cloud/orchestration.py +++ b/src/ndi/cloud/orchestration.py @@ -40,7 +40,7 @@ def downloadDataset( client: Authenticated cloud client (auto-created if omitted). Returns: - An ndi.Dataset backed by the target folder. + An ndi.ndi_dataset backed by the target folder. """ from .api import datasets as ds_api from .download import ( @@ -78,17 +78,17 @@ def downloadDataset( for dj in doc_jsons: updateFileInfoForRemoteFiles(dj, cloud_dataset_id) - # Convert to Document objects and create Dataset with them. + # Convert to ndi_document objects and create ndi_dataset with them. # Mirrors MATLAB: ndi.dataset.dir([], datasetFolder, ndiDocuments) - from ndi.dataset import DatasetDir + from ndi.dataset import ndi_dataset_dir documents = jsons2documents(doc_jsons) - dataset = DatasetDir("", target, documents=documents) + dataset = ndi_dataset_dir("", target, documents=documents) # Create remote link document if not already present - from ndi.query import Query + from ndi.query import ndi_query - existing = dataset.database_search(Query("").isa("dataset_remote")) + existing = dataset.database_search(ndi_query("").isa("dataset_remote")) if not existing: remote_doc = createRemoteDatasetDoc(cloud_dataset_id, dataset) try: @@ -127,7 +127,7 @@ def load_dataset_from_json_dir( Args: json_dir: Directory containing ``*.json`` document files. - target_folder: Path for the local Dataset. If *None*, a + target_folder: Path for the local ndi_dataset. If *None*, a temporary directory is created next to *json_dir*. verbose: Print progress messages. cloud_dataset_id: If given, rewrite file_info locations to @@ -138,7 +138,7 @@ def load_dataset_from_json_dir( dataset for on-demand file fetching. Returns: - An :class:`ndi.Dataset` backed by the target folder. + An :class:`ndi.ndi_dataset` backed by the target folder. """ import json as json_mod @@ -156,7 +156,7 @@ def load_dataset_from_json_dir( doc_jsons.append(json_mod.load(fh)) if verbose: - print(f" Read {len(doc_jsons)} documents, bulk-inserting into Dataset...") + print(f" Read {len(doc_jsons)} documents, bulk-inserting into ndi_dataset...") # Auto-detect cloud dataset ID from dataset_remote document if cloud_dataset_id is None: @@ -173,8 +173,8 @@ def load_dataset_from_json_dir( for dj in doc_jsons: updateFileInfoForRemoteFiles(dj, cloud_dataset_id) - # Create Dataset - from ndi.dataset import DatasetDir + # Create ndi_dataset + from ndi.dataset import ndi_dataset_dir if target_folder is None: target = json_path.parent / f"{json_path.name}_dataset" @@ -182,11 +182,11 @@ def load_dataset_from_json_dir( target = Path(target_folder) target.mkdir(parents=True, exist_ok=True) - # Convert JSON dicts to Document objects and create dataset with them + # Convert JSON dicts to ndi_document objects and create dataset with them from .download import jsons2documents as _j2d all_documents = _j2d(doc_jsons) - dataset = DatasetDir("", target, documents=all_documents) + dataset = ndi_dataset_dir("", target, documents=all_documents) added = len(all_documents) skipped = 0 @@ -195,7 +195,7 @@ def load_dataset_from_json_dir( dataset.cloud_client = client if verbose: - print(f" Dataset created at {target} with {added} documents ({skipped} skipped).") + print(f" ndi_dataset created at {target} with {added} documents ({skipped} skipped).") return dataset @@ -215,7 +215,7 @@ def uploadDataset( MATLAB equivalent: ndi.cloud.uploadDataset Args: - dataset: Local ndi.Dataset. + dataset: Local ndi.ndi_dataset. upload_as_new: If True, always create a new remote dataset. remote_name: Name for the remote dataset. sync_files: Upload binary files. @@ -236,7 +236,7 @@ def uploadDataset( if not cloud_id: # Create new remote dataset - name = remote_name or getattr(dataset, "name", "Unnamed Dataset") + name = remote_name or getattr(dataset, "name", "Unnamed ndi_dataset") org_id = client.config.org_id try: result = ds_api.createDataset(org_id, name, client=client) @@ -255,10 +255,10 @@ def uploadDataset( print(f"Uploading to cloud dataset: {cloud_id}") # Gather local documents - from ndi.query import Query + from ndi.query import ndi_query try: - all_docs = dataset.session.database_search(Query("")) + all_docs = dataset.session.database_search(ndi_query("")) except Exception: all_docs = [] @@ -302,7 +302,7 @@ def syncDataset( MATLAB equivalent: ndi.cloud.syncDataset Args: - dataset: Local ndi.Dataset. + dataset: Local ndi.ndi_dataset. sync_mode: One of ``'download_new'``, ``'upload_new'``, ``'mirror_from_remote'``, ``'mirror_to_remote'``, ``'two_way_sync'``. @@ -401,10 +401,10 @@ def _sync_download_new( remote_docs = docs_api.listDatasetDocumentsAll(cloud_id, client=client).data # Find local IDs - from ndi.query import Query + from ndi.query import ndi_query try: - local_docs = dataset.session.database_search(Query("")) + local_docs = dataset.session.database_search(ndi_query("")) except Exception: local_docs = [] @@ -450,10 +450,10 @@ def _sync_upload_new( remote_ids = listRemoteDocumentIds(cloud_id, client=client) - from ndi.query import Query + from ndi.query import ndi_query try: - local_docs = dataset.session.database_search(Query("")) + local_docs = dataset.session.database_search(ndi_query("")) except Exception: local_docs = [] diff --git a/src/ndi/cloud/upload.py b/src/ndi/cloud/upload.py index 56e0b51..03f8fd7 100644 --- a/src/ndi/cloud/upload.py +++ b/src/ndi/cloud/upload.py @@ -94,7 +94,7 @@ def zipForUpload( """Serialize documents to JSON and create a ZIP archive. Args: - documents: Document property dicts. + documents: ndi_document property dicts. dataset_id: Used for the archive filename. target_dir: Directory for the ZIP file. Defaults to a temp dir. @@ -248,14 +248,16 @@ def uploadToNDICloud( Returns: Tuple of ``(success, error_message)``. """ - from ndi.query import Query + from ndi.query import ndi_query from .api import documents as docs_api try: if verbose: print("Loading documents...") - all_docs = dataset.database_search(Query("")) if hasattr(dataset, "database_search") else [] + all_docs = ( + dataset.database_search(ndi_query("")) if hasattr(dataset, "database_search") else [] + ) if verbose: print("Getting list of previously uploaded documents...") @@ -318,12 +320,14 @@ def scanForUpload( Returns: Tuple of ``(doc_structs, file_structs, total_size_kb)``. """ - from ndi.query import Query + from ndi.query import ndi_query from .internal import listRemoteDocumentIds try: - all_docs = dataset.database_search(Query("")) if hasattr(dataset, "database_search") else [] + all_docs = ( + dataset.database_search(ndi_query("")) if hasattr(dataset, "database_search") else [] + ) except Exception: all_docs = [] diff --git a/src/ndi/common/__init__.py b/src/ndi/common/__init__.py index d984521..1ad76fb 100644 --- a/src/ndi/common/__init__.py +++ b/src/ndi/common/__init__.py @@ -6,7 +6,7 @@ Provides path constants, cache, logger, and other shared utilities. MATLAB functions: - ndi.common.PathConstants + ndi.common.ndi_common_PathConstants ndi.common.assertDIDInstalled ndi.common.getCache ndi.common.getDatabaseHierarchy @@ -21,7 +21,7 @@ from typing import TYPE_CHECKING, Any -class PathConstants: +class ndi_common_PathConstants: """NDI path constants for document definitions and schemas. This class provides paths to NDI document definitions, schemas, @@ -166,22 +166,22 @@ def getCache() -> Any: MATLAB equivalent: ndi.common.getCache - Returns a shared :class:`ndi.cache.Cache` instance, creating it on first + Returns a shared :class:`ndi.cache.ndi_cache` instance, creating it on first call. Subsequent calls return the same object. Returns: - The global :class:`~ndi.cache.Cache` instance. + The global :class:`~ndi.cache.ndi_cache` instance. """ global _cache_singleton # noqa: PLW0603 if _cache_singleton is None: - from ..cache import Cache + from ..cache import ndi_cache - _cache_singleton = Cache() + _cache_singleton = ndi_cache() return _cache_singleton # --------------------------------------------------------------------------- -# Database hierarchy — mirrors MATLAB ndi.common.getDatabaseHierarchy +# ndi_database hierarchy — mirrors MATLAB ndi.common.getDatabaseHierarchy # --------------------------------------------------------------------------- _database_hierarchy_singleton: Any = None @@ -206,7 +206,7 @@ def getDatabaseHierarchy() -> dict[str, Any]: import json hierarchy: dict[str, Any] = {} - doc_path = PathConstants.DOCUMENT_PATH + doc_path = ndi_common_PathConstants.DOCUMENT_PATH if doc_path.is_dir(): for json_file in sorted(doc_path.rglob("*.json")): try: @@ -237,7 +237,7 @@ def getDatabaseHierarchy() -> dict[str, Any]: def assertDIDInstalled() -> None: - """Assert that the DID (Document Interface Database) package is installed. + """Assert that the DID (ndi_document Interface ndi_database) package is installed. MATLAB equivalent: ndi.common.assertDIDInstalled @@ -253,7 +253,7 @@ def assertDIDInstalled() -> None: __all__ = [ - "PathConstants", + "ndi_common_PathConstants", "timestamp", "getLogger", "getCache", diff --git a/src/ndi/common/ndi_matlab_python_bridge.yaml b/src/ndi/common/ndi_matlab_python_bridge.yaml index 6da073b..ba1d6e0 100644 --- a/src/ndi/common/ndi_matlab_python_bridge.yaml +++ b/src/ndi/common/ndi_matlab_python_bridge.yaml @@ -46,7 +46,7 @@ functions: input_arguments: [] output_arguments: - name: cache - type_python: "Cache" + type_python: "ndi_cache" decision_log: "Exact match. Returns a global singleton." - name: getDatabaseHierarchy @@ -79,14 +79,14 @@ functions: classes: # ========================================================================= - # ndi.common.PathConstants + # ndi.common.ndi_common_PathConstants # ========================================================================= - - name: PathConstants + - name: ndi_common_PathConstants type: class - matlab_path: "+ndi/+common/PathConstants.m" + matlab_path: "+ndi/+common/ndi_common_PathConstants.m" matlab_last_sync_hash: "fe64a9f5" python_path: "ndi/common/__init__.py" - python_class: "PathConstants" + python_class: "ndi_common_PathConstants" properties: - name: NDI_ROOT diff --git a/src/ndi/daq/__init__.py b/src/ndi/daq/__init__.py index fc15ec5..4b17419 100644 --- a/src/ndi/daq/__init__.py +++ b/src/ndi/daq/__init__.py @@ -1,37 +1,41 @@ """ -ndi.daq - Data acquisition module for NDI framework. +ndi.daq - ndi_gui_Data acquisition module for NDI framework. This module provides classes for reading data from various data acquisition systems used in neuroscience experiments. Classes: - DAQReader: Abstract base for DAQ readers - MFDAQReader: Multi-function DAQ reader - DAQSystem: Combines file navigator, reader, and metadata reader - MetadataReader: Reads stimulus/metadata parameters + ndi_daq_reader: Abstract base for DAQ readers + ndi_daq_reader_mfdaq: Multi-function DAQ reader + ndi_daq_system: Combines file navigator, reader, and metadata reader + ndi_daq_metadatareader: Reads stimulus/metadata parameters Submodules: reader: Concrete reader implementations Example: - >>> from ndi.daq import DAQReader, DAQSystem - >>> from ndi.daq.reader import MFDAQReader + >>> from ndi.daq import ndi_daq_reader, ndi_daq_system + >>> from ndi.daq.reader import ndi_daq_reader_mfdaq """ -from .daqsystemstring import DAQSystemString -from .metadatareader import MetadataReader, NewStimStimsReader, NielsenLabStimsReader -from .mfdaq import MFDAQReader -from .reader_base import DAQReader -from .system import DAQSystem -from .system_mfdaq import DAQSystemMFDAQ +from .daqsystemstring import ndi_daq_daqsystemstring +from .metadatareader import ( + ndi_daq_metadatareader, + ndi_daq_metadatareader_NewStimStims, + ndi_daq_metadatareader_NielsenLabStims, +) +from .mfdaq import ndi_daq_reader_mfdaq +from .reader_base import ndi_daq_reader +from .system import ndi_daq_system +from .system_mfdaq import ndi_daq_system_mfdaq __all__ = [ - "DAQReader", - "MFDAQReader", - "DAQSystem", - "DAQSystemMFDAQ", - "MetadataReader", - "NewStimStimsReader", - "NielsenLabStimsReader", - "DAQSystemString", + "ndi_daq_reader", + "ndi_daq_reader_mfdaq", + "ndi_daq_system", + "ndi_daq_system_mfdaq", + "ndi_daq_metadatareader", + "ndi_daq_metadatareader_NewStimStims", + "ndi_daq_metadatareader_NielsenLabStims", + "ndi_daq_daqsystemstring", ] diff --git a/src/ndi/daq/daqsystemstring.py b/src/ndi/daq/daqsystemstring.py index b6d30d1..23791d6 100644 --- a/src/ndi/daq/daqsystemstring.py +++ b/src/ndi/daq/daqsystemstring.py @@ -14,7 +14,7 @@ @dataclass -class DAQSystemString: +class ndi_daq_daqsystemstring: """ Describes a device and its channels. @@ -34,15 +34,15 @@ class DAQSystemString: channels: list[tuple[str, list[int]]] = field(default_factory=list) @classmethod - def parse(cls, devstr: str) -> DAQSystemString: + def parse(cls, devstr: str) -> ndi_daq_daqsystemstring: """ - Parse a device string into a DAQSystemString. + Parse a device string into a ndi_daq_daqsystemstring. Args: devstr: Device string like 'mydevice:ai1-5,7;di1' Returns: - Parsed DAQSystemString + Parsed ndi_daq_daqsystemstring Raises: ValueError: If the string format is invalid @@ -130,10 +130,10 @@ def __str__(self) -> str: return self.devicestring() def __repr__(self) -> str: - return f"DAQSystemString('{self.devicestring()}')" + return f"ndi_daq_daqsystemstring('{self.devicestring()}')" def __eq__(self, other) -> bool: - if not isinstance(other, DAQSystemString): + if not isinstance(other, ndi_daq_daqsystemstring): return False return self.devicename == other.devicename and self.channels == other.channels diff --git a/src/ndi/daq/metadatareader/__init__.py b/src/ndi/daq/metadatareader/__init__.py index 79b2336..86d9fe8 100644 --- a/src/ndi/daq/metadatareader/__init__.py +++ b/src/ndi/daq/metadatareader/__init__.py @@ -1,7 +1,7 @@ """ ndi.daq.metadatareader - Metadata reader for experiment metadata. -This module provides the MetadataReader class for reading stimulus +This module provides the ndi_daq_metadatareader class for reading stimulus and experiment metadata from data files, plus format-specific subclasses for different stimulus systems. """ @@ -13,14 +13,14 @@ from pathlib import Path from typing import Any -from ...ido import Ido +from ...ido import ndi_ido -class MetadataReader(Ido): +class ndi_daq_metadatareader(ndi_ido): """ Reader for experiment metadata such as stimulus parameters. - MetadataReader reads metadata from files associated with epochs, + ndi_daq_metadatareader reads metadata from files associated with epochs, typically containing stimulus parameters or experimental conditions. The base class supports tab-separated value (TSV) files with @@ -33,7 +33,7 @@ class MetadataReader(Ido): tab_separated_file_parameter: Regex pattern to find TSV files Example: - >>> reader = MetadataReader(r'.*stim.*\\.txt') + >>> reader = ndi_daq_metadatareader(r'.*stim.*\\.txt') >>> metadata = reader.readmetadata(['epoch1.txt', 'stim_params.txt']) """ @@ -45,7 +45,7 @@ def __init__( document: Any | None = None, ): """ - Create a new MetadataReader. + Create a new ndi_daq_metadatareader. Args: tsv_pattern: Regular expression to find TSV parameter files @@ -89,7 +89,7 @@ def readmetadata( ValueError: If multiple files match or no files match Example: - >>> reader = MetadataReader(r'stim.*\\.tsv') + >>> reader = ndi_daq_metadatareader(r'stim.*\\.tsv') >>> params = reader.readmetadata(['data.bin', 'stim_params.tsv']) >>> print(params[0]) # First stimulus parameters """ @@ -177,7 +177,7 @@ def readmetadata_ingested( Args: epochfiles: List of file paths (starting with epochid://) - session: Session object with database access + session: ndi_session object with database access Returns: List of parameter dictionaries @@ -201,12 +201,12 @@ def get_ingested_document( Args: epochfiles: List of file paths - session: Session object + session: ndi_session object Returns: - Document object or None + ndi_document object or None """ - from ...query import Query + from ...query import ndi_query # Check if ingested if not epochfiles or not epochfiles[0].startswith("epochid://"): @@ -214,8 +214,8 @@ def get_ingested_document( epochid = epochfiles[0][len("epochid://") :] - q = Query("").depends_on("daqmetadatareader_id", self.id) & ( - Query("epochid.epochid") == epochid + q = ndi_query("").depends_on("daqmetadatareader_id", self.id) & ( + ndi_query("epochid.epochid") == epochid ) results = session.database_search(q) @@ -233,16 +233,16 @@ def ingest_epochfiles( Args: epochfiles: List of file paths - epoch_id: Epoch identifier + epoch_id: ndi_epoch_epoch identifier Returns: - Document object + ndi_document object """ - from ...document import Document + from ...document import ndi_document epochid_struct = {"epochid": epoch_id} - doc = Document( + doc = ndi_document( "ingestion/daqmetadatareader_epochdata_ingested", epochid=epochid_struct, ) @@ -260,14 +260,14 @@ def ingest_epochfiles( def newdocument(self) -> Any: """ - Create a new document for this MetadataReader. + Create a new document for this ndi_daq_metadatareader. Returns: - Document object + ndi_document object """ - from ...document import Document + from ...document import ndi_document - doc = Document( + doc = ndi_document( "daq/daqmetadatareader", **{ "daqmetadatareader.ndi_daqmetadatareader_class": self.__class__.__name__, @@ -279,18 +279,18 @@ def newdocument(self) -> Any: def searchquery(self) -> Any: """ - Create a search query for this MetadataReader. + Create a search query for this ndi_daq_metadatareader. Returns: - Query object + ndi_query object """ - from ...query import Query + from ...query import ndi_query - return Query("base.id") == self.id + return ndi_query("base.id") == self.id def __eq__(self, other: Any) -> bool: """Test equality by class and properties.""" - if not isinstance(other, MetadataReader): + if not isinstance(other, ndi_daq_metadatareader): return False return ( self.__class__.__name__ == other.__class__.__name__ @@ -303,7 +303,11 @@ def __hash__(self) -> int: # Import subclass readers -from .newstim_stims import NewStimStimsReader -from .nielsenlab_stims import NielsenLabStimsReader - -__all__ = ["MetadataReader", "NewStimStimsReader", "NielsenLabStimsReader"] +from .newstim_stims import ndi_daq_metadatareader_NewStimStims +from .nielsenlab_stims import ndi_daq_metadatareader_NielsenLabStims + +__all__ = [ + "ndi_daq_metadatareader", + "ndi_daq_metadatareader_NewStimStims", + "ndi_daq_metadatareader_NielsenLabStims", +] diff --git a/src/ndi/daq/metadatareader/ndi_matlab_python_bridge.yaml b/src/ndi/daq/metadatareader/ndi_matlab_python_bridge.yaml index 3a33d77..b568ebf 100644 --- a/src/ndi/daq/metadatareader/ndi_matlab_python_bridge.yaml +++ b/src/ndi/daq/metadatareader/ndi_matlab_python_bridge.yaml @@ -19,7 +19,7 @@ classes: matlab_path: "+ndi/+daq/metadatareader.m" matlab_last_sync_hash: "fe64a9f5" python_path: "ndi/daq/metadatareader/__init__.py" - python_class: "MetadataReader" + python_class: "ndi_daq_metadatareader" inherits: "ndi.ido" properties: @@ -48,7 +48,7 @@ classes: default: "None" output_arguments: - name: reader_obj - type_python: "MetadataReader" + type_python: "ndi_daq_metadatareader" decision_log: "Exact match." - name: readmetadata @@ -143,7 +143,7 @@ classes: matlab_path: "+ndi/+daq/+metadatareader/NewStimStims.m" matlab_last_sync_hash: "fe64a9f5" python_path: "ndi/daq/metadatareader/newstim_stims.py" - python_class: "NewStimStimsReader" + python_class: "ndi_daq_metadatareader_NewStimStims" inherits: "ndi.daq.metadatareader" properties: @@ -170,7 +170,7 @@ classes: default: "None" output_arguments: - name: reader_obj - type_python: "NewStimStimsReader" + type_python: "ndi_daq_metadatareader_NewStimStims" decision_log: "Exact match." - name: readmetadata @@ -223,7 +223,7 @@ classes: matlab_path: "+ndi/+daq/+metadatareader/NielsenLabStims.m" matlab_last_sync_hash: "fe64a9f5" python_path: "ndi/daq/metadatareader/nielsenlab_stims.py" - python_class: "NielsenLabStimsReader" + python_class: "ndi_daq_metadatareader_NielsenLabStims" inherits: "ndi.daq.metadatareader" properties: @@ -250,7 +250,7 @@ classes: default: "None" output_arguments: - name: reader_obj - type_python: "NielsenLabStimsReader" + type_python: "ndi_daq_metadatareader_NielsenLabStims" decision_log: "Exact match." - name: readmetadata diff --git a/src/ndi/daq/metadatareader/newstim_stims.py b/src/ndi/daq/metadatareader/newstim_stims.py index f15ef5c..b2cb879 100644 --- a/src/ndi/daq/metadatareader/newstim_stims.py +++ b/src/ndi/daq/metadatareader/newstim_stims.py @@ -14,10 +14,10 @@ from pathlib import Path from typing import Any -from ..metadatareader import MetadataReader +from ..metadatareader import ndi_daq_metadatareader -class NewStimStimsReader(MetadataReader): +class ndi_daq_metadatareader_NewStimStims(ndi_daq_metadatareader): """ Metadata reader for NewStim stimulus systems. @@ -29,7 +29,7 @@ class NewStimStimsReader(MetadataReader): and extracts parameters for each stimulus condition. Example: - >>> reader = NewStimStimsReader() + >>> reader = ndi_daq_metadatareader_NewStimStims() >>> params = reader.readmetadata(['data.rhd', 'stims.mat']) """ @@ -165,4 +165,4 @@ def _extract_script_parameters(script_data: Any) -> list[dict[str, Any]]: return parameters def __repr__(self) -> str: - return f"NewStimStimsReader(id='{self.id[:8]}...')" + return f"ndi_daq_metadatareader_NewStimStims(id='{self.id[:8]}...')" diff --git a/src/ndi/daq/metadatareader/nielsenlab_stims.py b/src/ndi/daq/metadatareader/nielsenlab_stims.py index bd9ee7e..9ca4134 100644 --- a/src/ndi/daq/metadatareader/nielsenlab_stims.py +++ b/src/ndi/daq/metadatareader/nielsenlab_stims.py @@ -1,7 +1,7 @@ """ -ndi.daq.metadatareader.nielsenlab_stims - Nielsen Lab stimulus metadata reader. +ndi.daq.metadatareader.nielsenlab_stims - Nielsen ndi_gui_Lab stimulus metadata reader. -Reads stimulus parameters from Nielsen Lab .mat files containing +Reads stimulus parameters from Nielsen ndi_gui_Lab .mat files containing an 'Analyzer' structure with stimulus conditions and trial ordering. MATLAB equivalent: src/ndi/+ndi/+daq/+metadatareader/NielsenLabStims.m @@ -13,12 +13,12 @@ from pathlib import Path from typing import Any -from ..metadatareader import MetadataReader +from ..metadatareader import ndi_daq_metadatareader -class NielsenLabStimsReader(MetadataReader): +class ndi_daq_metadatareader_NielsenLabStims(ndi_daq_metadatareader): """ - Metadata reader for Nielsen Lab stimulus systems. + Metadata reader for Nielsen ndi_gui_Lab stimulus systems. Reads stimulus parameters from .mat files containing an 'Analyzer' structure. The Analyzer has: @@ -27,7 +27,7 @@ class NielsenLabStimsReader(MetadataReader): - loops.conds: Condition-specific parameter values Example: - >>> reader = NielsenLabStimsReader() + >>> reader = ndi_daq_metadatareader_NielsenLabStims() >>> params = reader.readmetadata(['data.rhd', 'analyzer.mat']) """ @@ -52,7 +52,7 @@ def readmetadata( epochfiles: list[str], ) -> list[dict[str, Any]]: """ - Read stimulus metadata from Nielsen Lab files. + Read stimulus metadata from Nielsen ndi_gui_Lab files. Args: epochfiles: List of file paths for the epoch @@ -84,7 +84,7 @@ def _find_analyzer_file(self, epochfiles: list[str]) -> str | None: def _read_analyzer_mat(self, filepath: str) -> list[dict[str, Any]]: """ - Read stimulus parameters from a Nielsen Lab analyzer .mat file. + Read stimulus parameters from a Nielsen ndi_gui_Lab analyzer .mat file. Args: filepath: Path to analyzer.mat file @@ -96,7 +96,7 @@ def _read_analyzer_mat(self, filepath: str) -> list[dict[str, Any]]: from scipy.io import loadmat except ImportError as exc: raise ImportError( - "scipy is required to read Nielsen Lab .mat files. " + "scipy is required to read Nielsen ndi_gui_Lab .mat files. " "Install with: pip install scipy" ) from exc @@ -241,4 +241,4 @@ def extract_display_order(analyzer: Any) -> list[int]: return [] def __repr__(self) -> str: - return f"NielsenLabStimsReader(id='{self.id[:8]}...')" + return f"ndi_daq_metadatareader_NielsenLabStims(id='{self.id[:8]}...')" diff --git a/src/ndi/daq/mfdaq.py b/src/ndi/daq/mfdaq.py index 5a8b82a..f36eeb7 100644 --- a/src/ndi/daq/mfdaq.py +++ b/src/ndi/daq/mfdaq.py @@ -1,7 +1,7 @@ """ ndi.daq.mfdaq - Multi-function DAQ reader class. -This module provides the MFDAQReader class for reading data from +This module provides the ndi_daq_reader_mfdaq class for reading data from multi-function data acquisition systems that sample various data types. """ @@ -14,8 +14,8 @@ import numpy as np -from ..time import DEV_LOCAL_TIME, ClockType -from .reader_base import DAQReader +from ..time import DEV_LOCAL_TIME, ndi_time_clocktype +from .reader_base import ndi_daq_reader class ChannelType(Enum): @@ -112,7 +112,7 @@ def standardize_channel_types(channel_types: list[str]) -> list[str]: return [standardize_channel_type(ct) for ct in channel_types] -class MFDAQReader(DAQReader): +class ndi_daq_reader_mfdaq(ndi_daq_reader): """ Multi-function DAQ reader for various data types. @@ -138,7 +138,7 @@ class MFDAQReader(DAQReader): - 'text' / 'tx': Text markers Example: - >>> class IntanReader(MFDAQReader): + >>> class ndi_daq_reader_mfdaq_intan(ndi_daq_reader_mfdaq): ... def getchannelsepoch(self, epochfiles): ... # Return channels from Intan file ... ... @@ -165,7 +165,7 @@ def __init__( document: Any | None = None, ): """ - Create a new MFDAQReader. + Create a new ndi_daq_reader_mfdaq. Args: identifier: Optional identifier @@ -177,7 +177,7 @@ def __init__( def epochclock( self, epochfiles: list[str], - ) -> list[ClockType]: + ) -> list[ndi_time_clocktype]: """ Return the clock types for an epoch. @@ -560,7 +560,7 @@ def getchannelsepoch_ingested( Args: epochfiles: List of file paths (starting with epochid://) - session: Session object with database access + session: ndi_session object with database access Returns: List of ChannelInfo objects @@ -610,7 +610,7 @@ def readchannels_epochsamples_ingested( epochfiles: Files for this epoch (starting with epochid://) s0: Start sample (1-indexed) s1: End sample (1-indexed) - session: Session object with database access + session: ndi_session object with database access Returns: Array with shape (num_samples, num_channels) @@ -662,7 +662,7 @@ def samplerate_ingested( epochfiles: Files for this epoch (starting with epochid://) channeltype: Type(s) of channel channel: Channel number(s) - session: Session object with database access + session: ndi_session object with database access Returns: Array of sample rates @@ -704,7 +704,7 @@ def epochsamples2times_ingested( channel: Channel number(s) epochfiles: Files for this epoch (starting with epochid://) samples: Sample indices (1-indexed) - session: Session object with database access + session: ndi_session object with database access Returns: Time values @@ -750,7 +750,7 @@ def epochtimes2samples_ingested( channel: Channel number(s) epochfiles: Files for this epoch (starting with epochid://) times: Time values - session: Session object with database access + session: ndi_session object with database access Returns: Sample indices (1-indexed) diff --git a/src/ndi/daq/ndi_matlab_python_bridge.yaml b/src/ndi/daq/ndi_matlab_python_bridge.yaml index 0e74eea..3f8d658 100644 --- a/src/ndi/daq/ndi_matlab_python_bridge.yaml +++ b/src/ndi/daq/ndi_matlab_python_bridge.yaml @@ -120,7 +120,7 @@ classes: matlab_path: "+ndi/+daq/daqsystemstring.m" matlab_last_sync_hash: "fe64a9f5" python_path: "ndi/daq/daqsystemstring.py" - python_class: "DAQSystemString" + python_class: "ndi_daq_daqsystemstring" properties: - name: devicename @@ -144,7 +144,7 @@ classes: type_python: "str" output_arguments: - name: dss - type_python: "DAQSystemString" + type_python: "ndi_daq_daqsystemstring" decision_log: > MATLAB constructor parses in-place. Python uses classmethod factory. @@ -183,14 +183,14 @@ classes: decision_log: "Mapped to Python __eq__ dunder method." # ========================================================================= - # ndi.daq.reader (DAQReader base class) + # ndi.daq.reader (ndi_daq_reader base class) # ========================================================================= - name: reader type: class matlab_path: "+ndi/+daq/reader.m" matlab_last_sync_hash: "fe64a9f5" python_path: "ndi/daq/reader_base.py" - python_class: "DAQReader" + python_class: "ndi_daq_reader" inherits: "ndi.ido" methods: @@ -211,7 +211,7 @@ classes: default: "None" output_arguments: - name: reader_obj - type_python: "DAQReader" + type_python: "ndi_daq_reader" decision_log: "Exact match." - name: epochclock @@ -221,7 +221,7 @@ classes: type_python: "list[str]" output_arguments: - name: ec - type_python: "list[ClockType]" + type_python: "list[ndi_time_clocktype]" decision_log: "Exact match. Base class returns [NO_TIME]." - name: t0_t1 @@ -269,7 +269,7 @@ classes: type_python: "Any" output_arguments: - name: ec - type_python: "list[ClockType]" + type_python: "list[ndi_time_clocktype]" decision_log: "Exact match." - name: t0_t1_ingested @@ -337,14 +337,14 @@ classes: decision_log: "Mapped to Python __eq__ dunder method." # ========================================================================= - # ndi.daq.reader.mfdaq_reader (MFDAQReader class) + # ndi.daq.reader.mfdaq_reader (ndi_daq_reader_mfdaq class) # ========================================================================= - name: mfdaq_reader type: class matlab_path: "+ndi/+daq/+reader/mfdaq.m" matlab_last_sync_hash: "41e84fed" python_path: "ndi/daq/mfdaq.py" - python_class: "MFDAQReader" + python_class: "ndi_daq_reader_mfdaq" inherits: "ndi.daq.reader" methods: @@ -355,7 +355,7 @@ classes: type_python: "list[str]" output_arguments: - name: ec - type_python: "list[ClockType]" + type_python: "list[ndi_time_clocktype]" decision_log: "Exact match. Returns [DEV_LOCAL_TIME]." - name: t0_t1 @@ -655,7 +655,7 @@ classes: matlab_path: "+ndi/+daq/system.m" matlab_last_sync_hash: "fe64a9f5" python_path: "ndi/daq/system.py" - python_class: "DAQSystem" + python_class: "ndi_daq_system" inherits: "ndi.ido" properties: @@ -673,7 +673,7 @@ classes: - name: daqreader type_matlab: "ndi.daq.reader" - type_python: "DAQReader | None" + type_python: "ndi_daq_reader | None" access: "property (read-only)" decision_log: "Exact match." @@ -703,7 +703,7 @@ classes: default: "None" - name: daqreader type_matlab: "ndi.daq.reader" - type_python: "DAQReader | None" + type_python: "ndi_daq_reader | None" default: "None" - name: daqmetadatareaders type_matlab: "cell array" @@ -720,7 +720,7 @@ classes: default: "None" output_arguments: - name: sys_obj - type_python: "DAQSystem" + type_python: "ndi_daq_system" decision_log: > Exact match. Python adds session/document kwargs for loading from database. @@ -732,7 +732,7 @@ classes: type_python: "list[Any]" output_arguments: - name: sys_obj - type_python: "DAQSystem" + type_python: "ndi_daq_system" decision_log: "Exact match. Returns self for chaining." - name: set_session @@ -742,7 +742,7 @@ classes: type_python: "Any" output_arguments: - name: sys_obj - type_python: "DAQSystem" + type_python: "ndi_daq_system" decision_log: "Exact match. Returns self for chaining." - name: epochclock @@ -752,7 +752,7 @@ classes: type_python: "int" output_arguments: - name: ec - type_python: "list[ClockType]" + type_python: "list[ndi_time_clocktype]" decision_log: > Semantic Parity: epoch_number is 1-indexed (user concept). Base class returns [NO_TIME]. @@ -898,7 +898,7 @@ classes: matlab_path: "+ndi/+daq/+system/mfdaq.m" matlab_last_sync_hash: "9f584d8a" python_path: "ndi/daq/system_mfdaq.py" - python_class: "DAQSystemMFDAQ" + python_class: "ndi_daq_system_mfdaq" inherits: "ndi.daq.system" methods: @@ -909,7 +909,7 @@ classes: type_python: "int" output_arguments: - name: ec - type_python: "list[ClockType]" + type_python: "list[ndi_time_clocktype]" decision_log: > Semantic Parity: epoch_number is 1-indexed (user concept). Returns [DEV_LOCAL_TIME]. diff --git a/src/ndi/daq/reader/__init__.py b/src/ndi/daq/reader/__init__.py index 00918f4..62efa91 100644 --- a/src/ndi/daq/reader/__init__.py +++ b/src/ndi/daq/reader/__init__.py @@ -5,22 +5,27 @@ for various data acquisition systems. Classes: - SpikeInterfaceReader: Reader using spikeinterface library + ndi_daq_reader_SpikeInterfaceReader: Reader using spikeinterface library NeoReader: Reader using neo library Example: - >>> from ndi.daq.reader import SpikeInterfaceReader - >>> reader = SpikeInterfaceReader() + >>> from ndi.daq.reader import ndi_daq_reader_SpikeInterfaceReader + >>> reader = ndi_daq_reader_SpikeInterfaceReader() >>> channels = reader.getchannelsepoch(['data.rhd']) """ -from .mfdaq import BlackrockReader, CEDSpike2Reader, IntanReader, SpikeGadgetsReader -from .spikeinterface_adapter import SpikeInterfaceReader +from .mfdaq import ( + ndi_daq_reader_mfdaq_blackrock, + ndi_daq_reader_mfdaq_cedspike2, + ndi_daq_reader_mfdaq_intan, + ndi_daq_reader_mfdaq_spikegadgets, +) +from .spikeinterface_adapter import ndi_daq_reader_SpikeInterfaceReader __all__ = [ - "SpikeInterfaceReader", - "IntanReader", - "BlackrockReader", - "CEDSpike2Reader", - "SpikeGadgetsReader", + "ndi_daq_reader_SpikeInterfaceReader", + "ndi_daq_reader_mfdaq_intan", + "ndi_daq_reader_mfdaq_blackrock", + "ndi_daq_reader_mfdaq_cedspike2", + "ndi_daq_reader_mfdaq_spikegadgets", ] diff --git a/src/ndi/daq/reader/mfdaq/__init__.py b/src/ndi/daq/reader/mfdaq/__init__.py index eebffa7..f20ddf5 100644 --- a/src/ndi/daq/reader/mfdaq/__init__.py +++ b/src/ndi/daq/reader/mfdaq/__init__.py @@ -1,22 +1,22 @@ """ ndi.daq.reader.mfdaq - Format-specific MFDAQ reader subclasses. -Thin wrappers around SpikeInterfaceReader that set format-specific defaults. +Thin wrappers around ndi_daq_reader_SpikeInterfaceReader that set format-specific defaults. Each reader class: - Sets the correct ndi_daqreader_class for document serialization - Provides the expected file extensions -- Delegates all data reading to SpikeInterfaceReader +- Delegates all data reading to ndi_daq_reader_SpikeInterfaceReader """ -from .blackrock import BlackrockReader -from .cedspike2 import CEDSpike2Reader -from .intan import IntanReader -from .spikegadgets import SpikeGadgetsReader +from .blackrock import ndi_daq_reader_mfdaq_blackrock +from .cedspike2 import ndi_daq_reader_mfdaq_cedspike2 +from .intan import ndi_daq_reader_mfdaq_intan +from .spikegadgets import ndi_daq_reader_mfdaq_spikegadgets __all__ = [ - "IntanReader", - "BlackrockReader", - "CEDSpike2Reader", - "SpikeGadgetsReader", + "ndi_daq_reader_mfdaq_intan", + "ndi_daq_reader_mfdaq_blackrock", + "ndi_daq_reader_mfdaq_cedspike2", + "ndi_daq_reader_mfdaq_spikegadgets", ] diff --git a/src/ndi/daq/reader/mfdaq/blackrock.py b/src/ndi/daq/reader/mfdaq/blackrock.py index e3465e0..f12f5e0 100644 --- a/src/ndi/daq/reader/mfdaq/blackrock.py +++ b/src/ndi/daq/reader/mfdaq/blackrock.py @@ -1,7 +1,7 @@ """ ndi.daq.reader.mfdaq.blackrock - Blackrock NSx/NEV reader. -Thin wrapper around SpikeInterfaceReader for Blackrock data files. +Thin wrapper around ndi_daq_reader_SpikeInterfaceReader for Blackrock data files. MATLAB equivalent: src/ndi/+ndi/+daq/+reader/+mfdaq/blackrock.m """ @@ -10,12 +10,12 @@ import logging -from ...mfdaq import ChannelInfo, MFDAQReader +from ...mfdaq import ChannelInfo, ndi_daq_reader_mfdaq logger = logging.getLogger(__name__) -class BlackrockReader(MFDAQReader): +class ndi_daq_reader_mfdaq_blackrock(ndi_daq_reader_mfdaq): """ Reader for Blackrock Microsystems NSx/NEV data files. @@ -31,9 +31,9 @@ def __init__(self, identifier=None, session=None, document=None): def _get_si_reader(self): try: - from ..spikeinterface_adapter import SpikeInterfaceReader + from ..spikeinterface_adapter import ndi_daq_reader_SpikeInterfaceReader - return SpikeInterfaceReader + return ndi_daq_reader_SpikeInterfaceReader except ImportError: return None @@ -44,7 +44,7 @@ def getchannelsepoch(self, epochfiles: list[str]) -> list[ChannelInfo]: try: return SI().getchannelsepoch(epochfiles) except Exception as exc: - logger.warning("BlackrockReader.getchannelsepoch failed: %s", exc) + logger.warning("ndi_daq_reader_mfdaq_blackrock.getchannelsepoch failed: %s", exc) return [] def readchannels_epochsamples(self, channeltype, channel, epochfiles, s0, s1): @@ -60,4 +60,4 @@ def samplerate(self, epochfiles, channeltype, channel): return SI().samplerate(epochfiles, channeltype, channel) def __repr__(self): - return f"BlackrockReader(id={self.id[:8]}...)" + return f"ndi_daq_reader_mfdaq_blackrock(id={self.id[:8]}...)" diff --git a/src/ndi/daq/reader/mfdaq/cedspike2.py b/src/ndi/daq/reader/mfdaq/cedspike2.py index eca7c51..755f152 100644 --- a/src/ndi/daq/reader/mfdaq/cedspike2.py +++ b/src/ndi/daq/reader/mfdaq/cedspike2.py @@ -1,7 +1,7 @@ """ ndi.daq.reader.mfdaq.cedspike2 - CED Spike2 SMR reader. -Thin wrapper around SpikeInterfaceReader for CED Spike2 data files. +Thin wrapper around ndi_daq_reader_SpikeInterfaceReader for CED Spike2 data files. MATLAB equivalent: src/ndi/+ndi/+daq/+reader/+mfdaq/cedspike2.m """ @@ -10,12 +10,12 @@ import logging -from ...mfdaq import ChannelInfo, MFDAQReader +from ...mfdaq import ChannelInfo, ndi_daq_reader_mfdaq logger = logging.getLogger(__name__) -class CEDSpike2Reader(MFDAQReader): +class ndi_daq_reader_mfdaq_cedspike2(ndi_daq_reader_mfdaq): """ Reader for CED Spike2 SMR files. @@ -31,9 +31,9 @@ def __init__(self, identifier=None, session=None, document=None): def _get_si_reader(self): try: - from ..spikeinterface_adapter import SpikeInterfaceReader + from ..spikeinterface_adapter import ndi_daq_reader_SpikeInterfaceReader - return SpikeInterfaceReader + return ndi_daq_reader_SpikeInterfaceReader except ImportError: return None @@ -44,7 +44,7 @@ def getchannelsepoch(self, epochfiles: list[str]) -> list[ChannelInfo]: try: return SI().getchannelsepoch(epochfiles) except Exception as exc: - logger.warning("CEDSpike2Reader.getchannelsepoch failed: %s", exc) + logger.warning("ndi_daq_reader_mfdaq_cedspike2.getchannelsepoch failed: %s", exc) return [] def readchannels_epochsamples(self, channeltype, channel, epochfiles, s0, s1): @@ -60,4 +60,4 @@ def samplerate(self, epochfiles, channeltype, channel): return SI().samplerate(epochfiles, channeltype, channel) def __repr__(self): - return f"CEDSpike2Reader(id={self.id[:8]}...)" + return f"ndi_daq_reader_mfdaq_cedspike2(id={self.id[:8]}...)" diff --git a/src/ndi/daq/reader/mfdaq/intan.py b/src/ndi/daq/reader/mfdaq/intan.py index 109c1e5..52c2c79 100644 --- a/src/ndi/daq/reader/mfdaq/intan.py +++ b/src/ndi/daq/reader/mfdaq/intan.py @@ -1,7 +1,7 @@ """ ndi.daq.reader.mfdaq.intan - Intan RHD/RHS reader. -Thin wrapper around SpikeInterfaceReader for Intan data files. +Thin wrapper around ndi_daq_reader_SpikeInterfaceReader for Intan data files. Falls back gracefully if spikeinterface is not installed. MATLAB equivalent: src/ndi/+ndi/+daq/+reader/+mfdaq/intan.m @@ -16,10 +16,10 @@ logger = logging.getLogger(__name__) -from ...mfdaq import ChannelInfo, MFDAQReader +from ...mfdaq import ChannelInfo, ndi_daq_reader_mfdaq -class IntanReader(MFDAQReader): +class ndi_daq_reader_mfdaq_intan(ndi_daq_reader_mfdaq): """ Reader for Intan RHD/RHS data files. @@ -30,7 +30,7 @@ class IntanReader(MFDAQReader): File extensions: .rhd, .rhs Example: - >>> reader = IntanReader() + >>> reader = ndi_daq_reader_mfdaq_intan() >>> channels = reader.getchannelsepoch(['data.rhd']) """ @@ -47,11 +47,11 @@ def __init__( self._ndi_daqreader_class = self.NDI_DAQREADER_CLASS def _get_si_reader(self): - """Get SpikeInterfaceReader lazily.""" + """Get ndi_daq_reader_SpikeInterfaceReader lazily.""" try: - from ..spikeinterface_adapter import SpikeInterfaceReader + from ..spikeinterface_adapter import ndi_daq_reader_SpikeInterfaceReader - return SpikeInterfaceReader + return ndi_daq_reader_SpikeInterfaceReader except ImportError: return None @@ -63,7 +63,7 @@ def getchannelsepoch(self, epochfiles: list[str]) -> list[ChannelInfo]: reader = SI() return reader.getchannelsepoch(epochfiles) except Exception as exc: - logger.warning("IntanReader.getchannelsepoch failed: %s", exc) + logger.warning("ndi_daq_reader_mfdaq_intan.getchannelsepoch failed: %s", exc) return [] def readchannels_epochsamples( @@ -88,4 +88,4 @@ def samplerate(self, epochfiles, channeltype, channel) -> np.ndarray: return reader.samplerate(epochfiles, channeltype, channel) def __repr__(self) -> str: - return f"IntanReader(id={self.id[:8]}...)" + return f"ndi_daq_reader_mfdaq_intan(id={self.id[:8]}...)" diff --git a/src/ndi/daq/reader/mfdaq/ndi_matlab_python_bridge.yaml b/src/ndi/daq/reader/mfdaq/ndi_matlab_python_bridge.yaml index 4129774..1113d91 100644 --- a/src/ndi/daq/reader/mfdaq/ndi_matlab_python_bridge.yaml +++ b/src/ndi/daq/reader/mfdaq/ndi_matlab_python_bridge.yaml @@ -19,7 +19,7 @@ classes: matlab_path: "+ndi/+daq/+reader/+mfdaq/intan.m" matlab_last_sync_hash: "80385294" python_path: "ndi/daq/reader/mfdaq/intan.py" - python_class: "IntanReader" + python_class: "ndi_daq_reader_mfdaq_intan" inherits: "ndi.daq.reader.mfdaq_reader" properties: @@ -48,7 +48,7 @@ classes: default: "None" output_arguments: - name: reader_obj - type_python: "IntanReader" + type_python: "ndi_daq_reader_mfdaq_intan" decision_log: "Exact match." - name: getchannelsepoch @@ -59,7 +59,7 @@ classes: output_arguments: - name: channels type_python: "list[ChannelInfo]" - decision_log: "Exact match. Delegates to SpikeInterfaceReader." + decision_log: "Exact match. Delegates to ndi_daq_reader_SpikeInterfaceReader." - name: readchannels_epochsamples input_arguments: @@ -83,7 +83,7 @@ classes: type_python: "np.ndarray" decision_log: > Semantic Parity: channel and s0/s1 are 1-indexed. - Delegates to SpikeInterfaceReader. + Delegates to ndi_daq_reader_SpikeInterfaceReader. - name: samplerate input_arguments: @@ -99,7 +99,7 @@ classes: output_arguments: - name: sr type_python: "np.ndarray" - decision_log: "Exact match. Delegates to SpikeInterfaceReader." + decision_log: "Exact match. Delegates to ndi_daq_reader_SpikeInterfaceReader." # ========================================================================= # ndi.daq.reader.mfdaq.blackrock @@ -109,7 +109,7 @@ classes: matlab_path: "+ndi/+daq/+reader/+mfdaq/blackrock.m" matlab_last_sync_hash: "fe64a9f5" python_path: "ndi/daq/reader/mfdaq/blackrock.py" - python_class: "BlackrockReader" + python_class: "ndi_daq_reader_mfdaq_blackrock" inherits: "ndi.daq.reader.mfdaq_reader" properties: @@ -138,7 +138,7 @@ classes: default: "None" output_arguments: - name: reader_obj - type_python: "BlackrockReader" + type_python: "ndi_daq_reader_mfdaq_blackrock" decision_log: "Exact match." - name: getchannelsepoch @@ -149,7 +149,7 @@ classes: output_arguments: - name: channels type_python: "list[ChannelInfo]" - decision_log: "Exact match. Delegates to SpikeInterfaceReader." + decision_log: "Exact match. Delegates to ndi_daq_reader_SpikeInterfaceReader." - name: readchannels_epochsamples input_arguments: @@ -168,7 +168,7 @@ classes: type_python: "np.ndarray" decision_log: > Semantic Parity: channel and s0/s1 are 1-indexed. - Delegates to SpikeInterfaceReader. + Delegates to ndi_daq_reader_SpikeInterfaceReader. - name: samplerate input_arguments: @@ -181,7 +181,7 @@ classes: output_arguments: - name: sr type_python: "np.ndarray" - decision_log: "Exact match. Delegates to SpikeInterfaceReader." + decision_log: "Exact match. Delegates to ndi_daq_reader_SpikeInterfaceReader." # ========================================================================= # ndi.daq.reader.mfdaq.cedspike2 @@ -191,7 +191,7 @@ classes: matlab_path: "+ndi/+daq/+reader/+mfdaq/cedspike2.m" matlab_last_sync_hash: "fe64a9f5" python_path: "ndi/daq/reader/mfdaq/cedspike2.py" - python_class: "CEDSpike2Reader" + python_class: "ndi_daq_reader_mfdaq_cedspike2" inherits: "ndi.daq.reader.mfdaq_reader" properties: @@ -220,7 +220,7 @@ classes: default: "None" output_arguments: - name: reader_obj - type_python: "CEDSpike2Reader" + type_python: "ndi_daq_reader_mfdaq_cedspike2" decision_log: "Exact match." - name: getchannelsepoch @@ -230,7 +230,7 @@ classes: output_arguments: - name: channels type_python: "list[ChannelInfo]" - decision_log: "Exact match. Delegates to SpikeInterfaceReader." + decision_log: "Exact match. Delegates to ndi_daq_reader_SpikeInterfaceReader." - name: readchannels_epochsamples input_arguments: @@ -249,7 +249,7 @@ classes: type_python: "np.ndarray" decision_log: > Semantic Parity: channel and s0/s1 are 1-indexed. - Delegates to SpikeInterfaceReader. + Delegates to ndi_daq_reader_SpikeInterfaceReader. - name: samplerate input_arguments: @@ -262,7 +262,7 @@ classes: output_arguments: - name: sr type_python: "np.ndarray" - decision_log: "Exact match. Delegates to SpikeInterfaceReader." + decision_log: "Exact match. Delegates to ndi_daq_reader_SpikeInterfaceReader." # ========================================================================= # ndi.daq.reader.mfdaq.spikegadgets @@ -272,7 +272,7 @@ classes: matlab_path: "+ndi/+daq/+reader/+mfdaq/spikegadgets.m" matlab_last_sync_hash: "fe64a9f5" python_path: "ndi/daq/reader/mfdaq/spikegadgets.py" - python_class: "SpikeGadgetsReader" + python_class: "ndi_daq_reader_mfdaq_spikegadgets" inherits: "ndi.daq.reader.mfdaq_reader" properties: @@ -301,7 +301,7 @@ classes: default: "None" output_arguments: - name: reader_obj - type_python: "SpikeGadgetsReader" + type_python: "ndi_daq_reader_mfdaq_spikegadgets" decision_log: "Exact match." - name: getchannelsepoch @@ -311,7 +311,7 @@ classes: output_arguments: - name: channels type_python: "list[ChannelInfo]" - decision_log: "Exact match. Delegates to SpikeInterfaceReader." + decision_log: "Exact match. Delegates to ndi_daq_reader_SpikeInterfaceReader." - name: readchannels_epochsamples input_arguments: @@ -330,7 +330,7 @@ classes: type_python: "np.ndarray" decision_log: > Semantic Parity: channel and s0/s1 are 1-indexed. - Delegates to SpikeInterfaceReader. + Delegates to ndi_daq_reader_SpikeInterfaceReader. - name: samplerate input_arguments: @@ -343,4 +343,4 @@ classes: output_arguments: - name: sr type_python: "np.ndarray" - decision_log: "Exact match. Delegates to SpikeInterfaceReader." + decision_log: "Exact match. Delegates to ndi_daq_reader_SpikeInterfaceReader." diff --git a/src/ndi/daq/reader/mfdaq/spikegadgets.py b/src/ndi/daq/reader/mfdaq/spikegadgets.py index 7f2ef37..e768cc9 100644 --- a/src/ndi/daq/reader/mfdaq/spikegadgets.py +++ b/src/ndi/daq/reader/mfdaq/spikegadgets.py @@ -1,7 +1,7 @@ """ ndi.daq.reader.mfdaq.spikegadgets - SpikeGadgets reader. -Thin wrapper around SpikeInterfaceReader for SpikeGadgets data files. +Thin wrapper around ndi_daq_reader_SpikeInterfaceReader for SpikeGadgets data files. MATLAB equivalent: src/ndi/+ndi/+daq/+reader/+mfdaq/spikegadgets.m """ @@ -10,12 +10,12 @@ import logging -from ...mfdaq import ChannelInfo, MFDAQReader +from ...mfdaq import ChannelInfo, ndi_daq_reader_mfdaq logger = logging.getLogger(__name__) -class SpikeGadgetsReader(MFDAQReader): +class ndi_daq_reader_mfdaq_spikegadgets(ndi_daq_reader_mfdaq): """ Reader for SpikeGadgets .rec files. @@ -31,9 +31,9 @@ def __init__(self, identifier=None, session=None, document=None): def _get_si_reader(self): try: - from ..spikeinterface_adapter import SpikeInterfaceReader + from ..spikeinterface_adapter import ndi_daq_reader_SpikeInterfaceReader - return SpikeInterfaceReader + return ndi_daq_reader_SpikeInterfaceReader except ImportError: return None @@ -44,7 +44,7 @@ def getchannelsepoch(self, epochfiles: list[str]) -> list[ChannelInfo]: try: return SI().getchannelsepoch(epochfiles) except Exception as exc: - logger.warning("SpikeGadgetsReader.getchannelsepoch failed: %s", exc) + logger.warning("ndi_daq_reader_mfdaq_spikegadgets.getchannelsepoch failed: %s", exc) return [] def readchannels_epochsamples(self, channeltype, channel, epochfiles, s0, s1): @@ -60,4 +60,4 @@ def samplerate(self, epochfiles, channeltype, channel): return SI().samplerate(epochfiles, channeltype, channel) def __repr__(self): - return f"SpikeGadgetsReader(id={self.id[:8]}...)" + return f"ndi_daq_reader_mfdaq_spikegadgets(id={self.id[:8]}...)" diff --git a/src/ndi/daq/reader/ndi_matlab_python_bridge.yaml b/src/ndi/daq/reader/ndi_matlab_python_bridge.yaml index 74f704b..f0aad63 100644 --- a/src/ndi/daq/reader/ndi_matlab_python_bridge.yaml +++ b/src/ndi/daq/reader/ndi_matlab_python_bridge.yaml @@ -32,17 +32,17 @@ functions: classes: # ========================================================================= - # ndi.daq.reader.SpikeInterfaceReader + # ndi.daq.reader.ndi_daq_reader_SpikeInterfaceReader # ========================================================================= - - name: SpikeInterfaceReader + - name: ndi_daq_reader_SpikeInterfaceReader type: class matlab_path: null python_path: "ndi/daq/reader/spikeinterface_adapter.py" - python_class: "SpikeInterfaceReader" + python_class: "ndi_daq_reader_SpikeInterfaceReader" inherits: "ndi.daq.reader.mfdaq_reader" methods: - - name: SpikeInterfaceReader + - name: ndi_daq_reader_SpikeInterfaceReader kind: constructor input_arguments: - name: stream_id @@ -59,7 +59,7 @@ classes: default: "None" output_arguments: - name: reader_obj - type_python: "SpikeInterfaceReader" + type_python: "ndi_daq_reader_SpikeInterfaceReader" decision_log: > Python-specific class. No direct MATLAB equivalent. Uses spikeinterface library for multi-format data access. @@ -70,7 +70,7 @@ classes: type_python: "list[str]" output_arguments: - name: ec - type_python: "list[ClockType]" + type_python: "list[ndi_time_clocktype]" decision_log: "Returns [DEV_LOCAL_TIME]." - name: t0_t1 @@ -164,4 +164,4 @@ classes: output_arguments: - name: doc type_python: "Any" - decision_log: "Creates document with class name 'SpikeInterfaceReader'." + decision_log: "Creates document with class name 'ndi_daq_reader_SpikeInterfaceReader'." diff --git a/src/ndi/daq/reader/spikeinterface_adapter.py b/src/ndi/daq/reader/spikeinterface_adapter.py index 6eb670a..cef5e0a 100644 --- a/src/ndi/daq/reader/spikeinterface_adapter.py +++ b/src/ndi/daq/reader/spikeinterface_adapter.py @@ -19,8 +19,8 @@ import numpy as np -from ...time import DEV_LOCAL_TIME, ClockType -from ..mfdaq import ChannelInfo, MFDAQReader, standardize_channel_type +from ...time import DEV_LOCAL_TIME, ndi_time_clocktype +from ..mfdaq import ChannelInfo, ndi_daq_reader_mfdaq, standardize_channel_type # Try to import spikeinterface try: @@ -83,12 +83,12 @@ def _get_extractor_for_file(filepath: str) -> Any | None: return None -class SpikeInterfaceReader(MFDAQReader): +class ndi_daq_reader_SpikeInterfaceReader(ndi_daq_reader_mfdaq): """ DAQ reader using spikeinterface for data access. This reader wraps the spikeinterface library to provide access - to many neuroscience data formats. It implements the MFDAQReader + to many neuroscience data formats. It implements the ndi_daq_reader_mfdaq interface for seamless integration with NDI. Supported formats include: @@ -102,7 +102,7 @@ class SpikeInterfaceReader(MFDAQReader): stream_id: Stream ID for multi-stream formats Example: - >>> reader = SpikeInterfaceReader() + >>> reader = ndi_daq_reader_SpikeInterfaceReader() >>> channels = reader.getchannelsepoch(['recording.rhd']) >>> data = reader.readchannels_epochsamples('ai', [1, 2], ['recording.rhd'], 0, 1000) """ @@ -115,7 +115,7 @@ def __init__( document: Any | None = None, ): """ - Create a SpikeInterfaceReader. + Create a ndi_daq_reader_SpikeInterfaceReader. Args: stream_id: Stream ID for multi-stream formats (e.g., Intan) @@ -125,7 +125,7 @@ def __init__( """ if not HAS_SPIKEINTERFACE: raise ImportError( - "spikeinterface is required for SpikeInterfaceReader. " + "spikeinterface is required for ndi_daq_reader_SpikeInterfaceReader. " "Install with: pip install spikeinterface" ) @@ -155,7 +155,7 @@ def _get_recording(self, epochfiles: list[str]) -> Any | None: def epochclock( self, epochfiles: list[str], - ) -> list[ClockType]: + ) -> list[ndi_time_clocktype]: """Return clock types for epoch. Returns DEV_LOCAL_TIME.""" return [DEV_LOCAL_TIME] @@ -441,12 +441,12 @@ def underlying_datatype( def newdocument(self) -> Any: """Create document for this reader.""" - from ...document import Document + from ...document import ndi_document - doc = Document( + doc = ndi_document( "daq/daqreader", **{ - "daqreader.ndi_daqreader_class": "SpikeInterfaceReader", + "daqreader.ndi_daqreader_class": "ndi_daq_reader_SpikeInterfaceReader", "base.id": self.id, }, ) diff --git a/src/ndi/daq/reader_base.py b/src/ndi/daq/reader_base.py index 2ba8e5b..e1f3564 100644 --- a/src/ndi/daq/reader_base.py +++ b/src/ndi/daq/reader_base.py @@ -1,7 +1,7 @@ """ ndi.daq.reader_base - Abstract base class for DAQ readers. -This module provides the DAQReader abstract base class that defines +This module provides the ndi_daq_reader abstract base class that defines the interface for reading data from data acquisition systems. """ @@ -12,27 +12,27 @@ import numpy as np -from ..ido import Ido -from ..time import NO_TIME, ClockType +from ..ido import ndi_ido +from ..time import NO_TIME, ndi_time_clocktype -class DAQReader(Ido, ABC): +class ndi_daq_reader(ndi_ido, ABC): """ Abstract base class for DAQ readers. - DAQReader defines the interface for objects that read samples from + ndi_daq_reader defines the interface for objects that read samples from data acquisition systems. Concrete implementations handle specific file formats (e.g., Intan, Blackrock, CED Spike2). - This class inherits from Ido to provide unique identification. + This class inherits from ndi_ido to provide unique identification. Attributes: identifier: Unique identifier for this reader instance Example: - >>> class MyReader(DAQReader): + >>> class MyReader(ndi_daq_reader): ... def epochclock(self, epochfiles): - ... return [ClockType.DEV_LOCAL_TIME] + ... return [ndi_time_clocktype.DEV_LOCAL_TIME] ... # ... implement other abstract methods """ @@ -43,7 +43,7 @@ def __init__( document: Any | None = None, ): """ - Create a new DAQReader. + Create a new ndi_daq_reader. Args: identifier: Optional identifier (generated if not provided) @@ -62,7 +62,7 @@ def __init__( def epochclock( self, epochfiles: list[str], - ) -> list[ClockType]: + ) -> list[ndi_time_clocktype]: """ Return the clock types for an epoch. @@ -70,10 +70,10 @@ def epochclock( epochfiles: List of file paths for the epoch Returns: - List of ClockType objects available for this epoch. + List of ndi_time_clocktype objects available for this epoch. The base class returns [NO_TIME]. - See also: ClockType + See also: ndi_time_clocktype """ return [NO_TIME] @@ -109,23 +109,23 @@ def getingesteddocument( Args: epochfiles: List of file paths (starting with epochid://) - session: Session object with database access + session: ndi_session object with database access Returns: - Document containing the ingested epoch data + ndi_document containing the ingested epoch data Raises: AssertionError: If not exactly one document found """ - from ..file.navigator import FileNavigator - from ..query import Query + from ..file.navigator import ndi_file_navigator + from ..query import ndi_query - epochid = FileNavigator.ingestedfiles_epochid(epochfiles) + epochid = ndi_file_navigator.ingestedfiles_epochid(epochfiles) q = ( - Query("").isa("daqreader_epochdata_ingested") - & Query("").depends_on("daqreader_id", self.id) - & (Query("epochid.epochid") == epochid) + ndi_query("").isa("daqreader_epochdata_ingested") + & ndi_query("").depends_on("daqreader_id", self.id) + & (ndi_query("epochid.epochid") == epochid) ) docs = session.database_search(q) @@ -141,15 +141,15 @@ def ingested2epochs_t0t1_epochclock( Create maps of all ingested epochs to t0t1 and epochclock. Args: - session: Session object with database access + session: ndi_session object with database access Returns: Dict with 't0t1' and 'epochclock' keys, each mapping epoch_id to the respective values """ - from ..query import Query + from ..query import ndi_query - q = Query("").isa("daqreader_epochdata_ingested") & Query("").depends_on( + q = ndi_query("").isa("daqreader_epochdata_ingested") & ndi_query("").depends_on( "daqreader_id", self.id ) d_ingested = session.database_search(q) @@ -165,7 +165,7 @@ def ingested2epochs_t0t1_epochclock( # Extract epoch clock ec_list = [] for ec_str in et.get("epochclock", []): - ec_list.append(ClockType(ec_str) if isinstance(ec_str, str) else ec_str) + ec_list.append(ndi_time_clocktype(ec_str) if isinstance(ec_str, str) else ec_str) epochclock_map[epochid] = ec_list # Extract t0_t1 @@ -186,18 +186,18 @@ def epochclock_ingested( self, epochfiles: list[str], session: Any, - ) -> list[ClockType]: + ) -> list[ndi_time_clocktype]: """ Return the clock types for an ingested epoch. Args: epochfiles: List of file paths (starting with epochid://) - session: Session object with database access + session: ndi_session object with database access Returns: - List of ClockType objects available for this epoch + List of ndi_time_clocktype objects available for this epoch - See also: epochclock, ClockType + See also: epochclock, ndi_time_clocktype """ doc = self.getingesteddocument(epochfiles, session) et = doc.document_properties.daqreader_epochdata_ingested.epochtable @@ -205,7 +205,7 @@ def epochclock_ingested( ec_list = [] for ec_str in et.get("epochclock", []): if isinstance(ec_str, str): - ec_list.append(ClockType(ec_str)) + ec_list.append(ndi_time_clocktype(ec_str)) else: ec_list.append(ec_str) @@ -221,7 +221,7 @@ def t0_t1_ingested( Args: epochfiles: List of file paths (starting with epochid://) - session: Session object with database access + session: ndi_session object with database access Returns: List of (t0, t1) tuples for each clock type @@ -275,16 +275,16 @@ def ingest_epochfiles( epoch_id: Unique identifier for this epoch Returns: - Document object describing the ingested data + ndi_document object describing the ingested data Note: The returned document is not added to any database. """ - from ..document import Document + from ..document import ndi_document # Get epoch clock and t0_t1 ec = self.epochclock(epochfiles) - ec_strings = [c.value if isinstance(c, ClockType) else str(c) for c in ec] + ec_strings = [c.value if isinstance(c, ndi_time_clocktype) else str(c) for c in ec] t0t1 = self.t0_t1(epochfiles) epochtable = { @@ -292,7 +292,7 @@ def ingest_epochfiles( "t0_t1": t0t1, } - doc = Document( + doc = ndi_document( "ingestion/daqreader_epochdata_ingested", daqreader_epochdata_ingested={"epochtable": epochtable}, epochid={"epochid": epoch_id}, @@ -303,17 +303,19 @@ def ingest_epochfiles( def newdocument(self) -> Any: """ - Create a new document for this DAQReader. + Create a new document for this ndi_daq_reader. Returns: - Document representing this reader + ndi_document representing this reader """ - from ..document import Document + from ..document import ndi_document - doc = Document( + doc = ndi_document( "daq/daqreader", **{ - "daqreader.ndi_daqreader_class": self.__class__.__name__, + "daqreader.ndi_daqreader_class": getattr( + self, "NDI_DAQREADER_CLASS", self.__class__.__name__ + ), "base.id": self.id, }, ) @@ -321,18 +323,18 @@ def newdocument(self) -> Any: def searchquery(self) -> Any: """ - Create a search query for this DAQReader. + Create a search query for this ndi_daq_reader. Returns: - Query object for finding this reader + ndi_query object for finding this reader """ - from ..query import Query + from ..query import ndi_query - return Query("base.id") == self.id + return ndi_query("base.id") == self.id def __eq__(self, other: Any) -> bool: """Test equality by class and ID.""" - if not isinstance(other, DAQReader): + if not isinstance(other, ndi_daq_reader): return False return self.__class__.__name__ == other.__class__.__name__ and self.id == other.id diff --git a/src/ndi/daq/system.py b/src/ndi/daq/system.py index c3b0ede..3eabc46 100644 --- a/src/ndi/daq/system.py +++ b/src/ndi/daq/system.py @@ -1,7 +1,7 @@ """ ndi.daq.system - DAQ system class combining navigator, reader, and metadata. -This module provides the DAQSystem class that combines file navigation, +This module provides the ndi_daq_system class that combines file navigation, data reading, and metadata reading for a complete data acquisition system. """ @@ -12,21 +12,21 @@ import numpy as np -from ..ido import Ido -from ..time import NO_TIME, ClockType -from .reader_base import DAQReader +from ..ido import ndi_ido +from ..time import NO_TIME, ndi_time_clocktype +from .reader_base import ndi_daq_reader logger = logging.getLogger(__name__) -class DAQSystem(Ido): +class ndi_daq_system(ndi_ido): """ Complete data acquisition system. - DAQSystem combines: - - FileNavigator: Finds and organizes data files - - DAQReader: Reads data from files - - MetadataReader(s): Read stimulus/experiment metadata + ndi_daq_system combines: + - ndi_file_navigator: Finds and organizes data files + - ndi_daq_reader: Reads data from files + - ndi_daq_metadatareader(s): Read stimulus/experiment metadata This provides a unified interface for accessing experimental data organized by epochs. @@ -38,30 +38,32 @@ class DAQSystem(Ido): daqmetadatareaders: List of metadata readers Example: - >>> from ndi.daq import DAQSystem - >>> from ndi.file import FileNavigator - >>> from ndi.daq.reader import IntanReader + >>> from ndi.daq import ndi_daq_system + >>> from ndi.file import ndi_file_navigator + >>> from ndi.daq.reader import ndi_daq_reader_mfdaq_intan >>> - >>> nav = FileNavigator(session, '*.rhd') - >>> reader = IntanReader() - >>> sys = DAQSystem('my_daq', nav, reader) + >>> nav = ndi_file_navigator(session, '*.rhd') + >>> reader = ndi_daq_reader_mfdaq_intan() + >>> sys = ndi_daq_system('my_daq', nav, reader) >>> >>> # Get epoch table >>> et = sys.epochtable() """ + NDI_DAQSYSTEM_CLASS = "ndi.daq.system.mfdaq" + def __init__( self, name: str = "", filenavigator: Any | None = None, - daqreader: DAQReader | None = None, + daqreader: ndi_daq_reader | None = None, daqmetadatareaders: list[Any] | None = None, identifier: str | None = None, session: Any | None = None, document: Any | None = None, ): """ - Create a new DAQSystem. + Create a new ndi_daq_system. Args: name: Name for this DAQ system @@ -87,11 +89,11 @@ def __init__( self._session = session # Validate reader - if daqreader is not None and not isinstance(daqreader, DAQReader): - raise TypeError("daqreader must be a DAQReader instance") + if daqreader is not None and not isinstance(daqreader, ndi_daq_reader): + raise TypeError("daqreader must be a ndi_daq_reader instance") def _load_from_document(self, session: Any, document: Any) -> None: - """Load DAQSystem from a document.""" + """Load ndi_daq_system from a document.""" doc_props = getattr(document, "document_properties", document) # Extract basic properties using dict access @@ -106,15 +108,15 @@ def _load_from_document(self, session: Any, document: Any) -> None: filenavigator_id = document.dependency_value("filenavigator_id", error_if_not_found=False) # Load reader and navigator from database - from ..query import Query + from ..query import ndi_query reader_docs = [] if daqreader_id: - reader_docs = session.database_search(Query("base.id") == daqreader_id) + reader_docs = session.database_search(ndi_query("base.id") == daqreader_id) nav_docs = [] if filenavigator_id: - nav_docs = session.database_search(Query("base.id") == filenavigator_id) + nav_docs = session.database_search(ndi_query("base.id") == filenavigator_id) # Load metadata readers metadata_ids = ( @@ -122,12 +124,14 @@ def _load_from_document(self, session: Any, document: Any) -> None: ) metadata_readers = [] for mid in metadata_ids: - m_docs = session.database_search(Query("base.id") == mid) + m_docs = session.database_search(ndi_query("base.id") == mid) if len(m_docs) == 1: - from .metadatareader import MetadataReader + from .metadatareader import ndi_daq_metadatareader try: - metadata_readers.append(MetadataReader(session=session, document=m_docs[0])) + metadata_readers.append( + ndi_daq_metadatareader(session=session, document=m_docs[0]) + ) except Exception: pass self._daqmetadatareaders = metadata_readers @@ -145,27 +149,11 @@ def _load_from_document(self, session: Any, document: Any) -> None: "daqreader.ndi_daqreader_class", "" ) - # Map both Python class names and MATLAB class names - _READER_CLASSES = { - # Python class names - "IntanReader": "ndi.daq.reader.mfdaq.intan.IntanReader", - "BlackrockReader": "ndi.daq.reader.mfdaq.blackrock.BlackrockReader", - "CEDSpike2Reader": "ndi.daq.reader.mfdaq.cedspike2.CEDSpike2Reader", - "SpikeGadgetsReader": "ndi.daq.reader.mfdaq.spikegadgets.SpikeGadgetsReader", - # MATLAB class names - "ndi.daq.reader.mfdaq.intan": "ndi.daq.reader.mfdaq.intan.IntanReader", - "ndi.daq.reader.mfdaq.blackrock": "ndi.daq.reader.mfdaq.blackrock.BlackrockReader", - "ndi.daq.reader.mfdaq.cedspike2": "ndi.daq.reader.mfdaq.cedspike2.CEDSpike2Reader", - "ndi.daq.reader.mfdaq.spikegadgets": "ndi.daq.reader.mfdaq.spikegadgets.SpikeGadgetsReader", - } - reader_path = _READER_CLASSES.get(reader_class_name) - if reader_path: - try: - module_path, cls_name = reader_path.rsplit(".", 1) - import importlib + from ..class_registry import get_class - mod = importlib.import_module(module_path) - ReaderCls = getattr(mod, cls_name) + ReaderCls = get_class(reader_class_name) + if ReaderCls is not None: + try: self._daqreader = ReaderCls(session=session, document=reader_doc) except Exception as exc: logger.warning( @@ -177,10 +165,10 @@ def _load_from_document(self, session: Any, document: Any) -> None: # Reconstruct file navigator from its document self._filenavigator = None if len(nav_docs) == 1: - from ..file.navigator import FileNavigator + from ..file.navigator import ndi_file_navigator try: - self._filenavigator = FileNavigator(session=session, document=nav_docs[0]) + self._filenavigator = ndi_file_navigator(session=session, document=nav_docs[0]) except Exception as exc: logger.warning("Could not reconstruct file navigator: %s", exc) @@ -195,7 +183,7 @@ def filenavigator(self) -> Any: return self._filenavigator @property - def daqreader(self) -> DAQReader | None: + def daqreader(self) -> ndi_daq_reader | None: """Get the DAQ reader.""" return self._daqreader @@ -214,28 +202,28 @@ def session(self) -> Any: def set_daqmetadatareaders( self, readers: list[Any], - ) -> DAQSystem: + ) -> ndi_daq_system: """ Set the metadata readers. Args: - readers: List of MetadataReader objects + readers: List of ndi_daq_metadatareader objects Returns: Self for chaining Raises: - TypeError: If any reader is not a MetadataReader + TypeError: If any reader is not a ndi_daq_metadatareader """ - from .metadatareader import MetadataReader + from .metadatareader import ndi_daq_metadatareader for i, r in enumerate(readers): - if not isinstance(r, MetadataReader): - raise TypeError(f"Element {i} is not a MetadataReader instance") + if not isinstance(r, ndi_daq_metadatareader): + raise TypeError(f"ndi_element {i} is not a ndi_daq_metadatareader instance") self._daqmetadatareaders = readers return self - def set_session(self, session: Any) -> DAQSystem: + def set_session(self, session: Any) -> ndi_daq_system: """ Set the session for this DAQ system. @@ -253,7 +241,7 @@ def set_session(self, session: Any) -> DAQSystem: def epochclock( self, epoch_number: int, - ) -> list[ClockType]: + ) -> list[ndi_time_clocktype]: """ Return clock types for an epoch. @@ -261,7 +249,7 @@ def epochclock( epoch_number: The epoch number (1-indexed) Returns: - List of ClockType objects + List of ndi_time_clocktype objects Note: The base class returns [NO_TIME]. @@ -294,7 +282,7 @@ def epochid( epoch_number: The epoch number (1-indexed) Returns: - Epoch identifier string + ndi_epoch_epoch identifier string """ if self._filenavigator is not None: return self._filenavigator.epochid(epoch_number) @@ -308,7 +296,7 @@ def epochtable(self) -> list[dict[str, Any]]: List of epoch entries with fields: - epoch_number: The epoch number - epoch_id: Unique epoch identifier - - epochprobemap: Probe mapping for the epoch + - epochprobemap: ndi_probe mapping for the epoch - epoch_clock: List of clock types - t0_t1: List of (t0, t1) tuples - underlying_epochs: Underlying file information @@ -368,10 +356,10 @@ def getprobes(self) -> list[dict[str, Any]]: Returns: List of probe dicts with: - - name: Probe name - - reference: Probe reference - - type: Probe type - - subject_id: Subject identifier + - name: ndi_probe name + - reference: ndi_probe reference + - type: ndi_probe type + - subject_id: ndi_subject identifier """ et = self.epochtable() probes = [] @@ -418,11 +406,11 @@ def getepochprobemap( Get the epoch probe map for an epoch. Args: - epoch: Epoch number + epoch: ndi_epoch_epoch number filenavepochprobemap: Optional probe map from navigator Returns: - Epoch probe map object + ndi_epoch_epoch probe map object """ # Check if reader has getepochprobemap method if self._daqreader is not None and hasattr(self._daqreader, "getepochprobemap"): @@ -447,7 +435,7 @@ def getmetadata( Get metadata for an epoch. Args: - epoch: Epoch number + epoch: ndi_epoch_epoch number channel: Metadata reader channel (1-indexed) Returns: @@ -550,7 +538,7 @@ def deleteepoch( et = self.epochtable() if epoch_number < 1 or epoch_number > len(et): - return False, f"Epoch {epoch_number} out of range (1..{len(et)})" + return False, f"ndi_epoch_epoch {epoch_number} out of range (1..{len(et)})" entry = et[epoch_number - 1] epoch_id = entry.get("epoch_id", "") @@ -565,14 +553,14 @@ def deleteepoch( # Delete from database if session exists if self.session is not None: - from ..query import Query + from ..query import ndi_query # Delete ingested epoch data documents if self._daqreader is not None: q = ( - Query("").isa("daqreader_epochdata_ingested") - & Query("").depends_on("daqreader_id", self._daqreader.id) - & (Query("epochid.epochid") == epoch_id) + ndi_query("").isa("daqreader_epochdata_ingested") + & ndi_query("").depends_on("daqreader_id", self._daqreader.id) + & (ndi_query("epochid.epochid") == epoch_id) ) docs = self.session.database_search(q) for doc in docs: @@ -581,9 +569,9 @@ def deleteepoch( # Delete ingested metadata documents for mreader in self._daqmetadatareaders: q = ( - Query("").isa("daqmetadatareader_epochdata_ingested") - & Query("").depends_on("daqmetadatareader_id", mreader.id) - & (Query("epochid.epochid") == epoch_id) + ndi_query("").isa("daqmetadatareader_epochdata_ingested") + & ndi_query("").depends_on("daqmetadatareader_id", mreader.id) + & (ndi_query("epochid.epochid") == epoch_id) ) docs = self.session.database_search(q) for doc in docs: @@ -600,7 +588,7 @@ def deleteepoch( except OSError: pass # Best effort deletion - return True, f"Epoch {epoch_number} deleted" + return True, f"ndi_epoch_epoch {epoch_number} deleted" def verifyepochprobemap( self, @@ -612,7 +600,7 @@ def verifyepochprobemap( Args: epochprobemap: The probe map to verify - epoch: Epoch number + epoch: ndi_epoch_epoch number Returns: Tuple of (is_valid, error_message) @@ -629,12 +617,12 @@ def newdocument(self) -> list[Any]: Returns: List of documents: - - [0]: FileNavigator document - - [1]: DAQReader document - - [2]: DAQSystem document - - [3+]: MetadataReader documents + - [0]: ndi_file_navigator document + - [1]: ndi_daq_reader document + - [2]: ndi_daq_system document + - [3+]: ndi_daq_metadatareader documents """ - from ..document import Document + from ..document import ndi_document docs = [] @@ -647,10 +635,10 @@ def newdocument(self) -> list[Any]: docs.append(self._daqreader.newdocument()) # System document - sys_doc = Document( + sys_doc = ndi_document( "daq/daqsystem", **{ - "daqsystem.ndi_daqsystem_class": self.__class__.__name__, + "daqsystem.ndi_daqsystem_class": self.NDI_DAQSYSTEM_CLASS, "base.id": self.id, "base.name": self._name, }, @@ -678,21 +666,21 @@ def searchquery(self) -> Any: Create a search query for this DAQ system. Returns: - Query object + ndi_query object """ - from ..query import Query + from ..query import ndi_query - q = Query("base.id") == self.id + q = ndi_query("base.id") == self.id if self._name: - q = q & (Query("base.name") == self._name) + q = q & (ndi_query("base.name") == self._name) if self.session is not None: - q = q & (Query("base.session_id") == self.session.id) + q = q & (ndi_query("base.session_id") == self.session.id) return q def __eq__(self, other: Any) -> bool: """Test equality by name and class.""" - if not isinstance(other, DAQSystem): + if not isinstance(other, ndi_daq_system): return False return self._name == other._name and self.__class__.__name__ == other.__class__.__name__ diff --git a/src/ndi/daq/system_mfdaq.py b/src/ndi/daq/system_mfdaq.py index 68f1dc9..841c466 100644 --- a/src/ndi/daq/system_mfdaq.py +++ b/src/ndi/daq/system_mfdaq.py @@ -1,7 +1,7 @@ """ ndi.daq.system_mfdaq - Multi-function DAQ system class. -Extends DAQSystem with multi-function DAQ specific behavior including +Extends ndi_daq_system with multi-function DAQ specific behavior including support for various channel types and time/sample conversions. MATLAB equivalent: src/ndi/+ndi/+daq/+system/mfdaq.m @@ -13,24 +13,24 @@ import numpy as np -from ..time import DEV_LOCAL_TIME, ClockType -from .mfdaq import MFDAQReader, standardize_channel_type -from .system import DAQSystem +from ..time import DEV_LOCAL_TIME, ndi_time_clocktype +from .mfdaq import ndi_daq_reader_mfdaq, standardize_channel_type +from .system import ndi_daq_system -class DAQSystemMFDAQ(DAQSystem): +class ndi_daq_system_mfdaq(ndi_daq_system): """ Multi-function DAQ system. - Extends DAQSystem for multi-function data acquisition systems + Extends ndi_daq_system for multi-function data acquisition systems that support various channel types (analog, digital, time, auxiliary, event, marker). - The MFDAQ system delegates data reading to its MFDAQReader and + The MFDAQ system delegates data reading to its ndi_daq_reader_mfdaq and provides time/sample conversion methods. Example: - >>> sys = DAQSystemMFDAQ('intan1', navigator, reader) + >>> sys = ndi_daq_system_mfdaq('intan1', navigator, reader) >>> channels = sys.getchannelsepoch(1) >>> data = sys.readchannels_epochsamples('ai', [1, 2], 1, 0, 1000) """ @@ -46,14 +46,14 @@ class DAQSystemMFDAQ(DAQSystem): "marker": "mk", } - def epochclock(self, epoch_number: int) -> list[ClockType]: + def epochclock(self, epoch_number: int) -> list[ndi_time_clocktype]: """ Return clock types for an epoch. MFDAQ systems return DEV_LOCAL_TIME by default. Args: - epoch_number: Epoch number (1-indexed) + epoch_number: ndi_epoch_epoch number (1-indexed) Returns: List containing DEV_LOCAL_TIME @@ -67,7 +67,7 @@ def t0_t1(self, epoch_number: int) -> list[tuple[float, float]]: Delegates to the DAQ reader if available. Args: - epoch_number: Epoch number (1-indexed) + epoch_number: ndi_epoch_epoch number (1-indexed) Returns: List of (t0, t1) tuples per clock type @@ -82,7 +82,7 @@ def getchannelsepoch(self, epoch_number: int) -> list[Any]: Get available channels for an epoch. Args: - epoch_number: Epoch number (1-indexed) + epoch_number: ndi_epoch_epoch number (1-indexed) Returns: List of ChannelInfo objects @@ -92,7 +92,7 @@ def getchannelsepoch(self, epoch_number: int) -> list[Any]: epochfiles = self._filenavigator.getepochfiles(epoch_number) - if isinstance(self._daqreader, MFDAQReader): + if isinstance(self._daqreader, ndi_daq_reader_mfdaq): return self._daqreader.getchannelsepoch(epochfiles) return [] @@ -132,7 +132,7 @@ def readchannels_epochsamples( Args: channeltype: Channel type(s) (e.g., 'ai', 'analog_in') channel: Channel number(s) (1-indexed) - epoch_number: Epoch number (1-indexed) + epoch_number: ndi_epoch_epoch number (1-indexed) s0: Start sample (1-indexed) s1: End sample (1-indexed) @@ -143,11 +143,11 @@ def readchannels_epochsamples( raise RuntimeError("No DAQ reader or file navigator configured") epochfiles = self._filenavigator.getepochfiles(epoch_number) - if isinstance(self._daqreader, MFDAQReader): + if isinstance(self._daqreader, ndi_daq_reader_mfdaq): return self._daqreader.readchannels_epochsamples( channeltype, channel, epochfiles, s0, s1 ) - raise TypeError("DAQ reader is not an MFDAQReader") + raise TypeError("DAQ reader is not an ndi_daq_reader_mfdaq") def readevents_epochsamples( self, @@ -163,7 +163,7 @@ def readevents_epochsamples( Args: channeltype: Event channel type(s) channel: Channel number(s) - epoch_number: Epoch number (1-indexed) + epoch_number: ndi_epoch_epoch number (1-indexed) t0: Start time t1: End time @@ -174,9 +174,9 @@ def readevents_epochsamples( raise RuntimeError("No DAQ reader or file navigator configured") epochfiles = self._filenavigator.getepochfiles(epoch_number) - if isinstance(self._daqreader, MFDAQReader): + if isinstance(self._daqreader, ndi_daq_reader_mfdaq): return self._daqreader.readevents_epochsamples(channeltype, channel, epochfiles, t0, t1) - raise TypeError("DAQ reader is not an MFDAQReader") + raise TypeError("DAQ reader is not an ndi_daq_reader_mfdaq") def samplerate( self, @@ -188,7 +188,7 @@ def samplerate( Get sample rate for channels in an epoch. Args: - epoch_number: Epoch number (1-indexed) + epoch_number: ndi_epoch_epoch number (1-indexed) channeltype: Channel type(s) channel: Channel number(s) @@ -199,9 +199,9 @@ def samplerate( raise RuntimeError("No DAQ reader or file navigator configured") epochfiles = self._filenavigator.getepochfiles(epoch_number) - if isinstance(self._daqreader, MFDAQReader): + if isinstance(self._daqreader, ndi_daq_reader_mfdaq): return self._daqreader.samplerate(epochfiles, channeltype, channel) - raise TypeError("DAQ reader is not an MFDAQReader") + raise TypeError("DAQ reader is not an ndi_daq_reader_mfdaq") def epochsamples2times( self, @@ -216,7 +216,7 @@ def epochsamples2times( Args: channeltype: Channel type(s) channel: Channel number(s) - epoch_number: Epoch number (1-indexed) + epoch_number: ndi_epoch_epoch number (1-indexed) samples: Sample indices (1-indexed) Returns: @@ -226,9 +226,9 @@ def epochsamples2times( raise RuntimeError("No DAQ reader or file navigator configured") epochfiles = self._filenavigator.getepochfiles(epoch_number) - if isinstance(self._daqreader, MFDAQReader): + if isinstance(self._daqreader, ndi_daq_reader_mfdaq): return self._daqreader.epochsamples2times(channeltype, channel, epochfiles, samples) - raise TypeError("DAQ reader is not an MFDAQReader") + raise TypeError("DAQ reader is not an ndi_daq_reader_mfdaq") def epochtimes2samples( self, @@ -243,7 +243,7 @@ def epochtimes2samples( Args: channeltype: Channel type(s) channel: Channel number(s) - epoch_number: Epoch number (1-indexed) + epoch_number: ndi_epoch_epoch number (1-indexed) times: Time values Returns: @@ -253,9 +253,9 @@ def epochtimes2samples( raise RuntimeError("No DAQ reader or file navigator configured") epochfiles = self._filenavigator.getepochfiles(epoch_number) - if isinstance(self._daqreader, MFDAQReader): + if isinstance(self._daqreader, ndi_daq_reader_mfdaq): return self._daqreader.epochtimes2samples(channeltype, channel, epochfiles, times) - raise TypeError("DAQ reader is not an MFDAQReader") + raise TypeError("DAQ reader is not an ndi_daq_reader_mfdaq") @staticmethod def mfdaq_channeltypes() -> list[str]: diff --git a/src/ndi/database.py b/src/ndi/database.py index 4481323..51c1d2a 100644 --- a/src/ndi/database.py +++ b/src/ndi/database.py @@ -7,13 +7,13 @@ Example: # Create a database for a session - db = Database('/path/to/session') + db = ndi_database('/path/to/session') # Add documents db.add(doc) - # Query documents - results = db.search(Query('element.name') == 'electrode1') + # ndi_query documents + results = db.search(ndi_query('element.name') == 'electrode1') # Find by ID doc = db.read(doc_id) @@ -21,16 +21,16 @@ from pathlib import Path -from .document import Document -from .query import Query +from .document import ndi_document +from .query import ndi_query class SQLiteDriver: """SQLite database driver using DID-python's SQLiteDB. This driver wraps DID-python's SQLiteDB implementation to provide - a consistent interface for the NDI Database class. Uses DID-python's - field_search() for query evaluation. + a consistent interface for the NDI ndi_database class. DID-python handles + doc_data population and SQL-based search natively. """ def __init__(self, db_path: Path, branch_id: str = "a"): @@ -42,14 +42,12 @@ def __init__(self, db_path: Path, branch_id: str = "a"): NDI-matlab have always used ``"a"`` as the default branch, so we match that for cross-language compatibility. """ - from did.datastructures import field_search from did.document import Document as DIDDocument from did.implementations.sqlitedb import SQLiteDB self._db_path = db_path self._branch_id = branch_id self._DIDDocument = DIDDocument - self._field_search = field_search # Initialize SQLiteDB self._db = SQLiteDB(str(db_path)) @@ -63,133 +61,41 @@ def add(self, document: dict) -> None: """Add a document to the database.""" doc_id = document.get("base", {}).get("id", "") if not doc_id: - raise ValueError("Document must have a base.id") + raise ValueError("ndi_document must have a base.id") # Check if document already exists existing_ids = self._db.get_doc_ids(self._branch_id) if doc_id in existing_ids: - raise FileExistsError(f"Document {doc_id} already exists") + raise FileExistsError(f"ndi_document {doc_id} already exists") - # Create DID Document and add + # Create DID ndi_document and add (DID-python now populates doc_data) did_doc = self._DIDDocument(document) self._db.add_docs([did_doc], self._branch_id) - # Populate doc_data indexed fields for cross-language compatibility. - # DID-python's _do_add_doc does not populate the fields/doc_data - # tables, but DID-matlab's search relies on them. - self._populate_doc_data(doc_id, document) - def bulk_add(self, documents: list[dict]) -> tuple[int, int]: """Add many documents at once, bypassing per-doc duplicate checks. - Uses a single transaction for all inserts. Duplicates (by - ``base.id``) are silently skipped. + Duplicates (by ``base.id``) are silently skipped. Returns: ``(added, skipped)`` counts. """ - import json as json_mod - import time - existing_ids = set(self._db.get_doc_ids(self._branch_id)) added = 0 skipped = 0 - cursor = self._db.dbid.cursor() - try: - for doc in documents: - doc_id = doc.get("base", {}).get("id", "") - if not doc_id or doc_id in existing_ids: - skipped += 1 - continue - - json_code = json_mod.dumps(doc) - cursor.execute( - "INSERT INTO docs (doc_id, json_code, timestamp) VALUES (?, ?, ?)", - (doc_id, json_code, time.time()), - ) - doc_idx = cursor.lastrowid - try: - cursor.execute( - "INSERT INTO branch_docs (branch_id, doc_idx, timestamp) VALUES (?, ?, ?)", - (self._branch_id, doc_idx, time.time()), - ) - except Exception: - pass - # Populate doc_data for cross-language compatibility - self._populate_doc_data_with_cursor(cursor, doc_idx, doc) - - existing_ids.add(doc_id) - added += 1 - self._db.dbid.commit() - except Exception: - self._db.dbid.rollback() - raise - - return added, skipped - - # ----------------------------------------------------------------- - # doc_data population for MATLAB DID compatibility - # ----------------------------------------------------------------- + for doc in documents: + doc_id = doc.get("base", {}).get("id", "") + if not doc_id or doc_id in existing_ids: + skipped += 1 + continue - @staticmethod - def _flatten_document(doc: dict, prefix: str = "") -> list[tuple[str, str]]: - """Flatten a document dict into (field_path, value) pairs. + did_doc = self._DIDDocument(doc) + self._db.add_docs([did_doc], self._branch_id) + existing_ids.add(doc_id) + added += 1 - Only includes leaf scalar values (str, int, float, bool). - Skips lists and nested dicts (those are traversed recursively). - """ - pairs: list[tuple[str, str]] = [] - for key, val in doc.items(): - path = f"{prefix}.{key}" if prefix else key - if isinstance(val, dict): - pairs.extend(SQLiteDriver._flatten_document(val, path)) - elif isinstance(val, list): - # Index list elements for depends_on etc. - for i, item in enumerate(val): - if isinstance(item, dict): - pairs.extend(SQLiteDriver._flatten_document(item, f"{path}({i})")) - elif item is not None: - pairs.append((f"{path}({i})", str(item))) - elif val is not None: - pairs.append((path, str(val))) - return pairs - - def _get_or_create_field_idx(self, cursor, field_name: str, doc_class: str = "") -> int: - """Get field_idx from the fields table, creating a new entry if needed.""" - cursor.execute("SELECT field_idx FROM fields WHERE field_name = ?", (field_name,)) - row = cursor.fetchone() - if row: - return row[0] if isinstance(row, (tuple, list)) else row["field_idx"] - cursor.execute( - "INSERT INTO fields (class, field_name, json_name, field_idx) " - "VALUES (?, ?, ?, NULL)", - (doc_class, field_name, field_name), - ) - return cursor.lastrowid - - def _populate_doc_data(self, doc_id: str, document: dict) -> None: - """Populate the doc_data table for a document after insertion.""" - cursor = self._db.dbid.cursor() - # Look up doc_idx - cursor.execute("SELECT doc_idx FROM docs WHERE doc_id = ?", (doc_id,)) - row = cursor.fetchone() - if not row: - return - doc_idx = row[0] if isinstance(row, (tuple, list)) else row["doc_idx"] - self._populate_doc_data_with_cursor(cursor, doc_idx, document) - self._db.dbid.commit() - - def _populate_doc_data_with_cursor(self, cursor, doc_idx: int, document: dict) -> None: - """Populate doc_data for a document using an existing cursor.""" - doc_class = document.get("document_class", {}).get("class_name", "") - pairs = self._flatten_document(document) - for field_name, value in pairs: - field_idx = self._get_or_create_field_idx(cursor, field_name, doc_class) - cursor.execute( - "INSERT INTO doc_data (doc_idx, field_idx, value) VALUES (?, ?, ?)", - (doc_idx, field_idx, value), - ) + return added, skipped def update(self, document: dict) -> None: """Update an existing document.""" @@ -198,25 +104,13 @@ def update(self, document: dict) -> None: # Check if document exists existing_ids = self._db.get_doc_ids(self._branch_id) if doc_id not in existing_ids: - raise FileNotFoundError(f"Document {doc_id} not found") - - # Remove old doc_data entries - cursor = self._db.dbid.cursor() - cursor.execute("SELECT doc_idx FROM docs WHERE doc_id = ?", (doc_id,)) - row = cursor.fetchone() - if row: - old_idx = row[0] if isinstance(row, (tuple, list)) else row["doc_idx"] - cursor.execute("DELETE FROM doc_data WHERE doc_idx = ?", (old_idx,)) - self._db.dbid.commit() - - # Remove old and add new (SQLiteDB doesn't have direct update) + raise FileNotFoundError(f"ndi_document {doc_id} not found") + + # Remove old and add new (DID handles doc_data cleanup and repopulation) self._db.remove_docs([doc_id], self._branch_id) did_doc = self._DIDDocument(document) self._db.add_docs([did_doc], self._branch_id) - # Repopulate doc_data - self._populate_doc_data(doc_id, document) - def delete_by_id(self, doc_id: str) -> bool: """Delete a document by ID.""" existing_ids = self._db.get_doc_ids(self._branch_id) @@ -239,33 +133,23 @@ def find_by_id(self, doc_id: str) -> dict | None: def find(self, query=None) -> list[dict]: """Find all documents matching query. - Uses DID-python's field_search() for query evaluation. + Uses DID-python's SQL-based search against the doc_data table + for query evaluation, falling back to brute-force for unsupported + operations. Retrieval and MATLAB normalization are handled by + DID-python's :meth:`get_docs` / :meth:`get_docs_by_branch`. """ - import json as json_mod - - # Fetch all JSON blobs in a single SQL query instead of one-by-one. - rows = self._db.do_run_sql_query( - "SELECT d.json_code FROM docs d " - "JOIN branch_docs bd ON d.doc_idx = bd.doc_idx " - "WHERE bd.branch_id = ?", - (self._branch_id,), - ) - - if not rows: - return [] - - documents = [json_mod.loads(r["json_code"]) for r in rows] - - # Filter by query if provided using DID-python's field_search if query is not None: - # Convert to DID-python compatible format - search_params = query.to_search_structure() - documents = [d for d in documents if self._field_search(d, search_params)] + doc_ids = self._db.search(query, self._branch_id) + if not doc_ids: + return [] + docs = self._db.get_docs(doc_ids, self._branch_id, OnMissing="ignore") + else: + docs = self._db.get_docs_by_branch(self._branch_id) - return documents + return [d.document_properties for d in docs if d is not None] -class Database: +class ndi_database: """NDI database interface. Provides document storage and querying using DID-python's SQLiteDB. @@ -275,9 +159,9 @@ class Database: session_path: Path to the session directory. Example: - db = Database('/path/to/session') + db = ndi_database('/path/to/session') db.add(doc) - docs = db.search(Query('element.type') == 'probe') + docs = db.search(ndi_query('element.type') == 'probe') """ def __init__(self, session_path: str | Path, db_name: str = ".ndi", **backend_kwargs): @@ -321,11 +205,11 @@ def binary_path(self) -> Path: # === CRUD Operations === - def add(self, document: Document) -> Document: + def add(self, document: ndi_document) -> ndi_document: """Add a document to the database. Args: - document: The Document to add. + document: The ndi_document to add. Returns: The added document. @@ -334,19 +218,19 @@ def add(self, document: Document) -> Document: ValueError: If document already exists in database. Example: - doc = Document({'base': {'id': '...', ...}}) + doc = ndi_document({'base': {'id': '...', ...}}) db.add(doc) """ try: self._driver.add(document.document_properties) except FileExistsError as exc: raise ValueError( - f"Document with ID {document.id} already exists. " + f"ndi_document with ID {document.id} already exists. " f"Use update() or add_or_replace()." ) from exc return document - def read(self, doc_id: str, isa_class: str | None = None) -> Document | None: + def read(self, doc_id: str, isa_class: str | None = None) -> ndi_document | None: """Read a document by ID. Args: @@ -355,7 +239,7 @@ def read(self, doc_id: str, isa_class: str | None = None) -> Document | None: if document is not of that class. Returns: - The Document, or None if not found. + The ndi_document, or None if not found. Example: doc = db.read('abc123') @@ -364,18 +248,18 @@ def read(self, doc_id: str, isa_class: str | None = None) -> Document | None: if result is None: return None - doc = Document(result) + doc = ndi_document(result) if isa_class and not doc.doc_isa(isa_class): return None return doc - def remove(self, document: Document | str) -> bool: + def remove(self, document: ndi_document | str) -> bool: """Remove a document from the database. Args: - document: The Document or document ID to remove. + document: The ndi_document or document ID to remove. Returns: True if removed, False if not found. @@ -384,14 +268,14 @@ def remove(self, document: Document | str) -> bool: db.remove(doc) db.remove('abc123') """ - doc_id = document.id if isinstance(document, Document) else document + doc_id = document.id if isinstance(document, ndi_document) else document return self._driver.delete_by_id(doc_id) - def update(self, document: Document) -> Document: + def update(self, document: ndi_document) -> ndi_document: """Update an existing document. Args: - document: The Document with updated properties. + document: The ndi_document with updated properties. Returns: The updated document. @@ -408,17 +292,17 @@ def update(self, document: Document) -> Document: self._driver.update(document.document_properties) except FileNotFoundError as exc: raise ValueError( - f"Document with ID {document.id} not found. " f"Use add() for new documents." + f"ndi_document with ID {document.id} not found. " f"Use add() for new documents." ) from exc return document - def add_or_replace(self, document: Document) -> Document: + def add_or_replace(self, document: ndi_document) -> ndi_document: """Add or replace a document. If document exists, replaces it. Otherwise, adds it. Args: - document: The Document to add or replace. + document: The ndi_document to add or replace. Returns: The document. @@ -434,13 +318,15 @@ def add_or_replace(self, document: Document) -> Document: return document - # === Query Operations === + # === ndi_query Operations === - def search(self, query: Query | None = None, isa_class: str | None = None) -> list[Document]: + def search( + self, query: ndi_query | None = None, isa_class: str | None = None + ) -> list[ndi_document]: """Search for documents matching a query. Args: - query: The Query to match. If None, returns all documents. + query: The ndi_query to match. If None, returns all documents. isa_class: Optional class filter. If provided, only returns documents that are instances of that class. @@ -452,30 +338,30 @@ def search(self, query: Query | None = None, isa_class: str | None = None) -> li all_docs = db.search() # Find by query - probes = db.search(Query('element.type') == 'probe') + probes = db.search(ndi_query('element.type') == 'probe') # Find all of a class elements = db.search(isa_class='element') # Combined my_probes = db.search( - Query('element.name').contains('elec'), + ndi_query('element.name').contains('elec'), isa_class='probe' ) """ # Build combined query combined = query if isa_class: - isa_query = Query("").isa(isa_class) + isa_query = ndi_query("").isa(isa_class) combined = (combined & isa_query) if combined else isa_query # Execute search results = self._driver.find(combined) - # Convert results to ndi.Document - return [Document(r) for r in results] + # Convert results to ndi.ndi_document + return [ndi_document(r) for r in results] - def find_by_id(self, doc_id: str) -> Document | None: + def find_by_id(self, doc_id: str) -> ndi_document | None: """Find a document by its ID. Alias for read() for MATLAB compatibility. @@ -484,7 +370,7 @@ def find_by_id(self, doc_id: str) -> Document | None: doc_id: The document ID. Returns: - The Document or None. + The ndi_document or None. """ return self.read(doc_id) @@ -507,20 +393,20 @@ def numdocs(self) -> int: # === Dependency Operations === - def find_depends_on(self, document: Document | str) -> list[Document]: + def find_depends_on(self, document: ndi_document | str) -> list[ndi_document]: """Find all documents that depend on a given document. Args: - document: The Document or document ID. + document: The ndi_document or document ID. Returns: List of Documents that depend on the given document. """ - doc_id = document.id if isinstance(document, Document) else document + doc_id = document.id if isinstance(document, ndi_document) else document # DID's depends_on query requires both name and value, but we want # all documents that depend on doc_id regardless of dependency name. # Search all documents and filter by depends_on value. - all_docs = self.search(Query.all()) + all_docs = self.search(ndi_query.all()) return [ doc for doc in all_docs @@ -531,11 +417,11 @@ def find_depends_on(self, document: Document | str) -> list[Document]: ) ] - def find_dependencies(self, document: Document | str) -> list[Document]: + def find_dependencies(self, document: ndi_document | str) -> list[ndi_document]: """Find all documents that a given document depends on. Args: - document: The Document or document ID. + document: The ndi_document or document ID. Returns: List of Documents that the given document depends on. @@ -555,7 +441,7 @@ def find_dependencies(self, document: Document | str) -> list[Document]: # === Batch Operations === - def add_many(self, documents: list[Document]) -> list[Document]: + def add_many(self, documents: list[ndi_document]) -> list[ndi_document]: """Add multiple documents. Args: @@ -573,12 +459,12 @@ def add_many(self, documents: list[Document]) -> list[Document]: return added def remove_many( - self, query: Query | None = None, documents: list[Document] | None = None + self, query: ndi_query | None = None, documents: list[ndi_document] | None = None ) -> int: """Remove multiple documents. Args: - query: Query to select documents to remove. + query: ndi_query to select documents to remove. documents: Explicit list of documents to remove. Returns: @@ -596,7 +482,7 @@ def remove_many( if documents: for doc in documents: - to_remove.add(doc.id if isinstance(doc, Document) else doc) + to_remove.add(doc.id if isinstance(doc, ndi_document) else doc) count = 0 for doc_id in to_remove: @@ -606,7 +492,7 @@ def remove_many( # === File Management === - def get_binary_path(self, document: Document, file_name: str) -> Path: + def get_binary_path(self, document: ndi_document, file_name: str) -> Path: """Get the path where a document's binary file should be stored. Args: @@ -619,14 +505,14 @@ def get_binary_path(self, document: Document, file_name: str) -> Path: return self._binary_dir / f"{document.id}_{file_name}" def __repr__(self) -> str: - return f"Database('{self.session_path}')" + return f"ndi_database('{self.session_path}')" # Convenience function -def open_database(session_path: str | Path, **kwargs) -> Database: +def open_database(session_path: str | Path, **kwargs) -> ndi_database: """Open or create an NDI database. - This is a convenience function for Database(). Uses DID-python's + This is a convenience function for ndi_database(). Uses DID-python's SQLiteDB for storage, ensuring compatibility with existing databases. Args: @@ -634,9 +520,9 @@ def open_database(session_path: str | Path, **kwargs) -> Database: **kwargs: Additional options (e.g., db_name, branch_id). Returns: - Database instance. + ndi_database instance. Example: db = open_database('/path/to/session') """ - return Database(session_path, **kwargs) + return ndi_database(session_path, **kwargs) diff --git a/src/ndi/database_fun.py b/src/ndi/database_fun.py index 1a8662a..50554ef 100644 --- a/src/ndi/database_fun.py +++ b/src/ndi/database_fun.py @@ -1,5 +1,5 @@ """ -ndi.database.fun - Database utility functions for NDI. +ndi.database.fun - ndi_database utility functions for NDI. MATLAB equivalents: +ndi/+database/+fun/*.m @@ -28,13 +28,13 @@ def findallantecedents( Args: session_or_dataset: An ndi.session or ndi.dataset with database_search. - *documents: One or more ndi.Document objects. + *documents: One or more ndi.ndi_document objects. visited: Set of already-visited IDs (for recursion). Returns: - List of all antecedent Document objects. + List of all antecedent ndi_document objects. """ - from .query import Query + from .query import ndi_query if visited is None: visited = set() @@ -62,9 +62,9 @@ def findallantecedents( return antecedents # Batch query for all dependency IDs - q = Query("base.id") == dep_ids[0] + q = ndi_query("base.id") == dep_ids[0] for did in dep_ids[1:]: - q = q | (Query("base.id") == did) + q = q | (ndi_query("base.id") == did) try: found = session_or_dataset.database_search(q) @@ -95,7 +95,7 @@ def findalldependencies( Recursively walks the dependency chain downwards. """ - from .query import Query + from .query import ndi_query if visited is None: visited = set() @@ -113,7 +113,7 @@ def findalldependencies( visited.add(doc_id) # Find documents whose depends_on references this doc - q = Query("").depends_on("*", doc_id) + q = ndi_query("").depends_on("*", doc_id) try: found = session_or_dataset.database_search(q) @@ -149,22 +149,22 @@ def docs_from_ids( MATLAB equivalent: ndi.database.fun.docs_from_ids Args: - session_or_dataset: Database-containing object. + session_or_dataset: ndi_database-containing object. document_ids: List of document IDs. Returns: List aligned with document_ids, each element the matching - Document or None if not found. + ndi_document or None if not found. """ - from .query import Query + from .query import ndi_query if not document_ids: return [] # Build OR query for all IDs - q = Query("base.id") == document_ids[0] + q = ndi_query("base.id") == document_ids[0] for did in document_ids[1:]: - q = q | (Query("base.id") == did) + q = q | (ndi_query("base.id") == did) try: found = session_or_dataset.database_search(q) @@ -194,7 +194,7 @@ def docs2graph( MATLAB equivalent: ndi.database.fun.docs2graph Args: - documents: List of ndi.Document objects. + documents: List of ndi.ndi_document objects. Returns: Tuple of (adjacency_dict, node_ids) where adjacency_dict maps @@ -234,12 +234,12 @@ def find_ingested_docs(session_or_dataset: Any) -> list[Any]: MATLAB equivalent: ndi.database.fun.find_ingested_docs """ - from .query import Query + from .query import ndi_query q = ( - Query("").isa("daqreader_mfdaq_epochdata_ingested") - | Query("").isa("daqmetadatareader_epochdata_ingested") - | Query("").isa("epochfiles_ingested") + ndi_query("").isa("daqreader_mfdaq_epochdata_ingested") + | ndi_query("").isa("daqmetadatareader_epochdata_ingested") + | ndi_query("").isa("epochfiles_ingested") ) try: @@ -265,7 +265,7 @@ def finddocs_elementEpochType( element_id dependency, and epoch_id exact match. Args: - session_or_dataset: Database-containing object. + session_or_dataset: ndi_database-containing object. element_id: The element document ID. epoch_id: The epoch ID string. document_type: The document type name (e.g. ``'spectrogram'``). @@ -273,11 +273,11 @@ def finddocs_elementEpochType( Returns: List of matching Documents. """ - from .query import Query + from .query import ndi_query - q1 = Query("").isa(document_type) - q2 = Query("").depends_on("element_id", element_id) - q3 = Query("epochid.epochid") == epoch_id + q1 = ndi_query("").isa(document_type) + q2 = ndi_query("").depends_on("element_id", element_id) + q3 = ndi_query("epochid.epochid") == epoch_id q = q1 & q2 & q3 try: @@ -298,20 +298,20 @@ def ndi_document2ndi_object( MATLAB equivalent: ndi.database.fun.ndi_document2ndi_object Inspects the document's class hierarchy and instantiates the - appropriate Python object (e.g. Element, Probe, Subject). + appropriate Python object (e.g. ndi_element, ndi_probe, ndi_subject). Args: - ndi_document_obj: An ndi.Document or a document ID string. + ndi_document_obj: An ndi.ndi_document or a document ID string. ndi_session_obj: The session object for database lookups. Returns: The reconstructed NDI object, or None if reconstruction fails. """ - from .query import Query + from .query import ndi_query # If given an ID string, look up the document if isinstance(ndi_document_obj, str): - results = ndi_session_obj.database_search(Query("base.id") == ndi_document_obj) + results = ndi_session_obj.database_search(ndi_query("base.id") == ndi_document_obj) if not results: return None ndi_document_obj = results[0] @@ -341,11 +341,11 @@ def _get_class_map() -> dict[str, Any]: constructors: dict[str, Any] = {} def _make_element(doc: Any, session: Any) -> Any: - from .element import Element + from .element import ndi_element p = doc.document_properties el = p.get("element", {}) - return Element( + return ndi_element( session=session, name=el.get("name", ""), reference=el.get("reference", 0), @@ -353,11 +353,11 @@ def _make_element(doc: Any, session: Any) -> Any: ) def _make_subject(doc: Any, session: Any) -> Any: - from .subject import Subject + from .subject import ndi_subject p = doc.document_properties subj = p.get("subject", {}) - return Subject( + return ndi_subject( session=session, local_identifier=subj.get("local_identifier", ""), description=subj.get("description", ""), @@ -386,7 +386,7 @@ def copy_session_to_dataset( Returns: Tuple ``(success, errmsg)`` where success is True/False. """ - from .query import Query + from .query import ndi_query # Check for already-copied sessions try: @@ -395,14 +395,14 @@ def copy_session_to_dataset( if session_id in session_ids: return ( False, - f"Session with ID {session_id} is already part of " f"the dataset.", + f"ndi_session with ID {session_id} is already part of " f"the dataset.", ) except Exception: pass # Get all documents from source session try: - all_docs = ndi_session_obj.database_search(Query("").isa("base")) + all_docs = ndi_session_obj.database_search(ndi_query("").isa("base")) except Exception: return False, "Failed to search source session database." @@ -443,14 +443,14 @@ def finddocs_missing_dependencies( MATLAB equivalent: ndi.database.fun.finddocs_missing_dependencies """ - from .query import Query + from .query import ndi_query # Find all docs with depends_on try: - all_docs = session_or_dataset.database_search(Query("").isa("base")) + all_docs = session_or_dataset.database_search(ndi_query("").isa("base")) except Exception: try: - all_docs = session_or_dataset.session.database_search(Query("").isa("base")) + all_docs = session_or_dataset.session.database_search(ndi_query("").isa("base")) except Exception: return [] @@ -637,7 +637,7 @@ def read_presentation_time_structure( # ========================================================================= -# Database export / extraction +# ndi_database export / extraction # ========================================================================= @@ -661,12 +661,12 @@ def database2json( import json from pathlib import Path - from .query import Query + from .query import ndi_query out = Path(output_path) out.mkdir(parents=True, exist_ok=True) - docs = session.database_search(Query("").isa("base")) + docs = session.database_search(ndi_query("").isa("base")) count = 0 for doc in docs: @@ -746,7 +746,7 @@ def extract_doc_files( import tempfile from pathlib import Path - from .query import Query + from .query import ndi_query if target_path is None: target_path = tempfile.mkdtemp(prefix="ndi_extract_") @@ -754,7 +754,7 @@ def extract_doc_files( out = Path(target_path) out.mkdir(parents=True, exist_ok=True) - docs = session.database_search(Query("").isa("base")) + docs = session.database_search(ndi_query("").isa("base")) for doc in docs: props = doc.document_properties if hasattr(doc, "document_properties") else doc diff --git a/src/ndi/database_ingestion.py b/src/ndi/database_ingestion.py index a0b6fa3..5060dfa 100644 --- a/src/ndi/database_ingestion.py +++ b/src/ndi/database_ingestion.py @@ -29,7 +29,7 @@ def ingest_plan( originals should be deleted afterward. Args: - document: An ndi.Document object. + document: An ndi.ndi_document object. ingestion_directory: Target directory for ingested files. Returns: diff --git a/src/ndi/dataset/__init__.py b/src/ndi/dataset/__init__.py index a3de736..f3108c6 100644 --- a/src/ndi/dataset/__init__.py +++ b/src/ndi/dataset/__init__.py @@ -1,28 +1,28 @@ """ ndi.dataset - Multi-session dataset container. -A Dataset manages multiple sessions, either linked (by reference) or +A ndi_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 -> ndi.dataset.ndi_dataset (or ndi.ndi_dataset) ndi.dataset.dir -> ndi.dataset.dir (constructor for directory-based datasets) """ -from ._dataset import Dataset as _DatasetBase # noqa: F401 -from ._dataset import DatasetDir +from ._dataset import ndi_dataset as _DatasetBase # noqa: F401 +from ._dataset import ndi_dataset_dir -# For backward compatibility, ``ndi.dataset.Dataset`` is ``DatasetDir``. +# For backward compatibility, ``ndi.dataset.ndi_dataset`` is ``ndi_dataset_dir``. # The base class is available as ``ndi.dataset._DatasetBase`` if needed. -Dataset = DatasetDir +ndi_dataset = ndi_dataset_dir # MATLAB compatibility: ``ndi.dataset.dir(path)`` creates a directory-based # dataset, mirroring the MATLAB constructor ``ndi.dataset.dir``. -dir = DatasetDir +dir = ndi_dataset_dir __all__ = [ - "Dataset", - "DatasetDir", + "ndi_dataset", + "ndi_dataset_dir", "dir", ] diff --git a/src/ndi/dataset/_dataset.py b/src/ndi/dataset/_dataset.py index 608e48d..ec7a0a6 100644 --- a/src/ndi/dataset/_dataset.py +++ b/src/ndi/dataset/_dataset.py @@ -1,13 +1,13 @@ """ ndi.dataset - Multi-session dataset container. -A Dataset manages multiple sessions, either linked (by reference) or +A ndi_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 -> Dataset (base class) - ndi.dataset.dir -> DatasetDir (directory-backed subclass) + ndi.dataset -> ndi_dataset (base class) + ndi.dataset.dir -> ndi_dataset_dir (directory-backed subclass) """ from __future__ import annotations @@ -16,29 +16,29 @@ from pathlib import Path from typing import Any -from ..document import Document -from ..query import Query +from ..document import ndi_document +from ..query import ndi_query logger = logging.getLogger(__name__) # ============================================================================ -# Dataset base class (mirrors MATLAB ndi.dataset) +# ndi_dataset base class (mirrors MATLAB ndi.dataset) # ============================================================================ -class Dataset: +class ndi_dataset: """ Multi-session dataset container (base class). MATLAB equivalent: ndi.dataset - A Dataset aggregates multiple sessions for cross-session analysis. + A ndi_dataset aggregates multiple sessions for cross-session analysis. Sessions can be: - **Linked**: Referenced by path/id, data stays in original location - **Ingested**: Documents copied into the dataset's own database - The ``session`` attribute is set by the subclass (e.g. DatasetDir). + The ``session`` attribute is set by the subclass (e.g. ndi_dataset_dir). Attributes: reference: Human-readable dataset reference name (from session) @@ -87,17 +87,17 @@ def getpath(self) -> Path: return self._session.getpath() # ========================================================================= - # Session Management + # ndi_session Management # ========================================================================= - def add_linked_session(self, session: Any) -> Dataset: + def add_linked_session(self, session: Any) -> ndi_dataset: """ Link an ndi.session to this dataset without ingesting. MATLAB equivalent: ``ndi.dataset/add_linked_session`` Args: - session: Session object to link + session: ndi_session object to link Returns: self for chaining @@ -111,7 +111,7 @@ def add_linked_session(self, session: Any) -> Dataset: existing = self._find_session_in_info(session.id()) if existing is not None: raise ValueError( - f"Session with id {session.id()} is already part of " f"dataset {self.id()}." + f"ndi_session with id {session.id()} is already part of " f"dataset {self.id()}." ) session_info_here = self._make_session_info(session, is_linked=True) @@ -123,14 +123,14 @@ def add_linked_session(self, session: Any) -> Dataset: return self - def add_ingested_session(self, session: Any) -> Dataset: + def add_ingested_session(self, session: Any) -> ndi_dataset: """ Ingest a session into this dataset by copying documents. MATLAB equivalent: ``ndi.dataset/add_ingested_session`` Args: - session: Session object to ingest + session: ndi_session object to ingest Returns: self for chaining @@ -145,12 +145,12 @@ def add_ingested_session(self, session: Any) -> Dataset: existing = self._find_session_in_info(session.id()) if existing is not None: raise ValueError( - f"Session with id {session.id()} is already part of " f"dataset {self.id()}." + f"ndi_session with id {session.id()} is already part of " f"dataset {self.id()}." ) if hasattr(session, "is_fully_ingested") and not session.is_fully_ingested(): raise ValueError( - f"Session with id {session.id()} and reference " + f"ndi_session with id {session.id()} and reference " f"{session.reference} is not yet fully ingested. " f"It must be fully ingested before it can be added " f"in ingested form to a dataset." @@ -161,7 +161,7 @@ def add_ingested_session(self, session: Any) -> Dataset: # enforces session_id == self._session.id(), but ingested docs retain # their *original* session_id so we can tell which session they came from. # Binary files are also copied from the source session. - all_docs = session.database_search(Query("").isa("base")) + all_docs = session.database_search(ndi_query("").isa("base")) for doc in all_docs: try: self._session._database.add(doc) @@ -185,7 +185,7 @@ def unlink_session( self, session_id: str, are_you_sure: bool = False, - ) -> Dataset: + ) -> ndi_dataset: """ Unlink a linked session from this dataset. @@ -209,11 +209,13 @@ def unlink_session( match = self._find_session_in_info(session_id) if match is None: - raise ValueError(f"Session with ID {session_id} not found in " f"dataset {self.id()}.") + raise ValueError( + f"ndi_session with ID {session_id} not found in " f"dataset {self.id()}." + ) if not match.get("is_linked", False): raise ValueError( - f"Session with ID {session_id} is an INGESTED session, " + f"ndi_session with ID {session_id} is an INGESTED session, " f"not a linked session. Cannot unlink. Use " f"deleteIngestedSession() instead." ) @@ -230,10 +232,10 @@ def open_session(self, session_id: str) -> Any | None: MATLAB equivalent: ``ndi.dataset/open_session`` Args: - session_id: Session identifier + session_id: ndi_session identifier Returns: - Session object, or None if not found + ndi_session object, or None if not found """ if not self._session_array: self.build_session_info() @@ -292,7 +294,7 @@ def session_list( - id_list: List of session ID strings - session_doc_ids: List of document IDs for the session_in_a_dataset documents - - dataset_session_doc_id: Document ID of the dataset's + - dataset_session_doc_id: ndi_document ID of the dataset's own session document (empty string if not found) """ if not self._session_info: @@ -303,7 +305,7 @@ def session_list( session_doc_ids = [si.get("session_doc_in_dataset_id", "") for si in self._session_info] dataset_session_doc_id = "" - q_ds = Query("").isa("session") & (Query("base.session_id") == self.id()) + q_ds = ndi_query("").isa("session") & (ndi_query("base.session_id") == self.id()) ds_docs = self._session.database_search(q_ds) if len(ds_docs) == 1: dataset_session_doc_id = ds_docs[0].id @@ -313,10 +315,10 @@ def session_list( return ref_list, id_list, session_doc_ids, dataset_session_doc_id # ========================================================================= - # Database Operations (delegated to internal session) + # ndi_database Operations (delegated to internal session) # ========================================================================= - def database_add(self, document: Document | list[Document]) -> Dataset: + def database_add(self, document: ndi_document | list[ndi_document]) -> ndi_dataset: """Add document(s) to the dataset database. MATLAB equivalent: ``ndi.dataset/database_add`` @@ -346,9 +348,9 @@ def database_add(self, document: Document | list[Document]) -> Dataset: def database_rm( self, - doc_or_id: Document | str | list, + doc_or_id: ndi_document | str | list, error_if_not_found: bool = False, - ) -> Dataset: + ) -> ndi_dataset: """Remove document(s) from the dataset database. MATLAB equivalent: ``ndi.dataset/database_rm`` @@ -360,7 +362,7 @@ def database_rm( self._session.database_rm(doc_or_id, error_if_not_found) return self - def database_search(self, query: Query) -> list[Document]: + def database_search(self, query: ndi_query) -> list[ndi_document]: """Search the dataset database and all linked sessions. MATLAB equivalent: ``ndi.dataset/database_search`` @@ -369,7 +371,7 @@ def database_search(self, query: Query) -> list[Document]: session_id), then also searches linked sessions. """ if self._session._database is None: - results: list[Document] = [] + results: list[ndi_document] = [] else: results = list(self._session._database.search(query)) @@ -406,7 +408,7 @@ def database_existbinarydoc( MATLAB equivalent: ``ndi.dataset/database_existbinarydoc`` Args: - doc_or_id: Document or document ID. + doc_or_id: ndi_document or document ID. filename: Name of the binary file. Returns: @@ -419,14 +421,14 @@ def database_closebinarydoc(self, fid: Any) -> None: self._session.database_closebinarydoc(fid) # ========================================================================= - # Ingested Session Management + # Ingested ndi_session Management # ========================================================================= def deleteIngestedSession( self, session_id: str, are_you_sure: bool = False, - ) -> Dataset: + ) -> ndi_dataset: """ Delete an ingested session and all its documents. @@ -440,16 +442,16 @@ def deleteIngestedSession( match = self._find_session_in_info(session_id) if match is None: - raise ValueError(f"Session {session_id} not found in dataset.") + raise ValueError(f"ndi_session {session_id} not found in dataset.") if match.get("is_linked", False): raise ValueError( - f"Session {session_id} is a linked session, not an " + f"ndi_session {session_id} is a linked session, not an " f"ingested one. Use unlink_session() instead." ) # Remove all documents with base.session_id == session_id - q_docs = Query("base.session_id") == session_id + q_docs = ndi_query("base.session_id") == session_id docs_to_delete = self.database_search(q_docs) # Remove the session_in_a_dataset doc @@ -468,7 +470,7 @@ def deleteIngestedSession( return self - def document_session(self, document: Document) -> Any | None: + def document_session(self, document: ndi_document) -> Any | None: """Find which session a document belongs to. MATLAB equivalent: ``ndi.dataset/document_session`` @@ -479,7 +481,7 @@ def document_session(self, document: Document) -> Any | None: return None # ========================================================================= - # Session info management (mirrors MATLAB build_session_info) + # ndi_session info management (mirrors MATLAB build_session_info) # ========================================================================= def build_session_info(self) -> None: @@ -491,13 +493,15 @@ def build_session_info(self) -> None: populates ``_session_info`` and ``_session_array``. """ # Check for legacy dataset_session_info docs and repair - q_legacy = Query("").isa("dataset_session_info") & (Query("base.session_id") == self.id()) + q_legacy = ndi_query("").isa("dataset_session_info") & ( + ndi_query("base.session_id") == self.id() + ) legacy_docs = self._session.database_search(q_legacy) if legacy_docs: self.repairDatasetSessionInfo(self, legacy_docs) # Find session_in_a_dataset docs belonging to this dataset - q = Query("").isa("session_in_a_dataset") & (Query("base.session_id") == self.id()) + q = ndi_query("").isa("session_in_a_dataset") & (ndi_query("base.session_id") == self.id()) info_docs = self._session.database_search(q) self._session_info = [] @@ -523,14 +527,14 @@ def build_session_info(self) -> None: @staticmethod def repairDatasetSessionInfo( - dataset_obj: Dataset, - docs: list[Document], - ) -> list[Document]: + dataset_obj: ndi_dataset, + docs: list[ndi_document], + ) -> list[ndi_document]: """Repair legacy dataset_session_info into session_in_a_dataset docs. MATLAB equivalent: ``ndi.dataset.repairDatasetSessionInfo`` """ - new_docs: list[Document] = [] + new_docs: list[ndi_document] = [] if not docs: return new_docs @@ -568,7 +572,7 @@ def repairDatasetSessionInfo( val = False props[f"session_in_a_dataset.{f}"] = val - new_doc = Document("session_in_a_dataset", **props) + new_doc = ndi_document("session_in_a_dataset", **props) new_doc = new_doc.set_session_id(current_dataset_id) new_docs.append(new_doc) @@ -582,9 +586,9 @@ def repairDatasetSessionInfo( @staticmethod def addSessionInfoToDataset( - dataset_obj: Dataset, + dataset_obj: ndi_dataset, session_info: dict[str, Any], - ) -> Document: + ) -> ndi_document: """Add a session_in_a_dataset document to the dataset. MATLAB equivalent: ``ndi.dataset.addSessionInfoToDataset`` @@ -594,22 +598,22 @@ def addSessionInfoToDataset( if key != "session_doc_in_dataset_id": props[f"session_in_a_dataset.{key}"] = val - new_doc = Document("session_in_a_dataset", **props) + new_doc = ndi_document("session_in_a_dataset", **props) new_doc = new_doc.set_session_id(dataset_obj.id()) dataset_obj._session.database_add(new_doc) return new_doc @staticmethod def removeSessionInfoFromDataset( - dataset_obj: Dataset, + dataset_obj: ndi_dataset, session_id: str, ) -> None: """Remove session_in_a_dataset document(s) for a given session ID. MATLAB equivalent: ``ndi.dataset.removeSessionInfoFromDataset`` """ - q = (Query("session_in_a_dataset.session_id") == session_id) & ( - Query("base.session_id") == dataset_obj.id() + q = (ndi_query("session_in_a_dataset.session_id") == session_id) & ( + ndi_query("base.session_id") == dataset_obj.id() ) docs = dataset_obj._session.database_search(q) for doc in docs: @@ -650,24 +654,24 @@ def _recreate_session( """Recreate a session from stored creator args.""" creator = info.get("session_creator", "") - if creator == "DirSession" or creator == "ndi.session.dir": - from ..session.dir import DirSession + if creator == "ndi_session_dir" or creator == "ndi.session.dir": + from ..session.dir import ndi_session_dir ref = info.get("session_creator_input1", "") if ref and path_arg: try: - return DirSession(ref, path_arg, session_id=session_id) + return ndi_session_dir(ref, path_arg, session_id=session_id) except Exception: pass elif path_arg: try: - return DirSession(path_arg) + return ndi_session_dir(path_arg) except Exception: pass return None - def _copy_binary_files(self, source_session: Any, doc: Document) -> None: + def _copy_binary_files(self, source_session: Any, doc: ndi_document) -> None: """Copy binary file attachments from a source session to this dataset.""" import shutil @@ -721,15 +725,15 @@ def _open_linked_sessions(self) -> None: def __repr__(self) -> str: """String representation.""" refs, _ids, _doc_ids, _ds_doc_id = self.session_list() - return f"Dataset('{self.reference}', sessions={len(refs)})" + return f"ndi_dataset('{self.reference}', sessions={len(refs)})" # ============================================================================ -# DatasetDir (mirrors MATLAB ndi.dataset.dir) +# ndi_dataset_dir (mirrors MATLAB ndi.dataset.dir) # ============================================================================ -class DatasetDir(Dataset): +class ndi_dataset_dir(ndi_dataset): """ Directory-backed dataset. @@ -740,14 +744,14 @@ class DatasetDir(Dataset): The constructor supports three calling conventions (mirroring MATLAB): - 1. ``DatasetDir(path)`` — open existing dataset - 2. ``DatasetDir(reference, path)`` — create / open with reference - 3. ``DatasetDir(reference, path, documents=docs)`` — create from + 1. ``ndi_dataset_dir(path)`` — open existing dataset + 2. ``ndi_dataset_dir(reference, path)`` — create / open with reference + 3. ``ndi_dataset_dir(reference, path, documents=docs)`` — create from pre-loaded documents (used by ``downloadDataset``) Example: - >>> dataset = DatasetDir('/path/to/dataset') - >>> dataset = DatasetDir('my_experiment', '/path/to/dataset') + >>> dataset = ndi_dataset_dir('/path/to/dataset') + >>> dataset = ndi_dataset_dir('my_experiment', '/path/to/dataset') """ def __init__( @@ -756,46 +760,46 @@ def __init__( path_or_ref: str | Path | None = None, *, reference: str | None = None, - documents: list[Document] | None = None, + documents: list[ndi_document] | None = None, ): """ - Create or open a directory-based Dataset. + Create or open a directory-based ndi_dataset. Supports multiple calling conventions: - - ``DatasetDir(path)`` — open existing - - ``DatasetDir(reference, path)`` — MATLAB style - - ``DatasetDir(path, reference='name')`` — keyword style + - ``ndi_dataset_dir(path)`` — open existing + - ``ndi_dataset_dir(reference, path)`` — MATLAB style + - ``ndi_dataset_dir(path, reference='name')`` — keyword style Args: reference_or_path: Either the dataset reference or path path_or_ref: Optional second positional arg (path or reference) reference: Keyword-only reference override - documents: Optional list of pre-loaded Document objects. + documents: Optional list of pre-loaded ndi_document objects. When provided, documents are bulk-inserted and the session is configured from them (hidden argument, used by downloadDataset). """ - from ..session.dir import DirSession + from ..session.dir import ndi_session_dir super().__init__() # Determine reference and path from arguments. # MATLAB convention: dir(reference, path_name) - # Old Python convention: Dataset(path, reference) + # Old Python convention: ndi_dataset(path, reference) # We support both by detecting whether the second arg is a path. if path_or_ref is None: - # 1-arg form: DatasetDir(path) + # 1-arg form: ndi_dataset_dir(path) self._path = Path(reference_or_path) ref = reference or "" elif isinstance(path_or_ref, Path) or ( isinstance(path_or_ref, str) and ("/" in path_or_ref or "\\" in path_or_ref) ): - # MATLAB-style: DatasetDir(reference, path) + # MATLAB-style: ndi_dataset_dir(reference, path) ref = str(reference_or_path) if reference_or_path else "" self._path = Path(path_or_ref) else: - # Old Python-style: DatasetDir(path, "reference_string") + # Old Python-style: ndi_dataset_dir(path, "reference_string") self._path = Path(reference_or_path) ref = str(path_or_ref) @@ -810,7 +814,7 @@ def __init__( # Mirrors MATLAB ndi.dataset.dir(reference, path_name, docs). dataset_session_id = self._dataset_session_id_from_docs(documents) # Create session with forced ID so docs can be inserted - self._session = DirSession( + self._session = ndi_session_dir( ref or "temp", self._path, session_id=dataset_session_id, @@ -822,18 +826,18 @@ def __init__( except Exception: pass # Re-create session without forced ID (reads from database) - self._session = DirSession(ref or "temp", self._path) + self._session = ndi_session_dir(ref or "temp", self._path) elif path_or_ref is None and not ref: # 1-arg form: try opening existing, or create with dir name as reference try: - self._session = DirSession(self._path) + self._session = ndi_session_dir(self._path) except ValueError: - self._session = DirSession(self._path.name, self._path) + self._session = ndi_session_dir(self._path.name, self._path) else: # 2-arg form - self._session = DirSession(ref or self._path.name, self._path) + self._session = ndi_session_dir(ref or self._path.name, self._path) - # Session discovery: find the correct session ID and reference + # ndi_session discovery: find the correct session ID and reference # from documents in the database. Mirrors the MATLAB # ndi.dataset.dir constructor logic. self._discover_correct_session(ref) @@ -850,26 +854,26 @@ def _discover_correct_session(self, initial_reference: str) -> None: for dataset_session_info → session_in_a_dataset → session documents to determine the correct session ID and reference. """ - from ..session.dir import DirSession + from ..session.dir import ndi_session_dir correct_session_id = "" # 1. Check for legacy dataset_session_info docs - dsi_docs = self.database_search(Query("").isa("dataset_session_info")) + dsi_docs = self.database_search(ndi_query("").isa("dataset_session_info")) if dsi_docs: correct_session_id = ( dsi_docs[0].document_properties.get("base", {}).get("session_id", "") ) else: # 2. Check for session_in_a_dataset docs - sia_docs = self.database_search(Query("").isa("session_in_a_dataset")) + sia_docs = self.database_search(ndi_query("").isa("session_in_a_dataset")) if sia_docs: correct_session_id = ( sia_docs[0].document_properties.get("base", {}).get("session_id", "") ) else: # 3. Check for a single session doc - session_docs = self.database_search(Query("").isa("session")) + session_docs = self.database_search(ndi_query("").isa("session")) if len(session_docs) == 1: correct_session_id = ( session_docs[0].document_properties.get("base", {}).get("session_id", "") @@ -877,17 +881,17 @@ def _discover_correct_session(self, initial_reference: str) -> None: if correct_session_id: # Find the session document with this ID - q = Query("").isa("session") & (Query("base.session_id") == correct_session_id) + q = ndi_query("").isa("session") & (ndi_query("base.session_id") == correct_session_id) candidate_docs = self.database_search(q) if len(candidate_docs) == 1: ref = candidate_docs[0].document_properties.get("session", {}).get("reference", "") sid = candidate_docs[0].document_properties.get("base", {}).get("session_id", "") # Re-create session with the correct reference and ID - self._session = DirSession(ref, self._path, session_id=sid) + self._session = ndi_session_dir(ref, self._path, session_id=sid) # Repair legacy dataset_session_info if found if dsi_docs: - dsi_docs2 = self.database_search(Query("").isa("dataset_session_info")) + dsi_docs2 = self.database_search(ndi_query("").isa("dataset_session_info")) if dsi_docs2: self.repairDatasetSessionInfo(self, dsi_docs2) @@ -903,7 +907,9 @@ def _ensure_session_tracking(self) -> None: return # Find already-tracked session IDs - q_tracked = Query("").isa("session_in_a_dataset") & (Query("base.session_id") == self.id()) + q_tracked = ndi_query("").isa("session_in_a_dataset") & ( + ndi_query("base.session_id") == self.id() + ) tracked_docs = self._session.database_search(q_tracked) tracked_ids: set[str] = set() for doc in tracked_docs: @@ -913,7 +919,7 @@ def _ensure_session_tracking(self) -> None: tracked_ids.add(sid) # Find session documents in the database - q_session = Query("").isa("session") + q_session = ndi_query("").isa("session") session_docs = list(self._session._database.search(q_session)) ds_session_id = self._session.id() @@ -924,13 +930,13 @@ def _ensure_session_tracking(self) -> None: if not sid or sid == ds_session_id or sid in tracked_ids: continue ref = props.get("session", {}).get("reference", "") - tracking_doc = Document( + tracking_doc = ndi_document( "session_in_a_dataset", **{ "session_in_a_dataset.session_id": sid, "session_in_a_dataset.session_reference": ref, "session_in_a_dataset.is_linked": False, - "session_in_a_dataset.session_creator": "DirSession", + "session_in_a_dataset.session_creator": "ndi_session_dir", }, ) tracking_doc = tracking_doc.set_session_id(ds_session_id) @@ -941,7 +947,7 @@ def _ensure_session_tracking(self) -> None: logger.debug("Could not register session %s: skipping", sid) @staticmethod - def dataset_erase(ndi_dataset_dir_obj: DatasetDir, areyousure: str = "no") -> None: + def dataset_erase(ndi_dataset_dir_obj: ndi_dataset_dir, areyousure: str = "no") -> None: """ Delete the entire dataset database folder. @@ -951,7 +957,7 @@ def dataset_erase(ndi_dataset_dir_obj: DatasetDir, areyousure: str = "no") -> No directory inside the dataset path will be permanently removed. Args: - ndi_dataset_dir_obj: The DatasetDir instance to erase. + ndi_dataset_dir_obj: The ndi_dataset_dir instance to erase. areyousure: Must be ``'yes'`` to proceed. """ import shutil @@ -967,7 +973,7 @@ def dataset_erase(ndi_dataset_dir_obj: DatasetDir, areyousure: str = "no") -> No ) @staticmethod - def _dataset_session_id_from_docs(documents: list[Document]) -> str: + def _dataset_session_id_from_docs(documents: list[ndi_document]) -> str: """Extract the dataset session ID from a list of documents. MATLAB equivalent: ``ndi.cloud.sync.internal.datasetSessionIdFromDocs`` @@ -1011,4 +1017,4 @@ def _dataset_session_id_from_docs(documents: list[Document]) -> str: def __repr__(self) -> str: """String representation.""" refs, _ids, _doc_ids, _ds_doc_id = self.session_list() - return f"Dataset('{self.reference}', sessions={len(refs)})" + return f"ndi_dataset('{self.reference}', sessions={len(refs)})" diff --git a/src/ndi/dataset/ndi_matlab_python_bridge.yaml b/src/ndi/dataset/ndi_matlab_python_bridge.yaml index 66dd7ba..b95a0fe 100644 --- a/src/ndi/dataset/ndi_matlab_python_bridge.yaml +++ b/src/ndi/dataset/ndi_matlab_python_bridge.yaml @@ -16,7 +16,7 @@ classes: matlab_path: "+ndi/dataset.m" matlab_last_sync_hash: "7512bcb0" python_path: "ndi/dataset/_dataset.py" - python_class: "Dataset" + python_class: "ndi_dataset" properties: - name: session @@ -47,7 +47,7 @@ classes: type_python: "str" output_arguments: - name: ndi_dataset_obj - type_python: "Dataset" + type_python: "ndi_dataset" decision_log: "Python __init__ takes no arguments; session is set by subclass." # --- Public instance methods --- @@ -78,7 +78,7 @@ classes: type_python: "Any" output_arguments: - name: ndi_dataset_obj - type_python: "Dataset" + type_python: "ndi_dataset" decision_log: "MATLAB name uses underscores. Exact match." - name: add_ingested_session @@ -88,7 +88,7 @@ classes: type_python: "Any" output_arguments: - name: ndi_dataset_obj - type_python: "Dataset" + type_python: "ndi_dataset" decision_log: "MATLAB name uses underscores. Exact match." - name: deleteIngestedSession @@ -102,7 +102,7 @@ classes: default: "False" output_arguments: - name: ndi_dataset_obj - type_python: "Dataset" + type_python: "ndi_dataset" decision_log: > MATLAB name is deleteIngestedSession (camelCase). Python was previously delete_ingested_session; renamed to match MATLAB. @@ -118,7 +118,7 @@ classes: default: "False" output_arguments: - name: ndi_dataset_obj - type_python: "Dataset" + type_python: "ndi_dataset" decision_log: "MATLAB name uses underscores. Exact match." - name: open_session @@ -148,34 +148,34 @@ classes: input_arguments: - name: document type_matlab: "ndi.document | cell" - type_python: "Document | list[Document]" + type_python: "ndi_document | list[ndi_document]" output_arguments: - name: ndi_dataset_obj - type_python: "Dataset" + type_python: "ndi_dataset" decision_log: "Exact match." - name: database_rm input_arguments: - name: doc_unique_id type_matlab: "ndi.document | char | cell" - type_python: "Document | str | list" + type_python: "ndi_document | str | list" - name: error_if_not_found type_matlab: "logical (name-value)" type_python: "bool" default: "False" output_arguments: - name: ndi_dataset_obj - type_python: "Dataset" + type_python: "ndi_dataset" decision_log: "Exact match." - name: database_search input_arguments: - name: searchparameters type_matlab: "ndi.query" - type_python: "Query" + type_python: "ndi_query" output_arguments: - name: ndi_document_obj - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: "Exact match." - name: database_openbinarydoc @@ -218,7 +218,7 @@ classes: input_arguments: - name: ndi_document_obj type_matlab: "ndi.document" - type_python: "Document" + type_python: "ndi_document" output_arguments: - name: ndi_session_obj type_python: "Any | None" @@ -242,31 +242,31 @@ classes: kind: static input_arguments: - name: ndi_dataset_obj - type_python: "Dataset" + type_python: "ndi_dataset" - name: doc - type_python: "list[Document]" + type_python: "list[ndi_document]" output_arguments: - name: new_docs - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: "MATLAB camelCase preserved exactly." - name: addSessionInfoToDataset kind: static input_arguments: - name: ndi_dataset_obj - type_python: "Dataset" + type_python: "ndi_dataset" - name: session_info type_python: "dict[str, Any]" output_arguments: - name: new_doc - type_python: "Document" + type_python: "ndi_document" decision_log: "MATLAB camelCase preserved exactly." - name: removeSessionInfoFromDataset kind: static input_arguments: - name: ndi_dataset_obj - type_python: "Dataset" + type_python: "ndi_dataset" - name: session_id type_python: "str" output_arguments: [] @@ -280,7 +280,7 @@ classes: matlab_path: "+ndi/+dataset/dir.m" matlab_last_sync_hash: "7512bcb0" python_path: "ndi/dataset/_dataset.py" - python_class: "DatasetDir" + python_class: "ndi_dataset_dir" inherits: "ndi.dataset" properties: @@ -302,10 +302,10 @@ classes: type_python: "str | Path | None" - name: docs type_matlab: "cell (hidden)" - type_python: "list[Document] | None" + type_python: "list[ndi_document] | None" output_arguments: - name: ndi_dataset_dir_obj - type_python: "DatasetDir" + type_python: "ndi_dataset_dir" decision_log: > Python constructor supports MATLAB calling conventions: dir(path), dir(reference, path), dir(reference, path, docs). @@ -315,7 +315,7 @@ classes: input_arguments: - name: ndi_dataset_dir_obj type_matlab: "ndi.dataset.dir" - type_python: "DatasetDir" + type_python: "ndi_dataset_dir" - name: areyousure type_matlab: "char" type_python: "str" diff --git a/src/ndi/document.py b/src/ndi/document.py index 6b3c206..dbc5200 100644 --- a/src/ndi/document.py +++ b/src/ndi/document.py @@ -1,7 +1,7 @@ """ ndi.document - NDI database storage item -The ndi.Document class is the primary method for storing data in the NDI database. +The ndi.ndi_document class is the primary method for storing data in the NDI database. Each document has a specific schema type (e.g., 'base', 'session', 'stimulus'). Important Rules for Creating Documents: @@ -29,14 +29,14 @@ except ImportError: DIDDocument = None -from .common import PathConstants, timestamp -from .ido import Ido +from .common import ndi_common_PathConstants, timestamp +from .ido import ndi_ido -class Document: +class ndi_document: """NDI document class for database storage. - The Document object is the primary method for storing data in the NDI database. + The ndi_document object is the primary method for storing data in the NDI database. Each document has a specific schema type and contains structured properties. Attributes: @@ -44,7 +44,7 @@ class Document: Example: # Create a new document - doc = Document('base', **{'base.name': 'my_document'}) + doc = ndi_document('base', **{'base.name': 'my_document'}) # Access properties doc_id = doc.id @@ -54,27 +54,27 @@ class Document: doc = doc.set_session_id('session_12345') """ - def __init__(self, document_type: Union[str, dict, "Document"] = "base", **kwargs): + def __init__(self, document_type: Union[str, dict, "ndi_document"] = "base", **kwargs): """Create a new NDI document. Args: document_type: Either: - A string document type name (e.g., 'base', 'stimulus') - A dict containing document properties (for loading existing) - - Another Document object (for copying) + - Another ndi_document object (for copying) - A DIDDocument object (for conversion, if DID-python is installed) **kwargs: Name/value pairs for setting document properties. Property names should be fully qualified (e.g., 'base.name'). Example: - doc = Document('base', **{'base.name': 'my_doc'}) - doc = Document(existing_doc.document_properties) + doc = ndi_document('base', **{'base.name': 'my_doc'}) + doc = ndi_document(existing_doc.document_properties) """ if isinstance(document_type, dict): # Loading from existing properties self._document_properties = deepcopy(document_type) - elif isinstance(document_type, Document): - # Copy from another Document + elif isinstance(document_type, ndi_document): + # Copy from another ndi_document self._document_properties = deepcopy(document_type.document_properties) elif DIDDocument is not None and isinstance(document_type, DIDDocument): # Convert from DIDDocument @@ -84,7 +84,7 @@ def __init__(self, document_type: Union[str, dict, "Document"] = "base", **kwarg self._document_properties = self.read_blank_definition(document_type) # Generate new ID and timestamp - ido = Ido() + ido = ndi_ido() self._document_properties["base"]["id"] = ido.id self._document_properties["base"]["datestamp"] = timestamp() @@ -107,7 +107,7 @@ def session_id(self) -> str: """Return the document's session identifier.""" return self._document_properties.get("base", {}).get("session_id", "") - def set_session_id(self, session_id: str) -> "Document": + def set_session_id(self, session_id: str) -> "ndi_document": """Set the session ID for this document. Args: @@ -172,7 +172,7 @@ def add_file( ingest: bool | None = None, delete_original: bool | None = None, location_type: str | None = None, - ) -> "Document": + ) -> "ndi_document": """Add a file to this document. Args: @@ -215,7 +215,7 @@ def add_file( location_type = detected_type # Generate unique ID for this file location - uid = Ido().id + uid = ndi_ido().id location_info = { "delete_original": delete_original, @@ -241,7 +241,7 @@ def add_file( return self - def remove_file(self, name: str, location: str | None = None) -> "Document": + def remove_file(self, name: str, location: str | None = None) -> "ndi_document": """Remove file information from this document. Args: @@ -389,7 +389,7 @@ def dependency_value_n( def set_dependency_value( self, dependency_name: str, value: str, error_if_not_found: bool = True - ) -> "Document": + ) -> "ndi_document": """Set the value of a dependency. Args: @@ -403,7 +403,7 @@ def set_dependency_value( """ if "depends_on" not in self._document_properties: if error_if_not_found: - raise KeyError("Document has no dependencies") + raise KeyError("ndi_document has no dependencies") self._document_properties["depends_on"] = [] depends_on = self._document_properties["depends_on"] @@ -422,7 +422,7 @@ def set_dependency_value( depends_on.append({"name": dependency_name, "value": value}) return self - def add_dependency_value_n(self, dependency_name: str, value: str) -> "Document": + def add_dependency_value_n(self, dependency_name: str, value: str) -> "ndi_document": """Add a value to a numbered dependency list. Args: @@ -436,7 +436,7 @@ def add_dependency_value_n(self, dependency_name: str, value: str) -> "Document" new_name = f"{dependency_name}_{len(existing) + 1}" return self.set_dependency_value(new_name, value, error_if_not_found=False) - # === Document Class Information === + # === ndi_document Class Information === def doc_class(self) -> str: """Get the document class type. @@ -455,6 +455,10 @@ def doc_superclass(self) -> list[str]: doc_class = self._document_properties.get("document_class", {}) superclasses = doc_class.get("superclasses", []) + # MATLAB may store superclasses as a single dict instead of a list + if isinstance(superclasses, dict): + superclasses = [superclasses] + sc_names = [] for sc in superclasses: # Each superclass has a 'definition' pointing to its JSON @@ -462,7 +466,7 @@ def doc_superclass(self) -> list[str]: definition = sc.get("definition", "") if definition: try: - sc_doc = Document( + sc_doc = ndi_document( definition.replace("$NDIDOCUMENTPATH/", "").replace(".json", "") ) sc_names.append(sc_doc.doc_class()) @@ -499,7 +503,7 @@ def remove_dependency_value_n( self, dependency_name: str, index: int | None = None, - ) -> "Document": + ) -> "ndi_document": """Remove a numbered dependency value. If index is None, removes all dependencies matching the base name. @@ -533,22 +537,22 @@ def remove_dependency_value_n( # === Comparison and Merging === - def __eq__(self, other: "Document") -> bool: + def __eq__(self, other: "ndi_document") -> bool: """Check equality based on document ID.""" - if not isinstance(other, Document): + if not isinstance(other, ndi_document): return False return self.id == other.id - def __add__(self, other: "Document") -> "Document": + def __add__(self, other: "ndi_document") -> "ndi_document": """Merge two documents. The result has fields from both documents. Fields in self take precedence over fields in other for conflicts. Returns: - New Document with merged properties. + New ndi_document with merged properties. """ - result = Document(self._document_properties) + result = ndi_document(self._document_properties) # Merge superclasses my_sc = result._document_properties.get("document_class", {}).get("superclasses", []) @@ -642,7 +646,7 @@ def to_json(self, indent: int = 2) -> str: """ return json.dumps(self._document_properties, indent=indent) - def setproperties(self, **kwargs) -> "Document": + def setproperties(self, **kwargs) -> "ndi_document": """Set multiple properties at once. Args: @@ -682,12 +686,12 @@ def validate(self, session=None): @staticmethod def find_doc_by_id( - doc_array: list["Document"], doc_id: str - ) -> tuple[Optional["Document"], int | None]: + doc_array: list["ndi_document"], doc_id: str + ) -> tuple[Optional["ndi_document"], int | None]: """Find a document in a list by its ID. Args: - doc_array: List of Document objects. + doc_array: List of ndi_document objects. doc_id: The ID to search for. Returns: @@ -699,17 +703,17 @@ def find_doc_by_id( return None, None @staticmethod - def find_newest(doc_array: list["Document"]) -> tuple["Document", int, datetime]: + def find_newest(doc_array: list["ndi_document"]) -> tuple["ndi_document", int, datetime]: """Find the newest document in a list. Args: - doc_array: List of Document objects. + doc_array: List of ndi_document objects. Returns: Tuple of (newest_document, index, datestamp). """ if not doc_array: - raise ValueError("Document array is empty") + raise ValueError("ndi_document array is empty") timestamps = [] for doc in doc_array: @@ -738,12 +742,12 @@ def read_blank_definition(document_type: str) -> dict: Dictionary with blank document structure. """ # Try to find the JSON definition - json_path = PathConstants.DOCUMENT_PATH / f"{document_type}.json" + json_path = ndi_common_PathConstants.DOCUMENT_PATH / f"{document_type}.json" if not json_path.exists(): # Try without path constants (for testing) raise FileNotFoundError( - f"Document definition not found: {json_path}. " + f"ndi_document definition not found: {json_path}. " f"Make sure NDI is properly installed." ) @@ -758,7 +762,7 @@ def read_blank_definition(document_type: str) -> dict: # Extract document type from definition path sc_type = sc_def.replace("$NDIDOCUMENTPATH/", "").replace(".json", "") try: - sc_props = Document.read_blank_definition(sc_type) + sc_props = ndi_document.read_blank_definition(sc_type) # Merge superclass properties for key, value in sc_props.items(): if key != "document_class" and key not in definition: @@ -769,4 +773,4 @@ def read_blank_definition(document_type: str) -> dict: return definition def __repr__(self) -> str: - return f"Document('{self.doc_class()}', id='{self.id}')" + return f"ndi_document('{self.doc_class()}', id='{self.id}')" diff --git a/src/ndi/documentservice.py b/src/ndi/documentservice.py index dd679b1..a5ff026 100644 --- a/src/ndi/documentservice.py +++ b/src/ndi/documentservice.py @@ -1,7 +1,7 @@ """ ndi.documentservice - Mixin for database document handling. -This module provides the DocumentService mixin class that defines +This module provides the ndi_documentservice mixin class that defines the interface for objects that can be stored in and loaded from the NDI database. """ @@ -12,15 +12,15 @@ from typing import TYPE_CHECKING, Any if TYPE_CHECKING: - from .document import Document - from .query import Query + from .document import ndi_document + from .query import ndi_query -class DocumentService(ABC): +class ndi_documentservice(ABC): """ Mixin for database document handling. - DocumentService defines the interface for objects that can be + ndi_documentservice defines the interface for objects that can be represented as documents in the NDI database. Classes that inherit from this mixin can create documents for storage and queries for retrieval. @@ -30,42 +30,42 @@ class DocumentService(ABC): - searchquery(): Create a query to find this object Example: - >>> class MyObject(DocumentService): + >>> class MyObject(ndi_documentservice): ... def newdocument(self): - ... return Document('myobject', **{'myobject.name': self.name}) + ... return ndi_document('myobject', **{'myobject.name': self.name}) ... def searchquery(self): - ... return Query('base.id') == self.id + ... return ndi_query('base.id') == self.id """ @abstractmethod - def newdocument(self) -> Document: + def newdocument(self) -> ndi_document: """ Create a new document for this object. Returns: - Document representing this object + ndi_document representing this object """ pass @abstractmethod - def searchquery(self) -> Query: + def searchquery(self) -> ndi_query: """ Create a query to find this object in the database. Returns: - Query that matches this object's document + ndi_query that matches this object's document """ pass - def load_element_doc(self, session: Any) -> Document | None: + def load_element_doc(self, session: Any) -> ndi_document | None: """ Load this object's document from the database. Args: - session: Session with database access + session: ndi_session with database access Returns: - Document if found, None otherwise + ndi_document if found, None otherwise """ q = self.searchquery() docs = session.database_search(q) @@ -78,10 +78,10 @@ def document_id(self, session: Any) -> str | None: Get the document ID for this object. Args: - session: Session with database access + session: ndi_session with database access Returns: - Document ID if found, None otherwise + ndi_document ID if found, None otherwise """ doc = self.load_element_doc(session) if doc is not None: diff --git a/src/ndi/element/__init__.py b/src/ndi/element/__init__.py index 6b81d2c..c2cc6fd 100644 --- a/src/ndi/element/__init__.py +++ b/src/ndi/element/__init__.py @@ -1,7 +1,7 @@ """ ndi.element - Base class for data elements. -This module provides the Element class that represents logical +This module provides the ndi_element class that represents logical data sources in neuroscience experiments (e.g., electrodes, stimulators, behavioral sensors). """ @@ -12,36 +12,36 @@ import numpy as np -from ..documentservice import DocumentService -from ..epoch.epochprobemap import EpochProbeMap -from ..epoch.epochset import EpochSet -from ..ido import Ido -from ..time import ClockType +from ..documentservice import ndi_documentservice +from ..epoch.epochprobemap import ndi_epoch_epochprobemap +from ..epoch.epochset import ndi_epoch_epochset +from ..ido import ndi_ido +from ..time import ndi_time_clocktype -class Element(Ido, EpochSet, DocumentService): +class ndi_element(ndi_ido, ndi_epoch_epochset, ndi_documentservice): """ Base class for data elements. - Element represents a logical data source or sink in an experiment. + ndi_element represents a logical data source or sink in an experiment. Elements can be electrodes, stimulators, behavioral sensors, or any other entity that produces or consumes time series data. - Elements manage epochs through the EpochSet interface and can - be stored in the database through DocumentService. + Elements manage epochs through the ndi_epoch_epochset interface and can + be stored in the database through ndi_documentservice. Attributes: session: Associated session object - name: Element name (no whitespace) + name: ndi_element name (no whitespace) reference: Reference number (non-negative) - type: Element type identifier (no whitespace) - underlying_element: Element this depends on (for derived elements) + type: ndi_element type identifier (no whitespace) + underlying_element: ndi_element this depends on (for derived elements) direct: If True, epochs come directly from underlying_element subject_id: Associated subject document ID dependencies: Additional named dependencies Example: - >>> elem = Element( + >>> elem = ndi_element( ... session=my_session, ... name='electrode1', ... reference=1, @@ -56,7 +56,7 @@ def __init__( name: str = "", reference: int = 0, type: str = "", - underlying_element: Element | None = None, + underlying_element: ndi_element | None = None, direct: bool = True, subject_id: str = "", dependencies: dict[str, str] | None = None, @@ -64,25 +64,25 @@ def __init__( document: Any | None = None, ): """ - Create a new Element. + Create a new ndi_element. Can be created from scratch or loaded from a document. Args: - session: Session object with database access - name: Element name (no whitespace allowed) + session: ndi_session object with database access + name: ndi_element name (no whitespace allowed) reference: Reference number (non-negative integer) - type: Element type identifier (no whitespace) - underlying_element: Element this depends on + type: ndi_element type identifier (no whitespace) + underlying_element: ndi_element this depends on direct: If True, use underlying_element epochs directly - subject_id: Subject document ID + subject_id: ndi_subject document ID dependencies: Dict of named dependencies identifier: Optional unique identifier document: Optional document to load from """ # Initialize base classes - Ido.__init__(self, identifier) - EpochSet.__init__(self) + ndi_ido.__init__(self, identifier) + ndi_epoch_epochset.__init__(self) # Load from document if provided if document is not None and session is not None: @@ -136,12 +136,12 @@ def _load_from_document(self, session: Any, document: Any) -> None: # Load underlying element if dependency exists underlying_id = document.dependency_value("underlying_element_id", error_if_not_found=False) if underlying_id: - from ..query import Query + from ..query import ndi_query - q = Query("base.id") == underlying_id + q = ndi_query("base.id") == underlying_id docs = session.database_search(q) if len(docs) == 1: - self._underlying_element = Element(session=session, document=docs[0]) + self._underlying_element = ndi_element(session=session, document=docs[0]) @property def session(self) -> Any: @@ -164,7 +164,7 @@ def type(self) -> str: return self._type @property - def underlying_element(self) -> Element | None: + def underlying_element(self) -> ndi_element | None: """Get the underlying element.""" return self._underlying_element @@ -207,7 +207,7 @@ def elementstring(self) -> str: return f"{self._name} | {self._reference}" # ========================================================================= - # EpochSet Implementation + # ndi_epoch_epochset Implementation # ========================================================================= def buildepochtable(self) -> list[dict[str, Any]]: @@ -262,10 +262,10 @@ def _build_registered_epochtable(self) -> list[dict[str, Any]]: if self._session is None: return [] - from ..query import Query + from ..query import ndi_query - # Query for registered epochs - q = Query("").isa("element_epoch") & Query("").depends_on("element_id", self.id) + # ndi_query for registered epochs + q = ndi_query("").isa("element_epoch") & ndi_query("").depends_on("element_id", self.id) epoch_docs = self._session.database_search(q) et = [] @@ -277,8 +277,8 @@ def _build_registered_epochtable(self) -> list[dict[str, Any]]: epoch_clock = [] for c in clock_raw: if isinstance(c, str): - epoch_clock.append(ClockType(c)) - elif isinstance(c, ClockType): + epoch_clock.append(ndi_time_clocktype(c)) + elif isinstance(c, ndi_time_clocktype): epoch_clock.append(c) # Parse t0_t1 @@ -319,15 +319,15 @@ def issyncgraphroot(self) -> bool: return self._underlying_element is None # ========================================================================= - # Epoch Management + # ndi_epoch_epoch Management # ========================================================================= def addepoch( self, epoch_id: str, - epoch_clock: list[ClockType], + epoch_clock: list[ndi_time_clocktype], t0_t1: list[tuple[float, float]], - ) -> tuple[Element, Any]: + ) -> tuple[ndi_element, Any]: """ Add a new epoch to this element. @@ -348,12 +348,12 @@ def addepoch( raise ValueError("Cannot add epochs to direct elements") if self._session is None: - raise ValueError("Session required to add epochs") + raise ValueError("ndi_session required to add epochs") - from ..document import Document + from ..document import ndi_document # Create epoch document - doc = Document( + doc = ndi_document( "element_epoch", **{ "element_epoch.epoch_clock": [str(c) for c in epoch_clock], @@ -382,9 +382,9 @@ def loadaddedepochs(self) -> tuple[list[dict[str, Any]], list[Any]]: if self._session is None: return [], [] - from ..query import Query + from ..query import ndi_query - q = Query("").isa("element_epoch") & Query("").depends_on("element_id", self.id) + q = ndi_query("").isa("element_epoch") & ndi_query("").depends_on("element_id", self.id) epoch_docs = self._session.database_search(q) et = self._build_registered_epochtable() @@ -392,7 +392,7 @@ def loadaddedepochs(self) -> tuple[list[dict[str, Any]], list[Any]]: return et, epoch_docs # ========================================================================= - # DocumentService Implementation + # ndi_documentservice Implementation # ========================================================================= def newdocument(self) -> Any: @@ -400,11 +400,11 @@ def newdocument(self) -> Any: Create a new document for this element. Returns: - Document representing this element + ndi_document representing this element """ - from ..document import Document + from ..document import ndi_document - doc = Document( + doc = ndi_document( "element", **{ "element.ndi_element_class": self.ndi_element_class(), @@ -443,19 +443,19 @@ def searchquery(self) -> Any: by the MATLAB implementation. Returns: - Query matching this element's document + ndi_query matching this element's document """ - from ..query import Query + from ..query import ndi_query - q = self._session.searchquery() if self._session is not None else Query("") - q = q & (Query("element.name") == self._name) - q = q & (Query("element.type") == self._type) - q = q & (Query("element.ndi_element_class") == self.ndi_element_class()) - q = q & (Query("element.reference") == self._reference) + q = self._session.searchquery() if self._session is not None else ndi_query("") + q = q & (ndi_query("element.name") == self._name) + q = q & (ndi_query("element.type") == self._type) + q = q & (ndi_query("element.ndi_element_class") == self.ndi_element_class()) + q = q & (ndi_query("element.reference") == self._reference) return q # ========================================================================= - # Cache Management + # ndi_cache Management # ========================================================================= def getcache(self) -> tuple[Any | None, str]: @@ -479,7 +479,7 @@ def getcache(self) -> tuple[Any | None, str]: def __eq__(self, other: Any) -> bool: """Test equality by name, reference, and type.""" - if not isinstance(other, Element): + if not isinstance(other, ndi_element): return False return ( self._name == other._name @@ -493,4 +493,4 @@ def __hash__(self) -> int: def __repr__(self) -> str: """String representation.""" - return f"Element({self._name}|{self._reference}|{self._type})" + return f"ndi_element({self._name}|{self._reference}|{self._type})" diff --git a/src/ndi/element/functions.py b/src/ndi/element/functions.py index a37d312..3a74bb0 100644 --- a/src/ndi/element/functions.py +++ b/src/ndi/element/functions.py @@ -1,5 +1,5 @@ """ -ndi.element.functions - Element utility functions. +ndi.element.functions - ndi_element utility functions. Standalone functions that operate on elements for common operations like finding missing epochs, creating single-epoch versions, @@ -84,7 +84,7 @@ def oneepoch( specific element/probe type being used. The function creates the structural scaffolding. """ - from . import Element + from . import ndi_element # Get epochs from input et = _get_epoch_table(element_in) @@ -92,7 +92,7 @@ def oneepoch( raise ValueError("Input element has no epochs") # Create output element - elem_out = Element( + elem_out = ndi_element( session=session, name=name_out, reference=reference_out, @@ -115,14 +115,14 @@ def spikesForProbe( Args: session: NDI session probe: Source probe object - name: Neuron name + name: ndi_neuron name reference: Reference number (used as unit_id) spikedata: List of dicts with: - - 'epochid': Epoch ID string + - 'epochid': ndi_epoch_epoch ID string - 'spiketimes': Array of spike times Returns: - Neuron element object + ndi_neuron element object Example: >>> spikes = [ @@ -133,9 +133,9 @@ def spikesForProbe( Note: This creates the structural framework for a neuron element. - Full neuron creation requires the Neuron class from ndi.neuron. + Full neuron creation requires the ndi_neuron class from ndi.neuron. """ - from . import Element + from . import ndi_element # Validate spikedata probe_et = _get_epoch_table(probe) @@ -147,7 +147,7 @@ def spikesForProbe( raise ValueError(f"Spike data epoch_id '{epoch_id}' not found in probe epochs") # Create neuron element - neuron = Element( + neuron = ndi_element( session=session, name=name, reference=reference, @@ -186,7 +186,7 @@ def downsample( filter. This function creates the framework structure and delegates to downsample_timeseries() for the actual processing. """ - from . import Element + from . import ndi_element et = _get_epoch_table(element_in) if not et: @@ -195,7 +195,7 @@ def downsample( if lp_freq <= 0: raise ValueError(f"Low-pass frequency must be positive, got {lp_freq}") - elem_out = Element( + elem_out = ndi_element( session=session, name=name_out, reference=reference_out, @@ -221,7 +221,7 @@ def downsample_timeseries( Args: t_in: Time vector of shape (N,) - d_in: Data matrix of shape (N, C) where C is number of channels + d_in: ndi_gui_Data matrix of shape (N, C) where C is number of channels lp_freq: Low-pass cutoff frequency in Hz Returns: @@ -293,7 +293,7 @@ def _get_epoch_table(obj: Any) -> list[dict]: """ Get epoch table from various object types. - Handles Element, Probe, and dict-like objects. + Handles ndi_element, ndi_probe, and dict-like objects. """ if hasattr(obj, "epochtable"): return obj.epochtable() diff --git a/src/ndi/element/ndi_matlab_python_bridge.yaml b/src/ndi/element/ndi_matlab_python_bridge.yaml index 8dde9f3..20a16a7 100644 --- a/src/ndi/element/ndi_matlab_python_bridge.yaml +++ b/src/ndi/element/ndi_matlab_python_bridge.yaml @@ -148,14 +148,14 @@ functions: classes: # ========================================================================= - # ndi.element (base class, extends Ido + EpochSet + DocumentService) + # ndi.element (base class, extends ndi_ido + ndi_epoch_epochset + ndi_documentservice) # ========================================================================= - name: element type: class matlab_path: "+ndi/element.m" matlab_last_sync_hash: "c0de73f0" python_path: "ndi/element/__init__.py" - python_class: "Element" + python_class: "ndi_element" inherits: "ndi.ido, ndi.epoch.epochset, ndi.documentservice" properties: @@ -179,7 +179,7 @@ classes: - name: underlying_element type_matlab: "ndi.element" - type_python: "Element | None" + type_python: "ndi_element | None" access: "GetAccess=public, SetAccess=protected" decision_log: "Implemented as @property in Python." @@ -226,7 +226,7 @@ classes: default: "''" - name: underlying_element type_matlab: "ndi.element" - type_python: "Element | None" + type_python: "ndi_element | None" default: "None" - name: direct type_matlab: "logical" @@ -250,13 +250,13 @@ classes: default: "None" output_arguments: - name: element_obj - type_python: "Element" + type_python: "ndi_element" decision_log: > Exact match. Can be created from scratch or loaded from a document. If underlying_element is provided and subject_id is empty, inherits subject_id from underlying. - # --- EpochSet Implementation --- + # --- ndi_epoch_epochset Implementation --- - name: buildepochtable input_arguments: [] output_arguments: @@ -283,7 +283,7 @@ classes: Exact match. Returns True if no underlying_element (stop traversal), False otherwise (continue). - # --- Element-specific methods --- + # --- ndi_element-specific methods --- - name: elementstring input_arguments: [] output_arguments: @@ -298,13 +298,13 @@ classes: type_python: "str" - name: epoch_clock type_matlab: "ndi.time.clocktype array" - type_python: "list[ClockType]" + type_python: "list[ndi_time_clocktype]" - name: t0_t1 type_matlab: "cell array of [t0 t1]" type_python: "list[tuple[float, float]]" output_arguments: - name: element_obj - type_python: "Element" + type_python: "ndi_element" - name: epoch_doc type_python: "Any" decision_log: > @@ -322,7 +322,7 @@ classes: Exact match. Returns 2-tuple (epoch_table, epoch_documents) from the database. - # --- DocumentService Implementation --- + # --- ndi_documentservice Implementation --- - name: newdocument input_arguments: [] output_arguments: @@ -337,9 +337,9 @@ classes: output_arguments: - name: q type_python: "Any" - decision_log: "Exact match. Returns a Query matching base.id." + decision_log: "Exact match. Returns a ndi_query matching base.id." - # --- Cache --- + # --- ndi_cache --- - name: getcache input_arguments: [] output_arguments: diff --git a/src/ndi/element_timeseries.py b/src/ndi/element_timeseries.py index 10ff64a..fb11b3f 100644 --- a/src/ndi/element_timeseries.py +++ b/src/ndi/element_timeseries.py @@ -1,10 +1,10 @@ """ ndi.element_timeseries - Time series element class. -This module provides ElementTimeseries, an extension of Element that +This module provides ndi_element_timeseries, an extension of ndi_element that can read and write time series data (e.g., voltage traces, spike times). -ElementTimeseries is the intermediate class between Element and Neuron. +ndi_element_timeseries is the intermediate class between ndi_element and ndi_neuron. """ from __future__ import annotations @@ -13,23 +13,23 @@ import numpy as np -from .element import Element -from .time import ClockType +from .element import ndi_element +from .time import ndi_time_clocktype -class ElementTimeseries(Element): +class ndi_element_timeseries(ndi_element): """ - Element that can store and retrieve time series data. + ndi_element that can store and retrieve time series data. - Extends Element with: + Extends ndi_element with: - readtimeseries(): Read recorded data for an epoch - addepoch(): Add epoch with actual data (stores in VHSB format) - samplerate(): Get the sampling rate for a channel/epoch - This is the base class for Neuron and other data-producing elements. + This is the base class for ndi_neuron and other data-producing elements. Example: - >>> ts_elem = ElementTimeseries( + >>> ts_elem = ndi_element_timeseries( ... session=session, name='neuron1', reference=1, ... type='neuron', underlying_element=probe, ... ) @@ -38,9 +38,9 @@ class ElementTimeseries(Element): def __init__(self, **kwargs): """ - Create a new ElementTimeseries. + Create a new ndi_element_timeseries. - Takes the same arguments as Element. + Takes the same arguments as ndi_element. """ super().__init__(**kwargs) @@ -57,7 +57,7 @@ def readtimeseries( and time range. Args: - timeref_or_epoch: TimeReference object or epoch number/id + timeref_or_epoch: ndi_time_timereference object or epoch number/id t0: Start time (seconds) t1: End time (seconds). -1 means end of epoch. @@ -65,13 +65,13 @@ def readtimeseries( Tuple of (data, times, timeref): - data: numpy array of shape (n_samples, n_channels) - times: numpy array of timestamps - - timeref: TimeReference for the returned data + - timeref: ndi_time_timereference for the returned data Raises: ValueError: If no underlying element or data source available """ if self._session is None: - raise ValueError("Session required to read time series") + raise ValueError("ndi_session required to read time series") # Resolve epoch epoch_number = self._resolve_epoch(timeref_or_epoch) @@ -105,7 +105,7 @@ def _resolve_epoch(self, timeref_or_epoch: Any) -> int | None: return entry.get("epoch_number") return None - # Try TimeReference + # Try ndi_time_timereference if hasattr(timeref_or_epoch, "epoch"): epoch = timeref_or_epoch.epoch if isinstance(epoch, int): @@ -131,10 +131,10 @@ def _read_from_ingested( if self._session is None: return None, None - from .query import Query + from .query import ndi_query # Find epoch document - q = Query("").isa("element_epoch") & Query("").depends_on("element_id", self.id) + q = ndi_query("").isa("element_epoch") & ndi_query("").depends_on("element_id", self.id) epoch_docs = self._session.database_search(q) if epoch_number < 1 or epoch_number > len(epoch_docs): @@ -180,15 +180,15 @@ def _get_samplerate_from_doc(self, doc: Any) -> float: def addepoch( self, epoch_id: str, - epoch_clock: list[ClockType], + epoch_clock: list[ndi_time_clocktype], t0_t1: list[tuple[float, float]], timepoints: np.ndarray | None = None, datapoints: np.ndarray | None = None, - ) -> tuple[ElementTimeseries, Any]: + ) -> tuple[ndi_element_timeseries, Any]: """ Add a new epoch with optional time series data. - Extends Element.addepoch() to also store binary data + Extends ndi_element.addepoch() to also store binary data if timepoints and datapoints are provided. Args: @@ -251,9 +251,11 @@ def samplerate(self, epoch: Any = None) -> float: if self._session is not None and epoch is not None: epoch_number = self._resolve_epoch(epoch) if not isinstance(epoch, int) else epoch if epoch_number is not None: - from .query import Query + from .query import ndi_query - q = Query("").isa("element_epoch") & Query("").depends_on("element_id", self.id) + q = ndi_query("").isa("element_epoch") & ndi_query("").depends_on( + "element_id", self.id + ) epoch_docs = self._session.database_search(q) if 0 < epoch_number <= len(epoch_docs): return self._get_samplerate_from_doc(epoch_docs[epoch_number - 1]) @@ -262,4 +264,4 @@ def samplerate(self, epoch: Any = None) -> float: def __repr__(self) -> str: """String representation.""" - return f"ElementTimeseries({self._name}|{self._reference}|{self._type})" + return f"ndi_element_timeseries({self._name}|{self._reference}|{self._type})" diff --git a/src/ndi/epoch/__init__.py b/src/ndi/epoch/__init__.py index f738214..71cf6a4 100644 --- a/src/ndi/epoch/__init__.py +++ b/src/ndi/epoch/__init__.py @@ -1,26 +1,26 @@ """ -ndi.epoch - Epoch management classes. +ndi.epoch - ndi_epoch_epoch management classes. This module provides classes for managing epochs (recording periods) in neuroscience experiments. Classes: - Epoch: Immutable data class representing a single epoch - EpochSet: Abstract base class for epoch management - EpochProbeMap: Mapping between probes and devices for an epoch + ndi_epoch_epoch: Immutable data class representing a single epoch + ndi_epoch_epochset: Abstract base class for epoch management + ndi_epoch_epochprobemap: Mapping between probes and devices for an epoch """ -from .epoch import Epoch -from .epochprobemap import EpochProbeMap, build_devicestring, parse_devicestring -from .epochprobemap_daqsystem import EpochProbeMapDAQSystem -from .epochset import EpochSet +from .epoch import ndi_epoch_epoch +from .epochprobemap import build_devicestring, ndi_epoch_epochprobemap, parse_devicestring +from .epochprobemap_daqsystem import ndi_epoch_epochprobemap__daqsystem +from .epochset import ndi_epoch_epochset from .functions import epochrange, findepochnode __all__ = [ - "Epoch", - "EpochSet", - "EpochProbeMap", - "EpochProbeMapDAQSystem", + "ndi_epoch_epoch", + "ndi_epoch_epochset", + "ndi_epoch_epochprobemap", + "ndi_epoch_epochprobemap__daqsystem", "build_devicestring", "epochrange", "findepochnode", diff --git a/src/ndi/epoch/epoch.py b/src/ndi/epoch/epoch.py index 6e4a4c7..c6b147f 100644 --- a/src/ndi/epoch/epoch.py +++ b/src/ndi/epoch/epoch.py @@ -1,7 +1,7 @@ """ ndi.epoch.epoch - Immutable epoch data class. -This module provides the Epoch class that represents a single +This module provides the ndi_epoch_epoch class that represents a single recording epoch with all its metadata. """ @@ -14,13 +14,13 @@ from pydantic import ConfigDict if TYPE_CHECKING: - from ..time import ClockType - from .epochprobemap import EpochProbeMap - from .epochset import EpochSet + from ..time import ndi_time_clocktype + from .epochprobemap import ndi_epoch_epochprobemap + from .epochset import ndi_epoch_epochset @dataclass(frozen=True) -class Epoch: +class ndi_epoch_epoch: """ Immutable data class representing a single epoch. @@ -38,19 +38,19 @@ class Epoch: epochprobemap: List of probe-device mappings epoch_clock: List of clock types for this epoch t0_t1: List of (t0, t1) time ranges per clock - epochset_object: Reference to containing EpochSet + epochset_object: Reference to containing ndi_epoch_epochset underlying_epochs: List of underlying epoch references underlying_files: List of associated file paths Example: - >>> from ndi.epoch import Epoch, EpochProbeMap + >>> from ndi.epoch import ndi_epoch_epoch, ndi_epoch_epochprobemap >>> from ndi.time import DEV_LOCAL_TIME >>> - >>> epoch = Epoch( + >>> epoch = ndi_epoch_epoch( ... epoch_number=1, ... epoch_id='ep_abc123', ... epoch_session_id='sess_xyz', - ... epochprobemap=[EpochProbeMap('elec1', 1, 'n-trode', 'intan1::', '')], + ... epochprobemap=[ndi_epoch_epochprobemap('elec1', 1, 'n-trode', 'intan1::', '')], ... epoch_clock=[DEV_LOCAL_TIME], ... t0_t1=[(0.0, 100.0)], ... ) @@ -59,11 +59,11 @@ class Epoch: epoch_number: int = 0 epoch_id: str = "" epoch_session_id: str = "" - epochprobemap: tuple[EpochProbeMap, ...] = field(default_factory=tuple) - epoch_clock: tuple[ClockType, ...] = field(default_factory=tuple) + epochprobemap: tuple[ndi_epoch_epochprobemap, ...] = field(default_factory=tuple) + epoch_clock: tuple[ndi_time_clocktype, ...] = field(default_factory=tuple) t0_t1: tuple[tuple[float, float], ...] = field(default_factory=tuple) - epochset_object: EpochSet | None = None - underlying_epochs: tuple[Epoch, ...] = field(default_factory=tuple) + epochset_object: ndi_epoch_epochset | None = None + underlying_epochs: tuple[ndi_epoch_epoch, ...] = field(default_factory=tuple) underlying_files: tuple[str, ...] = field(default_factory=tuple) def __post_init__(self): @@ -83,36 +83,36 @@ def __post_init__(self): object.__setattr__(self, "underlying_files", tuple(self.underlying_files)) @classmethod - def from_dict(cls, data: dict[str, Any]) -> Epoch: + def from_dict(cls, data: dict[str, Any]) -> ndi_epoch_epoch: """ - Create an Epoch from a dictionary. + Create an ndi_epoch_epoch from a dictionary. Args: data: Dictionary with epoch fields Returns: - New Epoch instance + New ndi_epoch_epoch instance """ - from ..time import ClockType - from .epochprobemap import EpochProbeMap + from ..time import ndi_time_clocktype + from .epochprobemap import ndi_epoch_epochprobemap # Parse epochprobemap epm_raw = data.get("epochprobemap", []) epochprobemap = [] for epm in epm_raw: - if isinstance(epm, EpochProbeMap): + if isinstance(epm, ndi_epoch_epochprobemap): epochprobemap.append(epm) elif isinstance(epm, dict): - epochprobemap.append(EpochProbeMap.from_dict(epm)) + epochprobemap.append(ndi_epoch_epochprobemap.from_dict(epm)) # Parse epoch_clock clock_raw = data.get("epoch_clock", []) epoch_clock = [] for clock in clock_raw: - if isinstance(clock, ClockType): + if isinstance(clock, ndi_time_clocktype): epoch_clock.append(clock) elif isinstance(clock, str): - epoch_clock.append(ClockType(clock)) + epoch_clock.append(ndi_time_clocktype(clock)) # Parse t0_t1 t0t1_raw = data.get("t0_t1", []) @@ -125,7 +125,7 @@ def from_dict(cls, data: dict[str, Any]) -> Epoch: underlying_raw = data.get("underlying_epochs", []) underlying_epochs = [] for ue in underlying_raw: - if isinstance(ue, Epoch): + if isinstance(ue, ndi_epoch_epoch): underlying_epochs.append(ue) elif isinstance(ue, dict): underlying_epochs.append(cls.from_dict(ue)) @@ -162,24 +162,24 @@ def to_dict(self) -> dict[str, Any]: "underlying_files": list(self.underlying_files), } - def has_clock(self, clock: ClockType) -> bool: + def has_clock(self, clock: ndi_time_clocktype) -> bool: """ Check if this epoch has a specific clock type. Args: - clock: ClockType to check for + clock: ndi_time_clocktype to check for Returns: True if the clock type is present """ return clock in self.epoch_clock - def time_range(self, clock: ClockType) -> tuple[float, float] | None: + def time_range(self, clock: ndi_time_clocktype) -> tuple[float, float] | None: """ Get time range for a specific clock type. Args: - clock: ClockType to get range for + clock: ndi_time_clocktype to get range for Returns: (t0, t1) tuple or None if clock not present @@ -200,9 +200,9 @@ def matches_probe( Check if any epochprobemap matches the probe criteria. Args: - name: Probe name + name: ndi_probe name reference: Reference number - type: Probe type + type: ndi_probe type Returns: True if any epochprobemap matches @@ -216,20 +216,20 @@ def matches_probe( @pydantic.validate_call(config=ConfigDict(arbitrary_types_allowed=True)) def is_epoch_or_empty(value: Any) -> bool: """ - Validate that a value is an Epoch or empty. + Validate that a value is an ndi_epoch_epoch or empty. Args: value: Value to check Returns: - True if value is Epoch, None, or empty + True if value is ndi_epoch_epoch, None, or empty """ if value is None: return True - if isinstance(value, Epoch): + if isinstance(value, ndi_epoch_epoch): return True if isinstance(value, (list, tuple)) and len(value) == 0: return True - if isinstance(value, (list, tuple)) and all(isinstance(x, Epoch) for x in value): + if isinstance(value, (list, tuple)) and all(isinstance(x, ndi_epoch_epoch) for x in value): return True return False diff --git a/src/ndi/epoch/epochprobemap.py b/src/ndi/epoch/epochprobemap.py index 47db431..3d43fcd 100644 --- a/src/ndi/epoch/epochprobemap.py +++ b/src/ndi/epoch/epochprobemap.py @@ -1,7 +1,7 @@ """ -ndi.epoch.epochprobemap - Probe-device mapping for epochs. +ndi.epoch.epochprobemap - ndi_probe-device mapping for epochs. -This module provides the EpochProbeMap class that describes how +This module provides the ndi_epoch_epochprobemap class that describes how probes (logical measurement devices) map to physical DAQ channels during an epoch. """ @@ -15,27 +15,27 @@ @dataclass -class EpochProbeMap: +class ndi_epoch_epochprobemap: """ Mapping between a probe and a device for an epoch. - EpochProbeMap describes how a logical probe (e.g., an electrode) + ndi_epoch_epochprobemap describes how a logical probe (e.g., an electrode) maps to physical channels on a data acquisition device during a specific epoch. Attributes: - name: Probe name (no whitespace allowed) + name: ndi_probe name (no whitespace allowed) reference: Reference number (non-negative integer) - type: Probe type identifier (no whitespace) + type: ndi_probe type identifier (no whitespace) devicestring: Device identifier string (format: "devicename:class:details") - subjectstring: Subject identifier string + subjectstring: ndi_subject identifier string Example: - >>> epm = EpochProbeMap( + >>> epm = ndi_epoch_epochprobemap( ... name='electrode1', ... reference=1, ... type='n-trode', - ... devicestring='intan1:SpikeInterfaceReader:', + ... devicestring='intan1:ndi_daq_reader_SpikeInterfaceReader:', ... subjectstring='mouse001', ... ) """ @@ -86,9 +86,9 @@ def matches( Check if this probe map matches the given criteria. Args: - name: Probe name to match (None = any) + name: ndi_probe name to match (None = any) reference: Reference number to match (None = any) - type: Probe type to match (None = any) + type: ndi_probe type to match (None = any) Returns: True if all specified criteria match @@ -112,7 +112,7 @@ def to_dict(self) -> dict[str, Any]: } @classmethod - def from_dict(cls, data: dict[str, Any]) -> EpochProbeMap: + def from_dict(cls, data: dict[str, Any]) -> ndi_epoch_epochprobemap: """Create from dictionary representation.""" return cls( name=data.get("name", ""), @@ -128,7 +128,7 @@ def __str__(self) -> str: def __eq__(self, other: Any) -> bool: """Test equality by all fields.""" - if not isinstance(other, EpochProbeMap): + if not isinstance(other, ndi_epoch_epochprobemap): return False return ( self.name == other.name diff --git a/src/ndi/epoch/epochprobemap_daqsystem.py b/src/ndi/epoch/epochprobemap_daqsystem.py index 8d99310..3b41e78 100644 --- a/src/ndi/epoch/epochprobemap_daqsystem.py +++ b/src/ndi/epoch/epochprobemap_daqsystem.py @@ -1,7 +1,7 @@ """ -ndi.epoch.epochprobemap_daqsystem - EpochProbeMap with DAQ system device strings. +ndi.epoch.epochprobemap_daqsystem - ndi_epoch_epochprobemap with DAQ system device strings. -Extends EpochProbeMap with structured device string parsing via DAQSystemString, +Extends ndi_epoch_epochprobemap with structured device string parsing via ndi_daq_daqsystemstring, plus serialization and file I/O. MATLAB equivalent: src/ndi/+ndi/+epoch/epochprobemap_daqsystem.m @@ -15,23 +15,23 @@ import pydantic -from ..daq.daqsystemstring import DAQSystemString -from .epochprobemap import EpochProbeMap +from ..daq.daqsystemstring import ndi_daq_daqsystemstring +from .epochprobemap import ndi_epoch_epochprobemap @dataclass -class EpochProbeMapDAQSystem(EpochProbeMap): +class ndi_epoch_epochprobemap__daqsystem(ndi_epoch_epochprobemap): """ - Epoch probe map with DAQ system device string support. + ndi_epoch_epoch probe map with DAQ system device string support. - Extends EpochProbeMap with a structured DAQSystemString for the + Extends ndi_epoch_epochprobemap with a structured ndi_daq_daqsystemstring for the devicestring field, plus serialization and file I/O support. - The devicestring is parsed into a DAQSystemString that provides + The devicestring is parsed into a ndi_daq_daqsystemstring that provides access to device name, channel types, and channel lists. Example: - >>> epm = EpochProbeMapDAQSystem( + >>> epm = ndi_epoch_epochprobemap__daqsystem( ... name='electrode1', ... reference=1, ... type='n-trode', @@ -45,13 +45,13 @@ class EpochProbeMapDAQSystem(EpochProbeMap): def __post_init__(self): """Validate fields and parse device string.""" super().__post_init__() - self._daqsystemstring: DAQSystemString | None = None + self._daqsystemstring: ndi_daq_daqsystemstring | None = None @property - def daqsystemstring(self) -> DAQSystemString: - """Get parsed DAQSystemString from devicestring.""" + def daqsystemstring(self) -> ndi_daq_daqsystemstring: + """Get parsed ndi_daq_daqsystemstring from devicestring.""" if self._daqsystemstring is None: - self._daqsystemstring = DAQSystemString.parse(self.devicestring) + self._daqsystemstring = ndi_daq_daqsystemstring.parse(self.devicestring) return self._daqsystemstring def serialization_struct(self) -> dict[str, Any]: @@ -99,7 +99,7 @@ def savetofile(self, filename: str) -> None: f.write(self.serialize() + "\n") @classmethod - def decode(cls, s: str) -> EpochProbeMapDAQSystem: + def decode(cls, s: str) -> ndi_epoch_epochprobemap__daqsystem: """ Decode from a serialized string. @@ -107,7 +107,7 @@ def decode(cls, s: str) -> EpochProbeMapDAQSystem: s: Tab-delimited string Returns: - EpochProbeMapDAQSystem object + ndi_epoch_epochprobemap__daqsystem object Raises: ValueError: If the string cannot be parsed @@ -125,17 +125,17 @@ def decode(cls, s: str) -> EpochProbeMapDAQSystem: ) @classmethod - def loadfromfile(cls, filename: str) -> list[EpochProbeMapDAQSystem]: + def loadfromfile(cls, filename: str) -> list[ndi_epoch_epochprobemap__daqsystem]: """ Load epoch probe maps from a file. - Each line is one serialized EpochProbeMapDAQSystem. + Each line is one serialized ndi_epoch_epochprobemap__daqsystem. Args: filename: Path to read from Returns: - List of EpochProbeMapDAQSystem objects + List of ndi_epoch_epochprobemap__daqsystem objects """ results = [] with open(filename) as f: @@ -147,7 +147,7 @@ def loadfromfile(cls, filename: str) -> list[EpochProbeMapDAQSystem]: def __repr__(self) -> str: return ( - f"EpochProbeMapDAQSystem(name='{self.name}', " + f"ndi_epoch_epochprobemap__daqsystem(name='{self.name}', " f"reference={self.reference}, type='{self.type}', " f"devicestring='{self.devicestring}')" ) diff --git a/src/ndi/epoch/epochset.py b/src/ndi/epoch/epochset.py index bc1b397..0f09531 100644 --- a/src/ndi/epoch/epochset.py +++ b/src/ndi/epoch/epochset.py @@ -1,7 +1,7 @@ """ ndi.epoch.epochset - Abstract base class for epoch management. -This module provides the EpochSet abstract base class that defines +This module provides the ndi_epoch_epochset abstract base class that defines the interface for objects that manage epochs (recording periods). """ @@ -15,14 +15,14 @@ import pydantic from pydantic import Field -from ..time import ClockType +from ..time import ndi_time_clocktype -class EpochSet(ABC): +class ndi_epoch_epochset(ABC): """ Abstract base class for epoch management. - EpochSet defines the interface for objects that manage epochs, + ndi_epoch_epochset defines the interface for objects that manage epochs, providing methods for accessing epoch tables, clock types, and time ranges. @@ -38,7 +38,7 @@ class EpochSet(ABC): _epochtable_hash: Hash of cached epoch table Example: - >>> class MyEpochSet(EpochSet): + >>> class MyEpochSet(ndi_epoch_epochset): ... def buildepochtable(self): ... return [{'epoch_number': 1, 'epoch_id': 'ep1', ...}] ... def epochsetname(self): @@ -64,9 +64,9 @@ def buildepochtable(self) -> list[dict[str, Any]]: List of epoch entries, each with fields: - epoch_number: Integer position (1-indexed) - epoch_id: Unique identifier string - - epoch_session_id: Session containing this epoch - - epochprobemap: List of EpochProbeMap objects - - epoch_clock: List of ClockType objects + - epoch_session_id: ndi_session containing this epoch + - epochprobemap: List of ndi_epoch_epochprobemap objects + - epoch_clock: List of ndi_time_clocktype objects - t0_t1: List of (t0, t1) tuples per clock - underlying_epochs: Dict with underlying epoch info """ @@ -151,22 +151,22 @@ def numepochs(self) -> int: return len(et) @pydantic.validate_call - def epochclock(self, epoch_number: Annotated[int, Field(ge=1)]) -> list[ClockType]: + def epochclock(self, epoch_number: Annotated[int, Field(ge=1)]) -> list[ndi_time_clocktype]: """ Get clock types for an epoch. Args: - epoch_number: Epoch number (1-indexed) + epoch_number: ndi_epoch_epoch number (1-indexed) Returns: - List of ClockType objects for this epoch + List of ndi_time_clocktype objects for this epoch Raises: IndexError: If epoch_number is out of range """ et, _ = self.epochtable() if epoch_number > len(et): - raise IndexError(f"Epoch {epoch_number} out of range (1..{len(et)})") + raise IndexError(f"ndi_epoch_epoch {epoch_number} out of range (1..{len(et)})") entry = et[epoch_number - 1] return entry.get("epoch_clock", []) @@ -177,7 +177,7 @@ def t0_t1(self, epoch_number: Annotated[int, Field(ge=1)]) -> list[tuple[float, Get time range for an epoch. Args: - epoch_number: Epoch number (1-indexed) + epoch_number: ndi_epoch_epoch number (1-indexed) Returns: List of (t0, t1) tuples, one per clock type @@ -187,7 +187,7 @@ def t0_t1(self, epoch_number: Annotated[int, Field(ge=1)]) -> list[tuple[float, """ et, _ = self.epochtable() if epoch_number > len(et): - raise IndexError(f"Epoch {epoch_number} out of range (1..{len(et)})") + raise IndexError(f"ndi_epoch_epoch {epoch_number} out of range (1..{len(et)})") entry = et[epoch_number - 1] return entry.get("t0_t1", [(np.nan, np.nan)]) @@ -198,17 +198,17 @@ def epochid(self, epoch_number: Annotated[int, Field(ge=1)]) -> str: Get epoch ID for an epoch number. Args: - epoch_number: Epoch number (1-indexed) + epoch_number: ndi_epoch_epoch number (1-indexed) Returns: - Epoch identifier string + ndi_epoch_epoch identifier string Raises: IndexError: If epoch_number is out of range """ et, _ = self.epochtable() if epoch_number > len(et): - raise IndexError(f"Epoch {epoch_number} out of range (1..{len(et)})") + raise IndexError(f"ndi_epoch_epoch {epoch_number} out of range (1..{len(et)})") return et[epoch_number - 1].get("epoch_id", "") @@ -218,10 +218,10 @@ def epochnumber(self, epoch_id: str) -> int: Get epoch number for an epoch ID. Args: - epoch_id: Epoch identifier string + epoch_id: ndi_epoch_epoch identifier string Returns: - Epoch number (1-indexed) + ndi_epoch_epoch number (1-indexed) Raises: ValueError: If epoch_id not found @@ -231,7 +231,7 @@ def epochnumber(self, epoch_id: str) -> int: if entry.get("epoch_id") == epoch_id: return i + 1 - raise ValueError(f"Epoch ID not found: {epoch_id}") + raise ValueError(f"ndi_epoch_epoch ID not found: {epoch_id}") def matchedepochtable( self, @@ -266,17 +266,17 @@ def epochtableentry(self, epoch_number: Annotated[int, Field(ge=1)]) -> dict[str Get a single epoch table entry. Args: - epoch_number: Epoch number (1-indexed) + epoch_number: ndi_epoch_epoch number (1-indexed) Returns: - Epoch table entry dict + ndi_epoch_epoch table entry dict Raises: IndexError: If epoch_number is out of range """ et, _ = self.epochtable() if epoch_number > len(et): - raise IndexError(f"Epoch {epoch_number} out of range (1..{len(et)})") + raise IndexError(f"ndi_epoch_epoch {epoch_number} out of range (1..{len(et)})") return et[epoch_number - 1] @@ -285,13 +285,13 @@ def epochgraph(self) -> list[dict[str, Any]]: Build epoch graph nodes for time synchronization. Creates a list of graph nodes, one for each (epoch, clock) pair. - This is used by SyncGraph for time conversion. + This is used by ndi_time_syncgraph for time conversion. Returns: List of epoch graph nodes with fields: - - epoch_id: Epoch identifier - - epochset: Reference to this EpochSet - - clock: ClockType for this node + - epoch_id: ndi_epoch_epoch identifier + - epochset: Reference to this ndi_epoch_epochset + - clock: ndi_time_clocktype for this node - t0: Start time - t1: End time """ diff --git a/src/ndi/epoch/functions.py b/src/ndi/epoch/functions.py index 5cc93c3..c612360 100644 --- a/src/ndi/epoch/functions.py +++ b/src/ndi/epoch/functions.py @@ -1,5 +1,5 @@ """ -ndi.epoch.functions - Epoch utility functions. +ndi.epoch.functions - ndi_epoch_epoch utility functions. MATLAB equivalents: - src/ndi/+ndi/+epoch/epochrange.m @@ -13,13 +13,13 @@ import pydantic from pydantic import ConfigDict -from ..time import ClockType +from ..time import ndi_time_clocktype @pydantic.validate_call(config=ConfigDict(arbitrary_types_allowed=True)) def epochrange( epochset_obj: Any, - clocktype: ClockType, + clocktype: ndi_time_clocktype, first_epoch: int | str, last_epoch: int | str, ) -> tuple[list[str], list[dict], list[tuple[float, float]]]: @@ -27,7 +27,7 @@ def epochrange( Return range of epochs between first and last epoch. Args: - epochset_obj: An object with epochtable() method (EpochSet, Element, etc.) + epochset_obj: An object with epochtable() method (ndi_epoch_epochset, ndi_element, etc.) clocktype: Clock type to use for t0_t1 first_epoch: First epoch number (1-indexed) or epoch_id string last_epoch: Last epoch number (1-indexed) or epoch_id string @@ -100,7 +100,7 @@ def _resolve_epoch_index( Args: epoch_table: The epoch table - epoch: Epoch number (1-indexed) or epoch_id string + epoch: ndi_epoch_epoch number (1-indexed) or epoch_id string Returns: 0-based index into epoch_table @@ -111,7 +111,7 @@ def _resolve_epoch_index( if isinstance(epoch, int): idx = epoch - 1 # Convert 1-indexed to 0-indexed if idx < 0 or idx >= len(epoch_table): - raise ValueError(f"Epoch number {epoch} out of range [1, {len(epoch_table)}]") + raise ValueError(f"ndi_epoch_epoch number {epoch} out of range [1, {len(epoch_table)}]") return idx # Search by epoch_id @@ -119,7 +119,7 @@ def _resolve_epoch_index( if entry.get("epoch_id") == epoch: return i - raise ValueError(f"Epoch ID '{epoch}' not found") + raise ValueError(f"ndi_epoch_epoch ID '{epoch}' not found") @pydantic.validate_call(config=ConfigDict(arbitrary_types_allowed=True)) @@ -139,7 +139,7 @@ def findepochnode( objectname, objectclass, epoch_id, epoch_session_id Special fields: - epoch_clock - uses ``==`` (ClockType equality) + epoch_clock - uses ``==`` (ndi_time_clocktype equality) time_value - checks whether the value falls within the candidate's ``t0_t1`` range ``[t0, t1]`` diff --git a/src/ndi/epoch/ndi_matlab_python_bridge.yaml b/src/ndi/epoch/ndi_matlab_python_bridge.yaml index 0b80795..c33487b 100644 --- a/src/ndi/epoch/ndi_matlab_python_bridge.yaml +++ b/src/ndi/epoch/ndi_matlab_python_bridge.yaml @@ -22,7 +22,7 @@ functions: type_python: "Any" - name: clocktype type_matlab: "ndi.time.clocktype" - type_python: "ClockType" + type_python: "ndi_time_clocktype" - name: first_epoch type_matlab: "integer | char" type_python: "int | str" @@ -38,7 +38,7 @@ functions: type_python: "list[tuple[float, float]]" decision_log: > Exact match. Returns a 3-tuple. - Epoch numbers are 1-indexed (user concept). + ndi_epoch_epoch numbers are 1-indexed (user concept). - name: findepochnode type: function @@ -127,7 +127,7 @@ classes: matlab_path: "+ndi/+epoch/epochset.m" matlab_last_sync_hash: "fe64a9f5" python_path: "ndi/epoch/epochset.py" - python_class: "EpochSet" + python_class: "ndi_epoch_epochset" methods: # --- Abstract methods --- @@ -186,7 +186,7 @@ classes: type_python: "int" output_arguments: - name: ec - type_python: "list[ClockType]" + type_python: "list[ndi_time_clocktype]" decision_log: > Semantic Parity: epoch_number is 1-indexed (user concept). Internally maps to 0-based index. @@ -273,7 +273,7 @@ classes: type: class matlab_path: "+ndi/+epoch/epoch.m" python_path: "ndi/epoch/epoch.py" - python_class: "Epoch" + python_class: "ndi_epoch_epoch" properties: - name: epoch_number @@ -293,12 +293,12 @@ classes: - name: epochprobemap type_matlab: "ndi.epoch.epochprobemap array" - type_python: "tuple[EpochProbeMap, ...]" + type_python: "tuple[ndi_epoch_epochprobemap, ...]" decision_log: "Python uses immutable tuple. MATLAB uses cell array." - name: epoch_clock type_matlab: "ndi.time.clocktype array" - type_python: "tuple[ClockType, ...]" + type_python: "tuple[ndi_time_clocktype, ...]" decision_log: "Python uses immutable tuple. MATLAB uses cell array." - name: t0_t1 @@ -308,12 +308,12 @@ classes: - name: epochset_object type_matlab: "ndi.epoch.epochset" - type_python: "EpochSet | None" + type_python: "ndi_epoch_epochset | None" decision_log: "Exact match. None when not associated." - name: underlying_epochs type_matlab: "ndi.epoch.epoch array" - type_python: "tuple[Epoch, ...]" + type_python: "tuple[ndi_epoch_epoch, ...]" decision_log: "Python uses immutable tuple." - name: underlying_files @@ -329,7 +329,7 @@ classes: type_python: "dict[str, Any]" output_arguments: - name: epoch_obj - type_python: "Epoch" + type_python: "ndi_epoch_epoch" decision_log: > Python-specific factory method for deserialization. No direct MATLAB equivalent. @@ -346,7 +346,7 @@ classes: - name: has_clock input_arguments: - name: clock - type_python: "ClockType" + type_python: "ndi_time_clocktype" output_arguments: - name: b type_python: "bool" @@ -355,7 +355,7 @@ classes: - name: time_range input_arguments: - name: clock - type_python: "ClockType" + type_python: "ndi_time_clocktype" output_arguments: - name: t0_t1 type_python: "tuple[float, float] | None" @@ -385,7 +385,7 @@ classes: matlab_path: "+ndi/+epoch/epochprobemap.m" matlab_last_sync_hash: "fe64a9f5" python_path: "ndi/epoch/epochprobemap.py" - python_class: "EpochProbeMap" + python_class: "ndi_epoch_epochprobemap" properties: - name: name @@ -436,7 +436,7 @@ classes: default: "''" output_arguments: - name: epm_obj - type_python: "EpochProbeMap" + type_python: "ndi_epoch_epochprobemap" decision_log: "Exact match." - name: devicename @@ -482,7 +482,7 @@ classes: type_python: "dict[str, Any]" output_arguments: - name: epm_obj - type_python: "EpochProbeMap" + type_python: "ndi_epoch_epochprobemap" decision_log: "Python-specific factory method." - name: eq @@ -502,13 +502,13 @@ classes: matlab_path: "+ndi/+epoch/epochprobemap_daqsystem.m" matlab_last_sync_hash: "fe64a9f5" python_path: "ndi/epoch/epochprobemap_daqsystem.py" - python_class: "EpochProbeMapDAQSystem" + python_class: "ndi_epoch_epochprobemap__daqsystem" inherits: "ndi.epoch.epochprobemap" properties: - name: daqsystemstring type_matlab: "ndi.daq.daqsystemstring" - type_python: "DAQSystemString" + type_python: "ndi_daq_daqsystemstring" access: "computed property" decision_log: > Lazy-parsed from devicestring. MATLAB stores explicitly; @@ -537,7 +537,7 @@ classes: default: "''" output_arguments: - name: epm_daq_obj - type_python: "EpochProbeMapDAQSystem" + type_python: "ndi_epoch_epochprobemap__daqsystem" decision_log: "Exact match." - name: serialization_struct @@ -570,7 +570,7 @@ classes: type_python: "str" output_arguments: - name: epm_daq_obj - type_python: "EpochProbeMapDAQSystem" + type_python: "ndi_epoch_epochprobemap__daqsystem" decision_log: "MATLAB is a static method. Python uses classmethod." - name: loadfromfile @@ -581,5 +581,5 @@ classes: type_python: "str" output_arguments: - name: epm_list - type_python: "list[EpochProbeMapDAQSystem]" + type_python: "list[ndi_epoch_epochprobemap__daqsystem]" decision_log: "MATLAB is a static method. Python uses classmethod." diff --git a/src/ndi/file/__init__.py b/src/ndi/file/__init__.py index f7c9c17..bddb873 100644 --- a/src/ndi/file/__init__.py +++ b/src/ndi/file/__init__.py @@ -16,25 +16,25 @@ """ from . import type as filetype -from .navigator import FileNavigator -from .navigator.epochdir import EpochDirNavigator +from .navigator import ndi_file_navigator +from .navigator.epochdir import ndi_file_navigator_epochdir 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 +# ndi_file_navigator, mirroring the MATLAB constructor ``ndi.file.navigator``. +navigator = ndi_file_navigator # MATLAB compatibility: ``ndi.file.navigator_epochdir(session, patterns)`` -# creates an EpochDirNavigator, mirroring ``ndi.file.navigator_epochdir``. -navigator_epochdir = EpochDirNavigator +# creates an ndi_file_navigator_epochdir, mirroring ``ndi.file.navigator_epochdir``. +navigator_epochdir = ndi_file_navigator_epochdir # MATLAB compatibility: ``ndi.file.pfilemirror(src, dest)`` calls the # directory-mirror utility, mirroring ``ndi.file.pfilemirror``. pfilemirror = _pfilemirror_func __all__ = [ - "FileNavigator", - "EpochDirNavigator", + "ndi_file_navigator", + "ndi_file_navigator_epochdir", "filetype", "navigator", "navigator_epochdir", diff --git a/src/ndi/file/navigator/__init__.py b/src/ndi/file/navigator/__init__.py index 332fddb..674fa6d 100644 --- a/src/ndi/file/navigator/__init__.py +++ b/src/ndi/file/navigator/__init__.py @@ -1,7 +1,7 @@ """ ndi.file.navigator - File navigator for organizing epoch files. -This module provides the FileNavigator class that finds and organizes +This module provides the ndi_file_navigator class that finds and organizes data files into epochs based on file patterns. """ @@ -15,8 +15,60 @@ from pathlib import Path from typing import Any, Dict, List, Optional, Tuple, Union -from ...ido import Ido -from ...time import NO_TIME, ClockType +from ...ido import ndi_ido +from ...time import NO_TIME, ndi_time_clocktype + + +def _to_matlab_cell_str(items: list[str]) -> str: + """Convert a list of strings to MATLAB cell-array syntax. + + Example: ``['#.rhd', '#.epochprobemap.ndi']`` becomes + ``"{ '#.rhd', '#.epochprobemap.ndi' }"``. + + Returns an empty string when *items* is empty or falsy, matching + the NDI document schema default. + """ + if not items: + return "" + quoted = ", ".join(f"'{s}'" for s in items) + return f"{{ {quoted} }}" + + +def _parse_fileparameters(fp_str: str) -> list[str] | None: + """Parse a fileparameters string, preserving element order. + + MATLAB stores file parameters as cell-array syntax, e.g. + ``"{ '#.rhd', '#.epochprobemap.ndi' }"``. ``eval()`` would turn + this into a Python ``set`` and lose the ordering, which matters + for epoch-ID filename generation. This helper extracts the + quoted elements in document order. + + Returns: + Ordered list of pattern strings, or *None* when the string is + empty or cannot be parsed (callers should fall back to an + empty filematch list). + """ + if not fp_str: + return None + # Fast path: MATLAB cell-array literal { 'a', 'b', ... } + stripped = fp_str.strip() + if stripped.startswith("{") and stripped.endswith("}"): + inner = stripped[1:-1] + items = re.findall(r"'([^']*)'", inner) + if items: + return items + # Fallback: try Python eval (handles repr() output from Python-created docs) + try: + result = eval(fp_str) # noqa: S307 + if isinstance(result, (list, tuple)): + return list(result) + if isinstance(result, (set, frozenset)): + return sorted(result) + if isinstance(result, str): + return [result] + except Exception: + pass + return None @lru_cache(maxsize=10) @@ -57,34 +109,40 @@ def find_file_groups( groups = [] for _directory, files in all_files.items(): - matched_files = [] + # Track which pattern matched each file so we can preserve + # pattern order (MATLAB returns epochfiles ordered by pattern). + matched_files: list[tuple[int, str]] = [] for f in files: filename = os.path.basename(f) - for pattern in patterns: + for pat_idx, pattern in enumerate(patterns): + # Convert MATLAB '#' wildcard to glob '*' + glob_pattern = pattern.replace("#", "*") # Try glob pattern first - if fnmatch.fnmatch(filename, pattern): - matched_files.append(f) + if fnmatch.fnmatch(filename, glob_pattern): + matched_files.append((pat_idx, f)) break # Try regex pattern try: if re.search(pattern, filename): - matched_files.append(f) + matched_files.append((pat_idx, f)) break except re.error: pass if matched_files: - groups.append(sorted(matched_files)) + # Sort by pattern index first, then by filename for stability + matched_files.sort(key=lambda x: (x[0], x[1])) + groups.append([f for _, f in matched_files]) return groups -class FileNavigator(Ido): +class ndi_file_navigator(ndi_ido): """ Navigator for finding and organizing epoch files. - FileNavigator finds data files on disk and organizes them into epochs - based on file patterns. It provides the file foundation for DAQSystem. + ndi_file_navigator finds data files on disk and organizes them into epochs + based on file patterns. It provides the file foundation for ndi_daq_system. Attributes: session: The NDI session @@ -93,22 +151,24 @@ class FileNavigator(Ido): epochprobemap_class: Class to use for epoch probe maps Example: - >>> nav = FileNavigator(session, ['*.rhd', '*.dat']) + >>> nav = ndi_file_navigator(session, ['*.rhd', '*.dat']) >>> epochs = nav.selectfilegroups() >>> files = nav.getepochfiles(1) # Get files for epoch 1 """ + NDI_FILENAVIGATOR_CLASS = "ndi.file.navigator" + def __init__( self, session: Any | None = None, fileparameters: str | list[str] | dict[str, Any] | None = None, - epochprobemap_class: str = "ndi.epoch.EpochProbeMap", + epochprobemap_class: str = "ndi.epoch.ndi_epoch_epochprobemap", epochprobemap_fileparameters: str | list[str] | dict[str, Any] | None = None, identifier: str | None = None, document: Any | None = None, ): """ - Create a new FileNavigator. + Create a new ndi_file_navigator. Args: session: NDI session object @@ -124,6 +184,7 @@ def __init__( super().__init__(identifier) self._session = session self._epochprobemap_class = epochprobemap_class + self._raw_fileparameters_str = "" self._cached_epoch_filenames: dict[int, list[str]] = {} # Load from document if provided @@ -141,19 +202,29 @@ def _load_from_document(self, document: Any) -> None: """Load navigator from a document.""" doc_props = getattr(document, "document_properties", document) - if hasattr(doc_props, "base") and hasattr(doc_props.base, "id"): - self.identifier = doc_props.base.id + def _prop(obj: Any, key: str, default: Any = None) -> Any: + """Get a property from a dict or object.""" + if isinstance(obj, dict): + return obj.get(key, default) + return getattr(obj, key, default) + + base = _prop(doc_props, "base") + if base is not None: + base_id = _prop(base, "id") + if base_id is not None: + self.identifier = base_id - filenavigator = getattr(doc_props, "filenavigator", None) + filenavigator = _prop(doc_props, "filenavigator") if filenavigator: - fp = getattr(filenavigator, "fileparameters", "") - self._fileparameters = self._normalize_fileparameters(eval(fp) if fp else None) - self._epochprobemap_class = getattr( - filenavigator, "epochprobemap_class", "ndi.epoch.EpochProbeMap" + fp = _prop(filenavigator, "fileparameters", "") + self._raw_fileparameters_str = fp + self._fileparameters = self._normalize_fileparameters(_parse_fileparameters(fp)) + self._epochprobemap_class = _prop( + filenavigator, "epochprobemap_class", "ndi.epoch.ndi_epoch_epochprobemap" ) - epfp = getattr(filenavigator, "epochprobemap_fileparameters", "") + epfp = _prop(filenavigator, "epochprobemap_fileparameters", "") self._epochprobemap_fileparameters = self._normalize_fileparameters( - eval(epfp) if epfp else None + _parse_fileparameters(epfp) ) else: self._fileparameters = {"filematch": []} @@ -168,8 +239,10 @@ def _normalize_fileparameters( return {"filematch": []} if isinstance(params, str): return {"filematch": [params]} - if isinstance(params, list): - return {"filematch": params} + if isinstance(params, (set, frozenset)): + return {"filematch": sorted(params)} + if isinstance(params, (list, tuple)): + return {"filematch": list(params)} if isinstance(params, dict): fm = params.get("filematch", []) if isinstance(fm, str): @@ -207,7 +280,7 @@ def path(self) -> str: Get the path for this navigator. Returns: - Session path + ndi_session path Raises: ValueError: If no valid session @@ -216,7 +289,7 @@ def path(self) -> str: raise ValueError("No valid session associated with this navigator") return self._session.getpath() - def setsession(self, session: Any) -> FileNavigator: + def setsession(self, session: Any) -> ndi_file_navigator: """ Set the session for this navigator. @@ -234,7 +307,7 @@ def setsession(self, session: Any) -> FileNavigator: def setfileparameters( self, parameters: str | list[str] | dict[str, Any], - ) -> FileNavigator: + ) -> ndi_file_navigator: """ Set the file parameters. @@ -250,7 +323,7 @@ def setfileparameters( def setepochprobemapfileparameters( self, parameters: str | list[str] | dict[str, Any], - ) -> FileNavigator: + ) -> ndi_file_navigator: """ Set the epoch probe map file parameters. @@ -345,12 +418,12 @@ def find_ingested_documents(self) -> list[dict[str, Any]]: return [] try: - from ...query import Query + from ...query import ndi_query q = ( - Query("").isa("epochfiles_ingested") - & Query("").depends_on("filenavigator_id", self.id) - & (Query("base.session_id") == self._session.id) + ndi_query("").isa("epochfiles_ingested") + & ndi_query("").depends_on("filenavigator_id", self.id) + & (ndi_query("base.session_id") == self._session.id) ) docs = self._session.database_search(q) @@ -419,11 +492,11 @@ def epochid( Get the epoch ID for an epoch. Args: - epoch_number: Epoch number (1-indexed) + epoch_number: ndi_epoch_epoch number (1-indexed) epochfiles: Optional file list (fetched if not provided) Returns: - Epoch identifier string + ndi_epoch_epoch identifier string """ if epochfiles is None: epochfiles = self.getepochfiles_number(epoch_number) @@ -439,9 +512,9 @@ def epochid( return f.read().strip() # Generate new ID - from ...ido import Ido + from ...ido import ndi_ido - new_id = f"epoch_{Ido().id}" + new_id = f"epoch_{ndi_ido().id}" # Save to file if possible if eidfname: @@ -460,7 +533,7 @@ def epochidfilename( Get the filename for storing epoch ID. Args: - epoch_number: Epoch number + epoch_number: ndi_epoch_epoch number epochfiles: Optional file list Returns: @@ -477,7 +550,8 @@ def epochidfilename( fmstr = self.filematch_hashstring() parent, filename = os.path.split(epochfiles[0]) - return os.path.join(parent, f".{filename}.{fmstr}.epochid.ndi") + stem, _ = os.path.splitext(filename) + return os.path.join(parent, f".{stem}.{fmstr}.epochid.ndi") def epochprobemapfilename( self, @@ -487,7 +561,7 @@ def epochprobemapfilename( Get the filename for epoch probe map. Args: - epoch_number: Epoch number + epoch_number: ndi_epoch_epoch number Returns: Path for epoch probe map file, or None @@ -519,7 +593,7 @@ def defaultepochprobemapfilename( Get the default epoch probe map filename. Args: - epoch_number: Epoch number + epoch_number: ndi_epoch_epoch number Returns: Path for epoch probe map file, or None @@ -541,11 +615,11 @@ def getepochprobemap( Get the epoch probe map for an epoch. Args: - epoch_number: Epoch number + epoch_number: ndi_epoch_epoch number epochfiles: Optional file list Returns: - Epoch probe map object or None + ndi_epoch_epoch probe map object or None """ if epochfiles is None: epochfiles = self.getepochfiles_number(epoch_number) @@ -579,13 +653,13 @@ def getepochingesteddoc( epochid = self.ingestedfiles_epochid(epochfiles) - from ...query import Query + from ...query import ndi_query q = ( - Query("").isa("epochfiles_ingested") - & Query("").depends_on("filenavigator_id", self.id) - & (Query("base.session_id") == self._session.id) - & (Query("epochfiles_ingested.epoch_id") == epochid) + ndi_query("").isa("epochfiles_ingested") + & ndi_query("").depends_on("filenavigator_id", self.id) + & (ndi_query("base.session_id") == self._session.id) + & (ndi_query("epochfiles_ingested.epoch_id") == epochid) ) docs = self._session.database_search(q) @@ -603,12 +677,12 @@ def getepochfiles( MATLAB equivalent: ndi.file.navigator/getepochfiles Args: - epoch_number_or_id: Epoch number(s) or ID(s) + epoch_number_or_id: ndi_epoch_epoch number(s) or ID(s) Returns: Tuple of (fullpathfilenames, epochid): - fullpathfilenames: List of file paths (or list of lists) - - epochid: Epoch ID string (or list of strings) + - epochid: ndi_epoch_epoch ID string (or list of strings) """ et = self.epochtable() @@ -638,7 +712,7 @@ def getepochfiles( else: # Find by epoch number if item < 1 or item > len(et): - raise ValueError(f"Epoch number out of range: {item}") + raise ValueError(f"ndi_epoch_epoch number out of range: {item}") match = et[item - 1] underlying = match.get("underlying_epochs", {}) @@ -658,7 +732,7 @@ def getepochfiles_number( Get files for an epoch by number. Args: - epoch_number: Epoch number (1-indexed) + epoch_number: ndi_epoch_epoch number (1-indexed) Returns: List of file paths @@ -668,7 +742,7 @@ def getepochfiles_number( all_epochs, _ = self.selectfilegroups() if epoch_number < 1 or epoch_number > len(all_epochs): - raise ValueError(f"Epoch number out of range: {epoch_number}") + raise ValueError(f"ndi_epoch_epoch number out of range: {epoch_number}") files = all_epochs[epoch_number - 1] self._cached_epoch_filenames[epoch_number] = files @@ -678,9 +752,18 @@ def filematch_hashstring(self) -> str: """ Get a hash string based on file match patterns. + MATLAB hashes the raw ``fileparameters`` string stored in the + document (e.g. ``"{ '#.rhd', '#.epochprobemap.ndi' }"``). + When a navigator is loaded from a document we preserve that + raw string so the hash matches across languages. + Returns: - MD5 hash of concatenated patterns + MD5 hash of the file-parameter representation """ + # Prefer the raw document string so epoch-ID filenames match MATLAB + if self._raw_fileparameters_str: + return hashlib.md5(self._raw_fileparameters_str.encode()).hexdigest() + patterns = self._fileparameters.get("filematch", []) if not patterns: return "" @@ -695,7 +778,7 @@ def ingest(self) -> list[Any]: Returns: List of created documents """ - from ...document import Document + from ...document import ndi_document docs = [] et = self.epochtable() @@ -718,7 +801,7 @@ def ingest(self) -> list[Any]: if entry.get("epochprobemap"): epochfiles_struct["epochprobemap"] = entry["epochprobemap"] - doc = Document( + doc = ndi_document( "ingestion/epochfiles_ingested", epochfiles_ingested=epochfiles_struct, ) @@ -731,27 +814,27 @@ def ingest(self) -> list[Any]: def newdocument(self) -> Any: """ - Create a document for this FileNavigator. + Create a document for this ndi_file_navigator. Returns: - Document object + ndi_document object """ - from ...document import Document + from ...document import ndi_document fp = self._fileparameters.get("filematch", []) - fp_str = repr(fp) if fp else "" + fp_str = _to_matlab_cell_str(fp) epfp = self._epochprobemap_fileparameters.get("filematch", []) - epfp_str = repr(epfp) if epfp else "" + epfp_str = _to_matlab_cell_str(epfp) filenavigator_struct = { - "ndi_filenavigator_class": self.__class__.__name__, + "ndi_filenavigator_class": self.NDI_FILENAVIGATOR_CLASS, "fileparameters": fp_str, "epochprobemap_class": self._epochprobemap_class, "epochprobemap_fileparameters": epfp_str, } - doc = Document( + doc = ndi_document( "daq/filenavigator", filenavigator=filenavigator_struct, **{"base.id": self.id}, @@ -764,16 +847,16 @@ def newdocument(self) -> Any: def searchquery(self) -> Any: """ - Create a search query for this FileNavigator. + Create a search query for this ndi_file_navigator. Returns: - Query object + ndi_query object """ - from ...query import Query + from ...query import ndi_query - q = Query("base.id") == self.id + q = ndi_query("base.id") == self.id if self._session: - q = q & (Query("base.session_id") == self._session.id) + q = q & (ndi_query("base.session_id") == self._session.id) return q @staticmethod @@ -800,19 +883,19 @@ def ingestedfiles_epochid(epochfiles: list[str]) -> str: epochfiles: List of file paths Returns: - Epoch ID string + ndi_epoch_epoch ID string Raises: AssertionError: If epochfiles are not ingested """ - assert FileNavigator.isingested( + assert ndi_file_navigator.isingested( epochfiles ), "This function is only applicable to ingested epochfiles" return epochfiles[0][len("epochid://") :] def __eq__(self, other: Any) -> bool: """Test equality.""" - if not isinstance(other, FileNavigator): + if not isinstance(other, ndi_file_navigator): return False return ( self._session == other._session diff --git a/src/ndi/file/navigator/epochdir.py b/src/ndi/file/navigator/epochdir.py index 76ca00b..503cc6c 100644 --- a/src/ndi/file/navigator/epochdir.py +++ b/src/ndi/file/navigator/epochdir.py @@ -11,10 +11,10 @@ import hashlib from pathlib import Path -from . import FileNavigator +from . import ndi_file_navigator -class EpochDirNavigator(FileNavigator): +class ndi_file_navigator_epochdir(ndi_file_navigator): """ Navigator where each subdirectory is one epoch. @@ -26,7 +26,7 @@ class EpochDirNavigator(FileNavigator): recording session/trial is stored in its own directory. Example: - >>> nav = EpochDirNavigator(session, '*.rhd') + >>> nav = ndi_file_navigator_epochdir(session, '*.rhd') >>> # session_path/ >>> # trial_001/file.rhd -> epoch 1 >>> # trial_002/file.rhd -> epoch 2 @@ -46,11 +46,11 @@ def epochid( epoch directory name rather than creating random IDs. Args: - epoch_number: Epoch number (1-indexed) + epoch_number: ndi_epoch_epoch number (1-indexed) epochfiles: Optional file list (fetched if not provided) Returns: - Epoch identifier string based on directory name + ndi_epoch_epoch identifier string based on directory name """ if epochfiles is None: epochfiles = self.getepochfiles_number(epoch_number) @@ -75,9 +75,9 @@ def epochid( epoch_hash = hashlib.md5(hash_input.encode()).hexdigest()[:16] new_id = f"epoch_{epoch_hash}" else: - from ...ido import Ido + from ...ido import ndi_ido - new_id = f"epoch_{Ido().id}" + new_id = f"epoch_{ndi_ido().id}" # Save to file if possible if eidfname: @@ -164,4 +164,4 @@ def _match_files_in_dir( def __repr__(self) -> str: n_patterns = len(self._fileparameters.get("filematch", [])) - return f"EpochDirNavigator(patterns={n_patterns})" + return f"ndi_file_navigator_epochdir(patterns={n_patterns})" diff --git a/src/ndi/file/ndi_matlab_python_bridge.yaml b/src/ndi/file/ndi_matlab_python_bridge.yaml index 54c41ed..ebe4c7f 100644 --- a/src/ndi/file/ndi_matlab_python_bridge.yaml +++ b/src/ndi/file/ndi_matlab_python_bridge.yaml @@ -54,14 +54,14 @@ functions: classes: # ========================================================================= - # ndi.file.navigator (extends Ido + EpochSet.param + DocumentService + + # ndi.file.navigator (extends ndi_ido + ndi_epoch_epochset.param + ndi_documentservice + # database.ingestion_help) # ========================================================================= - name: navigator type: class matlab_path: "+ndi/+file/navigator.m" python_path: "ndi/file/navigator/__init__.py" - python_class: "FileNavigator" + python_class: "ndi_file_navigator" inherits: "ndi.ido, ndi.epoch.epochset.param, ndi.documentservice, ndi.database.ingestion_help" properties: @@ -99,7 +99,7 @@ classes: - name: epochprobemap_class type_matlab: "char" type_python: "str" - default: "'ndi.epoch.EpochProbeMap'" + default: "'ndi.epoch.ndi_epoch_epochprobemap'" - name: epochprobemap_fileparameters type_matlab: "struct | char | cell" type_python: "str | list[str] | dict[str, Any] | None" @@ -114,13 +114,13 @@ classes: default: "None" output_arguments: - name: ndi_filenavigator_obj - type_python: "FileNavigator" + type_python: "ndi_file_navigator" decision_log: > Exact match. Python adds identifier and document kwargs for reconstruction from database. MATLAB also accepts these via varargin patterns. - # --- EpochSet Implementation --- + # --- ndi_epoch_epochset Implementation --- - name: buildepochtable input_arguments: [] output_arguments: @@ -146,7 +146,7 @@ classes: type_python: "list[list[str]]" decision_log: "Exact match. Scans disk for matching file groups." - # --- Epoch File Access --- + # --- ndi_epoch_epoch File Access --- - name: getepochfiles input_arguments: - name: epoch_number_or_id @@ -172,7 +172,7 @@ classes: type_python: "list[str]" decision_log: "Exact match. Returns file paths for an epoch by number." - # --- Epoch ID --- + # --- ndi_epoch_epoch ID --- - name: epochid input_arguments: - name: epoch_number @@ -201,7 +201,7 @@ classes: type_python: "str | None" decision_log: "Exact match." - # --- Epoch Probe Map --- + # --- ndi_epoch_epoch ndi_probe Map --- - name: getepochprobemap input_arguments: - name: N @@ -244,7 +244,7 @@ classes: type_python: "Any" output_arguments: - name: ndi_filenavigator_obj - type_python: "FileNavigator" + type_python: "ndi_file_navigator" decision_log: > DISCREPANCY FIXED: Python ad-hoc port used 'set_session' (snake_case). Renamed to 'setsession' to match MATLAB. @@ -257,7 +257,7 @@ classes: type_python: "str | list[str] | dict[str, Any]" output_arguments: - name: ndi_filenavigator_obj - type_python: "FileNavigator" + type_python: "ndi_file_navigator" decision_log: "Exact match." - name: setepochprobemapfileparameters @@ -267,7 +267,7 @@ classes: type_python: "str | list[str] | dict[str, Any]" output_arguments: - name: ndi_filenavigator_obj - type_python: "FileNavigator" + type_python: "ndi_file_navigator" decision_log: "Exact match." # --- Path --- @@ -319,7 +319,7 @@ classes: 'getepochingesteddoc' to match MATLAB public API. Synchronized 2026-03-13. - # --- DocumentService --- + # --- ndi_documentservice --- - name: newdocument input_arguments: [] output_arguments: @@ -334,7 +334,7 @@ classes: type_python: "Any" decision_log: "Exact match." - # --- Cache --- + # --- ndi_cache --- - name: getcache input_arguments: [] output_arguments: @@ -384,7 +384,7 @@ classes: type: class matlab_path: "+ndi/+file/+navigator/epochdir.m" python_path: "ndi/file/navigator/epochdir.py" - python_class: "EpochDirNavigator" + python_class: "ndi_file_navigator_epochdir" inherits: "ndi.file.navigator" properties: [] @@ -398,7 +398,7 @@ classes: type_python: "*args, **kwargs" output_arguments: - name: obj - type_python: "EpochDirNavigator" + type_python: "ndi_file_navigator_epochdir" decision_log: > Exact match. Passes all arguments to parent constructor. diff --git a/src/ndi/file/type/__init__.py b/src/ndi/file/type/__init__.py index 7c7c7ed..4490fb4 100644 --- a/src/ndi/file/type/__init__.py +++ b/src/ndi/file/type/__init__.py @@ -7,10 +7,10 @@ ndi.file.type.mfdaq_epoch_channel -> ndi.file.type.mfdaq_epoch_channel """ -from .mfdaq_epoch_channel import MFDAQEpochChannel +from .mfdaq_epoch_channel import ndi_file_type_mfdaq__epoch__channel # MATLAB compatibility: ``ndi.file.type.mfdaq_epoch_channel(...)`` creates an -# MFDAQEpochChannel, mirroring the MATLAB constructor. -mfdaq_epoch_channel = MFDAQEpochChannel +# ndi_file_type_mfdaq__epoch__channel, mirroring the MATLAB constructor. +mfdaq_epoch_channel = ndi_file_type_mfdaq__epoch__channel -__all__ = ["MFDAQEpochChannel", "mfdaq_epoch_channel"] +__all__ = ["ndi_file_type_mfdaq__epoch__channel", "mfdaq_epoch_channel"] diff --git a/src/ndi/file/type/mfdaq_epoch_channel.py b/src/ndi/file/type/mfdaq_epoch_channel.py index 8ebb762..b83644a 100644 --- a/src/ndi/file/type/mfdaq_epoch_channel.py +++ b/src/ndi/file/type/mfdaq_epoch_channel.py @@ -1,7 +1,7 @@ """ ndi.file.type.mfdaq_epoch_channel - Channel metadata for MFDAQ epoch files. -Provides the MFDAQEpochChannel dataclass describing channel information +Provides the ndi_file_type_mfdaq__epoch__channel dataclass describing channel information for multi-function DAQ recordings. MATLAB equivalent: src/ndi/+ndi/+file/+type/mfdaq_epoch_channel.m @@ -48,7 +48,7 @@ def from_dict(cls, d: dict[str, Any]) -> ChannelInfo: @dataclass -class MFDAQEpochChannel: +class ndi_file_type_mfdaq__epoch__channel: """ Channel metadata for a multi-function DAQ epoch. @@ -60,7 +60,7 @@ class MFDAQEpochChannel: Example: >>> ch = ChannelInfo(name='ai1', type='analog_in', sample_rate=30000.0, number=1) - >>> mec = MFDAQEpochChannel([ch]) + >>> mec = ndi_file_type_mfdaq__epoch__channel([ch]) >>> mec.channels_of_type('analog_in') [ChannelInfo(name='ai1', ...)] """ @@ -97,7 +97,7 @@ def create_properties( self, channel_structure: list[dict[str, Any]] | list[ChannelInfo], **kwargs: Any, - ) -> MFDAQEpochChannel: + ) -> ndi_file_type_mfdaq__epoch__channel: """ Create/set channel properties from a structure. @@ -118,7 +118,7 @@ def create_properties( self.channel_information.append(ChannelInfo.from_dict(item)) return self - def readFromFile(self, filename: str) -> MFDAQEpochChannel: + def readFromFile(self, filename: str) -> ndi_file_type_mfdaq__epoch__channel: """ Read channel information from a JSON file. @@ -224,4 +224,4 @@ def __len__(self) -> int: return len(self.channel_information) def __repr__(self) -> str: - return f"MFDAQEpochChannel(n_channels={len(self.channel_information)})" + return f"ndi_file_type_mfdaq__epoch__channel(n_channels={len(self.channel_information)})" diff --git a/src/ndi/file/type/ndi_matlab_python_bridge.yaml b/src/ndi/file/type/ndi_matlab_python_bridge.yaml index 1a33168..e8ddb47 100644 --- a/src/ndi/file/type/ndi_matlab_python_bridge.yaml +++ b/src/ndi/file/type/ndi_matlab_python_bridge.yaml @@ -18,7 +18,7 @@ classes: type: class matlab_path: "+ndi/+file/+type/mfdaq_epoch_channel.m" python_path: "ndi/file/type/mfdaq_epoch_channel.py" - python_class: "MFDAQEpochChannel" + python_class: "ndi_file_type_mfdaq__epoch__channel" inherits: "" properties: @@ -40,7 +40,7 @@ classes: default: "None" output_arguments: - name: obj - type_python: "MFDAQEpochChannel" + type_python: "ndi_file_type_mfdaq__epoch__channel" decision_log: > Exact match. MATLAB accepts variable arguments; Python accepts an optional list of ChannelInfo. @@ -56,7 +56,7 @@ classes: type_python: "**kwargs" output_arguments: - name: obj - type_python: "MFDAQEpochChannel" + type_python: "ndi_file_type_mfdaq__epoch__channel" decision_log: > DISCREPANCY FIXED: Python ad-hoc port was missing this method. Added to match MATLAB. Sets channel_information from a struct @@ -70,7 +70,7 @@ classes: type_python: "str" output_arguments: - name: mfdaq_epoch_channel_obj - type_python: "MFDAQEpochChannel" + type_python: "ndi_file_type_mfdaq__epoch__channel" decision_log: > DISCREPANCY FIXED: Python ad-hoc port used 'read_from_file' (snake_case). Renamed to 'readFromFile' to match MATLAB. diff --git a/src/ndi/fun/data.py b/src/ndi/fun/data.py index c4511ee..8378364 100644 --- a/src/ndi/fun/data.py +++ b/src/ndi/fun/data.py @@ -44,7 +44,7 @@ def readngrid( Args: file_path: Path to the binary file. data_size: Shape tuple, e.g. ``(100, 3)``. - data_type: Element type name (``'double'``, ``'int16'``, etc.). + data_type: ndi_element type name (``'double'``, ``'int16'``, etc.). Returns: numpy array with the given shape. @@ -67,7 +67,7 @@ def readngrid( raw = np.fromfile(str(p), dtype=dtype) if raw.size != expected: - raise ValueError(f"Data count mismatch: expected {expected}, got {raw.size}") + raise ValueError(f"ndi_gui_Data count mismatch: expected {expected}, got {raw.size}") return raw.reshape(data_size) @@ -83,7 +83,7 @@ def writengrid( Args: data: numpy array to write. file_path: Output file path. - data_type: Element type name. + data_type: ndi_element type name. Raises: ValueError: If data type is unknown. @@ -143,7 +143,7 @@ def evaluate_fitcurve( and evaluates the equation at the provided input values. Args: - fitcurve_doc: A Document with ``fitcurve`` properties containing + fitcurve_doc: A ndi_document with ``fitcurve`` properties containing ``fit_equation``, ``fit_parameter_names``, ``fit_parameter_values``, and ``fit_variable_names``. *args: Arrays of independent variable values. @@ -216,7 +216,7 @@ def readImageStack( Args: session: The session or dataset object for database access. - doc: Document or document ID specifying the binary file. + doc: ndi_document or document ID specifying the binary file. fmt: Format string — ``'tif'``, ``'png'``, ``'mp4'``, ``'ngrid'`` (raw binary), or ``'auto'`` (detect from file header, falling back to ngrid). diff --git a/src/ndi/fun/dataset.py b/src/ndi/fun/dataset.py index 87fa75e..66cdcf4 100644 --- a/src/ndi/fun/dataset.py +++ b/src/ndi/fun/dataset.py @@ -1,5 +1,5 @@ """ -ndi.fun.dataset - Dataset comparison utilities. +ndi.fun.dataset - ndi_dataset comparison utilities. MATLAB equivalent: +ndi/+fun/+dataset/diff.m """ diff --git a/src/ndi/fun/doc.py b/src/ndi/fun/doc.py index 2beb714..fde900f 100644 --- a/src/ndi/fun/doc.py +++ b/src/ndi/fun/doc.py @@ -1,5 +1,5 @@ """ -ndi.fun.doc - Document utility functions. +ndi.fun.doc - ndi_document utility functions. MATLAB equivalents: +ndi/+fun/+doc/diff.m, findFuid.m, allTypes.m, getDocTypes.m """ @@ -18,10 +18,10 @@ def allTypes() -> list[str]: Returns: Sorted list of document type names. """ - from ndi.common import PathConstants + from ndi.common import ndi_common_PathConstants types: set[str] = set() - doc_folder = PathConstants.COMMON_FOLDER / "database_documents" + doc_folder = ndi_common_PathConstants.COMMON_FOLDER / "database_documents" search_paths = [doc_folder] # Also check calculator doc paths @@ -56,9 +56,9 @@ def findFuid(session: Any, fuid: str) -> tuple[Any | None, str]: Returns: Tuple of ``(document, filename)`` or ``(None, '')`` if not found. """ - from ndi.query import Query + from ndi.query import ndi_query - docs = session.database_search(Query("").isa("base")) + docs = session.database_search(ndi_query("").isa("base")) for doc in docs: props = doc.document_properties if hasattr(doc, "document_properties") else doc if not isinstance(props, dict): @@ -97,7 +97,7 @@ def makeSpeciesStrainSex( Args: session: NDI session instance. - subjectID: Subject document identifier string. + subjectID: ndi_subject document identifier string. Species: Species ontology identifier (e.g. ``'NCBITaxon:10116'``). Strain: Strain ontology identifier (e.g. ``'RRID:RGD_70508'``). Requires ``Species`` to also be provided. @@ -106,7 +106,7 @@ def makeSpeciesStrainSex( AddToSession: If True, add documents to the session database. Returns: - List of created NDI Document objects. + List of created NDI ndi_document objects. """ from ndi.openminds_convert import openminds_obj_to_ndi_document @@ -263,12 +263,12 @@ def probeLocations4probes( Returns: List of created probe_location documents. """ - from ndi.document import Document + from ndi.document import ndi_document docs: list[Any] = [] for probe_doc, lookup_str in zip(probe_docs, ontology_lookup_strings): probe_id = probe_doc.document_properties.get("base", {}).get("id", "") - doc = Document("probe/probe_location") + doc = ndi_document("probe/probe_location") doc = doc.set_session_id(session.id()) # Resolve ontology lookup string to name and ontology ID @@ -330,9 +330,9 @@ def diff( checkFiles: Whether to compare file contents. checkFileList: Whether to compare the file_info lists (default True). - session1: Session for doc1 (used for cross-session file + session1: ndi_session for doc1 (used for cross-session file comparison when *checkFiles* is True). - session2: Session for doc2 (used for cross-session file + session2: ndi_session for doc2 (used for cross-session file comparison when *checkFiles* is True). Returns: @@ -418,9 +418,9 @@ def ontologyTableRowVars( Tuple of ``(names, variable_names, ontology_nodes)`` where each is a sorted list of unique strings. """ - from ndi.query import Query + from ndi.query import ndi_query - docs = session.database_search(Query("").isa("ontologyTableRow")) + docs = session.database_search(ndi_query("").isa("ontologyTableRow")) names_set: dict[str, tuple[str, str]] = {} @@ -480,9 +480,9 @@ def getDocTypes( """ from collections import Counter - from ndi.query import Query + from ndi.query import ndi_query - docs = session.database_search(Query("").isa("base")) + docs = session.database_search(ndi_query("").isa("base")) type_counter: Counter[str] = Counter() for doc in docs: diff --git a/src/ndi/fun/doc_table.py b/src/ndi/fun/doc_table.py index fcf7647..ad01b9c 100644 --- a/src/ndi/fun/doc_table.py +++ b/src/ndi/fun/doc_table.py @@ -1,5 +1,5 @@ """ -ndi.fun.doc_table - Document-to-table conversion utilities. +ndi.fun.doc_table - ndi_document-to-table conversion utilities. MATLAB equivalents: +ndi/+fun/+docTable/*.m @@ -38,7 +38,7 @@ def ontologyTableRowDoc2Table( into DataFrame rows. Args: - documents: List of NDI Document objects with ontologyTableRow data. + documents: List of NDI ndi_document objects with ontologyTableRow data. StackAll: If True, stack all documents into a single table regardless of variable names. Default groups by variable names. @@ -137,7 +137,7 @@ def docCellArray2Table( the document's properties dict, flattened one level deep. Args: - documents: List of NDI Document objects. + documents: List of NDI ndi_document objects. Returns: DataFrame with one row per document. @@ -177,9 +177,9 @@ def element( DataFrame with element properties. """ _require_pandas() - from ndi.query import Query + from ndi.query import ndi_query - docs = session.database_search(Query("").isa("element")) + docs = session.database_search(ndi_query("").isa("element")) rows: list[dict[str, Any]] = [] for doc in docs: @@ -211,9 +211,9 @@ def subjectBasic( DataFrame with subject properties. """ _require_pandas() - from ndi.query import Query + from ndi.query import ndi_query - docs = session.database_search(Query("").isa("subject")) + docs = session.database_search(ndi_query("").isa("subject")) rows: list[dict[str, Any]] = [] for doc in docs: @@ -249,16 +249,16 @@ def probe( DataFrame with probe properties. """ _require_pandas() - from ndi.query import Query + from ndi.query import ndi_query # 1. Get all element docs that are probes - docs = session.database_search(Query("").isa("probe")) + docs = session.database_search(ndi_query("").isa("probe")) if not docs: # Fallback: try all element docs - docs = session.database_search(Query("").isa("element")) + docs = session.database_search(ndi_query("").isa("element")) # 2. Build probe_location index: probe_id -> location info - probe_loc_docs = session.database_search(Query("").isa("probe_location")) + probe_loc_docs = session.database_search(ndi_query("").isa("probe_location")) loc_by_probe: dict[str, dict] = {} for pld in probe_loc_docs: props = pld.document_properties if hasattr(pld, "document_properties") else pld @@ -268,7 +268,7 @@ def probe( loc_by_probe[probe_id] = pl # 3. Build openminds_element index: element_id -> cell type info - om_elem_docs = session.database_search(Query("").isa("openminds_element")) + om_elem_docs = session.database_search(ndi_query("").isa("openminds_element")) ct_by_element: dict[str, dict] = {} for omed in om_elem_docs: props = omed.document_properties if hasattr(omed, "document_properties") else omed @@ -293,7 +293,7 @@ def probe( "ProbeReference": el.get("reference", 0), } - # Probe location + # ndi_probe location loc = loc_by_probe.get(probe_id, {}) row["ProbeLocationName"] = loc.get("name", "") row["ProbeLocationOntology"] = loc.get("ontology_name", "") @@ -334,10 +334,10 @@ def epoch( import csv import io - from ndi.query import Query + from ndi.query import ndi_query # 1. Build element name → (id, subject_id) map - elem_docs = session.database_search(Query("").isa("element")) + elem_docs = session.database_search(ndi_query("").isa("element")) elem_by_key: dict[str, dict] = {} for doc in elem_docs: props = doc.document_properties if hasattr(doc, "document_properties") else doc @@ -353,7 +353,7 @@ def epoch( } # 2. Build epoch → stimulus_bath mapping - sb_docs = session.database_search(Query("").isa("stimulus_bath")) + sb_docs = session.database_search(ndi_query("").isa("stimulus_bath")) sb_by_epoch: dict[str, list[dict]] = {} for doc in sb_docs: props = doc.document_properties if hasattr(doc, "document_properties") else doc @@ -363,7 +363,7 @@ def epoch( sb_by_epoch.setdefault(eid, []).append(sb) # 3. Build epoch → openminds_stimulus (approach) mapping - approach_docs = session.database_search(Query("").isa("openminds_stimulus")) + approach_docs = session.database_search(ndi_query("").isa("openminds_stimulus")) approach_by_epoch: dict[str, list[dict]] = {} for doc in approach_docs: props = doc.document_properties if hasattr(doc, "document_properties") else doc @@ -373,7 +373,7 @@ def epoch( approach_by_epoch.setdefault(eid, []).append(fields) # 4. Parse epochfiles_ingested docs to get epoch → probe mappings - efi_docs = session.database_search(Query("").isa("epochfiles_ingested")) + efi_docs = session.database_search(ndi_query("").isa("epochfiles_ingested")) epoch_counter: dict[str, int] = {} # probe_id -> running count rows: list[dict[str, Any]] = [] @@ -427,7 +427,7 @@ def epoch( probe_id = elem_info.get("id", "") subject_id = elem_info.get("subject_id", "") - # Epoch counter per probe + # ndi_epoch_epoch counter per probe epoch_counter.setdefault(probe_id, 0) epoch_counter[probe_id] += 1 @@ -475,9 +475,9 @@ def openminds( dependency IDs. """ _require_pandas() - from ndi.query import Query + from ndi.query import ndi_query - docs = session.database_search(Query("").isa(doc_type)) + docs = session.database_search(ndi_query("").isa(doc_type)) rows: list[dict[str, Any]] = [] doc_ids: list[str] = [] dependency_ids: list[str] = [] @@ -535,9 +535,9 @@ def treatment( of document IDs, and *dependency_ids* is a list of dependency IDs. """ _require_pandas() - from ndi.query import Query + from ndi.query import ndi_query - docs = session.database_search(Query("").isa("treatment")) + docs = session.database_search(ndi_query("").isa("treatment")) rows: list[dict[str, Any]] = [] doc_ids: list[str] = [] dependency_ids: list[str] = [] @@ -617,10 +617,10 @@ def subject( Treatment_FoodRestrictionOnsetTime, Treatment_FoodRestrictionOffsetTime. """ _require_pandas() - from ndi.query import Query + from ndi.query import ndi_query # 1. Get all subject docs — build subject_id → base info - subject_docs = session.database_search(Query("").isa("subject")) + subject_docs = session.database_search(ndi_query("").isa("subject")) subject_info: dict[str, dict[str, str]] = {} for doc in subject_docs: props = doc.document_properties if hasattr(doc, "document_properties") else doc @@ -640,7 +640,7 @@ def subject( return pd.DataFrame() # 2. Get all openminds_subject docs — index by subject_id and type - om_docs = session.database_search(Query("").isa("openminds_subject")) + om_docs = session.database_search(ndi_query("").isa("openminds_subject")) # Per-subject openminds data: {subject_id: {type: [doc_props, ...]}} om_by_subject: dict[str, dict[str, list[dict]]] = {sid: {} for sid in subject_info} @@ -661,10 +661,10 @@ def subject( # 3. Get all treatment/measurement docs — index by subject_id # MATLAB queries: treatment | treatment_drug | virus_injection | measurement - treat_docs = session.database_search(Query("").isa("treatment")) + treat_docs = session.database_search(ndi_query("").isa("treatment")) for extra_type in ("treatment_drug", "virus_injection", "measurement"): try: - treat_docs.extend(session.database_search(Query("").isa(extra_type))) + treat_docs.extend(session.database_search(ndi_query("").isa(extra_type))) except Exception: pass treat_by_subject: dict[str, list[dict]] = {sid: [] for sid in subject_info} diff --git a/src/ndi/fun/epoch.py b/src/ndi/fun/epoch.py index fb71903..c2f1404 100644 --- a/src/ndi/fun/epoch.py +++ b/src/ndi/fun/epoch.py @@ -1,5 +1,5 @@ """ -ndi.fun.epoch - Epoch utility functions. +ndi.fun.epoch - ndi_epoch_epoch utility functions. MATLAB equivalents: +ndi/+fun/+epoch/epochid2element.m, filename2epochid.m """ @@ -30,12 +30,12 @@ def epochid2element( Returns: Dict mapping each epoch_id to a list of matching elements. """ - from ndi.query import Query + from ndi.query import ndi_query # Get all elements - q = Query("").isa("element") + q = ndi_query("").isa("element") if element_name: - q = q & (Query("element.name") == element_name) + q = q & (ndi_query("element.name") == element_name) docs = session.database_search(q) result: dict[str, list[Any]] = {eid: [] for eid in epoch_ids} @@ -84,10 +84,10 @@ def filename2epochid( Returns: Dict mapping each filename to a list of matching epoch IDs. """ - from ndi.query import Query + from ndi.query import ndi_query # Search for DAQ system documents - docs = session.database_search(Query("").isa("daq_system")) + docs = session.database_search(ndi_query("").isa("daq_system")) result: dict[str, list[str]] = {fn: [] for fn in filenames} diff --git a/src/ndi/fun/plot.py b/src/ndi/fun/plot.py index dbe97ed..df6b818 100644 --- a/src/ndi/fun/plot.py +++ b/src/ndi/fun/plot.py @@ -186,7 +186,7 @@ def stimulusTimeseries( Args: stimulus_probe: An NDI probe/element object with a ``readtimeseries`` method. - timeref: An :class:`ndi.time.TimeReference` specifying the time + timeref: An :class:`ndi.time.ndi_time_timereference` specifying the time reference for the plot. y: Y-coordinate at which to draw the stimulus bars. stimid: Optional stimulus ID numbers. If *None*, the function diff --git a/src/ndi/fun/probe/__init__.py b/src/ndi/fun/probe/__init__.py index 5c10abd..a39d1b2 100644 --- a/src/ndi/fun/probe/__init__.py +++ b/src/ndi/fun/probe/__init__.py @@ -1,5 +1,5 @@ """ -ndi.fun.probe - Probe utility functions. +ndi.fun.probe - ndi_probe utility functions. MATLAB equivalent: +ndi/+fun/+probe/ diff --git a/src/ndi/fun/probe/export_binary.py b/src/ndi/fun/probe/export_binary.py index c7dd9c8..62a4927 100644 --- a/src/ndi/fun/probe/export_binary.py +++ b/src/ndi/fun/probe/export_binary.py @@ -26,8 +26,8 @@ def export_binary( MATLAB equivalent: ndi.fun.probe.export_binary - Exports data from *probe* (an :class:`ndi.element.Element` or - :class:`ndi.probe.Probe` of type ``n-trode``) to a binary file. + Exports data from *probe* (an :class:`ndi.element.ndi_element` or + :class:`ndi.probe.ndi_probe` of type ``n-trode``) to a binary file. Before converting to the output precision the data are scaled by *multiplier*. A text metadata file is created alongside *outputfile* with the extension ``.metadata``. diff --git a/src/ndi/fun/probe/location.py b/src/ndi/fun/probe/location.py index 2cdd33d..0020176 100644 --- a/src/ndi/fun/probe/location.py +++ b/src/ndi/fun/probe/location.py @@ -18,33 +18,33 @@ def location( MATLAB equivalent: ndi.fun.probe.location Given an NDI element *element*, traverse down the ``underlying_element`` - dependency tree until an :class:`ndi.probe.Probe` object is found, then + dependency tree until an :class:`ndi.probe.ndi_probe` object is found, then return all ``probe_location`` documents associated with that probe. Args: session: An NDI session or dataset object. - element: An :class:`ndi.element.Element` object **or** the string + element: An :class:`ndi.element.ndi_element` object **or** the string identifier of an element. Returns: Tuple of ``(probe_locations, probe_obj)`` where *probe_locations* is a list of probe-location documents and *probe_obj* is the - :class:`ndi.probe.Probe` found (or ``None`` if none was found). + :class:`ndi.probe.ndi_probe` found (or ``None`` if none was found). """ from ndi.database_fun import ndi_document2ndi_object - from ndi.probe import Probe - from ndi.query import Query + from ndi.probe import ndi_probe + from ndi.query import ndi_query # Step 1: resolve string identifier to an element object if isinstance(element, str): - docs = session.database_search(Query("base.id", "exact_string", element, "")) + docs = session.database_search(ndi_query("base.id", "exact_string", element, "")) if not docs: raise ValueError(f"Could not find an element with id '{element}'.") element = ndi_document2ndi_object(docs[0], session) # Step 2: traverse down to the probe current = element - while not isinstance(current, Probe): + while not isinstance(current, ndi_probe): underlying = getattr(current, "underlying_element", None) if underlying is None: break @@ -52,7 +52,7 @@ def location( underlying = underlying() current = underlying - probe_obj: Any | None = current if isinstance(current, Probe) else None + probe_obj: Any | None = current if isinstance(current, ndi_probe) else None if probe_obj is None: return [], None @@ -63,7 +63,7 @@ def location( probe_id = probe_id() # Step 4: query for probe_location documents - q = Query("", "depends_on", "probe_id", probe_id) & Query("", "isa", "probe_location") + q = ndi_query("", "depends_on", "probe_id", probe_id) & ndi_query("", "isa", "probe_location") probe_locations = session.database_search(q) return probe_locations, probe_obj diff --git a/src/ndi/fun/probe/ndi_matlab_python_bridge.yaml b/src/ndi/fun/probe/ndi_matlab_python_bridge.yaml index c61e257..6ef1c21 100644 --- a/src/ndi/fun/probe/ndi_matlab_python_bridge.yaml +++ b/src/ndi/fun/probe/ndi_matlab_python_bridge.yaml @@ -38,8 +38,8 @@ functions: decision_log: > Exact match. Exports data from a probe to a binary file. Creates metadata file alongside outputfile with .metadata - extension. Probe must be an ndi.element.Element or - ndi.probe.Probe of type 'n-trode'. + extension. ndi_probe must be an ndi.element.ndi_element or + ndi.probe.ndi_probe of type 'n-trode'. - name: export_all_binary type: function @@ -86,5 +86,5 @@ functions: decision_log: > Exact match. Finds probe location documents and probe object for an NDI element. Traverses underlying_element dependency - tree until Probe object is found. element can be an - ndi.element.Element object or string identifier. + tree until ndi_probe object is found. element can be an + ndi.element.ndi_element object or string identifier. diff --git a/src/ndi/fun/session.py b/src/ndi/fun/session.py index e2030c3..4c00f81 100644 --- a/src/ndi/fun/session.py +++ b/src/ndi/fun/session.py @@ -1,5 +1,5 @@ """ -ndi.fun.session - Session comparison utilities. +ndi.fun.session - ndi_session comparison utilities. MATLAB equivalent: +ndi/+fun/+session/diff.m """ @@ -39,14 +39,14 @@ def diff( if exclude_fields is None: exclude_fields = ["base.session_id"] - from ndi.query import Query + from ndi.query import ndi_query if verbose: print("Searching session 1 for documents...") - docs1 = session1.database_search(Query("").isa("base")) + docs1 = session1.database_search(ndi_query("").isa("base")) if verbose: print("Searching session 2 for documents...") - docs2 = session2.database_search(Query("").isa("base")) + docs2 = session2.database_search(ndi_query("").isa("base")) def _doc_id(doc: Any) -> str: props = doc.document_properties if hasattr(doc, "document_properties") else doc diff --git a/src/ndi/fun/stimulus.py b/src/ndi/fun/stimulus.py index 3cbd68c..0cb5845 100644 --- a/src/ndi/fun/stimulus.py +++ b/src/ndi/fun/stimulus.py @@ -31,7 +31,7 @@ def tuning_curve_to_response_type( Returns: Tuple of ``(response_type, stimulus_response_scalar_doc)``. """ - from ndi.query import Query + from ndi.query import ndi_query props = doc.document_properties if hasattr(doc, "document_properties") else doc if not isinstance(props, dict): @@ -46,7 +46,7 @@ def tuning_curve_to_response_type( dep_name = dep.get("name", "") dep_value = dep.get("value", "") if "stimulus_response_scalar" in dep_name and dep_value: - results = session.database_search(Query("base.id") == dep_value) + results = session.database_search(ndi_query("base.id") == dep_value) if results: scalar_doc = results[0] sp = ( @@ -66,7 +66,7 @@ def tuning_curve_to_response_type( dep_name = dep.get("name", "") dep_value = dep.get("value", "") if "stimulus_tuningcurve" in dep_name and dep_value: - results = session.database_search(Query("base.id") == dep_value) + results = session.database_search(ndi_query("base.id") == dep_value) if results: return tuning_curve_to_response_type(session, results[0]) @@ -193,10 +193,12 @@ def stimulustemporalfrequency( """ if config_path is None: try: - from ndi.common import PathConstants + from ndi.common import ndi_common_PathConstants config_path = str( - PathConstants.COMMON_FOLDER / "stimulus" / "temporal_frequency_rules.json" + ndi_common_PathConstants.COMMON_FOLDER + / "stimulus" + / "temporal_frequency_rules.json" ) except Exception: return None, "" @@ -262,7 +264,7 @@ def stimulus_tuningcurve_log( Returns: The log string, or ``''`` if not found. """ - from ndi.query import Query + from ndi.query import ndi_query props = doc.document_properties if hasattr(doc, "document_properties") else doc if not isinstance(props, dict): @@ -278,7 +280,7 @@ def stimulus_tuningcurve_log( if not stim_tune_doc_id: return "" - q = (Query("base.id") == stim_tune_doc_id) & Query("").isa("tuningcurve_calc") + q = (ndi_query("base.id") == stim_tune_doc_id) & ndi_query("").isa("tuningcurve_calc") results = session.database_search(q) if results: diff --git a/src/ndi/gui/__init__.py b/src/ndi/gui/__init__.py index 60a0772..643d28b 100644 --- a/src/ndi/gui/__init__.py +++ b/src/ndi/gui/__init__.py @@ -9,24 +9,24 @@ gui Simple session viewer (v1). gui_v2 - Enhanced viewer with Lab and Database tabs. + Enhanced viewer with ndi_gui_Lab and ndi_database tabs. Classes ------- -Data - Document table view with search/filter and graph visualisation. -Icon - Draggable icon for the Lab view. -Lab +ndi_gui_Data + ndi_document table view with search/filter and graph visualisation. +ndi_gui_Icon + Draggable icon for the ndi_gui_Lab view. +ndi_gui_Lab Experiment view with connection wires. -docViewer +ndi_gui_docViewer Standalone document viewer window. Sub-packages ------------ component - Progress bars and monitors (``ProgressBarWindow``, - ``NDIProgressBar``, ``CommandWindowProgressMonitor``). + Progress bars and monitors (``ndi_gui_component_ProgressBarWindow``, + ``ndi_gui_component_NDIProgressBar``, ``ndi_gui_component_CommandWindowProgressMonitor``). utility Helper functions (``centerFigure``). @@ -38,10 +38,10 @@ __all__ = [ "gui", "gui_v2", - "Data", - "Icon", - "Lab", - "docViewer", + "ndi_gui_Data", + "ndi_gui_Icon", + "ndi_gui_Lab", + "ndi_gui_docViewer", ] @@ -51,10 +51,10 @@ def __getattr__(name: str): # noqa: ANN204 _lazy = { "gui": ("ndi.gui.gui", "gui"), "gui_v2": ("ndi.gui.gui_v2", "gui_v2"), - "Data": ("ndi.gui.data", "Data"), - "Icon": ("ndi.gui.icon", "Icon"), - "Lab": ("ndi.gui.lab", "Lab"), - "docViewer": ("ndi.gui.docViewer", "docViewer"), + "ndi_gui_Data": ("ndi.gui.data", "ndi_gui_Data"), + "ndi_gui_Icon": ("ndi.gui.icon", "ndi_gui_Icon"), + "ndi_gui_Lab": ("ndi.gui.lab", "ndi_gui_Lab"), + "ndi_gui_docViewer": ("ndi.gui.ndi_gui_docViewer", "ndi_gui_docViewer"), } if name in _lazy: import importlib diff --git a/src/ndi/gui/component/CommandWindowProgressMonitor.py b/src/ndi/gui/component/CommandWindowProgressMonitor.py index bad5277..e3f7c21 100644 --- a/src/ndi/gui/component/CommandWindowProgressMonitor.py +++ b/src/ndi/gui/component/CommandWindowProgressMonitor.py @@ -1,6 +1,6 @@ -"""CommandWindowProgressMonitor — Console-based progress display. +"""ndi_gui_component_CommandWindowProgressMonitor — Console-based progress display. -Mirrors MATLAB: ndi.gui.component.CommandWindowProgressMonitor +Mirrors MATLAB: ndi.gui.component.ndi_gui_component_CommandWindowProgressMonitor Displays progress updates in the terminal/console with optional timestamps and in-place updating. @@ -12,17 +12,19 @@ from datetime import datetime from typing import Any -from ndi.gui.component.abstract.ProgressMonitor import ProgressMonitor +from ndi.gui.component.abstract.ndi_gui_component_abstract_ProgressMonitor import ( + ndi_gui_component_abstract_ProgressMonitor, +) -class CommandWindowProgressMonitor(ProgressMonitor): +class ndi_gui_component_CommandWindowProgressMonitor(ndi_gui_component_abstract_ProgressMonitor): """Progress monitor that prints updates to stdout. Parameters ---------- **kwargs Property overrides. In addition to those accepted by - :class:`ProgressMonitor`, this class supports: + :class:`ndi_gui_component_abstract_ProgressMonitor`, this class supports: * ``IndentSize`` (int, default 0) * ``ShowTimeStamp`` (bool, default False) @@ -38,7 +40,7 @@ def __init__(self, **kwargs: Any) -> None: self.UpdateInplace: bool = kwargs.get("UpdateInplace", False) self._prev_msg_len: int = 0 - # -- ProgressMonitor overrides ---------------------------------------- + # -- ndi_gui_component_abstract_ProgressMonitor overrides ---------------------------------------- def updateProgressDisplay(self) -> None: """Print current progress to stdout.""" diff --git a/src/ndi/gui/component/NDIProgressBar.py b/src/ndi/gui/component/NDIProgressBar.py index d4feb84..66febb6 100644 --- a/src/ndi/gui/component/NDIProgressBar.py +++ b/src/ndi/gui/component/NDIProgressBar.py @@ -1,6 +1,6 @@ -"""NDIProgressBar — A styled progress bar widget. +"""ndi_gui_component_NDIProgressBar — A styled progress bar widget. -Mirrors MATLAB: ndi.gui.component.NDIProgressBar +Mirrors MATLAB: ndi.gui.component.ndi_gui_component_NDIProgressBar Provides a single progress bar with NDI styling (blue colour scheme), a text label, and optional time-remaining display. Uses PySide6. @@ -11,7 +11,9 @@ from typing import Any from ndi.gui._qt_helpers import require_qt -from ndi.gui.component.abstract.ProgressMonitor import ProgressMonitor +from ndi.gui.component.abstract.ndi_gui_component_abstract_ProgressMonitor import ( + ndi_gui_component_abstract_ProgressMonitor, +) # Guard the Qt import so the module can still be *imported* without # PySide6 installed (the error fires only on construction). @@ -20,18 +22,18 @@ except ImportError: pass -# NDI colour constants (matching MATLAB NDIProgressBar) +# NDI colour constants (matching MATLAB ndi_gui_component_NDIProgressBar) _BG_COLOR = "#4472C4" # background blue _FG_COLOR = "#2F5597" # foreground dark-blue -class NDIProgressBar(ProgressMonitor): +class ndi_gui_component_NDIProgressBar(ndi_gui_component_abstract_ProgressMonitor): """A single progress bar widget with NDI styling. Parameters ---------- **kwargs - Property overrides accepted by :class:`ProgressMonitor` plus: + Property overrides accepted by :class:`ndi_gui_component_abstract_ProgressMonitor` plus: * ``Value`` (float, 0–1) — initial progress fraction. * ``Message`` (str) — initial status text. @@ -83,7 +85,7 @@ def widget(self) -> QtWidgets.QFrame: """The root QFrame containing the progress bar.""" return self._frame - # -- ProgressMonitor overrides ---------------------------------------- + # -- ndi_gui_component_abstract_ProgressMonitor overrides ---------------------------------------- def updateProgressDisplay(self) -> None: """Update the visual bar to reflect current progress.""" diff --git a/src/ndi/gui/component/ProgressBarWindow.py b/src/ndi/gui/component/ProgressBarWindow.py index 60a813c..d864e80 100644 --- a/src/ndi/gui/component/ProgressBarWindow.py +++ b/src/ndi/gui/component/ProgressBarWindow.py @@ -1,6 +1,6 @@ -"""ProgressBarWindow — Multi-bar progress window. +"""ndi_gui_component_ProgressBarWindow — Multi-bar progress window. -Mirrors MATLAB: ndi.gui.component.ProgressBarWindow +Mirrors MATLAB: ndi.gui.component.ndi_gui_component_ProgressBarWindow Creates and manages a PySide6 window that can display one or more progress bars. Bars can be added, updated, and removed dynamically. @@ -22,7 +22,7 @@ pass # Module-level registry of existing windows (mirrors MATLAB findall) -_ACTIVE_WINDOWS: dict[str, ProgressBarWindow] = {} +_ACTIVE_WINDOWS: dict[str, ndi_gui_component_ProgressBarWindow] = {} class _BarRecord: @@ -60,7 +60,7 @@ def __init__(self, tag: str, auto: bool, color: tuple[float, float, float]): self.btn_widget: QtWidgets.QPushButton | None = None -class ProgressBarWindow: +class ndi_gui_component_ProgressBarWindow: """Multi-bar progress window using PySide6. Parameters @@ -315,14 +315,14 @@ def getBarNum(self, barID: int | str) -> tuple[int | None, dict[str, str]]: """ status: dict[str, str] = {"identifier": "", "message": ""} if not self._bars: - status["identifier"] = "ProgressBarWindow:NoBarsExist" + status["identifier"] = "ndi_gui_component_ProgressBarWindow:NoBarsExist" status["message"] = "No progress bars have been added yet." return None, status if isinstance(barID, int): if 0 <= barID < len(self._bars): return barID, status - status["identifier"] = "ProgressBarWindow:InvalidBarIndex" + status["identifier"] = "ndi_gui_component_ProgressBarWindow:InvalidBarIndex" status["message"] = f"Numeric BarID {barID} is out of bounds (0-{len(self._bars) - 1})." return None, status @@ -330,7 +330,7 @@ def getBarNum(self, barID: int | str) -> tuple[int | None, dict[str, str]]: for i, rec in enumerate(self._bars): if rec.Tag.lower() == str(barID).lower(): return i, status - status["identifier"] = "ProgressBarWindow:InvalidBarTag" + status["identifier"] = "ndi_gui_component_ProgressBarWindow:InvalidBarTag" status["message"] = f'BarID Tag "{barID}" not found.' return None, status diff --git a/src/ndi/gui/component/__init__.py b/src/ndi/gui/component/__init__.py index b7748b5..582d9a9 100644 --- a/src/ndi/gui/component/__init__.py +++ b/src/ndi/gui/component/__init__.py @@ -6,32 +6,36 @@ Subpackages ----------- abstract - Abstract base classes (``ProgressMonitor``). + Abstract base classes (``ndi_gui_component_abstract_ProgressMonitor``). internal - Progress tracking internals (``ProgressTracker``, - ``AsynchProgressTracker``, event data classes). + Progress tracking internals (``ndi_gui_component_internal_ProgressTracker``, + ``ndi_gui_component_internal_AsynchProgressTracker``, event data classes). """ -from ndi.gui.component.CommandWindowProgressMonitor import ( - CommandWindowProgressMonitor, +from ndi.gui.component.ndi_gui_component_CommandWindowProgressMonitor import ( + ndi_gui_component_CommandWindowProgressMonitor, ) # Lazy imports for Qt-dependent components to avoid hard PySide6 # dependency at module load time. __all__ = [ - "CommandWindowProgressMonitor", - "NDIProgressBar", - "ProgressBarWindow", + "ndi_gui_component_CommandWindowProgressMonitor", + "ndi_gui_component_NDIProgressBar", + "ndi_gui_component_ProgressBarWindow", ] def __getattr__(name: str): # noqa: ANN204 - if name == "NDIProgressBar": - from ndi.gui.component.NDIProgressBar import NDIProgressBar - - return NDIProgressBar - if name == "ProgressBarWindow": - from ndi.gui.component.ProgressBarWindow import ProgressBarWindow - - return ProgressBarWindow + if name == "ndi_gui_component_NDIProgressBar": + from ndi.gui.component.ndi_gui_component_NDIProgressBar import ( + ndi_gui_component_NDIProgressBar, + ) + + return ndi_gui_component_NDIProgressBar + if name == "ndi_gui_component_ProgressBarWindow": + from ndi.gui.component.ndi_gui_component_ProgressBarWindow import ( + ndi_gui_component_ProgressBarWindow, + ) + + return ndi_gui_component_ProgressBarWindow raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/src/ndi/gui/component/abstract/ProgressMonitor.py b/src/ndi/gui/component/abstract/ProgressMonitor.py index 62701c0..0f67b0b 100644 --- a/src/ndi/gui/component/abstract/ProgressMonitor.py +++ b/src/ndi/gui/component/abstract/ProgressMonitor.py @@ -1,6 +1,6 @@ -"""ProgressMonitor — Abstract base class for progress display. +"""ndi_gui_component_abstract_ProgressMonitor — Abstract base class for progress display. -Mirrors MATLAB: ndi.gui.component.abstract.ProgressMonitor +Mirrors MATLAB: ndi.gui.component.abstract.ndi_gui_component_abstract_ProgressMonitor Provides timing, event-listener wiring, and time-remaining estimation. Subclasses must implement :meth:`updateProgressDisplay`, @@ -13,18 +13,20 @@ from abc import ABC, abstractmethod from typing import Any -from ndi.gui.component.internal.ProgressTracker import ProgressTracker +from ndi.gui.component.internal.ndi_gui_component_internal_ProgressTracker import ( + ndi_gui_component_internal_ProgressTracker, +) -class ProgressMonitor(ABC): - """Abstract monitor that listens to a :class:`ProgressTracker`. +class ndi_gui_component_abstract_ProgressMonitor(ABC): + """Abstract monitor that listens to a :class:`ndi_gui_component_internal_ProgressTracker`. Parameters ---------- **kwargs Arbitrary property overrides (``Title``, ``UpdateInterval``, ``DisplayElapsedTime``, ``DisplayRemainingTime``, - ``ProgressTracker``). + ``ndi_gui_component_internal_ProgressTracker``). """ def __init__(self, **kwargs: Any) -> None: @@ -33,15 +35,17 @@ def __init__(self, **kwargs: Any) -> None: self.DisplayElapsedTime: bool = kwargs.get("DisplayElapsedTime", False) self.DisplayRemainingTime: bool = kwargs.get("DisplayRemainingTime", True) - self.ProgressTracker: ProgressTracker | None = kwargs.get("ProgressTracker", None) + self.ndi_gui_component_internal_ProgressTracker: ( + ndi_gui_component_internal_ProgressTracker | None + ) = kwargs.get("ndi_gui_component_internal_ProgressTracker", None) self._start_time: float | None = None self._last_update_time: float = 0.0 self._is_initialized: bool = False self._listener_handles: list[Any] = [] - if self.ProgressTracker is not None: - self._attach_listeners(self.ProgressTracker) + if self.ndi_gui_component_internal_ProgressTracker is not None: + self._attach_listeners(self.ndi_gui_component_internal_ProgressTracker) # -- Public API ------------------------------------------------------- @@ -53,14 +57,14 @@ def reset(self) -> None: def markComplete(self) -> None: """Signal that the task is complete.""" - if self.ProgressTracker is not None: - self.ProgressTracker.setCompleted() + if self.ndi_gui_component_internal_ProgressTracker is not None: + self.ndi_gui_component_internal_ProgressTracker.setCompleted() self.finish() - def setProgressTracker(self, tracker: ProgressTracker) -> None: - """Attach a new :class:`ProgressTracker` and wire up listeners.""" + def setProgressTracker(self, tracker: ndi_gui_component_internal_ProgressTracker) -> None: + """Attach a new :class:`ndi_gui_component_internal_ProgressTracker` and wire up listeners.""" self._detach_listeners() - self.ProgressTracker = tracker + self.ndi_gui_component_internal_ProgressTracker = tracker self._attach_listeners(tracker) # -- Abstract methods (subclasses MUST implement) --------------------- @@ -81,14 +85,17 @@ def finish(self) -> None: def getProgressValue(self) -> float: """Return current progress as a fraction 0–1.""" - if self.ProgressTracker is None: + if self.ndi_gui_component_internal_ProgressTracker is None: return 0.0 - return self.ProgressTracker.PercentageComplete / 100.0 + return self.ndi_gui_component_internal_ProgressTracker.PercentageComplete / 100.0 def getProgressTitle(self) -> str: """Return the tracker's rendered message, or *Title*.""" - if self.ProgressTracker is not None and self.ProgressTracker.Message: - return self.ProgressTracker.Message + if ( + self.ndi_gui_component_internal_ProgressTracker is not None + and self.ndi_gui_component_internal_ProgressTracker.Message + ): + return self.ndi_gui_component_internal_ProgressTracker.Message return self.Title def formatMessage(self, message: str) -> str: @@ -121,15 +128,15 @@ def _formatDuration(seconds: float) -> str: # -- Listener wiring -------------------------------------------------- - def _attach_listeners(self, tracker: ProgressTracker) -> None: + def _attach_listeners(self, tracker: ndi_gui_component_internal_ProgressTracker) -> None: tracker.on_progress_updated.append(self._on_progress) tracker.on_message_updated.append(self._on_message) tracker.on_task_completed.append(self._on_complete) def _detach_listeners(self) -> None: - if self.ProgressTracker is None: + if self.ndi_gui_component_internal_ProgressTracker is None: return - t = self.ProgressTracker + t = self.ndi_gui_component_internal_ProgressTracker for lst, cb in [ (t.on_progress_updated, self._on_progress), (t.on_message_updated, self._on_message), diff --git a/src/ndi/gui/component/abstract/__init__.py b/src/ndi/gui/component/abstract/__init__.py index e54279c..d8fbaeb 100644 --- a/src/ndi/gui/component/abstract/__init__.py +++ b/src/ndi/gui/component/abstract/__init__.py @@ -1,7 +1,9 @@ """ndi.gui.component.abstract — Abstract base classes for GUI components.""" -from ndi.gui.component.abstract.ProgressMonitor import ProgressMonitor +from ndi.gui.component.abstract.ndi_gui_component_abstract_ProgressMonitor import ( + ndi_gui_component_abstract_ProgressMonitor, +) __all__ = [ - "ProgressMonitor", + "ndi_gui_component_abstract_ProgressMonitor", ] diff --git a/src/ndi/gui/component/internal/AsynchProgressTracker.py b/src/ndi/gui/component/internal/AsynchProgressTracker.py index 6069452..937fbab 100644 --- a/src/ndi/gui/component/internal/AsynchProgressTracker.py +++ b/src/ndi/gui/component/internal/AsynchProgressTracker.py @@ -1,8 +1,8 @@ -"""AsynchProgressTracker — File-backed asynchronous progress tracker. +"""ndi_gui_component_internal_AsynchProgressTracker — File-backed asynchronous progress tracker. -Mirrors MATLAB: ndi.gui.component.internal.AsynchProgressTracker +Mirrors MATLAB: ndi.gui.component.internal.ndi_gui_component_internal_AsynchProgressTracker -Extends :class:`ProgressTracker` with the ability to serialise progress +Extends :class:`ndi_gui_component_internal_ProgressTracker` with the ability to serialise progress state to a JSON file, enabling cross-process monitoring. """ @@ -11,13 +11,15 @@ import json import time -from ndi.gui.component.internal.ProgressTracker import ProgressTracker +from ndi.gui.component.internal.ndi_gui_component_internal_ProgressTracker import ( + ndi_gui_component_internal_ProgressTracker, +) -class AsynchProgressTracker(ProgressTracker): +class ndi_gui_component_internal_AsynchProgressTracker(ndi_gui_component_internal_ProgressTracker): """Progress tracker that dumps state to a file for async monitoring. - Inherits all behaviour from :class:`ProgressTracker` and adds + Inherits all behaviour from :class:`ndi_gui_component_internal_ProgressTracker` and adds throttled file-based serialisation via :meth:`updateProgress`. """ diff --git a/src/ndi/gui/component/internal/ProgressTracker.py b/src/ndi/gui/component/internal/ProgressTracker.py index 1834277..9e137f5 100644 --- a/src/ndi/gui/component/internal/ProgressTracker.py +++ b/src/ndi/gui/component/internal/ProgressTracker.py @@ -1,6 +1,6 @@ -"""ProgressTracker — Tracks progress of a multi-step task. +"""ndi_gui_component_internal_ProgressTracker — Tracks progress of a multi-step task. -Mirrors MATLAB: ndi.gui.component.internal.ProgressTracker +Mirrors MATLAB: ndi.gui.component.internal.ndi_gui_component_internal_ProgressTracker Provides step counting, percentage calculation, template-based messages, and event callbacks for progress updates, message changes, and completion. @@ -14,7 +14,7 @@ from typing import Any -class ProgressTracker: +class ndi_gui_component_internal_ProgressTracker: """Track progress of a task with *TotalSteps* discrete steps. Parameters @@ -115,16 +115,20 @@ def reset(self) -> None: # -- Event helpers ---------------------------------------------------- def _fire_progress_updated(self) -> None: - from ndi.gui.component.internal.event import ProgressUpdatedEventData + from ndi.gui.component.internal.event import ( + ndi_gui_component_internal_event_ProgressUpdatedEventData, + ) - evt = ProgressUpdatedEventData(self.PercentageComplete) + evt = ndi_gui_component_internal_event_ProgressUpdatedEventData(self.PercentageComplete) for cb in self.on_progress_updated: cb(self, evt) def _fire_message_updated(self) -> None: - from ndi.gui.component.internal.event import MessageUpdatedEventData + from ndi.gui.component.internal.event import ( + ndi_gui_component_internal_event_MessageUpdatedEventData, + ) - evt = MessageUpdatedEventData(self.Message) + evt = ndi_gui_component_internal_event_MessageUpdatedEventData(self.Message) for cb in self.on_message_updated: cb(self, evt) diff --git a/src/ndi/gui/component/internal/__init__.py b/src/ndi/gui/component/internal/__init__.py index b401bd9..1c0ece1 100644 --- a/src/ndi/gui/component/internal/__init__.py +++ b/src/ndi/gui/component/internal/__init__.py @@ -1,9 +1,13 @@ """ndi.gui.component.internal — Internal progress tracking infrastructure.""" -from ndi.gui.component.internal.AsynchProgressTracker import AsynchProgressTracker -from ndi.gui.component.internal.ProgressTracker import ProgressTracker +from ndi.gui.component.internal.ndi_gui_component_internal_AsynchProgressTracker import ( + ndi_gui_component_internal_AsynchProgressTracker, +) +from ndi.gui.component.internal.ndi_gui_component_internal_ProgressTracker import ( + ndi_gui_component_internal_ProgressTracker, +) __all__ = [ - "ProgressTracker", - "AsynchProgressTracker", + "ndi_gui_component_internal_ProgressTracker", + "ndi_gui_component_internal_AsynchProgressTracker", ] diff --git a/src/ndi/gui/component/internal/event/MessageUpdatedEventData.py b/src/ndi/gui/component/internal/event/MessageUpdatedEventData.py index acf7166..04c9c31 100644 --- a/src/ndi/gui/component/internal/event/MessageUpdatedEventData.py +++ b/src/ndi/gui/component/internal/event/MessageUpdatedEventData.py @@ -1,12 +1,12 @@ -"""MessageUpdatedEventData — Event data for message update notifications. +"""ndi_gui_component_internal_event_MessageUpdatedEventData — Event data for message update notifications. -Mirrors MATLAB: ndi.gui.component.internal.event.MessageUpdatedEventData +Mirrors MATLAB: ndi.gui.component.internal.event.ndi_gui_component_internal_event_MessageUpdatedEventData """ from __future__ import annotations -class MessageUpdatedEventData: +class ndi_gui_component_internal_event_MessageUpdatedEventData: """Event data carrying an updated status message. Parameters diff --git a/src/ndi/gui/component/internal/event/ProgressUpdatedEventData.py b/src/ndi/gui/component/internal/event/ProgressUpdatedEventData.py index 9496f10..e8011cb 100644 --- a/src/ndi/gui/component/internal/event/ProgressUpdatedEventData.py +++ b/src/ndi/gui/component/internal/event/ProgressUpdatedEventData.py @@ -1,12 +1,12 @@ -"""ProgressUpdatedEventData — Event data for progress update notifications. +"""ndi_gui_component_internal_event_ProgressUpdatedEventData — Event data for progress update notifications. -Mirrors MATLAB: ndi.gui.component.internal.event.ProgressUpdatedEventData +Mirrors MATLAB: ndi.gui.component.internal.event.ndi_gui_component_internal_event_ProgressUpdatedEventData """ from __future__ import annotations -class ProgressUpdatedEventData: +class ndi_gui_component_internal_event_ProgressUpdatedEventData: """Event data carrying the current percentage complete. Parameters diff --git a/src/ndi/gui/component/internal/event/__init__.py b/src/ndi/gui/component/internal/event/__init__.py index 5bfac28..57a5080 100644 --- a/src/ndi/gui/component/internal/event/__init__.py +++ b/src/ndi/gui/component/internal/event/__init__.py @@ -1,13 +1,13 @@ """ndi.gui.component.internal.event — Event data classes for progress tracking.""" -from ndi.gui.component.internal.event.MessageUpdatedEventData import ( - MessageUpdatedEventData, +from ndi.gui.component.internal.event.ndi_gui_component_internal_event_MessageUpdatedEventData import ( + ndi_gui_component_internal_event_MessageUpdatedEventData, ) -from ndi.gui.component.internal.event.ProgressUpdatedEventData import ( - ProgressUpdatedEventData, +from ndi.gui.component.internal.event.ndi_gui_component_internal_event_ProgressUpdatedEventData import ( + ndi_gui_component_internal_event_ProgressUpdatedEventData, ) __all__ = [ - "ProgressUpdatedEventData", - "MessageUpdatedEventData", + "ndi_gui_component_internal_event_ProgressUpdatedEventData", + "ndi_gui_component_internal_event_MessageUpdatedEventData", ] diff --git a/src/ndi/gui/component/ndi_matlab_python_bridge.yaml b/src/ndi/gui/component/ndi_matlab_python_bridge.yaml index 4e66dba..bc2512b 100644 --- a/src/ndi/gui/component/ndi_matlab_python_bridge.yaml +++ b/src/ndi/gui/component/ndi_matlab_python_bridge.yaml @@ -12,13 +12,13 @@ project_metadata: classes: # ----------------------------------------------------------------------- - # ndi.gui.component.abstract.ProgressMonitor + # ndi.gui.component.abstract.ndi_gui_component_abstract_ProgressMonitor # ----------------------------------------------------------------------- - - name: ProgressMonitor + - name: ndi_gui_component_abstract_ProgressMonitor type: class - matlab_path: "+ndi/+gui/+component/+abstract/ProgressMonitor.m" - python_path: "ndi/gui/component/abstract/ProgressMonitor.py" - python_class: "ProgressMonitor" + matlab_path: "+ndi/+gui/+component/+abstract/ndi_gui_component_abstract_ProgressMonitor.m" + python_path: "ndi/gui/component/abstract/ndi_gui_component_abstract_ProgressMonitor.py" + python_class: "ndi_gui_component_abstract_ProgressMonitor" inherits: "handle (abstract)" properties: @@ -31,9 +31,9 @@ classes: type_matlab: "double" type_python: "float" decision_log: "Exact match. Seconds between display refreshes." - - name: ProgressTracker - type_matlab: "ndi.gui.component.internal.ProgressTracker" - type_python: "ProgressTracker" + - name: ndi_gui_component_internal_ProgressTracker + type_matlab: "ndi.gui.component.internal.ndi_gui_component_internal_ProgressTracker" + type_python: "ndi_gui_component_internal_ProgressTracker" decision_log: "Exact match." - name: DisplayElapsedTime type_matlab: "logical" @@ -45,7 +45,7 @@ classes: decision_log: "Exact match." methods: - - name: ProgressMonitor + - name: ndi_gui_component_abstract_ProgressMonitor kind: constructor input_arguments: - name: kwargs @@ -53,7 +53,7 @@ classes: type_python: "**kwargs" output_arguments: - name: obj - type_python: "ProgressMonitor" + type_python: "ndi_gui_component_abstract_ProgressMonitor" decision_log: > Ported. Accepts property name-value pairs. @@ -88,8 +88,8 @@ classes: - name: setProgressTracker input_arguments: - name: tracker - type_matlab: "ndi.gui.component.internal.ProgressTracker" - type_python: "ProgressTracker" + type_matlab: "ndi.gui.component.internal.ndi_gui_component_internal_ProgressTracker" + type_python: "ndi_gui_component_internal_ProgressTracker" output_arguments: [] decision_log: > Exact match. Detaches from previous tracker and attaches @@ -102,7 +102,7 @@ classes: type_python: "float" decision_log: > Exact match. Returns current progress as a fraction 0-1 - from the attached ProgressTracker. Synchronized 2026-03-13. + from the attached ndi_gui_component_internal_ProgressTracker. Synchronized 2026-03-13. - name: getProgressTitle input_arguments: [] @@ -127,13 +127,13 @@ classes: Synchronized 2026-03-13. # ----------------------------------------------------------------------- - # ndi.gui.component.internal.ProgressTracker + # ndi.gui.component.internal.ndi_gui_component_internal_ProgressTracker # ----------------------------------------------------------------------- - - name: ProgressTracker + - name: ndi_gui_component_internal_ProgressTracker type: class - matlab_path: "+ndi/+gui/+component/+internal/ProgressTracker.m" - python_path: "ndi/gui/component/internal/ProgressTracker.py" - python_class: "ProgressTracker" + matlab_path: "+ndi/+gui/+component/+internal/ndi_gui_component_internal_ProgressTracker.m" + python_path: "ndi/gui/component/internal/ndi_gui_component_internal_ProgressTracker.py" + python_class: "ndi_gui_component_internal_ProgressTracker" inherits: "handle" properties: @@ -175,10 +175,10 @@ classes: type_python: "str | None" decision_log: > Exact match. Path for async file-based progress dumps. - Used by AsynchProgressTracker subclass. Synchronized 2026-03-13. + Used by ndi_gui_component_internal_AsynchProgressTracker subclass. Synchronized 2026-03-13. methods: - - name: ProgressTracker + - name: ndi_gui_component_internal_ProgressTracker kind: constructor input_arguments: - name: totalSteps @@ -187,7 +187,7 @@ classes: default: "0" output_arguments: - name: obj - type_python: "ProgressTracker" + type_python: "ndi_gui_component_internal_ProgressTracker" decision_log: "Exact match." - name: updateProgress @@ -212,14 +212,14 @@ classes: decision_log: "Exact match. Reinitializes the tracker." # ----------------------------------------------------------------------- - # ndi.gui.component.internal.AsynchProgressTracker + # ndi.gui.component.internal.ndi_gui_component_internal_AsynchProgressTracker # ----------------------------------------------------------------------- - - name: AsynchProgressTracker + - name: ndi_gui_component_internal_AsynchProgressTracker type: class - matlab_path: "+ndi/+gui/+component/+internal/AsynchProgressTracker.m" - python_path: "ndi/gui/component/internal/AsynchProgressTracker.py" - python_class: "AsynchProgressTracker" - inherits: "ndi.gui.component.internal.ProgressTracker" + matlab_path: "+ndi/+gui/+component/+internal/ndi_gui_component_internal_AsynchProgressTracker.m" + python_path: "ndi/gui/component/internal/ndi_gui_component_internal_AsynchProgressTracker.py" + python_class: "ndi_gui_component_internal_AsynchProgressTracker" + inherits: "ndi.gui.component.internal.ndi_gui_component_internal_ProgressTracker" methods: - name: updateProgress @@ -248,13 +248,13 @@ classes: TotalSteps, TemplateMessage keys. Synchronized 2026-03-13. # ----------------------------------------------------------------------- - # ndi.gui.component.internal.event.ProgressUpdatedEventData + # ndi.gui.component.internal.event.ndi_gui_component_internal_event_ProgressUpdatedEventData # ----------------------------------------------------------------------- - - name: ProgressUpdatedEventData + - name: ndi_gui_component_internal_event_ProgressUpdatedEventData type: class - matlab_path: "+ndi/+gui/+component/+internal/+event/ProgressUpdatedEventData.m" - python_path: "ndi/gui/component/internal/event/ProgressUpdatedEventData.py" - python_class: "ProgressUpdatedEventData" + matlab_path: "+ndi/+gui/+component/+internal/+event/ndi_gui_component_internal_event_ProgressUpdatedEventData.m" + python_path: "ndi/gui/component/internal/event/ndi_gui_component_internal_event_ProgressUpdatedEventData.py" + python_class: "ndi_gui_component_internal_event_ProgressUpdatedEventData" inherits: "event.EventData" properties: @@ -264,7 +264,7 @@ classes: decision_log: "Exact match." methods: - - name: ProgressUpdatedEventData + - name: ndi_gui_component_internal_event_ProgressUpdatedEventData kind: constructor input_arguments: - name: percentageComplete @@ -272,17 +272,17 @@ classes: type_python: "float" output_arguments: - name: obj - type_python: "ProgressUpdatedEventData" + type_python: "ndi_gui_component_internal_event_ProgressUpdatedEventData" decision_log: "Exact match." # ----------------------------------------------------------------------- - # ndi.gui.component.internal.event.MessageUpdatedEventData + # ndi.gui.component.internal.event.ndi_gui_component_internal_event_MessageUpdatedEventData # ----------------------------------------------------------------------- - - name: MessageUpdatedEventData + - name: ndi_gui_component_internal_event_MessageUpdatedEventData type: class - matlab_path: "+ndi/+gui/+component/+internal/+event/MessageUpdatedEventData.m" - python_path: "ndi/gui/component/internal/event/MessageUpdatedEventData.py" - python_class: "MessageUpdatedEventData" + matlab_path: "+ndi/+gui/+component/+internal/+event/ndi_gui_component_internal_event_MessageUpdatedEventData.m" + python_path: "ndi/gui/component/internal/event/ndi_gui_component_internal_event_MessageUpdatedEventData.py" + python_class: "ndi_gui_component_internal_event_MessageUpdatedEventData" inherits: "event.EventData" properties: @@ -292,7 +292,7 @@ classes: decision_log: "Exact match." methods: - - name: MessageUpdatedEventData + - name: ndi_gui_component_internal_event_MessageUpdatedEventData kind: constructor input_arguments: - name: message @@ -300,18 +300,18 @@ classes: type_python: "str" output_arguments: - name: obj - type_python: "MessageUpdatedEventData" + type_python: "ndi_gui_component_internal_event_MessageUpdatedEventData" decision_log: "Exact match." # ----------------------------------------------------------------------- - # ndi.gui.component.CommandWindowProgressMonitor + # ndi.gui.component.ndi_gui_component_CommandWindowProgressMonitor # ----------------------------------------------------------------------- - - name: CommandWindowProgressMonitor + - name: ndi_gui_component_CommandWindowProgressMonitor type: class - matlab_path: "+ndi/+gui/+component/CommandWindowProgressMonitor.m" - python_path: "ndi/gui/component/CommandWindowProgressMonitor.py" - python_class: "CommandWindowProgressMonitor" - inherits: "ndi.gui.component.abstract.ProgressMonitor" + matlab_path: "+ndi/+gui/+component/ndi_gui_component_CommandWindowProgressMonitor.m" + python_path: "ndi/gui/component/ndi_gui_component_CommandWindowProgressMonitor.py" + python_class: "ndi_gui_component_CommandWindowProgressMonitor" + inherits: "ndi.gui.component.abstract.ndi_gui_component_abstract_ProgressMonitor" properties: - name: IndentSize @@ -334,7 +334,7 @@ classes: in-place on the same line. methods: - - name: CommandWindowProgressMonitor + - name: ndi_gui_component_CommandWindowProgressMonitor kind: constructor input_arguments: - name: kwargs @@ -342,18 +342,18 @@ classes: type_python: "**kwargs" output_arguments: - name: obj - type_python: "CommandWindowProgressMonitor" + type_python: "ndi_gui_component_CommandWindowProgressMonitor" decision_log: "Exact match." # ----------------------------------------------------------------------- - # ndi.gui.component.NDIProgressBar + # ndi.gui.component.ndi_gui_component_NDIProgressBar # ----------------------------------------------------------------------- - - name: NDIProgressBar + - name: ndi_gui_component_NDIProgressBar type: class - matlab_path: "+ndi/+gui/+component/NDIProgressBar.m" - python_path: "ndi/gui/component/NDIProgressBar.py" - python_class: "NDIProgressBar" - inherits: "ndi.gui.component.abstract.ProgressMonitor" + matlab_path: "+ndi/+gui/+component/ndi_gui_component_NDIProgressBar.m" + python_path: "ndi/gui/component/ndi_gui_component_NDIProgressBar.py" + python_class: "ndi_gui_component_NDIProgressBar" + inherits: "ndi.gui.component.abstract.ndi_gui_component_abstract_ProgressMonitor" properties: - name: Value @@ -374,7 +374,7 @@ classes: decision_log: "Exact match. x, y position." methods: - - name: NDIProgressBar + - name: ndi_gui_component_NDIProgressBar kind: constructor input_arguments: - name: kwargs @@ -382,7 +382,7 @@ classes: type_python: "**kwargs" output_arguments: - name: obj - type_python: "NDIProgressBar" + type_python: "ndi_gui_component_NDIProgressBar" decision_log: "Ported. Uses PySide6 QProgressBar with NDI styling." - name: updateProgressDisplay @@ -396,13 +396,13 @@ classes: decision_log: "Exact match. Shows completion message." # ----------------------------------------------------------------------- - # ndi.gui.component.ProgressBarWindow + # ndi.gui.component.ndi_gui_component_ProgressBarWindow # ----------------------------------------------------------------------- - - name: ProgressBarWindow + - name: ndi_gui_component_ProgressBarWindow type: class - matlab_path: "+ndi/+gui/+component/ProgressBarWindow.m" - python_path: "ndi/gui/component/ProgressBarWindow.py" - python_class: "ProgressBarWindow" + matlab_path: "+ndi/+gui/+component/ndi_gui_component_ProgressBarWindow.m" + python_path: "ndi/gui/component/ndi_gui_component_ProgressBarWindow.py" + python_class: "ndi_gui_component_ProgressBarWindow" inherits: "matlab.apps.AppBase" properties: @@ -431,7 +431,7 @@ classes: Timer. Python uses list of dicts with equivalent keys. methods: - - name: ProgressBarWindow + - name: ndi_gui_component_ProgressBarWindow kind: constructor input_arguments: - name: title @@ -448,7 +448,7 @@ classes: default: "True" output_arguments: - name: app - type_python: "ProgressBarWindow" + type_python: "ndi_gui_component_ProgressBarWindow" decision_log: > Ported. Creates a PySide6 window for managing multiple progress bars. GrabMostRecent and IgnoreTitle options diff --git a/src/ndi/gui/data.py b/src/ndi/gui/data.py index ef48ea7..c09f3f9 100644 --- a/src/ndi/gui/data.py +++ b/src/ndi/gui/data.py @@ -1,6 +1,6 @@ -"""Data — Document data-table view with search/filter and graph visualisation. +"""ndi_gui_Data — ndi_document data-table view with search/filter and graph visualisation. -Mirrors MATLAB: ndi.gui.Data +Mirrors MATLAB: ndi.gui.ndi_gui_Data Provides a table of NDI documents with search, filtering (contains, begins with, ends with), a detail panel, and dependency-graph @@ -20,8 +20,8 @@ pass -class Data: - """Database view widget showing a searchable document table. +class ndi_gui_Data: + """ndi_database view widget showing a searchable document table. The widget is embedded inside :func:`ndi.gui.gui_v2` but can also be used standalone. diff --git a/src/ndi/gui/docViewer.py b/src/ndi/gui/docViewer.py index 3cd764d..1d17ccd 100644 --- a/src/ndi/gui/docViewer.py +++ b/src/ndi/gui/docViewer.py @@ -1,6 +1,6 @@ -"""docViewer — Standalone NDI document viewer window. +"""ndi_gui_docViewer — Standalone NDI document viewer window. -Mirrors MATLAB: ndi.gui.docViewer +Mirrors MATLAB: ndi.gui.ndi_gui_docViewer A self-contained window with a document table, detail panel, search/filter controls, and dependency-graph visualisation. @@ -19,7 +19,7 @@ pass -class docViewer: +class ndi_gui_docViewer: """Standalone document viewer window. Opens its own :class:`QMainWindow`. Populate it with :meth:`addDoc`. @@ -36,7 +36,7 @@ def __init__(self) -> None: self.docs: list[Any] = [] self.fig = QtWidgets.QMainWindow() - self.fig.setWindowTitle("Document Viewer") + self.fig.setWindowTitle("ndi_document Viewer") self.fig.resize(900, 600) central = QtWidgets.QWidget() @@ -389,7 +389,7 @@ def _field_contains(d: dict, field_name: str, value: str) -> bool: if isinstance(v, str) and value in v.lower(): return True if isinstance(v, dict): - if docViewer._field_contains(v, field_name, value): + if ndi_gui_docViewer._field_contains(v, field_name, value): return True return False @@ -400,6 +400,6 @@ def _has_field(d: dict, field_name: str) -> bool: if k.lower() == field_name: return True if isinstance(v, dict): - if docViewer._has_field(v, field_name): + if ndi_gui_docViewer._has_field(v, field_name): return True return False diff --git a/src/ndi/gui/gui.py b/src/ndi/gui/gui.py index 01a996d..99b241b 100644 --- a/src/ndi/gui/gui.py +++ b/src/ndi/gui/gui.py @@ -4,7 +4,7 @@ Opens a QMainWindow showing probes/things, DAQ readers, cache, database documents, and document-property details for a given -:class:`ndi.session.Session`. +:class:`ndi.session.ndi_session`. """ from __future__ import annotations @@ -20,7 +20,7 @@ def gui(ndi_session_obj: Any) -> None: Parameters ---------- - ndi_session_obj : ndi.session.Session + ndi_session_obj : ndi.session.ndi_session The session to display. Notes @@ -91,27 +91,27 @@ def __init__(self, session: Any) -> None: left.addWidget(self._things_list) body.addLayout(left, 1) - # Middle column: DAQ Readers + Cache + # Middle column: DAQ Readers + ndi_cache mid = QtWidgets.QVBoxLayout() mid.addWidget(QtWidgets.QLabel("DAQ-Readers")) self._daq_list = QtWidgets.QListWidget() mid.addWidget(self._daq_list) - mid.addWidget(QtWidgets.QLabel("Cache")) + mid.addWidget(QtWidgets.QLabel("ndi_cache")) self._cache_list = QtWidgets.QListWidget() mid.addWidget(self._cache_list) body.addLayout(mid, 1) - # Right column: Database + Doc Properties + # Right column: ndi_database + Doc Properties right_layout = QtWidgets.QVBoxLayout() - right_layout.addWidget(QtWidgets.QLabel("Database")) + right_layout.addWidget(QtWidgets.QLabel("ndi_database")) self._db_list = QtWidgets.QListWidget() self._db_list.currentRowChanged.connect(self._on_db_select) right_layout.addWidget(self._db_list) body.addLayout(right_layout, 2) props = QtWidgets.QVBoxLayout() - props.addWidget(QtWidgets.QLabel("Document Properties")) + props.addWidget(QtWidgets.QLabel("ndi_document Properties")) self._doc_props = QtWidgets.QTextEdit() self._doc_props.setReadOnly(True) props.addWidget(self._doc_props) @@ -137,10 +137,10 @@ def _update_db_list(self) -> None: self._doc_ids.clear() self._doc_cache.clear() try: - from ndi.query import Query + from ndi.query import ndi_query doc_list = self._session.database_search( - Query("document_class.class_name", "regex", "(.*)", "") + ndi_query("document_class.class_name", "regex", "(.*)", "") ) except Exception: doc_list = [] diff --git a/src/ndi/gui/gui_v2.py b/src/ndi/gui/gui_v2.py index fc17462..e1de383 100644 --- a/src/ndi/gui/gui_v2.py +++ b/src/ndi/gui/gui_v2.py @@ -1,11 +1,11 @@ -"""gui_v2 — Enhanced NDI session GUI with Experiment and Database views. +"""gui_v2 — Enhanced NDI session GUI with Experiment and ndi_database views. Mirrors MATLAB: ndi.gui.gui_v2 Opens a QMainWindow with two tab views: -- **Experiment View** (Lab): shows subjects, probes, and DAQs as +- **Experiment View** (ndi_gui_Lab): shows subjects, probes, and DAQs as draggable icons with connection wires. -- **Database View** (Data): shows a searchable/filterable document +- **ndi_database View** (ndi_gui_Data): shows a searchable/filterable document table with dependency graph visualisation. """ @@ -21,7 +21,7 @@ def gui_v2(ndi_session_obj: Any) -> None: Parameters ---------- - ndi_session_obj : ndi.session.Session + ndi_session_obj : ndi.session.ndi_session The session to display. """ require_qt() @@ -36,13 +36,13 @@ def _build_v2_window(session: Any) -> Any: from PySide6 import QtWidgets class _NDIV2Window(QtWidgets.QMainWindow): - """Enhanced GUI window with Lab + Data tabs.""" + """Enhanced GUI window with ndi_gui_Lab + ndi_gui_Data tabs.""" def __init__(self, session: Any) -> None: super().__init__() self._session = session - self.setWindowTitle("Neuroscience Data Interface") + self.setWindowTitle("Neuroscience ndi_gui_Data Interface") screen = QtWidgets.QApplication.primaryScreen() geom = screen.availableGeometry() self.resize(geom.width() // 2, geom.height() // 2) @@ -51,15 +51,15 @@ def __init__(self, session: Any) -> None: self._tabs = QtWidgets.QTabWidget() self.setCentralWidget(self._tabs) - # Experiment View (Lab) + # Experiment View (ndi_gui_Lab) self._lab_widget = QtWidgets.QWidget() self._init_lab_tab() self._tabs.addTab(self._lab_widget, "Experiment View") - # Database View (Data) + # ndi_database View (ndi_gui_Data) self._data_widget = QtWidgets.QWidget() self._init_data_tab() - self._tabs.addTab(self._data_widget, "Database View") + self._tabs.addTab(self._data_widget, "ndi_database View") # Load data from session self._load_session() @@ -67,11 +67,11 @@ def __init__(self, session: Any) -> None: # -- Tab initialisation ------------------------------------------- def _init_lab_tab(self) -> None: - from ndi.gui.lab import Lab + from ndi.gui.lab import ndi_gui_Lab layout = QtWidgets.QHBoxLayout(self._lab_widget) - self._lab = Lab(self._lab_widget) + self._lab = ndi_gui_Lab(self._lab_widget) left = QtWidgets.QVBoxLayout() left.addWidget(self._lab._edit_btn) @@ -86,11 +86,11 @@ def _init_lab_tab(self) -> None: layout.addWidget(self._lab.panel, stretch=1) def _init_data_tab(self) -> None: - from ndi.gui.data import Data + from ndi.gui.data import ndi_gui_Data layout = QtWidgets.QHBoxLayout(self._data_widget) - self._data = Data(self._data_widget) + self._data = ndi_gui_Data(self._data_widget) left = QtWidgets.QVBoxLayout() @@ -104,7 +104,7 @@ def _init_data_tab(self) -> None: layout.addLayout(left, stretch=3) layout.addWidget(self._data.panel, stretch=1) - # -- Session loading ---------------------------------------------- + # -- ndi_session loading ---------------------------------------------- def _load_session(self) -> None: s = self._session @@ -115,9 +115,9 @@ def _load_session(self) -> None: elements = [] try: - from ndi.query import Query + from ndi.query import ndi_query - docs = s.database_search(Query("base.id", "regex", "(.*)", "")) + docs = s.database_search(ndi_query("base.id", "regex", "(.*)", "")) except Exception: docs = [] if not isinstance(docs, list): @@ -151,15 +151,15 @@ def _load_session(self) -> None: subjects: list[Any] = [] for sid in subject_ids: try: - from ndi.query import Query + from ndi.query import ndi_query - result = s.database_search(Query("base.id", "exact_string", sid, "")) + result = s.database_search(ndi_query("base.id", "exact_string", sid, "")) if result: subjects.append(result) except Exception: pass - # Populate Lab view + # Populate ndi_gui_Lab view if subjects: self._lab.addSubject(subjects) if probes: @@ -167,7 +167,7 @@ def _load_session(self) -> None: if daqs: self._lab.addDAQ(daqs) - # Populate Data view + # Populate ndi_gui_Data view if docs: self._data.addDoc(docs) diff --git a/src/ndi/gui/icon.py b/src/ndi/gui/icon.py index 546b246..a0a1187 100644 --- a/src/ndi/gui/icon.py +++ b/src/ndi/gui/icon.py @@ -1,6 +1,6 @@ -"""Icon — Draggable visual icon for the Lab view. +"""ndi_gui_Icon — Draggable visual icon for the ndi_gui_Lab view. -Mirrors MATLAB: ndi.gui.Icon +Mirrors MATLAB: ndi.gui.ndi_gui_Icon Represents a subject, probe, or DAQ device as a coloured rectangle with an image and a connection terminal in the QGraphicsScene. @@ -18,13 +18,13 @@ pass -class Icon: - """Graphical icon shown in the :class:`Lab` view. +class ndi_gui_Icon: + """Graphical icon shown in the :class:`ndi_gui_Lab` view. Parameters ---------- - src : Lab - The parent Lab instance. + src : ndi_gui_Lab + The parent ndi_gui_Lab instance. length : int Sequence index (used for initial positioning). elem : Any @@ -46,7 +46,7 @@ class Icon: def __init__( self, - src: Any, # Lab + src: Any, # ndi_gui_Lab length: int, elem: Any, hShift: float, diff --git a/src/ndi/gui/lab.py b/src/ndi/gui/lab.py index 5e708a3..fd82f2b 100644 --- a/src/ndi/gui/lab.py +++ b/src/ndi/gui/lab.py @@ -1,6 +1,6 @@ -"""Lab — Experiment view with draggable icons and connection wires. +"""ndi_gui_Lab — Experiment view with draggable icons and connection wires. -Mirrors MATLAB: ndi.gui.Lab +Mirrors MATLAB: ndi.gui.ndi_gui_Lab Provides a graphical canvas (QGraphicsScene) where subjects, probes, and DAQ devices are displayed as draggable icons. Connections between @@ -14,17 +14,17 @@ import numpy as np from ndi.gui._qt_helpers import require_qt -from ndi.gui.icon import Icon +from ndi.gui.icon import ndi_gui_Icon try: from PySide6 import QtCore, QtGui, QtWidgets except ImportError: pass -_UNIT = Icon._UNIT +_UNIT = ndi_gui_Icon._UNIT -class Lab: +class ndi_gui_Lab: """Experiment / lab view widget. Manages a :class:`QGraphicsScene` containing subject, probe, and DAQ @@ -35,10 +35,10 @@ def __init__(self, parent: QtWidgets.QWidget | None = None) -> None: require_qt() self.editable: bool = False - self.subjects: list[Icon] = [] - self.probes: list[Icon] = [] - self.DAQs: list[Icon] = [] - self.drag: Icon | None = None + self.subjects: list[ndi_gui_Icon] = [] + self.probes: list[ndi_gui_Icon] = [] + self.DAQs: list[ndi_gui_Icon] = [] + self.drag: ndi_gui_Icon | None = None self.dragPt: tuple[float, float] | None = None self.moved: bool = False self.connects: np.ndarray = np.zeros((0, 0), dtype=int) @@ -88,21 +88,21 @@ def __init__(self, parent: QtWidgets.QWidget | None = None) -> None: def addSubject(self, subj: list[Any]) -> None: """Add subject icons (blue) to the view.""" for s in subj: - icon = Icon(self, len(self.subjects), s, 1, 1, 4, 3, (0.2, 0.4, 1.0)) + icon = ndi_gui_Icon(self, len(self.subjects), s, 1, 1, 4, 3, (0.2, 0.4, 1.0)) self.subjects.append(icon) self._rebuild_connects() def addProbe(self, prob: list[Any]) -> None: """Add probe icons (green) to the view.""" for p in prob: - icon = Icon(self, len(self.probes), p, 6, 6, 2, 3, (0.0, 0.6, 0.0)) + icon = ndi_gui_Icon(self, len(self.probes), p, 6, 6, 2, 3, (0.0, 0.6, 0.0)) self.probes.append(icon) self._rebuild_connects() def addDAQ(self, daq: list[Any]) -> None: """Add DAQ icons (orange) to the view.""" for d in daq: - icon = Icon(self, len(self.DAQs), d, 9, 12, 4, 2, (1.0, 0.6, 0.0)) + icon = ndi_gui_Icon(self, len(self.DAQs), d, 9, 12, 4, 2, (1.0, 0.6, 0.0)) self.DAQs.append(icon) self._rebuild_connects() @@ -121,12 +121,12 @@ def grid(self) -> None: for y in range(0, 17 * _UNIT, _UNIT): self.scene.addLine(0, y, 24 * _UNIT, y, pen).setZValue(-1) - def details(self, src: Icon) -> None: + def details(self, src: ndi_gui_Icon) -> None: """Show detail panel for the given icon.""" c = src.c elem = src.elem if c == (0.2, 0.4, 1.0): - # Subject + # ndi_subject if isinstance(elem, list) and len(elem) > 0: elem = elem[0] dp = getattr(elem, "document_properties", {}) @@ -136,12 +136,12 @@ def details(self, src: Icon) -> None: subj.get("local_identifier", "Not Found") if isinstance(subj, dict) else "Not Found" ) desc = subj.get("description", "") if isinstance(subj, dict) else "" - kind = "Subject" + kind = "ndi_subject" ident = name elif c == (0.0, 0.6, 0.0): - # Probe + # ndi_probe name = getattr(elem, "name", "") - kind = getattr(elem, "type", "Probe") + kind = getattr(elem, "type", "ndi_probe") ident = getattr(elem, "identifier", "") desc = "" else: @@ -188,7 +188,7 @@ def move(self, scene_x: float, scene_y: float) -> None: self.dragPt = (new_x, new_y) self.updateConnections() - def connect(self, src: Icon | None = None) -> None: + def connect(self, src: ndi_gui_Icon | None = None) -> None: """Create/complete a connection using a two-click pattern.""" if src is None: return diff --git a/src/ndi/gui/ndi_matlab_python_bridge.yaml b/src/ndi/gui/ndi_matlab_python_bridge.yaml index bdebda2..2a5740a 100644 --- a/src/ndi/gui/ndi_matlab_python_bridge.yaml +++ b/src/ndi/gui/ndi_matlab_python_bridge.yaml @@ -18,7 +18,7 @@ functions: input_arguments: - name: ndi_session_obj type_matlab: "ndi.session" - type_python: "ndi.session.Session" + type_python: "ndi.session.ndi_session" output_arguments: [] decision_log: > Ported from MATLAB. Opens a GUI window to display the contents of @@ -34,12 +34,12 @@ functions: input_arguments: - name: ndi_session_obj type_matlab: "ndi.session" - type_python: "ndi.session.Session" + type_python: "ndi.session.ndi_session" output_arguments: [] decision_log: > Ported from MATLAB. Opens an enhanced GUI with two views: - "Experiment View" (Lab) showing subjects, probes, and DAQs as - draggable icons with connections, and "Database View" (Data) showing + "Experiment View" (ndi_gui_Lab) showing subjects, probes, and DAQs as + draggable icons with connections, and "ndi_database View" (ndi_gui_Data) showing a searchable/filterable document table with dependency graph visualization. MATLAB uses axes-based rendering; Python uses PySide6 with QGraphicsScene for the lab view. @@ -50,11 +50,11 @@ functions: # ========================================================================= classes: - - name: Data + - name: ndi_gui_Data type: class - matlab_path: "+ndi/+gui/Data.m" + matlab_path: "+ndi/+gui/ndi_gui_Data.m" python_path: "ndi/gui/data.py" - python_class: "Data" + python_class: "ndi_gui_Data" inherits: "handle" properties: @@ -76,12 +76,12 @@ classes: decision_log: "Exact match. Filtered table rows." methods: - - name: Data + - name: ndi_gui_Data kind: constructor input_arguments: [] output_arguments: - name: obj - type_python: "Data" + type_python: "ndi_gui_Data" decision_log: > Ported. Creates a QWidget with a table, detail panel, and search controls. MATLAB uses uitable/uipanel; Python uses @@ -140,11 +140,11 @@ classes: Ported. Builds and displays a subgraph showing only direct dependencies of the selected document. - - name: Icon + - name: ndi_gui_Icon type: class - matlab_path: "+ndi/+gui/Icon.m" + matlab_path: "+ndi/+gui/ndi_gui_Icon.m" python_path: "ndi/gui/icon.py" - python_class: "Icon" + python_class: "ndi_gui_Icon" inherits: "handle" properties: @@ -182,12 +182,12 @@ classes: decision_log: "Exact match. Unique tag identifier." methods: - - name: Icon + - name: ndi_gui_Icon kind: constructor input_arguments: - name: src - type_matlab: "ndi.gui.Lab" - type_python: "Lab" + type_matlab: "ndi.gui.ndi_gui_Lab" + type_python: "ndi_gui_Lab" - name: length type_matlab: "numeric" type_python: "int" @@ -211,7 +211,7 @@ classes: type_python: "tuple[float, float, float]" output_arguments: - name: obj - type_python: "Icon" + type_python: "ndi_gui_Icon" decision_log: > Ported. Creates a visual icon with image, rectangle border, and connection terminal. MATLAB uses image/rectangle on axes; @@ -224,11 +224,11 @@ classes: Ported. Opens a file dialog to select an image for the icon. MATLAB uses uigetfile; Python uses QFileDialog.getOpenFileName. - - name: Lab + - name: ndi_gui_Lab type: class - matlab_path: "+ndi/+gui/Lab.m" + matlab_path: "+ndi/+gui/ndi_gui_Lab.m" python_path: "ndi/gui/lab.py" - python_class: "Lab" + python_class: "ndi_gui_Lab" inherits: "handle" properties: @@ -237,20 +237,20 @@ classes: type_python: "bool" decision_log: "Exact match." - name: subjects - type_matlab: "ndi.gui.Icon[]" - type_python: "list[Icon]" + type_matlab: "ndi.gui.ndi_gui_Icon[]" + type_python: "list[ndi_gui_Icon]" decision_log: "Exact match." - name: probes - type_matlab: "ndi.gui.Icon[]" - type_python: "list[Icon]" + type_matlab: "ndi.gui.ndi_gui_Icon[]" + type_python: "list[ndi_gui_Icon]" decision_log: "Exact match." - name: DAQs - type_matlab: "ndi.gui.Icon[]" - type_python: "list[Icon]" + type_matlab: "ndi.gui.ndi_gui_Icon[]" + type_python: "list[ndi_gui_Icon]" decision_log: "Exact match." - name: drag - type_matlab: "ndi.gui.Icon | []" - type_python: "Icon | None" + type_matlab: "ndi.gui.ndi_gui_Icon | []" + type_python: "ndi_gui_Icon | None" decision_log: "Exact match. Currently dragged icon or None." - name: moved type_matlab: "logical" @@ -266,12 +266,12 @@ classes: decision_log: "Exact match. Connection state toggle." methods: - - name: Lab + - name: ndi_gui_Lab kind: constructor input_arguments: [] output_arguments: - name: obj - type_python: "Lab" + type_python: "ndi_gui_Lab" decision_log: > Ported. Creates the experiment view with axes, panel, edit toggle, and zoom controls. @@ -313,8 +313,8 @@ classes: - name: details input_arguments: - name: src - type_matlab: "ndi.gui.Icon" - type_python: "Icon" + type_matlab: "ndi.gui.ndi_gui_Icon" + type_python: "ndi_gui_Icon" output_arguments: [] decision_log: "Exact match. Shows details panel for an icon." @@ -337,8 +337,8 @@ classes: - name: connect input_arguments: - name: src - type_matlab: "ndi.gui.Icon" - type_python: "Icon" + type_matlab: "ndi.gui.ndi_gui_Icon" + type_python: "ndi_gui_Icon" output_arguments: [] decision_log: > Exact match. Creates a connection between two icons @@ -359,11 +359,11 @@ classes: output_arguments: [] decision_log: "Exact match. Removes a connection wire." - - name: docViewer + - name: ndi_gui_docViewer type: class - matlab_path: "+ndi/+gui/docViewer.m" - python_path: "ndi/gui/docViewer.py" - python_class: "docViewer" + matlab_path: "+ndi/+gui/ndi_gui_docViewer.m" + python_path: "ndi/gui/ndi_gui_docViewer.py" + python_class: "ndi_gui_docViewer" inherits: "handle" properties: @@ -389,12 +389,12 @@ classes: decision_log: "Exact match. Original docs reference." methods: - - name: docViewer + - name: ndi_gui_docViewer kind: constructor input_arguments: [] output_arguments: - name: obj - type_python: "docViewer" + type_python: "ndi_gui_docViewer" decision_log: > Ported. Creates a standalone document viewer window with table, detail panel, and search/filter controls. @@ -479,7 +479,7 @@ classes: type_matlab: "numeric" type_python: "int" output_arguments: [] - decision_log: "Exact match. Same as Data.graph." + decision_log: "Exact match. Same as ndi_gui_Data.graph." - name: subgraph input_arguments: @@ -487,4 +487,4 @@ classes: type_matlab: "numeric" type_python: "int" output_arguments: [] - decision_log: "Exact match. Same as Data.subgraph." + decision_log: "Exact match. Same as ndi_gui_Data.subgraph." diff --git a/src/ndi/ido.py b/src/ndi/ido.py index ba6a5d0..7c76ef1 100644 --- a/src/ndi/ido.py +++ b/src/ndi/ido.py @@ -7,7 +7,7 @@ in the order of time of creation. Example: - i = Ido() + i = ndi_ido() id_value = i.id # view the id that was created """ @@ -16,7 +16,7 @@ import time -class Ido: +class ndi_ido: """NDI identifier object class. Creates and stores globally unique IDs based on current time and @@ -30,7 +30,7 @@ class Ido: id (str): The unique identifier string. Example: - >>> ido = Ido() + >>> ido = ndi_ido() >>> print(ido.id) # prints something like '1a2b3c4d5e6f_7a8b9c0d1e2f' """ @@ -100,13 +100,13 @@ def is_valid(id_value: str) -> bool: return False def __repr__(self) -> str: - return f"Ido({self.id})" + return f"ndi_ido({self.id})" def __str__(self) -> str: return self.id def __eq__(self, other) -> bool: - if isinstance(other, Ido): + if isinstance(other, ndi_ido): return self.id == other.id if isinstance(other, str): return self.id == other diff --git a/src/ndi/mock/__init__.py b/src/ndi/mock/__init__.py index d0638c4..10b933b 100644 --- a/src/ndi/mock/__init__.py +++ b/src/ndi/mock/__init__.py @@ -33,22 +33,22 @@ def subject_stimulator_neuron( Dict with ``'subject'``, ``'stimulator'``, ``'spikes'`` keys containing document/element objects. """ - from ndi.document import Document + from ndi.document import ndi_document ref_num = 20000 + random.randint(0, 999) subject_name = f"mock{ref_num}@nosuchlab.org" # Create subject document - subject_doc = Document("subject") + subject_doc = ndi_document("subject") subject_doc._set_nested_property("subject.local_identifier", subject_name) # Create stimulator element document - stim_doc = Document("element") + stim_doc = ndi_document("element") stim_doc._set_nested_property("element.name", f"mock_stimulator_{ref_num}") stim_doc._set_nested_property("element.type", "stimulator") # Create spiking neuron element document - spikes_doc = Document("element") + spikes_doc = ndi_document("element") spikes_doc._set_nested_property("element.name", f"mock_spikes_{ref_num}") spikes_doc._set_nested_property("element.type", "spikes") @@ -84,7 +84,7 @@ def stimulus_presentation( reps: Number of repetitions of each stimulus. stim_duration: Duration of each stimulus in seconds. interstimulus_interval: Gap between stimuli in seconds. - epoch_id: Epoch identifier. + epoch_id: ndi_epoch_epoch identifier. Returns: Dict with ``'presentations'`` (list of dicts with timing info), @@ -171,7 +171,7 @@ def stimulus_response( reps: Number of repetitions per stimulus. stim_duration: Duration of each stimulus in seconds. interstimulus_interval: Gap between stimuli in seconds. - epochid: Epoch identifier. + epochid: ndi_epoch_epoch identifier. Returns: Dict with keys: ``'subject'``, ``'stimulator'``, ``'spikes'``, @@ -223,16 +223,18 @@ def clear_mock_docs(session: Any) -> None: Args: session: An NDI session instance. """ - from ndi.query import Query + from ndi.query import ndi_query try: - docs = session.database_search(Query("subject.local_identifier", "contains_string", "mock")) + docs = session.database_search( + ndi_query("subject.local_identifier", "contains_string", "mock") + ) if docs: session.database_rm(docs) except Exception: # Fallback: search and remove individually try: - docs = session.database_search(Query("").isa("subject")) + docs = session.database_search(ndi_query("").isa("subject")) for doc in docs: props = doc.document_properties if hasattr(doc, "document_properties") else doc if isinstance(props, dict): @@ -246,7 +248,7 @@ def clear_mock_docs(session: Any) -> None: pass -class CalculatorTest: +class ndi_mock_ctest: """Base class for calculator testing framework. MATLAB equivalent: ndi.mock.ctest @@ -355,7 +357,7 @@ def test( try: actual = self.calculator.run(mock_data.get("input_docs", [])) except Exception as e: - results.append((False, f"Calculator error: {e}")) + results.append((False, f"ndi_calculator error: {e}")) continue else: results.append((False, "No calculator configured")) @@ -373,5 +375,5 @@ def test( "stimulus_presentation", "stimulus_response", "clear_mock_docs", - "CalculatorTest", + "ndi_mock_ctest", ] diff --git a/src/ndi/mock/ndi_matlab_python_bridge.yaml b/src/ndi/mock/ndi_matlab_python_bridge.yaml index ccb1fb1..b55c35a 100644 --- a/src/ndi/mock/ndi_matlab_python_bridge.yaml +++ b/src/ndi/mock/ndi_matlab_python_bridge.yaml @@ -169,7 +169,7 @@ classes: type: class matlab_path: "src/ndi/+ndi/+mock/ctest.m" python_path: "ndi/mock/__init__.py" - python_class: "CalculatorTest" + python_class: "ndi_mock_ctest" methods: - name: ctest @@ -181,7 +181,7 @@ classes: default: "None" output_arguments: - name: obj - type_python: "CalculatorTest" + type_python: "ndi_mock_ctest" decision_log: > MATLAB constructor takes no args. Python accepts optional calculator parameter for convenience. @@ -261,7 +261,7 @@ classes: output_arguments: [] decision_log: > DISCREPANCY NOTED: Method exists in MATLAB but is not - implemented in Python CalculatorTest. Not critical — + implemented in Python ndi_mock_ctest. Not critical — Python tests use pytest fixtures for cleanup. - name: calc_path diff --git a/src/ndi/ndi_matlab_python_bridge.yaml b/src/ndi/ndi_matlab_python_bridge.yaml index b4c23ae..b03f0e8 100644 --- a/src/ndi/ndi_matlab_python_bridge.yaml +++ b/src/ndi/ndi_matlab_python_bridge.yaml @@ -46,7 +46,7 @@ classes: type: class matlab_path: "+ndi/ido.m" python_path: "ndi/ido.py" - python_class: "Ido" + python_class: "ndi_ido" inherits: "did.ido" properties: @@ -68,7 +68,7 @@ classes: default: "None" output_arguments: - name: obj - type_python: "Ido" + type_python: "ndi_ido" decision_log: "Exact match. Generates new ID if none provided." - name: unique_id @@ -104,17 +104,17 @@ classes: decision_log: "Mapped to Python __eq__. Compares by ID string." decision_log: > - Thin wrapper over did.ido. Python Ido class provides the same + Thin wrapper over did.ido. Python ndi_ido class provides the same unique ID generation. Synchronized 2026-03-13. # ========================================================================= - # ndi.documentservice - Document Interface Mixin + # ndi.documentservice - ndi_document Interface Mixin # ========================================================================= - name: documentservice type: class matlab_path: "+ndi/documentservice.m" python_path: "ndi/documentservice.py" - python_class: "DocumentService" + python_class: "ndi_documentservice" inherits: "" properties: [] @@ -125,7 +125,7 @@ classes: input_arguments: [] output_arguments: - name: obj - type_python: "DocumentService" + type_python: "ndi_documentservice" decision_log: "Exact match." - name: newdocument @@ -153,7 +153,7 @@ classes: type_python: "Any" output_arguments: - name: doc - type_python: "Document | None" + type_python: "ndi_document | None" decision_log: > Exact match. Loads this object's document from the database via searchquery(). Returns None if not found. @@ -177,13 +177,13 @@ classes: Synchronized 2026-03-13. # ========================================================================= - # ndi.document - Document Model + # ndi.document - ndi_document Model # ========================================================================= - name: document type: class matlab_path: "+ndi/document.m" python_path: "ndi/document.py" - python_class: "Document" + python_class: "ndi_document" inherits: "" properties: @@ -206,7 +206,7 @@ classes: type_python: "**kwargs" output_arguments: - name: ndi_document_obj - type_python: "Document" + type_python: "ndi_document" decision_log: "Exact match." # --- Identity --- @@ -231,7 +231,7 @@ classes: type_python: "str" decision_log: "Exact match. Alias for id()." - # --- Session --- + # --- ndi_session --- - name: set_session_id input_arguments: - name: session_id @@ -239,7 +239,7 @@ classes: type_python: "str" output_arguments: - name: ndi_document_obj - type_python: "Document" + type_python: "ndi_document" decision_log: "Exact match. Returns self for chaining." # --- Dependency Management --- @@ -257,7 +257,7 @@ classes: default: "True" output_arguments: - name: ndi_document_obj - type_python: "Document" + type_python: "ndi_document" decision_log: "Exact match. Returns self for chaining." - name: dependency @@ -314,7 +314,7 @@ classes: type_python: "str" output_arguments: - name: ndi_document_obj - type_python: "Document" + type_python: "ndi_document" decision_log: > Exact match. Appends to numbered dependency list. Returns self for chaining. Synchronized 2026-03-13. @@ -330,7 +330,7 @@ classes: default: "None" output_arguments: - name: ndi_document_obj - type_python: "Document" + type_python: "ndi_document" decision_log: > Exact match. Removes numbered dependency by index, or all if index is None. Returns self for chaining. @@ -368,7 +368,7 @@ classes: default: "None" output_arguments: - name: ndi_document_obj - type_python: "Document" + type_python: "ndi_document" decision_log: > Exact match. Adds a file to the document's file_info. Auto-detects location_type from URL scheme. @@ -385,7 +385,7 @@ classes: default: "None" output_arguments: - name: ndi_document_obj - type_python: "Document" + type_python: "ndi_document" decision_log: > Exact match. Removes a file or specific location from the document. Returns self for chaining. @@ -400,7 +400,7 @@ classes: Exact match. Returns list of file names currently associated with this document. Synchronized 2026-03-13. - # --- Document Class Information --- + # --- ndi_document Class Information --- - name: doc_class input_arguments: [] output_arguments: @@ -453,7 +453,7 @@ classes: type_python: "**kwargs" output_arguments: - name: ndi_document_obj - type_python: "Document" + type_python: "ndi_document" decision_log: > Exact match. Sets multiple properties at once using dot-notation paths. Returns self for chaining. @@ -511,10 +511,10 @@ classes: input_arguments: - name: other type_matlab: "ndi.document" - type_python: "Document" + type_python: "ndi_document" output_arguments: - name: merged_doc - type_python: "Document" + type_python: "ndi_document" decision_log: > Exact match. Merges two documents. Fields in self take precedence. Mapped to Python __add__. Merges superclasses, @@ -536,13 +536,13 @@ classes: input_arguments: - name: doc_array type_matlab: "cell array of ndi.document" - type_python: "list[Document]" + type_python: "list[ndi_document]" - name: doc_id type_matlab: "char" type_python: "str" output_arguments: - name: doc - type_python: "Document | None" + type_python: "ndi_document | None" - name: index type_python: "int | None" decision_log: > @@ -555,10 +555,10 @@ classes: input_arguments: - name: doc_array type_matlab: "cell array of ndi.document" - type_python: "list[Document]" + type_python: "list[ndi_document]" output_arguments: - name: newest_doc - type_python: "Document" + type_python: "ndi_document" - name: index type_python: "int" - name: datestamp @@ -587,13 +587,13 @@ classes: document definitions. Synchronized 2026-03-13. # ========================================================================= - # ndi.query - Database Query + # ndi.query - ndi_database ndi_query # ========================================================================= - name: query type: class matlab_path: "+ndi/query.m" python_path: "ndi/query.py" - python_class: "Query" + python_class: "ndi_query" inherits: "did.query" properties: @@ -617,7 +617,7 @@ classes: Computed property. Returns the query value (param1). Synchronized 2026-03-13. - name: queries - type_python: "list[Query]" + type_python: "list[ndi_query]" decision_log: > List of sub-queries for composite (AND/OR) queries. Synchronized 2026-03-13. @@ -644,7 +644,7 @@ classes: default: "None" output_arguments: - name: q - type_python: "Query" + type_python: "ndi_query" decision_log: "Exact match." # --- NDI-specific query types --- @@ -655,7 +655,7 @@ classes: type_python: "str" output_arguments: - name: q - type_python: "Query" + type_python: "ndi_query" decision_log: "Exact match. Shorthand for isa query." - name: depends_on @@ -669,7 +669,7 @@ classes: default: "''" output_arguments: - name: q - type_python: "Query" + type_python: "ndi_query" decision_log: "Exact match. Shorthand for depends_on query." # --- Comparison operators (MATLAB overloads -> Python __eq__ etc.) --- @@ -679,7 +679,7 @@ classes: type_python: "Any" output_arguments: - name: q - type_python: "Query" + type_python: "ndi_query" decision_log: > MATLAB == operator overload. Mapped to Python __eq__. Resolves to exact_string or exact_number. @@ -691,7 +691,7 @@ classes: type_python: "Any" output_arguments: - name: q - type_python: "Query" + type_python: "ndi_query" decision_log: > MATLAB ~= operator overload. Mapped to Python __ne__. Synchronized 2026-03-13. @@ -702,7 +702,7 @@ classes: type_python: "Any" output_arguments: - name: q - type_python: "Query" + type_python: "ndi_query" decision_log: "MATLAB < overload. Mapped to Python __lt__. Synchronized 2026-03-13." - name: le @@ -711,7 +711,7 @@ classes: type_python: "Any" output_arguments: - name: q - type_python: "Query" + type_python: "ndi_query" decision_log: "MATLAB <= overload. Mapped to Python __le__. Synchronized 2026-03-13." - name: gt @@ -720,7 +720,7 @@ classes: type_python: "Any" output_arguments: - name: q - type_python: "Query" + type_python: "ndi_query" decision_log: "MATLAB > overload. Mapped to Python __gt__. Synchronized 2026-03-13." - name: ge @@ -729,7 +729,7 @@ classes: type_python: "Any" output_arguments: - name: q - type_python: "Query" + type_python: "ndi_query" decision_log: "MATLAB >= overload. Mapped to Python __ge__. Synchronized 2026-03-13." # --- Logical operators --- @@ -737,10 +737,10 @@ classes: input_arguments: - name: other type_matlab: "ndi.query" - type_python: "Query" + type_python: "ndi_query" output_arguments: - name: q - type_python: "Query" + type_python: "ndi_query" decision_log: > MATLAB & overload. Mapped to Python __and__. Concatenates search_structures for AND semantics. Synchronized 2026-03-13. @@ -749,10 +749,10 @@ classes: input_arguments: - name: other type_matlab: "ndi.query" - type_python: "Query" + type_python: "ndi_query" output_arguments: - name: q - type_python: "Query" + type_python: "ndi_query" decision_log: > MATLAB | overload. Mapped to Python __or__. Creates nested 'or' operation in search_structure. Synchronized 2026-03-13. @@ -761,7 +761,7 @@ classes: input_arguments: [] output_arguments: - name: q - type_python: "Query" + type_python: "ndi_query" decision_log: > MATLAB ~ prefix. Mapped to Python __invert__. Negates all operations by toggling ~ prefix. Synchronized 2026-03-13. @@ -774,7 +774,7 @@ classes: type_python: "str" output_arguments: - name: q - type_python: "Query" + type_python: "ndi_query" decision_log: > Exact match. Resolves to contains_string operation. Synchronized 2026-03-13. @@ -786,7 +786,7 @@ classes: type_python: "str" output_arguments: - name: q - type_python: "Query" + type_python: "ndi_query" decision_log: > Exact match. Resolves to regexp operation. Synchronized 2026-03-13. @@ -797,7 +797,7 @@ classes: type_python: "Any" output_arguments: - name: q - type_python: "Query" + type_python: "ndi_query" decision_log: > Explicit equality method. Same as == operator. Synchronized 2026-03-13. @@ -809,7 +809,7 @@ classes: type_python: "Any" output_arguments: - name: q - type_python: "Query" + type_python: "ndi_query" decision_log: "Explicit less-than method. Synchronized 2026-03-13." - name: less_than_or_equal_to @@ -818,7 +818,7 @@ classes: type_python: "Any" output_arguments: - name: q - type_python: "Query" + type_python: "ndi_query" decision_log: "Explicit less-than-or-equal method. Synchronized 2026-03-13." - name: greater_than @@ -827,7 +827,7 @@ classes: type_python: "Any" output_arguments: - name: q - type_python: "Query" + type_python: "ndi_query" decision_log: "Explicit greater-than method. Synchronized 2026-03-13." - name: greater_than_or_equal_to @@ -836,7 +836,7 @@ classes: type_python: "Any" output_arguments: - name: q - type_python: "Query" + type_python: "ndi_query" decision_log: "Explicit greater-than-or-equal method. Synchronized 2026-03-13." # --- Field existence --- @@ -844,7 +844,7 @@ classes: input_arguments: [] output_arguments: - name: q - type_python: "Query" + type_python: "ndi_query" decision_log: > Exact match. Resolves to hasfield operation. Synchronized 2026-03-13. @@ -855,7 +855,7 @@ classes: type_python: "Any" output_arguments: - name: q - type_python: "Query" + type_python: "ndi_query" decision_log: > Exact match. Resolves to hasmember operation. Synchronized 2026-03-13. @@ -866,7 +866,7 @@ classes: input_arguments: [] output_arguments: - name: q - type_python: "Query" + type_python: "ndi_query" decision_log: > Exact match. Returns query matching all documents (isa 'base'). Synchronized 2026-03-13. @@ -876,7 +876,7 @@ classes: input_arguments: [] output_arguments: - name: q - type_python: "Query" + type_python: "ndi_query" decision_log: > Exact match. Returns query matching no documents. Synchronized 2026-03-13. @@ -900,7 +900,7 @@ classes: default: "''" output_arguments: - name: q - type_python: "Query" + type_python: "ndi_query" decision_log: > Exact match. MATLAB-compatible factory method. Creates query using DID-style operation strings. @@ -926,7 +926,7 @@ classes: Unwraps single-element lists. Synchronized 2026-03-13. decision_log: > - Database query class supporting operators: regexp, + ndi_database query class supporting operators: regexp, exact_string, contains_string, exact_number, lessthan, lessthaneq, greaterthan, greaterthaneq, hasfield, isa, depends_on, hasmember, and negated versions. @@ -960,7 +960,7 @@ not_applicable: status: not_applicable decision_log: > MATLAB-specific path resolution. Python uses - ndi.common.PathConstants. + ndi.common.ndi_common_PathConstants. - name: cpipeline matlab_path: "+ndi/cpipeline.m" diff --git a/src/ndi/ndi_matlab_python_bridge_database.yaml b/src/ndi/ndi_matlab_python_bridge_database.yaml index 4564017..eac6214 100644 --- a/src/ndi/ndi_matlab_python_bridge_database.yaml +++ b/src/ndi/ndi_matlab_python_bridge_database.yaml @@ -19,7 +19,7 @@ classes: matlab_path: "+ndi/database.m" matlab_last_sync_hash: "ad81fd7a" python_path: "ndi/database.py" - python_class: "Database" + python_class: "ndi_database" properties: - name: path @@ -53,10 +53,10 @@ classes: default: "''" output_arguments: - name: ndi_database_obj - type_python: "Database" + type_python: "ndi_database" decision_log: > MATLAB: database(PATH, REFERENCE). - Python: Database(session_path, db_name='.ndi', **backend_kwargs). + Python: ndi_database(session_path, db_name='.ndi', **backend_kwargs). Python adds db_name parameter for directory naming flexibility. - name: newdocument @@ -67,23 +67,23 @@ classes: default: "'base'" output_arguments: - name: ndi_document_obj - type_python: "Document" + type_python: "ndi_document" decision_log: > Present in MATLAB database.m. In Python this is handled by - the session object (session.newdocument), not the Database class directly. + the session object (session.newdocument), not the ndi_database class directly. - name: add input_arguments: - name: ndi_document_obj type_matlab: "ndi.document" - type_python: "Document" + type_python: "ndi_document" - name: Update type_matlab: "logical (name-value)" type_python: "bool" default: "True" output_arguments: - name: ndi_database_obj - type_python: "Database" + type_python: "ndi_database" decision_log: > MATLAB add() supports an 'Update' name-value pair (default 1) that controls whether an existing document is updated or errors. @@ -97,7 +97,7 @@ classes: type_python: "str" output_arguments: - name: ndi_document_obj - type_python: "Document | None" + type_python: "ndi_document | None" decision_log: > MATLAB returns empty [] when not found; Python returns None. Synchronized 2026-03-13. @@ -106,7 +106,7 @@ classes: input_arguments: - name: ndi_document_id type_matlab: "ndi.document | char | cell" - type_python: "Document | str" + type_python: "ndi_document | str" output_arguments: - name: ndi_database_obj type_python: "bool" @@ -119,10 +119,10 @@ classes: input_arguments: - name: searchparams type_matlab: "ndi.query" - type_python: "Query | None" + type_python: "ndi_query | None" output_arguments: - name: ndi_document_objs - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: > MATLAB returns cell array of ndi.document; Python returns list. Synchronized 2026-03-13. @@ -144,14 +144,14 @@ classes: default: "'no'" output_arguments: [] decision_log: > - Present in MATLAB. Not yet implemented in Python Database class. + Present in MATLAB. Not yet implemented in Python ndi_database class. Should be added for parity. Synchronized 2026-03-13. - name: openbinarydoc input_arguments: - name: ndi_document_or_id type_matlab: "ndi.document | char" - type_python: "Document | str" + type_python: "ndi_document | str" - name: filename type_matlab: "char" type_python: "str" @@ -161,13 +161,13 @@ classes: decision_log: > Present in MATLAB database.m. In Python this is handled by the session object (session.database_openbinarydoc), not - the Database class directly. Synchronized 2026-03-13. + the ndi_database class directly. Synchronized 2026-03-13. - name: existbinarydoc input_arguments: - name: ndi_document_or_id type_matlab: "ndi.document | char" - type_python: "Document | str" + type_python: "ndi_document | str" - name: filename type_matlab: "char" type_python: "str" @@ -198,10 +198,10 @@ classes: - name: update input_arguments: - name: ndi_document_obj - type_python: "Document" + type_python: "ndi_document" output_arguments: - name: ndi_document_obj - type_python: "Document" + type_python: "ndi_document" decision_log: > From didsqlite subclass. Updates an existing document. Raises ValueError if not found. Synchronized 2026-03-13. @@ -209,10 +209,10 @@ classes: - name: add_or_replace input_arguments: - name: ndi_document_obj - type_python: "Document" + type_python: "ndi_document" output_arguments: - name: ndi_document_obj - type_python: "Document" + type_python: "ndi_document" decision_log: > Python convenience. Upsert: adds if new, replaces if exists. Corresponds to MATLAB add() with Update=1. @@ -224,7 +224,7 @@ classes: type_python: "str" output_arguments: - name: ndi_document_obj - type_python: "Document | None" + type_python: "ndi_document | None" decision_log: > Alias for read(). Convenience for MATLAB compatibility. Synchronized 2026-03-13. @@ -241,10 +241,10 @@ classes: - name: find_depends_on input_arguments: - name: document - type_python: "Document | str" + type_python: "ndi_document | str" output_arguments: - name: dependent_docs - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: > Python convenience. Finds all documents that depend on the given document. Synchronized 2026-03-13. @@ -252,10 +252,10 @@ classes: - name: find_dependencies input_arguments: - name: document - type_python: "Document | str" + type_python: "ndi_document | str" output_arguments: - name: dependency_docs - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: > Python convenience. Finds all documents that the given document depends on. Synchronized 2026-03-13. @@ -263,10 +263,10 @@ classes: - name: add_many input_arguments: - name: documents - type_python: "list[Document]" + type_python: "list[ndi_document]" output_arguments: - name: added_docs - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: > Python convenience. Batch add. Stops on first error. Synchronized 2026-03-13. @@ -274,10 +274,10 @@ classes: - name: remove_many input_arguments: - name: query - type_python: "Query | None" + type_python: "ndi_query | None" default: "None" - name: documents - type_python: "list[Document] | None" + type_python: "list[ndi_document] | None" default: "None" output_arguments: - name: count @@ -290,7 +290,7 @@ classes: - name: get_binary_path input_arguments: - name: document - type_python: "Document" + type_python: "ndi_document" - name: file_name type_python: "str" output_arguments: @@ -301,7 +301,7 @@ classes: document's binary file. Synchronized 2026-03-13. decision_log: > - Python Database class merges the abstract ndi.database base class + Python ndi_database class merges the abstract ndi.database base class with the concrete ndi.database.implementations.database.didsqlite subclass. Synchronized 2026-03-13. @@ -324,10 +324,10 @@ functions: type_python: "**kwargs" output_arguments: - name: db - type_python: "Database" + type_python: "ndi_database" decision_log: > MATLAB: opendatabase(database_path, session_unique_reference). Python: open_database(session_path, **kwargs). The Python version - is a simpler factory that creates a Database instance directly, + is a simpler factory that creates a ndi_database instance directly, while MATLAB searches a hierarchy of database implementations. Synchronized 2026-03-13. diff --git a/src/ndi/ndi_matlab_python_bridge_database_fun.yaml b/src/ndi/ndi_matlab_python_bridge_database_fun.yaml index 498b0b5..ce718ac 100644 --- a/src/ndi/ndi_matlab_python_bridge_database_fun.yaml +++ b/src/ndi/ndi_matlab_python_bridge_database_fun.yaml @@ -34,7 +34,7 @@ functions: output_arguments: - name: d type_matlab: "cell array of ndi.document" - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: "Synchronized with MATLAB main as of 2026-03-13." - name: findalldependencies @@ -55,7 +55,7 @@ functions: output_arguments: - name: d type_matlab: "cell array of ndi.document" - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: "Synchronized with MATLAB main as of 2026-03-13." - name: docs_from_ids @@ -73,7 +73,7 @@ functions: output_arguments: - name: docs type_matlab: "cell array (same size as document_ids)" - type_python: "list[Document | None]" + type_python: "list[ndi_document | None]" decision_log: "Exact match. Synchronized with MATLAB main as of 2026-03-13." - name: docs2graph @@ -84,7 +84,7 @@ functions: input_arguments: - name: ndi_document_obj type_matlab: "cell array of ndi.document" - type_python: "list[Document]" + type_python: "list[ndi_document]" output_arguments: - name: G type_matlab: "sparse adjacency matrix" @@ -119,7 +119,7 @@ functions: output_arguments: - name: d type_matlab: "cell array of ndi.document" - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: "Exact match. Synchronized with MATLAB main as of 2026-03-13." - name: finddocs_elementEpochType @@ -144,7 +144,7 @@ functions: output_arguments: - name: docs type_matlab: "cell array of ndi.document" - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: > RENAMED from finddocs_element_epoch_type to finddocs_elementEpochType to match MATLAB. MATLAB queries 'epochid.epochid' field; Python was @@ -158,7 +158,7 @@ functions: input_arguments: - name: ndi_document_obj type_matlab: "ndi.document | char" - type_python: "Document | str" + type_python: "ndi_document | str" - name: ndi_session_obj type_matlab: "ndi.session" type_python: "Any" @@ -211,7 +211,7 @@ functions: output_arguments: - name: d type_matlab: "cell array of ndi.document" - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: > MATLAB searches for docs with 'depends_on' hasfield first, then validates each dependency against a cache. Python gets @@ -337,7 +337,7 @@ functions: output_arguments: - name: docs type_matlab: "cell array of ndi.document" - type_python: "list[Document]" + type_python: "list[ndi_document]" - name: target_path type_matlab: "char" type_python: "str" @@ -393,7 +393,7 @@ not_applicable: decision_log: > MATLAB factory for instantiating a new database based on the database hierarchy preferences. In Python, this functionality is - absorbed into Database.__init__() which directly creates the + absorbed into ndi_database.__init__() which directly creates the SQLite backend. No separate factory needed. - name: databasehierarchyinit @@ -412,7 +412,7 @@ not_applicable: decision_log: > MATLAB utility for reading/writing dataset metadata structures. In Python, dataset metadata is handled natively via dict/JSON - serialization in the Document class. No separate utility needed. + serialization in the ndi_document class. No separate utility needed. - name: openMINDSobj2ndi_document matlab_path: "+ndi/+database/+fun/openMINDSobj2ndi_document.m" diff --git a/src/ndi/neuron.py b/src/ndi/neuron.py index 1b0a230..b7473fe 100644 --- a/src/ndi/neuron.py +++ b/src/ndi/neuron.py @@ -1,7 +1,7 @@ """ -ndi.neuron - Neuron element class. +ndi.neuron - ndi_neuron element class. -A Neuron is an ElementTimeseries with type='neuron'. It represents +A ndi_neuron is an ndi_element_timeseries with type='neuron'. It represents a single neuron identified through spike sorting or other means. Neurons are typically derived from probe-type elements and produce @@ -12,18 +12,18 @@ from typing import TYPE_CHECKING, Any -from .element_timeseries import ElementTimeseries +from .element_timeseries import ndi_element_timeseries if TYPE_CHECKING: - from .document import Document - from .query import Query + from .document import ndi_document + from .query import ndi_query -class Neuron(ElementTimeseries): +class ndi_neuron(ndi_element_timeseries): """ Represents a single neuron. - Neurons are ElementTimeseries objects with type='neuron'. They + Neurons are ndi_element_timeseries objects with type='neuron'. They are typically derived from electrode/probe data through spike sorting. @@ -32,7 +32,7 @@ class Neuron(ElementTimeseries): - subject_id: The experimental subject Example: - >>> neuron = Neuron( + >>> neuron = ndi_neuron( ... session=session, ... name='neuron1', ... reference=1, @@ -54,15 +54,15 @@ def __init__( document: Any | None = None, ): """ - Create a new Neuron. + Create a new ndi_neuron. Args: - session: Session with database access - name: Neuron name + session: ndi_session with database access + name: ndi_neuron name reference: Reference number - underlying_element: Probe/electrode this neuron was sorted from + underlying_element: ndi_probe/electrode this neuron was sorted from direct: If True, use underlying element epochs - subject_id: Subject document ID + subject_id: ndi_subject document ID dependencies: Additional named dependencies identifier: Optional unique identifier document: Optional document to load from @@ -80,26 +80,26 @@ def __init__( document=document, ) - def newdocument(self) -> Document: + def newdocument(self) -> ndi_document: """ Create a new document for this neuron. Returns: - Document representing this neuron element + ndi_document representing this neuron element """ # Use parent's newdocument - type is already 'neuron' return super().newdocument() - def searchquery(self) -> Query: + def searchquery(self) -> ndi_query: """ Create a query to find this neuron. Returns: - Query matching this neuron by ID and type + ndi_query matching this neuron by ID and type """ - from .query import Query + from .query import ndi_query - return Query("base.id") == self.id + return ndi_query("base.id") == self.id def epochsetname(self) -> str: """Return the name of this epoch set.""" @@ -111,4 +111,4 @@ def issyncgraphroot(self) -> bool: def __repr__(self) -> str: """String representation.""" - return f"Neuron({self._name}|{self._reference})" + return f"ndi_neuron({self._name}|{self._reference})" diff --git a/src/ndi/ontology/__init__.py b/src/ndi/ontology/__init__.py index 7c64ae8..389aa54 100644 --- a/src/ndi/ontology/__init__.py +++ b/src/ndi/ontology/__init__.py @@ -91,9 +91,9 @@ def to_dict(self) -> dict[str, Any]: def _load_prefix_map() -> dict[str, str]: """Load prefix mappings from ontology_list.json if available.""" try: - from ndi.common import PathConstants + from ndi.common import ndi_common_PathConstants - json_path = PathConstants.COMMON_FOLDER / "ontology" / "ontology_list.json" + json_path = ndi_common_PathConstants.COMMON_FOLDER / "ontology" / "ontology_list.json" if json_path.exists(): with open(json_path) as f: data = json.load(f) @@ -166,7 +166,7 @@ def lookup(lookup_string: str) -> OntologyResult: except Exception: result = OntologyResult() - # Cache (with eviction) + # ndi_cache (with eviction) if len(_lookup_cache) >= _CACHE_MAX: # Remove oldest entry oldest = next(iter(_lookup_cache)) diff --git a/src/ndi/ontology/ndi_matlab_python_bridge.yaml b/src/ndi/ontology/ndi_matlab_python_bridge.yaml index d963c3e..ef72be9 100644 --- a/src/ndi/ontology/ndi_matlab_python_bridge.yaml +++ b/src/ndi/ontology/ndi_matlab_python_bridge.yaml @@ -26,24 +26,24 @@ classes: # MATLAB has hidden Constant properties for file paths - name: ONTOLOGY_FILENAME type_matlab: "Constant Hidden char" - type_python: "N/A (handled by PathConstants)" + type_python: "N/A (handled by ndi_common_PathConstants)" decision_log: > MATLAB stores ontology file path as class constant. - Python resolves paths via ndi.common.PathConstants. + Python resolves paths via ndi.common.ndi_common_PathConstants. - name: ONTOLOGY_SUBFOLDER_JSON type_matlab: "Constant Hidden char" - type_python: "N/A (handled by PathConstants)" + type_python: "N/A (handled by ndi_common_PathConstants)" decision_log: "Same as ONTOLOGY_FILENAME." - name: ONTOLOGY_SUBFOLDER_NDIC type_matlab: "Constant Hidden char" - type_python: "N/A (handled by PathConstants)" + type_python: "N/A (handled by ndi_common_PathConstants)" decision_log: "Same as ONTOLOGY_FILENAME." - name: NDIC_FILENAME type_matlab: "Constant Hidden char" - type_python: "N/A (handled by PathConstants)" + type_python: "N/A (handled by ndi_common_PathConstants)" decision_log: "Same as ONTOLOGY_FILENAME." methods: diff --git a/src/ndi/ontology/providers.py b/src/ndi/ontology/providers.py index 32772ac..c0fc5d4 100644 --- a/src/ndi/ontology/providers.py +++ b/src/ndi/ontology/providers.py @@ -188,9 +188,9 @@ def _load_data(self) -> list[dict[str, str]]: return NDICProvider._data try: - from ndi.common import PathConstants + from ndi.common import ndi_common_PathConstants - path = PathConstants.COMMON_FOLDER / "controlled_vocabulary" / "NDIC.txt" + path = ndi_common_PathConstants.COMMON_FOLDER / "controlled_vocabulary" / "NDIC.txt" if not path.exists(): NDICProvider._data = [] return [] @@ -348,7 +348,7 @@ def _search_name(self, name: str) -> Any: class WBStrainProvider(OntologyProvider): - """WormBase Strain Database.""" + """WormBase Strain ndi_database.""" name = "WBStrain" @@ -498,7 +498,7 @@ class EMPTYProvider(OntologyProvider): MATLAB equivalent: +ndi/+ontology/EMPTY.m - Fetches the EMPTY ontology as JSON from the Waltham-Data-Science + Fetches the EMPTY ontology as JSON from the Waltham-ndi_gui_Data-Science GitHub repository and caches it in memory. Supports lookup by numeric ID (e.g. ``"0000074"``), full OBO-style ID (e.g. ``"EMPTY_0000074"``), or label substring @@ -508,7 +508,7 @@ class EMPTYProvider(OntologyProvider): name = "EMPTY" _JSON_URL = ( - "https://raw.githubusercontent.com/Waltham-Data-Science/empty-ontology" + "https://raw.githubusercontent.com/Waltham-ndi_gui_Data-Science/empty-ontology" "/main/empty-base.json" ) diff --git a/src/ndi/openminds_convert.py b/src/ndi/openminds_convert.py index 85ef6e4..20edd8e 100644 --- a/src/ndi/openminds_convert.py +++ b/src/ndi/openminds_convert.py @@ -39,7 +39,7 @@ def openminds_obj_to_dict( Returns: List of flat dicts, one per object in the graph. """ - from .ido import Ido + from .ido import ndi_ido if visited is None: visited = {} @@ -58,7 +58,7 @@ def openminds_obj_to_dict( openminds_type = _get_openminds_type(item) python_type = type(item).__module__ + "." + type(item).__qualname__ openminds_id = _get_openminds_id(item) - ndi_id = Ido().id + ndi_id = ndi_ido().id entry = { "openminds_type": openminds_type, @@ -138,12 +138,12 @@ def openminds_obj_to_ndi_document( dependency_type is set). Returns: - List of NDI Document objects. + List of NDI ndi_document objects. Raises: ValueError: If dependency_type is set but dependency_value is empty. """ - from .document import Document + from .document import ndi_document if dependency_type and not dependency_value: raise ValueError("dependency_value must not be empty if dependency_type is given.") @@ -170,7 +170,7 @@ def openminds_obj_to_ndi_document( documents = [] for s in struct_list: - doc = Document(doc_schema) + doc = ndi_document(doc_schema) doc = doc.set_session_id(session_id) # Store the openMINDS data diff --git a/src/ndi/probe/__init__.py b/src/ndi/probe/__init__.py index 7cf2309..4462c57 100644 --- a/src/ndi/probe/__init__.py +++ b/src/ndi/probe/__init__.py @@ -1,7 +1,7 @@ """ ndi.probe - Specialized element for measurement devices. -This module provides the Probe class that represents measurement +This module provides the ndi_probe class that represents measurement or stimulation devices in neuroscience experiments. """ @@ -11,16 +11,16 @@ import numpy as np -from ..element import Element -from ..epoch.epochprobemap import EpochProbeMap -from ..time import ClockType +from ..element import ndi_element +from ..epoch.epochprobemap import ndi_epoch_epochprobemap +from ..time import ndi_time_clocktype -class Probe(Element): +class ndi_probe(ndi_element): """ Specialized element for measurement/stimulation devices. - Probe represents a physical or virtual device that records or + ndi_probe represents a physical or virtual device that records or stimulates (e.g., electrodes, optical fibers, cameras). Probes build their epoch tables by matching epochprobemap entries from DAQ systems. @@ -31,10 +31,10 @@ class Probe(Element): - Match by name, reference, and type against device probe maps Attributes: - Inherits all from Element + Inherits all from ndi_element Example: - >>> probe = Probe( + >>> probe = ndi_probe( ... session=my_session, ... name='electrode1', ... reference=1, @@ -55,14 +55,14 @@ def __init__( document: Any | None = None, ): """ - Create a new Probe. + Create a new ndi_probe. Args: - session: Session object with database access - name: Probe name (no whitespace allowed) + session: ndi_session object with database access + name: ndi_probe name (no whitespace allowed) reference: Reference number (non-negative integer) - type: Probe type identifier (no whitespace) - subject_id: Subject document ID + type: ndi_probe type identifier (no whitespace) + subject_id: ndi_subject document ID identifier: Optional unique identifier document: Optional document to load from """ @@ -82,7 +82,7 @@ def __init__( ) # ========================================================================= - # EpochSet Overrides + # ndi_epoch_epochset Overrides # ========================================================================= def buildepochtable(self) -> list[dict[str, Any]]: @@ -155,18 +155,18 @@ def _get_daqsystems(self) -> list[Any]: return self._session.daqsystems # Fall back to querying database - from ..query import Query + from ..query import ndi_query - q = Query("").isa("daqsystem") + q = ndi_query("").isa("daqsystem") docs = self._session.database_search(q) - # Load DAQSystem objects from documents - from ..daq.system import DAQSystem + # Load ndi_daq_system objects from documents + from ..daq.system import ndi_daq_system systems = [] for doc in docs: try: - sys = DAQSystem(session=self._session, document=doc) + sys = ndi_daq_system(session=self._session, document=doc) systems.append(sys) except Exception: pass @@ -176,7 +176,7 @@ def _get_daqsystems(self) -> list[Any]: def _find_matching_epochprobemap( self, epochprobemaps: list[Any], - ) -> EpochProbeMap | None: + ) -> ndi_epoch_epochprobemap | None: """ Find an epochprobemap matching this probe. @@ -184,11 +184,11 @@ def _find_matching_epochprobemap( epochprobemaps: List of probe maps to search Returns: - Matching EpochProbeMap or None + Matching ndi_epoch_epochprobemap or None """ for epm in epochprobemaps: - # Handle both EpochProbeMap objects and dicts - if isinstance(epm, EpochProbeMap): + # Handle both ndi_epoch_epochprobemap objects and dicts + if isinstance(epm, ndi_epoch_epochprobemap): if epm.matches(self._name, self._reference, self._type): return epm elif isinstance(epm, dict): @@ -197,14 +197,14 @@ def _find_matching_epochprobemap( and epm.get("reference") == self._reference and epm.get("type") == self._type ): - return EpochProbeMap.from_dict(epm) + return ndi_epoch_epochprobemap.from_dict(epm) elif hasattr(epm, "name") and hasattr(epm, "reference") and hasattr(epm, "type"): if ( epm.name == self._name and epm.reference == self._reference and epm.type == self._type ): - return EpochProbeMap( + return ndi_epoch_epochprobemap( name=epm.name, reference=epm.reference, type=epm.type, @@ -231,7 +231,7 @@ def issyncgraphroot(self) -> bool: return False # ========================================================================= - # Probe-specific Methods + # ndi_probe-specific Methods # ========================================================================= def probestring(self) -> str: @@ -253,12 +253,12 @@ def epochprobemapmatch( Check if an epochprobemap matches this probe. Args: - epochprobemap: EpochProbeMap to check + epochprobemap: ndi_epoch_epochprobemap to check Returns: True if matches this probe's name, reference, type """ - if isinstance(epochprobemap, EpochProbeMap): + if isinstance(epochprobemap, ndi_epoch_epochprobemap): return epochprobemap.matches(self._name, self._reference, self._type) elif isinstance(epochprobemap, dict): return ( @@ -282,18 +282,18 @@ def getchanneldevinfo( Get device and channel information for an epoch. Args: - epoch_number: Epoch number (1-indexed) + epoch_number: ndi_epoch_epoch number (1-indexed) Returns: Dict with: - daqsystem: The DAQ system object - - device_epochnumber: Epoch number in device + - device_epochnumber: ndi_epoch_epoch number in device - channels: Channel mappings """ et, _ = self.epochtable() if epoch_number < 1 or epoch_number > len(et): - raise IndexError(f"Epoch {epoch_number} out of range (1..{len(et)})") + raise IndexError(f"ndi_epoch_epoch {epoch_number} out of range (1..{len(et)})") entry = et[epoch_number - 1] underlying = entry.get("underlying_epochs", {}) @@ -305,7 +305,7 @@ def getchanneldevinfo( } # ========================================================================= - # Element overrides + # ndi_element overrides # ========================================================================= def ndi_element_class(self) -> str: @@ -314,7 +314,7 @@ def ndi_element_class(self) -> str: MATLAB equivalent: ``class(ndi_probe_obj)`` Returns ``'ndi.probe'``. Subclasses (e.g. - ``ProbeTimeseriesMFDAQ``) override this to return their own + ``ndi_probe_timeseries_mfdaq``) override this to return their own MATLAB class name. Returns: @@ -328,7 +328,7 @@ def ndi_element_class(self) -> str: @staticmethod def buildmultipleepochtables( - probes: list[Probe], + probes: list[ndi_probe], session: Any, ) -> dict[str, list[dict[str, Any]]]: """ @@ -338,8 +338,8 @@ def buildmultipleepochtables( avoiding redundant database queries. Args: - probes: List of Probe objects - session: Session object + probes: List of ndi_probe objects + session: ndi_session object Returns: Dict mapping probe.id to epoch table @@ -350,16 +350,16 @@ def buildmultipleepochtables( elif hasattr(session, "daqsystems"): daqsystems = session.daqsystems else: - from ..query import Query + from ..query import ndi_query - q = Query("").isa("daqsystem") + q = ndi_query("").isa("daqsystem") docs = session.database_search(q) - from ..daq.system import DAQSystem + from ..daq.system import ndi_daq_system daqsystems = [] for doc in docs: try: - sys = DAQSystem(session=session, document=doc) + sys = ndi_daq_system(session=session, document=doc) daqsystems.append(sys) except Exception: pass @@ -413,11 +413,11 @@ def buildmultipleepochtables( def __repr__(self) -> str: """String representation.""" - return f"Probe({self._name}|{self._reference}|{self._type})" + return f"ndi_probe({self._name}|{self._reference}|{self._type})" # ========================================================================= -# Probe Type Map utilities +# ndi_probe Type Map utilities # ========================================================================= _PROBE_TYPE_MAP: dict[str, str] | None = None @@ -437,9 +437,9 @@ def initProbeTypeMap() -> dict[str, str]: import json try: - from ..common import PathConstants + from ..common import ndi_common_PathConstants - json_path = PathConstants.COMMON_FOLDER / "probe" / "probetype2object.json" + json_path = ndi_common_PathConstants.COMMON_FOLDER / "probe" / "probetype2object.json" except Exception: return {} diff --git a/src/ndi/probe/ndi_matlab_python_bridge.yaml b/src/ndi/probe/ndi_matlab_python_bridge.yaml index 869c1f5..11a5597 100644 --- a/src/ndi/probe/ndi_matlab_python_bridge.yaml +++ b/src/ndi/probe/ndi_matlab_python_bridge.yaml @@ -44,14 +44,14 @@ functions: classes: # ========================================================================= - # ndi.probe (base probe class, extends Element) + # ndi.probe (base probe class, extends ndi_element) # ========================================================================= - name: probe type: class matlab_path: "+ndi/probe.m" matlab_last_sync_hash: "e6890be7" python_path: "ndi/probe/__init__.py" - python_class: "Probe" + python_class: "ndi_probe" inherits: "ndi.element" properties: @@ -59,19 +59,19 @@ classes: type_matlab: "char" type_python: "str" access: "GetAccess=public, SetAccess=protected" - decision_log: "Inherited from Element. @property in Python." + decision_log: "Inherited from ndi_element. @property in Python." - name: reference type_matlab: "integer" type_python: "int" access: "GetAccess=public, SetAccess=protected" - decision_log: "Inherited from Element. @property in Python." + decision_log: "Inherited from ndi_element. @property in Python." - name: type type_matlab: "char" type_python: "str" access: "GetAccess=public, SetAccess=protected" - decision_log: "Inherited from Element. @property in Python." + decision_log: "Inherited from ndi_element. @property in Python." methods: # --- Constructor --- @@ -107,19 +107,19 @@ classes: default: "None" output_arguments: - name: probe_obj - type_python: "Probe" + type_python: "ndi_probe" decision_log: > Probes are always direct=False and have no underlying_element. These are hardcoded in the constructor. - # --- EpochSet Overrides --- + # --- ndi_epoch_epochset Overrides --- - name: buildepochtable input_arguments: [] output_arguments: - name: et type_python: "list[dict[str, Any]]" decision_log: > - Overrides Element. Scans all DAQ systems in the session and + Overrides ndi_element. Scans all DAQ systems in the session and collects epochs where the epochprobemap matches this probe. - name: epochsetname @@ -138,7 +138,7 @@ classes: Always returns False for probes (continue traversal to underlying DAQ systems). - # --- Probe-specific methods --- + # --- ndi_probe-specific methods --- - name: probestring input_arguments: [] output_arguments: @@ -169,14 +169,14 @@ classes: Internally maps to 0-based index. Returns dict with daqsystem, device_epoch_id, epochprobemap. - # --- DocumentService Override --- + # --- ndi_documentservice Override --- - name: newdocument input_arguments: [] output_arguments: - name: doc type_python: "Any" decision_log: > - Overrides Element. Sets element.direct=False for probes. + Overrides ndi_element. Sets element.direct=False for probes. - name: eq input_arguments: @@ -185,14 +185,14 @@ classes: output_arguments: - name: b type_python: "bool" - decision_log: "Inherited from Element. Mapped to Python __eq__ dunder method." + decision_log: "Inherited from ndi_element. Mapped to Python __eq__ dunder method." static_methods: - name: buildmultipleepochtables input_arguments: - name: probes type_matlab: "ndi.probe array" - type_python: "list[Probe]" + type_python: "list[ndi_probe]" - name: session type_matlab: "ndi.session" type_python: "Any" @@ -205,14 +205,14 @@ classes: mapping probe.id to epoch table. # ========================================================================= - # ndi.probe.timeseries (timeseries probe, extends Probe) + # ndi.probe.timeseries (timeseries probe, extends ndi_probe) # ========================================================================= - name: timeseries type: class matlab_path: "+ndi/+probe/timeseries.m" matlab_last_sync_hash: "0b39ad6c" python_path: "ndi/probe/timeseries.py" - python_class: "ProbeTimeseries" + python_class: "ndi_probe_timeseries" inherits: "ndi.probe" methods: @@ -321,7 +321,7 @@ classes: matlab_path: "+ndi/+probe/+timeseries/mfdaq.m" matlab_last_sync_hash: "1b188d97" python_path: "ndi/probe/timeseries_mfdaq.py" - python_class: "ProbeTimeseriesMFDAQ" + python_class: "ndi_probe_timeseries_mfdaq" inherits: "ndi.probe.timeseries" methods: @@ -368,7 +368,7 @@ classes: - name: timeref_out type_python: "Any | None" decision_log: > - Overrides ProbeTimeseries. Converts t0/t1 to sample + Overrides ndi_probe_timeseries. Converts t0/t1 to sample indices and delegates to read_epochsamples. - name: samplerate @@ -380,7 +380,7 @@ classes: - name: sr type_python: "float" decision_log: > - Overrides ProbeTimeseries. Gets sample rate from + Overrides ndi_probe_timeseries. Gets sample rate from underlying DAQ device. - name: getchanneldevinfo @@ -392,7 +392,7 @@ classes: - name: devinfo type_python: "tuple[Any, Any, Any, list[int]] | None" decision_log: > - Overrides Probe.getchanneldevinfo. Returns 4-tuple + Overrides ndi_probe.getchanneldevinfo. Returns 4-tuple (device, device_epoch, channeltype, channellist) or None. Semantic Parity: epoch is 1-indexed. @@ -404,7 +404,7 @@ classes: matlab_path: "+ndi/+probe/+timeseries/stimulator.m" matlab_last_sync_hash: "209d46b3" python_path: "ndi/probe/timeseries_stimulator.py" - python_class: "ProbeTimeseriesStimulator" + python_class: "ndi_probe_timeseries_stimulator" inherits: "ndi.probe.timeseries" methods: @@ -429,7 +429,7 @@ classes: default: "'stimulator'" output_arguments: - name: stim_obj - type_python: "ProbeTimeseriesStimulator" + type_python: "ndi_probe_timeseries_stimulator" decision_log: > Exact match. Defaults type to 'stimulator' and reference to 1. @@ -455,7 +455,7 @@ classes: - name: timeref_out type_python: "Any | None" decision_log: > - Overrides ProbeTimeseries. Returns structured data dicts + Overrides ndi_probe_timeseries. Returns structured data dicts instead of arrays. data has keys: stimid, parameters, analog. times has keys: stimon, stimoff, stimopenclose, stimevents. Semantic Parity: epoch is 1-indexed. diff --git a/src/ndi/probe/timeseries.py b/src/ndi/probe/timeseries.py index 67c4a02..50f8522 100644 --- a/src/ndi/probe/timeseries.py +++ b/src/ndi/probe/timeseries.py @@ -1,8 +1,8 @@ """ ndi.probe.timeseries - Timeseries probe class. -Provides the ProbeTimeseries class that adds time-domain data reading -capabilities to the base Probe class. +Provides the ndi_probe_timeseries class that adds time-domain data reading +capabilities to the base ndi_probe class. MATLAB equivalent: src/ndi/+ndi/+probe/timeseries.m """ @@ -13,22 +13,22 @@ import numpy as np -from . import Probe +from . import ndi_probe -class ProbeTimeseries(Probe): +class ndi_probe_timeseries(ndi_probe): """ - Probe that reads timeseries data. + ndi_probe that reads timeseries data. - Extends Probe with the ability to read timeseries data across + Extends ndi_probe with the ability to read timeseries data across epochs, using the session's syncgraph for time conversion when reading across different time references. - This is a base class. Subclasses (e.g., ProbeTimeseriesMFDAQ) + This is a base class. Subclasses (e.g., ndi_probe_timeseries_mfdaq) implement readtimeseriesepoch() for specific data formats. Example: - >>> probe = ProbeTimeseriesMFDAQ(session, 'electrode1', 1, 'n-trode') + >>> probe = ndi_probe_timeseries_mfdaq(session, 'electrode1', 1, 'n-trode') >>> data, t, timeref = probe.readtimeseries(epoch=1, t0=0, t1=10) """ @@ -50,7 +50,7 @@ def readtimeseries( timeref is provided, converts time using the session's syncgraph. Args: - epoch: Epoch number (1-indexed) or epoch_id string + epoch: ndi_epoch_epoch number (1-indexed) or epoch_id string t0: Start time t1: End time timeref: Optional time reference for cross-epoch reading @@ -123,7 +123,7 @@ def readtimeseriesepoch( Subclasses must override this method with format-specific reading. Args: - epoch: Epoch number (1-indexed) or epoch_id + epoch: ndi_epoch_epoch number (1-indexed) or epoch_id t0: Start time t1: End time @@ -138,7 +138,7 @@ def samplerate(self, epoch: int | str) -> float: Get sample rate for this probe in an epoch. Args: - epoch: Epoch number or epoch_id + epoch: ndi_epoch_epoch number or epoch_id Returns: Sample rate in Hz, or -1 if not applicable @@ -154,7 +154,7 @@ def times2samples( Convert times to sample indices. Args: - epoch: Epoch number or epoch_id + epoch: ndi_epoch_epoch number or epoch_id times: Time values Returns: @@ -175,7 +175,7 @@ def samples2times( Convert sample indices to times. Args: - epoch: Epoch number or epoch_id + epoch: ndi_epoch_epoch number or epoch_id samples: Sample indices (1-indexed) Returns: @@ -189,6 +189,6 @@ def samples2times( def __repr__(self) -> str: return ( - f"ProbeTimeseries(name='{self._name}', " + f"ndi_probe_timeseries(name='{self._name}', " f"reference={self._reference}, type='{self._type}')" ) diff --git a/src/ndi/probe/timeseries_mfdaq.py b/src/ndi/probe/timeseries_mfdaq.py index a46a82c..2d5a1b9 100644 --- a/src/ndi/probe/timeseries_mfdaq.py +++ b/src/ndi/probe/timeseries_mfdaq.py @@ -1,7 +1,7 @@ """ ndi.probe.timeseries_mfdaq - MFDAQ timeseries probe. -Provides ProbeTimeseriesMFDAQ that reads data from multi-function +Provides ndi_probe_timeseries_mfdaq that reads data from multi-function DAQ systems via the probe's underlying DAQ system and device info. MATLAB equivalent: src/ndi/+ndi/+probe/+timeseries/mfdaq.m @@ -13,10 +13,10 @@ import numpy as np -from .timeseries import ProbeTimeseries +from .timeseries import ndi_probe_timeseries -class ProbeTimeseriesMFDAQ(ProbeTimeseries): +class ndi_probe_timeseries_mfdaq(ndi_probe_timeseries): """ MFDAQ timeseries probe. @@ -30,7 +30,7 @@ class ProbeTimeseriesMFDAQ(ProbeTimeseries): - samplerate(): Get sampling rate Example: - >>> probe = ProbeTimeseriesMFDAQ(session, 'electrode1', 1, 'n-trode') + >>> probe = ndi_probe_timeseries_mfdaq(session, 'electrode1', 1, 'n-trode') >>> data, t, tr = probe.read_epochsamples(1, 0, 30000) >>> sr = probe.samplerate(1) """ @@ -49,7 +49,7 @@ def read_epochsamples( Read data from an epoch by sample indices. Args: - epoch: Epoch number (1-indexed) or epoch_id + epoch: ndi_epoch_epoch number (1-indexed) or epoch_id s0: Start sample (1-indexed) s1: End sample (1-indexed) @@ -94,7 +94,7 @@ def readtimeseriesepoch( Converts t0/t1 to sample indices and delegates to read_epochsamples. Args: - epoch: Epoch number (1-indexed) or epoch_id + epoch: ndi_epoch_epoch number (1-indexed) or epoch_id t0: Start time t1: End time @@ -125,7 +125,7 @@ def samplerate(self, epoch: int | str) -> float: Get sample rate for this probe in an epoch. Args: - epoch: Epoch number or epoch_id + epoch: ndi_epoch_epoch number or epoch_id Returns: Sample rate in Hz @@ -158,7 +158,7 @@ def getchanneldevinfo( channels are associated with this probe in the given epoch. Args: - epoch: Epoch number or epoch_id + epoch: ndi_epoch_epoch number or epoch_id Returns: Tuple of (device, device_epoch, channeltype, channellist) @@ -208,8 +208,8 @@ def _resolve_device( Resolve device info from a probe map entry. Args: - probe_map: EpochProbeMap object - epoch_entry: Epoch table entry + probe_map: ndi_epoch_epochprobemap object + epoch_entry: ndi_epoch_epoch table entry Returns: Tuple of (device, device_epoch, channeltype, channellist) @@ -218,9 +218,9 @@ def _resolve_device( return None # Parse device string to get device name and channels - from ..daq.daqsystemstring import DAQSystemString + from ..daq.daqsystemstring import ndi_daq_daqsystemstring - dss = DAQSystemString.parse(probe_map.devicestring) + dss = ndi_daq_daqsystemstring.parse(probe_map.devicestring) # Find the DAQ system by name if self._session is None: @@ -255,6 +255,6 @@ def _resolve_device( def __repr__(self) -> str: return ( - f"ProbeTimeseriesMFDAQ(name='{self._name}', " + f"ndi_probe_timeseries_mfdaq(name='{self._name}', " f"reference={self._reference}, type='{self._type}')" ) diff --git a/src/ndi/probe/timeseries_stimulator.py b/src/ndi/probe/timeseries_stimulator.py index 1d9aa0b..e50b7a0 100644 --- a/src/ndi/probe/timeseries_stimulator.py +++ b/src/ndi/probe/timeseries_stimulator.py @@ -1,7 +1,7 @@ """ ndi.probe.timeseries_stimulator - Stimulus delivery probe. -Provides the ProbeTimeseriesStimulator class for reading stimulus +Provides the ndi_probe_timeseries_stimulator class for reading stimulus delivery data from probes that control stimulus presentation. MATLAB equivalent: src/ndi/+ndi/+probe/+timeseries/stimulator.m @@ -13,12 +13,12 @@ import numpy as np -from .timeseries import ProbeTimeseries +from .timeseries import ndi_probe_timeseries -class ProbeTimeseriesStimulator(ProbeTimeseries): +class ndi_probe_timeseries_stimulator(ndi_probe_timeseries): """ - Probe for stimulus delivery devices. + ndi_probe for stimulus delivery devices. Reads stimulus presentation data from probes that deliver stimuli, extracting stimulus identity, timing, and parameters from marker, @@ -48,7 +48,7 @@ class ProbeTimeseriesStimulator(ProbeTimeseries): - t.analog: Analog sample times Example: - >>> stim = ProbeTimeseriesStimulator(session, 'visual_stim', 1, 'stimulator') + >>> stim = ndi_probe_timeseries_stimulator(session, 'visual_stim', 1, 'stimulator') >>> data, t, timeref = stim.readtimeseriesepoch(1, 0, 100) """ @@ -85,7 +85,7 @@ def readtimeseriesepoch( parameters, and optional analog data. Args: - epoch: Epoch number (1-indexed) or epoch_id + epoch: ndi_epoch_epoch number (1-indexed) or epoch_id t0: Start time t1: End time @@ -377,16 +377,16 @@ def readtimeseriesepoch( t.setdefault("stimevents", []) # Build time reference - from ..time import ClockType + from ..time import ndi_time_clocktype eid = self.epochid(epoch) if hasattr(self, "epochid") else str(epoch) try: - from ..time.timereference import TimeReference + from ..time.timereference import ndi_time_timereference - timeref = TimeReference(self, ClockType.DEV_LOCAL_TIME, eid, 0) + timeref = ndi_time_timereference(self, ndi_time_clocktype.DEV_LOCAL_TIME, eid, 0) except Exception: - timeref = ClockType.DEV_LOCAL_TIME + timeref = ndi_time_clocktype.DEV_LOCAL_TIME return data, t, timeref @@ -401,12 +401,12 @@ def getchanneldevinfo( needed by readtimeseriesepoch. Args: - epoch: Epoch number (1-indexed) or epoch_id + epoch: ndi_epoch_epoch number (1-indexed) or epoch_id Returns: Dict with daqsystem, device_epoch_id, channeltype, channel """ - # Use the parent Probe.getchanneldevinfo which returns a dict + # Use the parent ndi_probe.getchanneldevinfo which returns a dict # with daqsystem, device_epoch_id, epochprobemap try: base_info = super().getchanneldevinfo(epoch) @@ -429,9 +429,9 @@ def getchanneldevinfo( for epm in epms: if hasattr(epm, "devicestring") and epm.devicestring: try: - from ..daq.daqsystemstring import DAQSystemString + from ..daq.daqsystemstring import ndi_daq_daqsystemstring - dss = DAQSystemString.parse(epm.devicestring) + dss = ndi_daq_daqsystemstring.parse(epm.devicestring) for ct, ch_list in dss.channels: for ch in ch_list: channeltype.append(ct) @@ -450,9 +450,9 @@ def _get_epoch_timeref(self, epoch: int | str) -> Any | None: """Get the time reference for an epoch.""" if self._session is None: return None - from ..time import ClockType + from ..time import ndi_time_clocktype - return ClockType.DEV_LOCAL_TIME + return ndi_time_clocktype.DEV_LOCAL_TIME def parse_marker_data( self, @@ -532,4 +532,6 @@ def parse_marker_data( } def __repr__(self) -> str: - return f"ProbeTimeseriesStimulator(name='{self._name}', " f"reference={self._reference})" + return ( + f"ndi_probe_timeseries_stimulator(name='{self._name}', " f"reference={self._reference})" + ) diff --git a/src/ndi/query.py b/src/ndi/query.py index 8bc3dde..5bb39b6 100644 --- a/src/ndi/query.py +++ b/src/ndi/query.py @@ -1,7 +1,7 @@ """ ndi.query - search an ndi.database for ndi.documents -Query objects define searches for ndi.documents; they are passed to the +ndi_query objects define searches for ndi.documents; they are passed to the ndi.database.search() function. Inherits from did.query.Query, using search_structure as the single @@ -9,13 +9,13 @@ operators) and MATLAB-compatible construction (via operation strings). Pythonic examples: - q = Query('element.name') == 'electrode1' - q = (Query('element.type') == 'probe') & (Query('element.name').contains('elec')) + q = ndi_query('element.name') == 'electrode1' + q = (ndi_query('element.type') == 'probe') & (ndi_query('element.name').contains('elec')) MATLAB-compatible examples: - q = Query('', 'isa', 'base') - q = Query('base.name', 'exact_string', 'test') - q = Query.from_search('ndi_document_property.name', 'regexp', '(.*)') + q = ndi_query('', 'isa', 'base') + q = ndi_query('base.name', 'exact_string', 'test') + q = ndi_query.from_search('ndi_document_property.name', 'regexp', '(.*)') """ from typing import Any @@ -27,37 +27,37 @@ # # In MATLAB ``ndi.query`` is a *class*, so ``ndi.query.all()`` reaches a # static method directly. In Python ``ndi.query`` is a *module* that -# contains the ``Query`` class. Expose the most common factory methods at +# contains the ``ndi_query`` class. Expose the most common factory methods at # module level so callers can write ``ndi.query.all()`` exactly as in # MATLAB. # --------------------------------------------------------------------------- -def all() -> "Query": +def all() -> "ndi_query": """Return a query that matches all documents. Convenience wrapper so ``ndi.query.all()`` works like MATLAB. """ - return Query.all() + return ndi_query.all() -def none() -> "Query": +def none() -> "ndi_query": """Return a query that matches no documents. Convenience wrapper so ``ndi.query.none()`` works like MATLAB. """ - return Query.none() + return ndi_query.none() -def from_search(field: str, operation: str, param1: Any = "", param2: Any = "") -> "Query": +def from_search(field: str, operation: str, param1: Any = "", param2: Any = "") -> "ndi_query": """Create a query using MATLAB-style parameters. Convenience wrapper so ``ndi.query.from_search(...)`` works like MATLAB. """ - return Query.from_search(field, operation, param1, param2) + return ndi_query.from_search(field, operation, param1, param2) -class Query(did.query.Query): +class ndi_query(did.query.Query): """NDI query class for searching documents. Inherits from did.query.Query, using ``search_structure`` as the @@ -68,14 +68,14 @@ class Query(did.query.Query): 1. Pythonic (recommended):: - q = Query('field') == 'value' - q = Query('field').contains('substring') + q = ndi_query('field') == 'value' + q = ndi_query('field').contains('substring') 2. MATLAB-compatible:: - q = Query('', 'isa', 'base') - q = Query('base.name', 'exact_string', 'test') - q = Query.from_search('field', 'exact_string', 'value') + q = ndi_query('', 'isa', 'base') + q = ndi_query('base.name', 'exact_string', 'test') + q = ndi_query.from_search('field', 'exact_string', 'value') Attributes: field (str): The field being queried (computed from search_structure). @@ -128,12 +128,12 @@ def __init__( Pythonic (chain operators after construction):: - q = Query('base.name') == 'test' + q = ndi_query('base.name') == 'test' MATLAB-compatible (pass operation and params directly):: - q = Query('', 'isa', 'base') - q = Query('base.name', 'exact_string', 'test') + q = ndi_query('', 'isa', 'base') + q = ndi_query('base.name', 'exact_string', 'test') Args: field: The document field to query (e.g., 'base.name', @@ -208,7 +208,7 @@ def value(self) -> Any: return None @property - def queries(self) -> list["Query"]: + def queries(self) -> list["ndi_query"]: """Get the list of sub-queries for composite queries.""" return self._queries @@ -216,7 +216,7 @@ def queries(self) -> list["Query"]: # Internal # ------------------------------------------------------------------ - def _resolve(self, py_op: str, value: Any) -> "Query": + def _resolve(self, py_op: str, value: Any) -> "ndi_query": """Set the query condition by building a DID search_structure entry.""" if self._resolved: raise ValueError("This query has already been resolved") @@ -250,39 +250,39 @@ def _resolve(self, py_op: str, value: Any) -> "Query": # Pythonic operators # ------------------------------------------------------------------ - def __eq__(self, other: Any) -> "Query": + def __eq__(self, other: Any) -> "ndi_query": """Equality comparison.""" - if isinstance(other, Query): + if isinstance(other, ndi_query): return NotImplemented return self._resolve("==", other) - def __ne__(self, other: Any) -> "Query": + def __ne__(self, other: Any) -> "ndi_query": """Inequality comparison.""" - if isinstance(other, Query): + if isinstance(other, ndi_query): return NotImplemented return self._resolve("!=", other) - def __lt__(self, other: Any) -> "Query": + def __lt__(self, other: Any) -> "ndi_query": """Less than comparison.""" return self._resolve("<", other) - def __le__(self, other: Any) -> "Query": + def __le__(self, other: Any) -> "ndi_query": """Less than or equal comparison.""" return self._resolve("<=", other) - def __gt__(self, other: Any) -> "Query": + def __gt__(self, other: Any) -> "ndi_query": """Greater than comparison.""" return self._resolve(">", other) - def __ge__(self, other: Any) -> "Query": + def __ge__(self, other: Any) -> "ndi_query": """Greater than or equal comparison.""" return self._resolve(">=", other) - def __and__(self, other: "Query") -> "Query": + def __and__(self, other: "ndi_query") -> "ndi_query": """Combine queries with AND (list concatenation of search_structures).""" - if not isinstance(other, Query): + if not isinstance(other, ndi_query): return NotImplemented - q = Query() + q = ndi_query() q.search_structure = self.search_structure + other.search_structure q._resolved = True q._composite = True @@ -290,11 +290,11 @@ def __and__(self, other: "Query") -> "Query": q._queries = [self, other] return q - def __or__(self, other: "Query") -> "Query": + def __or__(self, other: "ndi_query") -> "ndi_query": """Combine queries with OR (nested 'or' operation).""" - if not isinstance(other, Query): + if not isinstance(other, ndi_query): return NotImplemented - q = Query() + q = ndi_query() q.search_structure = [ { "field": "", @@ -309,11 +309,11 @@ def __or__(self, other: "Query") -> "Query": q._queries = [self, other] return q - def __invert__(self) -> "Query": + def __invert__(self) -> "ndi_query": """Negate a query.""" if not self._resolved: raise ValueError("Cannot negate an unresolved query") - q = Query() + q = ndi_query() q._pending_field = self._pending_field q._resolved = True q._composite = self._composite @@ -335,36 +335,36 @@ def __invert__(self) -> "Query": # String methods # ------------------------------------------------------------------ - def contains(self, value: str) -> "Query": + def contains(self, value: str) -> "ndi_query": """Check if field contains substring. Args: value: The substring to search for. Returns: - Resolved Query object. + Resolved ndi_query object. """ return self._resolve("contains", value) - def match(self, pattern: str) -> "Query": + def match(self, pattern: str) -> "ndi_query": """Match field against regex pattern. Args: pattern: Regular expression pattern. Returns: - Resolved Query object. + Resolved ndi_query object. """ return self._resolve("match", pattern) - def equals(self, value: Any) -> "Query": + def equals(self, value: Any) -> "ndi_query": """Exact equality check. Args: value: The value to compare. Returns: - Resolved Query object. + Resolved ndi_query object. """ return self._resolve("==", value) @@ -372,19 +372,19 @@ def equals(self, value: Any) -> "Query": # Comparison methods (for explicit calls) # ------------------------------------------------------------------ - def less_than(self, value: Any) -> "Query": + def less_than(self, value: Any) -> "ndi_query": """Check if field is less than value.""" return self._resolve("<", value) - def less_than_or_equal_to(self, value: Any) -> "Query": + def less_than_or_equal_to(self, value: Any) -> "ndi_query": """Check if field is less than or equal to value.""" return self._resolve("<=", value) - def greater_than(self, value: Any) -> "Query": + def greater_than(self, value: Any) -> "ndi_query": """Check if field is greater than value.""" return self._resolve(">", value) - def greater_than_or_equal_to(self, value: Any) -> "Query": + def greater_than_or_equal_to(self, value: Any) -> "ndi_query": """Check if field is greater than or equal to value.""" return self._resolve(">=", value) @@ -392,22 +392,22 @@ def greater_than_or_equal_to(self, value: Any) -> "Query": # Field existence # ------------------------------------------------------------------ - def has_field(self) -> "Query": + def has_field(self) -> "ndi_query": """Check if the field exists in the document. Returns: - Resolved Query object. + Resolved ndi_query object. """ return self._resolve("hasfield", True) - def has_member(self, value: Any) -> "Query": + def has_member(self, value: Any) -> "ndi_query": """Check if field (array) contains a specific member. Args: value: The value to look for in the array. Returns: - Resolved Query object. + Resolved ndi_query object. """ return self._resolve("hasmember", value) @@ -415,18 +415,18 @@ def has_member(self, value: Any) -> "Query": # NDI-specific queries # ------------------------------------------------------------------ - def isa(self, document_class: str) -> "Query": + def isa(self, document_class: str) -> "ndi_query": """Check if document is of a specific class or inherits from it. Args: document_class: The document class name to check against. Returns: - Resolved Query object. + Resolved ndi_query object. """ return self._resolve("isa", document_class) - def depends_on(self, name: str, value: str = "") -> "Query": + def depends_on(self, name: str, value: str = "") -> "ndi_query": """Check if document depends on another document. Args: @@ -434,7 +434,7 @@ def depends_on(self, name: str, value: str = "") -> "Query": value: The dependency value (document ID). Returns: - Resolved Query object. + Resolved ndi_query object. """ return self._resolve("depends_on", (name, value)) @@ -443,23 +443,25 @@ def depends_on(self, name: str, value: str = "") -> "Query": # ------------------------------------------------------------------ @staticmethod - def all() -> "Query": + def all() -> "ndi_query": """Return a query that matches all documents. Returns: - Query that matches any document with class 'base' or its subclasses. + ndi_query that matches any document with class 'base' or its subclasses. """ - q = Query("") + q = ndi_query("") return q.isa("base") @staticmethod - def none() -> "Query": + def none() -> "ndi_query": """Return a query that matches no documents.""" - q = Query("") + q = ndi_query("") return q.isa("_impossible_class_name_that_will_never_exist_") @classmethod - def from_search(cls, field: str, operation: str, param1: Any = "", param2: Any = "") -> "Query": + def from_search( + cls, field: str, operation: str, param1: Any = "", param2: Any = "" + ) -> "ndi_query": """Create a query using MATLAB-style parameters. This provides compatibility with MATLAB ndi.query construction. @@ -479,18 +481,18 @@ def from_search(cls, field: str, operation: str, param1: Any = "", param2: Any = - 'greaterthaneq': Field >= param1 - 'hasfield': Field exists (param1 ignored) - 'hasmember': Field array contains param1 - - 'isa': Document is or inherits from class param1 - - 'depends_on': Document depends on doc with ID param1 + - 'isa': ndi_document is or inherits from class param1 + - 'depends_on': ndi_document depends on doc with ID param1 - Prefix with '~' to negate (e.g., '~exact_string') param1: First parameter (meaning depends on operation). param2: Second parameter (used by some operations). Returns: - Resolved Query object. + Resolved ndi_query object. Example: - q = Query.from_search('base.name', 'exact_string', 'my_document') - q = Query.from_search('', 'isa', 'element') + q = ndi_query.from_search('base.name', 'exact_string', 'my_document') + q = ndi_query.from_search('', 'isa', 'element') """ # Translate Python-style aliases to DID ops _ALIAS = {"==": "exact_string", "notequal": "~exact_string"} @@ -515,7 +517,7 @@ def to_searchstructure(self) -> dict: or for composite queries: 'operation' and 'search' list. Example: - q = Query('base.name') == 'test' + q = ndi_query('base.name') == 'test' ss = q.to_searchstructure() # {'field': 'base.name', 'operation': '==', 'param1': 'test', 'param2': ''} """ @@ -570,12 +572,12 @@ def __len__(self) -> int: return len(self._queries) def __bool__(self) -> bool: - """Query is truthy if it has been resolved.""" + """ndi_query is truthy if it has been resolved.""" return self._resolved def __repr__(self) -> str: if self._composite: - return f"Query({self._composite_op}: {self._queries})" + return f"ndi_query({self._composite_op}: {self._queries})" if self._resolved: - return f"Query('{self.field}' {self.operator} {self.value!r})" - return f"Query('{self.field}')" + return f"ndi_query('{self.field}' {self.operator} {self.value!r})" + return f"ndi_query('{self.field}')" diff --git a/src/ndi/session/__init__.py b/src/ndi/session/__init__.py index 0f7afbd..0fe5fe8 100644 --- a/src/ndi/session/__init__.py +++ b/src/ndi/session/__init__.py @@ -1,29 +1,29 @@ """ -ndi.session - Session management for NDI. +ndi.session - ndi_session management for NDI. This module provides session classes for managing NDI experiments: -- Session: Abstract base class for session management -- DirSession: Directory-based session implementation +- ndi_session: Abstract base class for session management +- ndi_session_dir: Directory-based session implementation MATLAB equivalents: - ndi.session -> ndi.session.Session (or ndi.Session) + ndi.session -> ndi.session.ndi_session (or ndi.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 +from .dir import ndi_session_dir +from .mock import ndi_session_mock +from .session_base import empty_id, ndi_session +from .sessiontable import ndi_session_sessiontable # MATLAB compatibility: ``ndi.session.dir(path)`` creates a directory-based # session, mirroring the MATLAB constructor ``ndi.session.dir``. -dir = DirSession +dir = ndi_session_dir __all__ = [ - "Session", - "DirSession", - "MockSession", - "SessionTable", + "ndi_session", + "ndi_session_dir", + "ndi_session_mock", + "ndi_session_sessiontable", "empty_id", "dir", ] diff --git a/src/ndi/session/dir.py b/src/ndi/session/dir.py index af513ab..60b278a 100644 --- a/src/ndi/session/dir.py +++ b/src/ndi/session/dir.py @@ -1,7 +1,7 @@ """ ndi.session.dir - Directory-based NDI session. -This module provides DirSession, a Session implementation that +This module provides ndi_session_dir, a ndi_session implementation that stores all data in a directory on the filesystem. """ @@ -10,27 +10,27 @@ from pathlib import Path from typing import Any -from ..database import Database -from ..ido import Ido -from ..query import Query -from ..time.syncgraph import SyncGraph -from .session_base import Session +from ..database import ndi_database +from ..ido import ndi_ido +from ..query import ndi_query +from ..time.syncgraph import ndi_time_syncgraph +from .session_base import ndi_session -class DirSession(Session): +class ndi_session_dir(ndi_session): """ Directory-based session implementation. - DirSession stores session data in a directory structure with: + ndi_session_dir stores session data in a directory structure with: - .ndi/ subdirectory for database and metadata - reference.txt and unique_reference.txt for session identification Example: # Create or open a session - >>> session = DirSession('/path/to/experiment') + >>> session = ndi_session_dir('/path/to/experiment') # Create with explicit reference - >>> session = DirSession('MyExperiment', '/path/to/experiment') + >>> session = ndi_session_dir('MyExperiment', '/path/to/experiment') # Access session data >>> probes = session.getprobes() @@ -46,8 +46,8 @@ def __init__( Create or open a directory-based session. Can be called in two ways: - - DirSession(path) - Open existing session from path - - DirSession(reference, path) - Create/open with explicit reference + - ndi_session_dir(path) - Open existing session from path + - ndi_session_dir(reference, path) - Create/open with explicit reference Args: reference_or_path: Either the session reference or path @@ -87,10 +87,10 @@ def __init__( self._identifier = unique_ref_file.read_text().strip() else: # Create a new identifier - self._identifier = Ido().id + self._identifier = ndi_ido().id # Initialize database - self._database = Database(self._ndi_pathname(), db_name=".") + self._database = ndi_database(self._ndi_pathname(), db_name=".") # Try to load session info from database read_from_database = False @@ -98,7 +98,7 @@ def __init__( # Search without the session_id filter so we can discover the # existing session_id from documents already in the database # (e.g. artifacts produced by MATLAB). - session_docs = self._database.search(Query("").isa("session")) + session_docs = self._database.search(ndi_query("").isa("session")) if session_docs: # Use the oldest session document oldest_doc = session_docs[0] @@ -129,10 +129,10 @@ def __init__( ) # Create session document - from ..document import Document + from ..document import ndi_document try: - session_doc = Document("session", **{"session.reference": self._reference}) + session_doc = ndi_document("session", **{"session.reference": self._reference}) session_doc = session_doc.set_session_id(self._identifier) self.database_add(session_doc) except Exception: @@ -141,15 +141,15 @@ def __init__( # Load or create syncgraph syncgraph_docs = self.database_search( - Query("").isa("syncgraph") & (Query("base.session_id") == self.id()) + ndi_query("").isa("syncgraph") & (ndi_query("base.session_id") == self.id()) ) if not syncgraph_docs: - self._syncgraph = SyncGraph(self) + self._syncgraph = ndi_time_syncgraph(self) else: if len(syncgraph_docs) > 1: raise ValueError("Too many syncgraph documents found. There should be only 1.") - self._syncgraph = SyncGraph(session=self, document=syncgraph_docs[0]) + self._syncgraph = ndi_time_syncgraph(session=self, document=syncgraph_docs[0]) # Write reference files self._write_reference_files() @@ -213,7 +213,7 @@ def deleteSessionDataStructures( self, are_you_sure: bool = False, ask_user: bool = True, - ) -> DirSession | None: + ) -> ndi_session_dir | None: """ Delete the session's data structures. @@ -256,12 +256,12 @@ def exists(path: str | Path) -> bool: return ref_file.exists() @staticmethod - def database_erase(session: DirSession, areyousure: str) -> None: + def database_erase(session: ndi_session_dir, areyousure: str) -> None: """ Delete the entire session database. Args: - session: Session to erase + session: ndi_session to erase areyousure: Must be 'yes' to proceed """ import shutil @@ -276,7 +276,7 @@ def database_erase(session: DirSession, areyousure: str) -> None: def __eq__(self, other: Any) -> bool: """Check equality by ID and path.""" - if not isinstance(other, DirSession): + if not isinstance(other, ndi_session_dir): return False if not super().__eq__(other): return False @@ -284,4 +284,4 @@ def __eq__(self, other: Any) -> bool: def __repr__(self) -> str: """String representation.""" - return f"DirSession(reference='{self._reference}', path='{self._path}')" + return f"ndi_session_dir(reference='{self._reference}', path='{self._path}')" diff --git a/src/ndi/session/mock.py b/src/ndi/session/mock.py index 5cee870..d79cff6 100644 --- a/src/ndi/session/mock.py +++ b/src/ndi/session/mock.py @@ -1,7 +1,7 @@ """ ndi.session.mock - Mock session for testing. -Provides MockSession that creates a temporary directory-based session, +Provides ndi_session_mock that creates a temporary directory-based session, useful for unit tests and interactive experimentation. MATLAB equivalent: Conceptual (MATLAB tests create sessions manually) @@ -13,26 +13,26 @@ import tempfile from pathlib import Path -from .dir import DirSession +from .dir import ndi_session_dir -class MockSession(DirSession): +class ndi_session_mock(ndi_session_dir): """ Temporary session for testing. - Creates a DirSession backed by a temporary directory that is + Creates a ndi_session_dir backed by a temporary directory that is automatically cleaned up when the session is closed or garbage collected. Example: - >>> with MockSession('test') as session: - ... doc = Document('base') + >>> with ndi_session_mock('test') as session: + ... doc = ndi_document('base') ... session.database_add(doc) - ... results = session.database_search(Query('').isa('base')) + ... results = session.database_search(ndi_query('').isa('base')) >>> # temp directory is cleaned up >>> # Or without context manager: - >>> session = MockSession('test') + >>> session = ndi_session_mock('test') >>> # ... use session ... >>> session.close() # cleans up temp directory """ @@ -47,7 +47,7 @@ def __init__( Create a mock session with a temporary directory. Args: - reference: Session reference string + reference: ndi_session reference string prefix: Prefix for temp directory name cleanup: If True, remove temp dir on close/del """ @@ -60,7 +60,7 @@ def close(self) -> None: if self._cleanup and Path(self._tmpdir).exists(): shutil.rmtree(self._tmpdir, ignore_errors=True) - def __enter__(self) -> MockSession: + def __enter__(self) -> ndi_session_mock: return self def __exit__(self, exc_type, exc_val, exc_tb) -> None: @@ -73,4 +73,4 @@ def __del__(self) -> None: pass def __repr__(self) -> str: - return f"MockSession(path='{self._tmpdir}')" + return f"ndi_session_mock(path='{self._tmpdir}')" diff --git a/src/ndi/session/ndi_matlab_python_bridge.yaml b/src/ndi/session/ndi_matlab_python_bridge.yaml index 086f495..71786e9 100644 --- a/src/ndi/session/ndi_matlab_python_bridge.yaml +++ b/src/ndi/session/ndi_matlab_python_bridge.yaml @@ -35,7 +35,7 @@ classes: matlab_path: "+ndi/session.m" matlab_last_sync_hash: "a97ab7d0" python_path: "ndi/session/session_base.py" - python_class: "Session" + python_class: "ndi_session" properties: - name: reference @@ -52,19 +52,19 @@ classes: - name: syncgraph type_matlab: "ndi.time.syncgraph" - type_python: "SyncGraph | None" + type_python: "ndi_time_syncgraph | None" access: "GetAccess=public, SetAccess=protected, Transient" decision_log: "Implemented as @property in Python." - name: cache type_matlab: "ndi.cache" - type_python: "Cache" + type_python: "ndi_cache" access: "GetAccess=public, SetAccess=protected, Transient" decision_log: "Implemented as @property in Python." - name: database type_matlab: "ndi.database" - type_python: "Database | None" + type_python: "ndi_database | None" access: "GetAccess={?session, ?ndi.dataset}, SetAccess=protected, Transient" decision_log: "Implemented as @property in Python." @@ -78,7 +78,7 @@ classes: type_python: "str" output_arguments: - name: ndi_session_obj - type_python: "Session" + type_python: "ndi_session" decision_log: "Exact match." # --- Public instance methods --- @@ -105,7 +105,7 @@ classes: type_python: "Any" output_arguments: - name: ndi_session_obj - type_python: "Session" + type_python: "ndi_session" decision_log: "Exact match." - name: daqsystem_rm @@ -115,7 +115,7 @@ classes: type_python: "Any" output_arguments: - name: ndi_session_obj - type_python: "Session" + type_python: "ndi_session" decision_log: "Exact match." - name: daqsystem_load @@ -135,7 +135,7 @@ classes: input_arguments: [] output_arguments: - name: ndi_session_obj - type_python: "Session" + type_python: "ndi_session" decision_log: "Exact match." - name: newdocument @@ -149,48 +149,48 @@ classes: type_python: "**properties" output_arguments: - name: ndi_document_obj - type_python: "Document" + type_python: "ndi_document" decision_log: "Exact match." - name: searchquery input_arguments: [] output_arguments: - name: sq - type_python: "Query" + type_python: "ndi_query" decision_log: "Exact match." - name: database_add input_arguments: - name: document type_matlab: "ndi.document | cell" - type_python: "Document | list[Document]" + type_python: "ndi_document | list[ndi_document]" output_arguments: - name: ndi_session_obj - type_python: "Session" + type_python: "ndi_session" decision_log: "Exact match." - name: database_rm input_arguments: - name: doc_unique_id type_matlab: "ndi.document | char | cell" - type_python: "Document | str | list[Document | str]" + type_python: "ndi_document | str | list[ndi_document | str]" - name: error_if_not_found type_matlab: "logical (name-value)" type_python: "bool" default: "False" output_arguments: - name: ndi_session_obj - type_python: "Session" + type_python: "ndi_session" decision_log: "Exact match." - name: database_search input_arguments: - name: searchparameters type_matlab: "ndi.query" - type_python: "Query" + type_python: "ndi_query" output_arguments: - name: ndi_document_obj - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: "Exact match." - name: database_clear @@ -205,7 +205,7 @@ classes: input_arguments: - name: document type_matlab: "ndi.document | cell" - type_python: "Document | list[Document]" + type_python: "ndi_document | list[ndi_document]" output_arguments: - name: b type_python: "bool" @@ -217,7 +217,7 @@ classes: input_arguments: - name: ndi_document_or_id type_matlab: "ndi.document | char" - type_python: "Document | str" + type_python: "ndi_document | str" - name: filename type_matlab: "char" type_python: "str" @@ -232,7 +232,7 @@ classes: input_arguments: - name: ndi_document_or_id type_matlab: "ndi.document | char" - type_python: "Document | str" + type_python: "ndi_document | str" - name: filename type_matlab: "char" type_python: "str" @@ -254,10 +254,10 @@ classes: input_arguments: - name: rule type_matlab: "ndi.time.syncrule" - type_python: "SyncRule" + type_python: "ndi_time_syncrule" output_arguments: - name: ndi_session_obj - type_python: "Session" + type_python: "ndi_session" decision_log: "Exact match." - name: syncgraph_rmrule @@ -267,7 +267,7 @@ classes: type_python: "int" output_arguments: - name: ndi_session_obj - type_python: "Session" + type_python: "ndi_session" decision_log: "Exact match." - name: ingest @@ -283,7 +283,7 @@ classes: input_arguments: [] output_arguments: - name: d - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: "Exact match." - name: is_fully_ingested @@ -371,12 +371,12 @@ classes: - name: docinput2docs input_arguments: - name: ndi_session_obj - type_python: "Session" + type_python: "ndi_session" - name: doc_input - type_python: "str | Document | list[str | Document]" + type_python: "str | ndi_document | list[str | ndi_document]" output_arguments: - name: doc_list - type_python: "list[Document]" + type_python: "list[ndi_document]" decision_log: > MATLAB is a static method. Python implements as instance method _docinput2docs (protected convention). Functionality is equivalent. @@ -396,7 +396,7 @@ classes: matlab_path: "+ndi/+session/dir.m" matlab_last_sync_hash: "b640e6ff" python_path: "ndi/session/dir.py" - python_class: "DirSession" + python_class: "ndi_session_dir" inherits: "ndi.session" properties: @@ -421,7 +421,7 @@ classes: type_python: "str | None" output_arguments: - name: ndi_session_dir_obj - type_python: "DirSession" + type_python: "ndi_session_dir" decision_log: > Supports MATLAB calling conventions: dir(path) and dir(reference, path). session_id is an undocumented internal parameter in both languages. @@ -468,7 +468,7 @@ classes: default: "True" output_arguments: - name: obj_out - type_python: "DirSession | None" + type_python: "ndi_session_dir | None" decision_log: > MATLAB name is deleteSessionDataStructures (camelCase). Python was previously delete_session_data_structures; renamed to match MATLAB. @@ -487,7 +487,7 @@ classes: - name: database_erase input_arguments: - name: ndi_session_dir_obj - type_python: "DirSession" + type_python: "ndi_session_dir" - name: areyousure type_matlab: "char" type_python: "str" @@ -503,7 +503,7 @@ classes: matlab_path: "+ndi/+session/sessiontable.m" matlab_last_sync_hash: "fe64a9f5" python_path: "ndi/session/sessiontable.py" - python_class: "SessionTable" + python_class: "ndi_session_sessiontable" methods: - name: sessiontable @@ -513,7 +513,7 @@ classes: type_python: "Path | None" output_arguments: - name: ndi_sessiontable_obj - type_python: "SessionTable" + type_python: "ndi_session_sessiontable" decision_log: "Exact match." - name: getsessiontable @@ -644,7 +644,7 @@ classes: matlab_path: "+ndi/+session/mock.m" matlab_last_sync_hash: "fe64a9f5" python_path: "ndi/session/mock.py" - python_class: "MockSession" + python_class: "ndi_session_mock" inherits: "ndi.session.dir" methods: @@ -653,7 +653,7 @@ classes: input_arguments: [] output_arguments: - name: ndi_session_mock_obj - type_python: "MockSession" + type_python: "ndi_session_mock" decision_log: > MATLAB mock() takes no arguments; Python constructor adds reference, prefix, and cleanup parameters for flexibility. diff --git a/src/ndi/session/session_base.py b/src/ndi/session/session_base.py index c1ed1d1..c3af8a4 100644 --- a/src/ndi/session/session_base.py +++ b/src/ndi/session/session_base.py @@ -1,7 +1,7 @@ """ ndi.session.session_base - Base class for NDI sessions. -This module provides the Session abstract base class that manages +This module provides the ndi_session abstract base class that manages NDI experiments including DAQ systems, database, syncgraph, and probes. """ @@ -12,13 +12,13 @@ from pathlib import Path from typing import Any -from ..cache import Cache -from ..database import Database -from ..document import Document -from ..ido import Ido -from ..query import Query -from ..time.syncgraph import SyncGraph -from ..time.syncrule_base import SyncRule +from ..cache import ndi_cache +from ..database import ndi_database +from ..document import ndi_document +from ..ido import ndi_ido +from ..query import ndi_query +from ..time.syncgraph import ndi_time_syncgraph +from ..time.syncrule_base import ndi_time_syncrule logger = logging.getLogger(__name__) @@ -33,22 +33,22 @@ def empty_id() -> str: Returns: String '0000000000000000_0000000000000000' """ - ido = Ido() + ido = ndi_ido() base_id = ido.id # Replace all non-underscore characters with '0' return "".join("0" if c != "_" else "_" for c in base_id) -class Session(ABC): +class ndi_session(ABC): """ Abstract base class for NDI sessions. - Session represents a neuroscience experiment/recording session and + ndi_session represents a neuroscience experiment/recording session and provides access to: - - Database for document storage + - ndi_database for document storage - DAQ systems for data acquisition - - SyncGraph for time synchronization - - Cache for performance optimization + - ndi_time_syncgraph for time synchronization + - ndi_cache for performance optimization Subclasses must implement: - getpath(): Return the storage path @@ -59,23 +59,23 @@ class Session(ABC): identifier: Unique session ID Example: - >>> session = DirSession('/path/to/experiment') + >>> session = ndi_session_dir('/path/to/experiment') >>> session.daqsystem_add(my_daq) - >>> docs = session.database_search(Query('element.type') == 'probe') + >>> docs = session.database_search(ndi_query('element.type') == 'probe') """ def __init__(self, reference: str): """ - Create a new Session. + Create a new ndi_session. Args: reference: Human-readable reference for the session """ self._reference = reference - self._identifier = Ido().id - self._syncgraph: SyncGraph | None = None - self._cache = Cache() - self._database: Database | None = None + self._identifier = ndi_ido().id + self._syncgraph: ndi_time_syncgraph | None = None + self._cache = ndi_cache() + self._database: ndi_database | None = None self._cloud_client: Any = None @property @@ -122,17 +122,17 @@ def unique_reference_string(self) -> str: return f"{self._reference}_{self._identifier}" @property - def syncgraph(self) -> SyncGraph | None: + def syncgraph(self) -> ndi_time_syncgraph | None: """Get the session's syncgraph.""" return self._syncgraph @property - def cache(self) -> Cache: + def cache(self) -> ndi_cache: """Get the session's cache.""" return self._cache @property - def database(self) -> Database | None: + def database(self) -> ndi_database | None: """Get the session's database.""" return self._database @@ -150,24 +150,24 @@ def cloud_client(self, value: Any) -> None: # DAQ System Methods # ========================================================================= - def daqsystem_add(self, dev: Any) -> Session: + def daqsystem_add(self, dev: Any) -> ndi_session: """ Add a DAQ system to the session. Args: - dev: The DAQSystem object to add + dev: The ndi_daq_system object to add Returns: self for chaining Raises: - TypeError: If dev is not a DAQSystem + TypeError: If dev is not a ndi_daq_system ValueError: If DAQ system already exists """ - from ..daq.system import DAQSystem + from ..daq.system import ndi_daq_system - if not isinstance(dev, DAQSystem): - raise TypeError("dev must be an ndi.daq.DAQSystem") + if not isinstance(dev, ndi_daq_system): + raise TypeError("dev must be an ndi.daq.ndi_daq_system") # Set the session on the DAQ system dev = dev.setsession(self) @@ -177,7 +177,7 @@ def daqsystem_add(self, dev: Any) -> Session: search_result = self.database_search(sq) # Also check by name - sq1 = Query("").isa("daqsystem") & (Query("base.name") == dev.name) + sq1 = ndi_query("").isa("daqsystem") & (ndi_query("base.name") == dev.name) search_result1 = self.database_search(sq1) if len(search_result) == 0 and len(search_result1) == 0: @@ -192,24 +192,24 @@ def daqsystem_add(self, dev: Any) -> Session: return self - def daqsystem_rm(self, dev: Any) -> Session: + def daqsystem_rm(self, dev: Any) -> ndi_session: """ Remove a DAQ system from the session. Args: - dev: The DAQSystem to remove + dev: The ndi_daq_system to remove Returns: self for chaining Raises: - TypeError: If dev is not a DAQSystem + TypeError: If dev is not a ndi_daq_system ValueError: If DAQ system not found """ - from ..daq.system import DAQSystem + from ..daq.system import ndi_daq_system - if not isinstance(dev, DAQSystem): - raise TypeError("dev must be an ndi.daq.DAQSystem") + if not isinstance(dev, ndi_daq_system): + raise TypeError("dev must be an ndi.daq.ndi_daq_system") daqsys = self.daqsystem_load(name=dev.name) if not daqsys: @@ -224,7 +224,7 @@ def daqsystem_rm(self, dev: Any) -> Session: # Remove dependencies first names, deps = doc.dependency() for dep in deps: - dep_docs = self.database_search(Query("base.id") == dep["value"]) + dep_docs = self.database_search(ndi_query("base.id") == dep["value"]) for dep_doc in dep_docs: self.database_rm(dep_doc) # Remove the document itself @@ -241,7 +241,7 @@ def daqsystem_load(self, name: str | None = None, **kwargs) -> list[Any] | Any | **kwargs: Additional field=value pairs to match Returns: - List of DAQSystem objects, single DAQSystem if only one, + List of ndi_daq_system objects, single ndi_daq_system if only one, or None if none found. Example: @@ -250,23 +250,23 @@ def daqsystem_load(self, name: str | None = None, **kwargs) -> list[Any] | Any | """ # Build query - q = Query("").isa("daqsystem") - q = q & (Query("base.session_id") == self.id()) + q = ndi_query("").isa("daqsystem") + q = q & (ndi_query("base.session_id") == self.id()) # Add name filter (using regex match for compatibility) if name is not None: - q = q & (Query("base.name").match(name)) + q = q & (ndi_query("base.name").match(name)) # Add other filters for field, value in kwargs.items(): if field == "name": continue # Already handled - q = q & (Query(field) == value) + q = q & (ndi_query(field) == value) # Search database dev_docs = self.database_search(q) - # Convert to DAQSystem objects + # Convert to ndi_daq_system objects dev = [] for doc in dev_docs: try: @@ -283,7 +283,7 @@ def daqsystem_load(self, name: str | None = None, **kwargs) -> list[Any] | Any | else: return dev - def daqsystem_clear(self) -> Session: + def daqsystem_clear(self) -> ndi_session: """ Remove all DAQ systems from the session. @@ -299,15 +299,15 @@ def daqsystem_clear(self) -> Session: return self # ========================================================================= - # Database Methods + # ndi_database Methods # ========================================================================= - def database_add(self, document: Document | list[Document]) -> Session: + def database_add(self, document: ndi_document | list[ndi_document]) -> ndi_session: """ Add a document to the session database. Args: - document: Document or list of Documents to add + document: ndi_document or list of Documents to add Returns: self for chaining @@ -316,7 +316,7 @@ def database_add(self, document: Document | list[Document]) -> Session: ValueError: If document session_id doesn't match """ if self._database is None: - raise RuntimeError("Session has no database") + raise RuntimeError("ndi_session has no database") if not isinstance(document, list): document = [document] @@ -326,7 +326,8 @@ def database_add(self, document: Document | list[Document]) -> Session: session_id = doc.session_id if session_id and session_id != self.id() and session_id != empty_id(): raise ValueError( - f"Document session_id '{session_id}' doesn't match " f"session id '{self.id()}'" + f"ndi_document session_id '{session_id}' doesn't match " + f"session id '{self.id()}'" ) # Set session ID if empty or unset if not session_id or session_id == empty_id(): @@ -339,7 +340,7 @@ def database_add(self, document: Document | list[Document]) -> Session: return self - def _ingest_binary_files(self, doc: Document) -> None: + def _ingest_binary_files(self, doc: ndi_document) -> None: """Copy binary file attachments into the database's binary directory. For each file location with ``ingest=True``, the source file is @@ -375,21 +376,21 @@ def _ingest_binary_files(self, doc: Document) -> None: def database_rm( self, - doc_or_id: Document | str | list[Document | str], + doc_or_id: ndi_document | str | list[ndi_document | str], error_if_not_found: bool = False, - ) -> Session: + ) -> ndi_session: """ Remove a document from the session database. Args: - doc_or_id: Document, document ID, or list of either + doc_or_id: ndi_document, document ID, or list of either error_if_not_found: If True, raise error when not found Returns: self for chaining """ if self._database is None: - raise RuntimeError("Session has no database") + raise RuntimeError("ndi_session has no database") if not isinstance(doc_or_id, list): doc_or_id = [doc_or_id] @@ -405,12 +406,12 @@ def database_rm( return self - def database_search(self, query: Query) -> list[Document]: + def database_search(self, query: ndi_query) -> list[ndi_document]: """ Search for documents in the session database. Args: - query: Query to match + query: ndi_query to match Returns: List of matching Documents @@ -419,10 +420,10 @@ def database_search(self, query: Query) -> list[Document]: return [] # Add session filter - in_session = Query("base.session_id") == self.id() + in_session = ndi_query("base.session_id") == self.id() return self._database.search(query & in_session) - def database_clear(self, areyousure: str) -> Session: + def database_clear(self, areyousure: str) -> ndi_session: """ Delete all documents from the database. @@ -442,12 +443,12 @@ def database_clear(self, areyousure: str) -> Session: return self - def validate_documents(self, documents: Document | list[Document]) -> tuple[bool, str]: + def validate_documents(self, documents: ndi_document | list[ndi_document]) -> tuple[bool, str]: """ Validate that documents belong to this session. Args: - documents: Document or list to validate + documents: ndi_document or list to validate Returns: Tuple of (is_valid, error_message) @@ -456,32 +457,32 @@ def validate_documents(self, documents: Document | list[Document]) -> tuple[bool documents = [documents] for doc in documents: - if not isinstance(doc, Document): - return False, "All entries must be Document objects" + if not isinstance(doc, ndi_document): + return False, "All entries must be ndi_document objects" session_id = doc.session_id if session_id != self.id() and session_id != empty_id(): return False, ( - f"Document {doc.id} has session_id '{session_id}' " + f"ndi_document {doc.id} has session_id '{session_id}' " f"which doesn't match session id '{self.id()}'" ) return True, "" # ========================================================================= - # Binary Document Methods + # Binary ndi_document Methods # ========================================================================= def database_openbinarydoc( self, - doc_or_id: Document | str, + doc_or_id: ndi_document | str, filename: str, ) -> Any: """ Open a binary document for reading. Args: - doc_or_id: Document or document ID + doc_or_id: ndi_document or document ID filename: Name of the binary file Returns: @@ -491,12 +492,12 @@ def database_openbinarydoc( The file must be closed with database_closebinarydoc() """ if self._database is None: - raise RuntimeError("Session has no database") + raise RuntimeError("ndi_session has no database") - doc_id = doc_or_id.id if isinstance(doc_or_id, Document) else doc_or_id + doc_id = doc_or_id.id if isinstance(doc_or_id, ndi_document) else doc_or_id doc = self._database.read(doc_id) if doc is None: - raise FileNotFoundError(f"Document {doc_id} not found") + raise FileNotFoundError(f"ndi_document {doc_id} not found") file_path = self._database.get_binary_path(doc, filename) if not file_path.exists(): @@ -514,14 +515,14 @@ def database_openbinarydoc( def database_existbinarydoc( self, - doc_or_id: Document | str, + doc_or_id: ndi_document | str, filename: str, ) -> tuple[bool, Path | None]: """ Check if a binary document exists. Args: - doc_or_id: Document or document ID + doc_or_id: ndi_document or document ID filename: Name of the binary file Returns: @@ -530,7 +531,7 @@ def database_existbinarydoc( if self._database is None: return False, None - doc_id = doc_or_id.id if isinstance(doc_or_id, Document) else doc_or_id + doc_id = doc_or_id.id if isinstance(doc_or_id, ndi_document) else doc_or_id doc = self._database.read(doc_id) if doc is None: return False, None @@ -550,7 +551,7 @@ def database_closebinarydoc(self, file_obj: Any) -> None: def _try_cloud_fetch( self, - doc: Document, + doc: ndi_document, filename: str, target_path: Path, ) -> bool: @@ -626,27 +627,27 @@ def _try_cloud_fetch( return False # ========================================================================= - # SyncGraph Methods + # ndi_time_syncgraph Methods # ========================================================================= - def syncgraph_addrule(self, rule: SyncRule) -> Session: + def syncgraph_addrule(self, rule: ndi_time_syncrule) -> ndi_session: """ Add a sync rule to the session's syncgraph. Args: - rule: SyncRule to add + rule: ndi_time_syncrule to add Returns: self for chaining """ if self._syncgraph is None: - self._syncgraph = SyncGraph(self) + self._syncgraph = ndi_time_syncgraph(self) self._syncgraph.add_rule(rule) self._update_syncgraph_in_db() return self - def syncgraph_rmrule(self, index: int) -> Session: + def syncgraph_rmrule(self, index: int) -> ndi_session: """ Remove a sync rule from the session's syncgraph. @@ -668,14 +669,14 @@ def _update_syncgraph_in_db(self) -> None: # Remove old syncgraph docs old_docs = self.database_search( - Query("").isa("syncgraph") & (Query("base.session_id") == self.id()) + ndi_query("").isa("syncgraph") & (ndi_query("base.session_id") == self.id()) ) for doc in old_docs: self._database.remove(doc) # Remove old syncrule docs old_rules = self.database_search( - Query("").isa("syncrule") & (Query("base.session_id") == self.id()) + ndi_query("").isa("syncrule") & (ndi_query("base.session_id") == self.id()) ) for doc in old_rules: self._database.remove(doc) @@ -739,17 +740,17 @@ def ingest(self) -> tuple[bool, str]: return success, errmsg - def get_ingested_docs(self) -> list[Document]: + def get_ingested_docs(self) -> list[ndi_document]: """ Get all documents related to ingested data. Returns: List of ingested data documents """ - q_i1 = Query("").isa("daqreader_mfdaq_epochdata_ingested") - q_i2 = Query("").isa("daqmetadatareader_epochdata_ingested") - q_i3 = Query("").isa("epochfiles_ingested") - q_i4 = Query("").isa("syncrule_mapping") + q_i1 = ndi_query("").isa("daqreader_mfdaq_epochdata_ingested") + q_i2 = ndi_query("").isa("daqmetadatareader_epochdata_ingested") + q_i3 = ndi_query("").isa("epochfiles_ingested") + q_i4 = ndi_query("").isa("syncrule_mapping") return self.database_search(q_i1 | q_i2 | q_i3 | q_i4) @@ -789,8 +790,8 @@ def isIngestedInDataset(self) -> bool: if self._database is None: return False - q = Query("").isa("session_in_a_dataset") & ( - Query("session_in_a_dataset.session_id") == self.id() + q = ndi_query("").isa("session_in_a_dataset") & ( + ndi_query("session_in_a_dataset.session_id") == self.id() ) docs = self._database.search(q) @@ -806,7 +807,7 @@ def isIngestedInDataset(self) -> bool: return False # ========================================================================= - # Probe and Element Methods + # ndi_probe and ndi_element Methods # ========================================================================= def getprobes(self, classmatch: str | None = None, **kwargs) -> list[Any]: @@ -818,9 +819,9 @@ def getprobes(self, classmatch: str | None = None, **kwargs) -> list[Any]: **kwargs: Property filters (name, reference, type, subject_id) Returns: - List of Probe objects + List of ndi_probe objects """ - from ..probe import Probe + from ..probe import ndi_probe # Get probe structs from all DAQ systems probestructs = [] @@ -844,7 +845,9 @@ def getprobes(self, classmatch: str | None = None, **kwargs) -> list[Any]: unique_probes.append(ps) # Get existing probes from database - existing_docs = self.database_search(Query("element.ndi_element_class").contains("probe")) + existing_docs = self.database_search( + ndi_query("element.ndi_element_class").contains("probe") + ) # Convert existing docs to probe objects existing_probes = [] @@ -870,7 +873,7 @@ def getprobes(self, classmatch: str | None = None, **kwargs) -> list[Any]: found = True break if not found: - probe = Probe( + probe = ndi_probe( session=self, name=ps.get("name", ""), reference=ps.get("reference", 0), @@ -883,12 +886,12 @@ def getprobes(self, classmatch: str | None = None, **kwargs) -> list[Any]: # Filter by class if classmatch is not None: - from ..element import Element - from ..probe import Probe + from ..element import ndi_element + from ..probe import ndi_probe _CLASS_LOOKUP = { - "Probe": Probe, - "Element": Element, + "ndi_probe": ndi_probe, + "ndi_element": ndi_element, } cls = _CLASS_LOOKUP.get(classmatch) if cls is None: @@ -928,15 +931,15 @@ def getelements(self, **kwargs) -> list[Any]: **kwargs: Property filters (e.g., element.name, element.type) Returns: - List of Element objects + List of ndi_element objects """ - q = Query("").isa("element") + q = ndi_query("").isa("element") for field, value in kwargs.items(): if "reference" in field: - q = q & (Query(field) == value) + q = q & (ndi_query(field) == value) else: - q = q & (Query(field) == value) + q = q & (ndi_query(field) == value) docs = self.database_search(q) @@ -962,8 +965,8 @@ def findexpobj(self, obj_name: str, obj_classname: str) -> Any | None: Args: obj_name: Name of the object to find. - obj_classname: Class name to match (e.g. ``'Probe'``, - ``'DAQSystem'``). + obj_classname: Class name to match (e.g. ``'ndi_probe'``, + ``'ndi_daq_system'``). Returns: The matching object, or None if not found. @@ -1009,46 +1012,48 @@ def findexpobj(self, obj_name: str, obj_classname: str) -> Any | None: return None # ========================================================================= - # Document Service Methods + # ndi_document Service Methods # ========================================================================= - def newdocument(self, document_type: str = "base", **properties) -> Document: + def newdocument(self, document_type: str = "base", **properties) -> ndi_document: """ Create a new document for this session. Args: document_type: Type of document to create - **properties: Document properties + **properties: ndi_document properties Returns: - New Document with session_id set + New ndi_document with session_id set """ # Add session_id to properties properties["base.session_id"] = self.id() - return Document(document_type, **properties) + return ndi_document(document_type, **properties) - def searchquery(self) -> Query: + def searchquery(self) -> ndi_query: """ Create a query for documents in this session. Returns: - Query matching this session's documents + ndi_query matching this session's documents """ - return Query("base.session_id") == self.id() + return ndi_query("base.session_id") == self.id() # ========================================================================= # Helper Methods # ========================================================================= - def _docinput2docs(self, doc_input: str | Document | list[str | Document]) -> list[Document]: + def _docinput2docs( + self, doc_input: str | ndi_document | list[str | ndi_document] + ) -> list[ndi_document]: """Convert document IDs or Documents to Documents.""" if not isinstance(doc_input, list): doc_input = [doc_input] doc_list = [] for item in doc_input: - if isinstance(item, Document): + if isinstance(item, ndi_document): doc_list.append(item) elif isinstance(item, str): doc = self._database.read(item) if self._database else None @@ -1057,13 +1062,13 @@ def _docinput2docs(self, doc_input: str | Document | list[str | Document]) -> li return doc_list - def _find_all_dependencies(self, document: Document) -> list[Document]: + def _find_all_dependencies(self, document: ndi_document) -> list[ndi_document]: """Find all documents that depend on the given document.""" dependents = [] if self._database is None: return dependents - q = Query("").depends_on("", document.id) + q = ndi_query("").depends_on("", document.id) results = self.database_search(q) for doc in results: @@ -1073,61 +1078,48 @@ def _find_all_dependencies(self, document: Document) -> list[Document]: return dependents - def _document_to_object(self, document: Document) -> Any: + def _document_to_object(self, document: ndi_document) -> Any: """ Convert a document to its corresponding NDI object. MATLAB equivalent: ``ndi.database.fun.ndi_document2ndi_object`` - For element documents MATLAB reads - ``element.ndi_element_class`` (e.g. - ``'ndi.probe.timeseries.mfdaq'``) and calls - ``eval([class_string '(session, doc)'])``. Python mirrors - this by maintaining a registry that maps MATLAB class name - strings to the corresponding Python classes. + Uses the unified :mod:`ndi.class_registry` to map NDI class + identifier strings (stored in document properties) to their + Python implementations. Args: - document: Document to convert + document: ndi_document to convert Returns: The NDI object or None """ + from ..class_registry import get_class + # Check document type if document.doc_isa("daqsystem"): - from ..daq.system import DAQSystem + from ..daq.system import ndi_daq_system - return DAQSystem(session=self, document=document) + return ndi_daq_system(session=self, document=document) if document.doc_isa("element"): - # Mirror MATLAB ndi_document2ndi_object: read the - # ndi_element_class field and construct the right class. - from ..element import Element - from ..probe import Probe - from ..probe.timeseries import ProbeTimeseries - from ..probe.timeseries_mfdaq import ProbeTimeseriesMFDAQ - from ..probe.timeseries_stimulator import ProbeTimeseriesStimulator - - _NDI_CLASS_REGISTRY: dict[str, type] = { - "ndi.element": Element, - "ndi.probe": Probe, - "ndi.probe.timeseries": ProbeTimeseries, - "ndi.probe.timeseries.mfdaq": ProbeTimeseriesMFDAQ, - "ndi.probe.timeseries.stimulator": ProbeTimeseriesStimulator, - } - props = document.document_properties ndi_class = props.get("element", {}).get("ndi_element_class", "") - cls = _NDI_CLASS_REGISTRY.get(ndi_class) + cls = get_class(ndi_class) if cls is not None: return cls(session=self, document=document) # Fallback: if ndi_element_class contains "probe" but - # isn't a known key (e.g. "ndi.probe.timage"), use Probe. + # isn't a known key (e.g. "ndi.probe.timage"), use ndi_probe. + from ..probe import ndi_probe + if "probe" in ndi_class: - return Probe(session=self, document=document) - return Element(session=self, document=document) + return ndi_probe(session=self, document=document) + from ..element import ndi_element + + return ndi_element(session=self, document=document) if document.doc_isa("syncgraph"): - return SyncGraph(session=self, document=document) + return ndi_time_syncgraph(session=self, document=document) return None @@ -1161,7 +1153,7 @@ def creator_args(self) -> list[Any]: def __eq__(self, other: Any) -> bool: """Check equality by identifier.""" - if not isinstance(other, Session): + if not isinstance(other, ndi_session): return False return self.id() == other.id() diff --git a/src/ndi/session/sessiontable.py b/src/ndi/session/sessiontable.py index aa86506..0591688 100644 --- a/src/ndi/session/sessiontable.py +++ b/src/ndi/session/sessiontable.py @@ -15,7 +15,7 @@ from typing import Any -class SessionTable: +class ndi_session_sessiontable: """ Persistent local registry of NDI sessions. @@ -27,7 +27,7 @@ class SessionTable: by default. Example: - >>> table = SessionTable() + >>> table = ndi_session_sessiontable() >>> table.addtableentry('abc123', '/data/experiment1') >>> table.getsessionpath('abc123') '/data/experiment1' @@ -37,7 +37,7 @@ class SessionTable: def __init__(self, table_path: Path | None = None): """ - Create a SessionTable instance. + Create a ndi_session_sessiontable instance. Args: table_path: Override the default table file location. @@ -118,7 +118,7 @@ def addtableentry(self, session_id: str, path: str) -> None: If *session_id* already exists it is replaced with the new *path*. Args: - session_id: Session identifier (must be a non-empty string). + session_id: ndi_session identifier (must be a non-empty string). path: Filesystem path to the session directory. Raises: @@ -290,4 +290,4 @@ def _writesessiontable(self, entries: list[dict[str, str]]) -> None: writer.writerows(entries) def __repr__(self) -> str: - return f"SessionTable(path={self._table_path})" + return f"ndi_session_sessiontable(path={self._table_path})" diff --git a/src/ndi/subject.py b/src/ndi/subject.py index 496098b..bc29f9d 100644 --- a/src/ndi/subject.py +++ b/src/ndi/subject.py @@ -1,5 +1,5 @@ """ -ndi.subject - Subject class for managing experimental subjects. +ndi.subject - ndi_subject class for managing experimental subjects. Subjects represent the experimental entities (animals, humans, etc.) that are the source of recorded data. Each subject has a unique @@ -10,19 +10,19 @@ from typing import TYPE_CHECKING, Any -from .documentservice import DocumentService -from .ido import Ido +from .documentservice import ndi_documentservice +from .ido import ndi_ido if TYPE_CHECKING: - from .document import Document - from .query import Query + from .document import ndi_document + from .query import ndi_query -class Subject(Ido, DocumentService): +class ndi_subject(ndi_ido, ndi_documentservice): """ Represents an experimental subject. - A Subject is identified by a local_identifier that must contain '@' + A ndi_subject is identified by a local_identifier that must contain '@' (e.g., 'anteater23@nosuchlab.org') and an optional description. Can be created from scratch or loaded from a session database document. @@ -32,7 +32,7 @@ class Subject(Ido, DocumentService): description: Free-form description string Example: - >>> subject = Subject('mouse23@vhlab.org', 'Laboratory mouse, strain C57BL/6') + >>> subject = ndi_subject('mouse23@vhlab.org', 'Laboratory mouse, strain C57BL/6') >>> doc = subject.newdocument() """ @@ -43,20 +43,20 @@ def __init__( identifier: str | None = None, ): """ - Create a new Subject. + Create a new ndi_subject. Forms: - Subject(local_identifier, description) - Subject(session, document_or_id) + ndi_subject(local_identifier, description) + ndi_subject(session, document_or_id) Args: local_identifier_or_session: Either a local_identifier string - or a Session object (when loading from document) + or a ndi_session object (when loading from document) description_or_document: Either a description string - or a Document/document_id (when loading from session) + or a ndi_document/document_id (when loading from session) identifier: Optional unique identifier (auto-generated if None) """ - Ido.__init__(self, identifier) + ndi_ido.__init__(self, identifier) # Determine construction mode if hasattr(local_identifier_or_session, "database_search"): @@ -72,7 +72,7 @@ def __init__( description = str(description_or_document) if description_or_document else "" if local_identifier: - valid, msg = Subject.is_valid_local_identifier(local_identifier) + valid, msg = ndi_subject.is_valid_local_identifier(local_identifier) if not valid: raise ValueError(msg) @@ -81,21 +81,21 @@ def __init__( def _load_from_session(self, session: Any, doc_or_id: Any) -> None: """Load subject from a session database document.""" - from .document import Document + from .document import ndi_document if isinstance(doc_or_id, str): # It's a document ID - look it up - from .query import Query + from .query import ndi_query - q = Query("base.id") == doc_or_id + q = ndi_query("base.id") == doc_or_id docs = session.database_search(q) if not docs: raise ValueError(f"No document found with id '{doc_or_id}'") doc = docs[0] - elif isinstance(doc_or_id, Document): + elif isinstance(doc_or_id, ndi_document): doc = doc_or_id else: - raise TypeError(f"Expected Document or document ID string, got {type(doc_or_id)}") + raise TypeError(f"Expected ndi_document or document ID string, got {type(doc_or_id)}") props = doc.document_properties subject_props = props.get("subject", {}) @@ -119,19 +119,19 @@ def description(self) -> str: return self._description # ========================================================================= - # DocumentService Implementation + # ndi_documentservice Implementation # ========================================================================= - def newdocument(self) -> Document: + def newdocument(self) -> ndi_document: """ Create a new subject document. Returns: - Document of type 'subject' with local_identifier and description + ndi_document of type 'subject' with local_identifier and description """ - from .document import Document + from .document import ndi_document - doc = Document( + doc = ndi_document( "subject", **{ "subject.local_identifier": self._local_identifier, @@ -141,17 +141,17 @@ def newdocument(self) -> Document: ) return doc - def searchquery(self) -> Query: + def searchquery(self) -> ndi_query: """ Create a query to find this subject in the database. Returns: - Query matching subject by local_identifier + ndi_query matching subject by local_identifier """ - from .query import Query + from .query import ndi_query - return Query("").isa("subject") & ( - Query("subject.local_identifier") == self._local_identifier + return ndi_query("").isa("subject") & ( + ndi_query("subject.local_identifier") == self._local_identifier ) # ========================================================================= @@ -195,23 +195,23 @@ def does_subjectstring_match_session_document( Check if a subject string matches a subject in the session database. Args: - session: Session to search - subjectstring: Subject local_identifier to search for + session: ndi_session to search + subjectstring: ndi_subject local_identifier to search for make_if_missing: If True, create the subject if not found Returns: Tuple of (found, subject_document_id) """ - from .query import Query + from .query import ndi_query - q = Query("").isa("subject") & (Query("subject.local_identifier") == subjectstring) + q = ndi_query("").isa("subject") & (ndi_query("subject.local_identifier") == subjectstring) docs = session.database_search(q) if docs: return True, docs[0].id if make_if_missing: - subject = Subject(subjectstring, "") + subject = ndi_subject(subjectstring, "") doc = subject.newdocument() session.database_add(doc) return True, doc.id @@ -224,7 +224,7 @@ def does_subjectstring_match_session_document( def __eq__(self, other: Any) -> bool: """Test equality by local_identifier.""" - if not isinstance(other, Subject): + if not isinstance(other, ndi_subject): return False return self._local_identifier == other._local_identifier @@ -234,4 +234,4 @@ def __hash__(self) -> int: def __repr__(self) -> str: """String representation.""" - return f"Subject('{self._local_identifier}')" + return f"ndi_subject('{self._local_identifier}')" diff --git a/src/ndi/time/__init__.py b/src/ndi/time/__init__.py index ed97db5..66bfe6c 100644 --- a/src/ndi/time/__init__.py +++ b/src/ndi/time/__init__.py @@ -5,26 +5,26 @@ epochs and devices in neuroscience data. Classes: - ClockType: Enumeration of clock types (UTC, dev_local_time, etc.) - TimeMapping: Polynomial time transformation - TimeReference: Time specification relative to a clock - SyncRule: Abstract base for synchronization rules - SyncGraph: Graph-based time conversion + ndi_time_clocktype: Enumeration of clock types (UTC, dev_local_time, etc.) + ndi_time_timemapping: Polynomial time transformation + ndi_time_timereference: Time specification relative to a clock + ndi_time_syncrule: Abstract base for synchronization rules + ndi_time_syncgraph: Graph-based time conversion Submodules: - syncrule: Concrete SyncRule implementations (FileMatch, FileFind) + syncrule: Concrete ndi_time_syncrule implementations (ndi_time_syncrule_filematch, ndi_time_syncrule_filefind) Example: - >>> from ndi.time import ClockType, TimeMapping, SyncGraph - >>> from ndi.time.syncrule import FileMatch + >>> from ndi.time import ndi_time_clocktype, ndi_time_timemapping, ndi_time_syncgraph + >>> from ndi.time.syncrule import ndi_time_syncrule_filematch >>> >>> # Create a sync graph - >>> sg = SyncGraph(session) - >>> sg.add_rule(FileMatch()) + >>> sg = ndi_time_syncgraph(session) + >>> sg.add_rule(ndi_time_syncrule_filematch()) >>> >>> # Convert time between epochs >>> t_out, ref_out, msg = sg.time_convert( - ... timeref_in, t_in, referent_out, ClockType.UTC + ... timeref_in, t_in, referent_out, ndi_time_clocktype.UTC ... ) """ @@ -40,16 +40,16 @@ INHERITED, NO_TIME, UTC, - ClockType, + ndi_time_clocktype, ) -from .syncgraph import EpochNode, GraphInfo, SyncGraph -from .syncrule_base import SyncRule -from .timemapping import TimeMapping -from .timereference import TimeReference, TimeReferenceStruct +from .syncgraph import ndi_time_epochnode, ndi_time_graphinfo, ndi_time_syncgraph +from .syncrule_base import ndi_time_syncrule +from .timemapping import ndi_time_timemapping +from .timereference import ndi_time_timereference, ndi_time_timereference__struct __all__ = [ # Clock types - "ClockType", + "ndi_time_clocktype", "UTC", "APPROX_UTC", "EXP_GLOBAL_TIME", @@ -60,16 +60,16 @@ "NO_TIME", "INHERITED", # Time mapping - "TimeMapping", + "ndi_time_timemapping", # Time reference - "TimeReference", - "TimeReferenceStruct", + "ndi_time_timereference", + "ndi_time_timereference__struct", # Sync rule - "SyncRule", + "ndi_time_syncrule", # Sync graph - "SyncGraph", - "EpochNode", - "GraphInfo", + "ndi_time_syncgraph", + "ndi_time_epochnode", + "ndi_time_graphinfo", # Submodule "syncrule", ] diff --git a/src/ndi/time/clocktype.py b/src/ndi/time/clocktype.py index 557b8ae..43ad22d 100644 --- a/src/ndi/time/clocktype.py +++ b/src/ndi/time/clocktype.py @@ -1,7 +1,7 @@ """ ndi.time.clocktype - Clock type enumeration for NDI framework. -This module provides the ClockType class for specifying clock types +This module provides the ndi_time_clocktype class for specifying clock types used in time synchronization across epochs and devices. """ @@ -11,10 +11,10 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from .timemapping import TimeMapping + from .timemapping import ndi_time_timemapping -class ClockType(Enum): +class ndi_time_clocktype(Enum): """ Clock type enumeration for specifying timing precision and scope. @@ -42,15 +42,15 @@ class ClockType(Enum): INHERITED = "inherited" @classmethod - def from_string(cls, type_str: str) -> ClockType: + def from_string(cls, type_str: str) -> ndi_time_clocktype: """ - Create a ClockType from a string. + Create a ndi_time_clocktype from a string. Args: type_str: Clock type string (case-insensitive) Returns: - ClockType enum value + ndi_time_clocktype enum value Raises: ValueError: If the type string is not recognized @@ -72,7 +72,7 @@ def needs_epoch(self) -> bool: Returns: True if clock type is DEV_LOCAL_TIME, False otherwise """ - return self == ClockType.DEV_LOCAL_TIME + return self == ndi_time_clocktype.DEV_LOCAL_TIME def is_global(self) -> bool: """ @@ -84,17 +84,17 @@ def is_global(self) -> bool: True if this is a global clock type """ global_types = { - ClockType.UTC, - ClockType.APPROX_UTC, - ClockType.EXP_GLOBAL_TIME, - ClockType.APPROX_EXP_GLOBAL_TIME, - ClockType.DEV_GLOBAL_TIME, - ClockType.APPROX_DEV_GLOBAL_TIME, + ndi_time_clocktype.UTC, + ndi_time_clocktype.APPROX_UTC, + ndi_time_clocktype.EXP_GLOBAL_TIME, + ndi_time_clocktype.APPROX_EXP_GLOBAL_TIME, + ndi_time_clocktype.DEV_GLOBAL_TIME, + ndi_time_clocktype.APPROX_DEV_GLOBAL_TIME, } return self in global_types @staticmethod - def assert_global(clocktype: ClockType) -> None: + def assert_global(clocktype: ndi_time_clocktype) -> None: """ Assert that a clock type is global, raising an error if not. @@ -107,19 +107,21 @@ def assert_global(clocktype: ClockType) -> None: if not clocktype.is_global(): valid_types = ", ".join( [ - ClockType.UTC.value, - ClockType.APPROX_UTC.value, - ClockType.EXP_GLOBAL_TIME.value, - ClockType.APPROX_EXP_GLOBAL_TIME.value, - ClockType.DEV_GLOBAL_TIME.value, - ClockType.APPROX_DEV_GLOBAL_TIME.value, + ndi_time_clocktype.UTC.value, + ndi_time_clocktype.APPROX_UTC.value, + ndi_time_clocktype.EXP_GLOBAL_TIME.value, + ndi_time_clocktype.APPROX_EXP_GLOBAL_TIME.value, + ndi_time_clocktype.DEV_GLOBAL_TIME.value, + ndi_time_clocktype.APPROX_DEV_GLOBAL_TIME.value, ] ) raise AssertionError( f"Clock type must be one of: {valid_types}. Got: {clocktype.value}" ) - def epochgraph_edge(self, other: ClockType) -> tuple[float, TimeMapping | None]: + def epochgraph_edge( + self, other: ndi_time_clocktype + ) -> tuple[float, ndi_time_timemapping | None]: """ Provide epochgraph edge based purely on clock type. @@ -138,37 +140,37 @@ def epochgraph_edge(self, other: ClockType) -> tuple[float, TimeMapping | None]: Returns: Tuple of (cost, mapping) where cost is float and mapping is - a TimeMapping object or None if no mapping exists. + a ndi_time_timemapping object or None if no mapping exists. """ - from .timemapping import TimeMapping + from .timemapping import ndi_time_timemapping # No mapping possible with no_time - if self == ClockType.NO_TIME or other == ClockType.NO_TIME: + if self == ndi_time_clocktype.NO_TIME or other == ndi_time_clocktype.NO_TIME: return float("inf"), None # Define valid transitions valid_transitions = [ - (ClockType.UTC, ClockType.UTC), - (ClockType.UTC, ClockType.APPROX_UTC), - (ClockType.EXP_GLOBAL_TIME, ClockType.EXP_GLOBAL_TIME), - (ClockType.EXP_GLOBAL_TIME, ClockType.APPROX_EXP_GLOBAL_TIME), - (ClockType.DEV_GLOBAL_TIME, ClockType.DEV_GLOBAL_TIME), - (ClockType.DEV_GLOBAL_TIME, ClockType.APPROX_DEV_GLOBAL_TIME), + (ndi_time_clocktype.UTC, ndi_time_clocktype.UTC), + (ndi_time_clocktype.UTC, ndi_time_clocktype.APPROX_UTC), + (ndi_time_clocktype.EXP_GLOBAL_TIME, ndi_time_clocktype.EXP_GLOBAL_TIME), + (ndi_time_clocktype.EXP_GLOBAL_TIME, ndi_time_clocktype.APPROX_EXP_GLOBAL_TIME), + (ndi_time_clocktype.DEV_GLOBAL_TIME, ndi_time_clocktype.DEV_GLOBAL_TIME), + (ndi_time_clocktype.DEV_GLOBAL_TIME, ndi_time_clocktype.APPROX_DEV_GLOBAL_TIME), ] if (self, other) in valid_transitions: - return 100.0, TimeMapping.identity() + return 100.0, ndi_time_timemapping.identity() return float("inf"), None # Convenience aliases for common clock types -UTC = ClockType.UTC -APPROX_UTC = ClockType.APPROX_UTC -EXP_GLOBAL_TIME = ClockType.EXP_GLOBAL_TIME -APPROX_EXP_GLOBAL_TIME = ClockType.APPROX_EXP_GLOBAL_TIME -DEV_GLOBAL_TIME = ClockType.DEV_GLOBAL_TIME -APPROX_DEV_GLOBAL_TIME = ClockType.APPROX_DEV_GLOBAL_TIME -DEV_LOCAL_TIME = ClockType.DEV_LOCAL_TIME -NO_TIME = ClockType.NO_TIME -INHERITED = ClockType.INHERITED +UTC = ndi_time_clocktype.UTC +APPROX_UTC = ndi_time_clocktype.APPROX_UTC +EXP_GLOBAL_TIME = ndi_time_clocktype.EXP_GLOBAL_TIME +APPROX_EXP_GLOBAL_TIME = ndi_time_clocktype.APPROX_EXP_GLOBAL_TIME +DEV_GLOBAL_TIME = ndi_time_clocktype.DEV_GLOBAL_TIME +APPROX_DEV_GLOBAL_TIME = ndi_time_clocktype.APPROX_DEV_GLOBAL_TIME +DEV_LOCAL_TIME = ndi_time_clocktype.DEV_LOCAL_TIME +NO_TIME = ndi_time_clocktype.NO_TIME +INHERITED = ndi_time_clocktype.INHERITED diff --git a/src/ndi/time/ndi_matlab_python_bridge.yaml b/src/ndi/time/ndi_matlab_python_bridge.yaml index ea8cb7a..6230f62 100644 --- a/src/ndi/time/ndi_matlab_python_bridge.yaml +++ b/src/ndi/time/ndi_matlab_python_bridge.yaml @@ -19,7 +19,7 @@ classes: matlab_path: "+ndi/+time/clocktype.m" matlab_last_sync_hash: "821b5af5" python_path: "ndi/time/clocktype.py" - python_class: "ClockType" + python_class: "ndi_time_clocktype" properties: - name: UTC @@ -76,7 +76,7 @@ classes: type_python: "str" output_arguments: - name: ct - type_python: "ClockType" + type_python: "ndi_time_clocktype" decision_log: > Python-specific factory. MATLAB uses constructor. Case-insensitive matching. @@ -103,12 +103,12 @@ classes: input_arguments: - name: other type_matlab: "ndi.time.clocktype" - type_python: "ClockType" + type_python: "ndi_time_clocktype" output_arguments: - name: cost type_python: "float" - name: mapping - type_python: "TimeMapping | None" + type_python: "ndi_time_timemapping | None" decision_log: > Exact match. Returns 2-tuple (cost, mapping). Valid transitions (cost=100, identity mapping): @@ -120,7 +120,7 @@ classes: - name: assert_global input_arguments: - name: clocktype - type_python: "ClockType" + type_python: "ndi_time_clocktype" output_arguments: [] decision_log: > Exact match. Raises AssertionError if clock type is @@ -134,7 +134,7 @@ classes: matlab_path: "+ndi/+time/timemapping.m" matlab_last_sync_hash: "89d6b45f" python_path: "ndi/time/timemapping.py" - python_class: "TimeMapping" + python_class: "ndi_time_timemapping" properties: - name: mapping @@ -166,7 +166,7 @@ classes: default: "None" output_arguments: - name: tm_obj - type_python: "TimeMapping" + type_python: "ndi_time_timemapping" decision_log: > Exact match. Default is [1, 0] (identity). Tests mapping with t_in=0 on construction. @@ -186,7 +186,7 @@ classes: input_arguments: [] output_arguments: - name: tm_inv - type_python: "TimeMapping" + type_python: "ndi_time_timemapping" decision_log: > Exact match. Only supports linear mappings. Raises ValueError for higher-order or zero-scale mappings. @@ -195,10 +195,10 @@ classes: input_arguments: - name: other type_matlab: "ndi.time.timemapping" - type_python: "TimeMapping" + type_python: "ndi_time_timemapping" output_arguments: - name: tm_composed - type_python: "TimeMapping" + type_python: "ndi_time_timemapping" decision_log: > Exact match. Composes self then other. Only supports linear mappings. Raises ValueError for higher-order. @@ -228,7 +228,7 @@ classes: type_python: "dict" output_arguments: - name: tm_obj - type_python: "TimeMapping" + type_python: "ndi_time_timemapping" decision_log: "Python-specific factory method." static_methods: @@ -237,8 +237,8 @@ classes: input_arguments: [] output_arguments: - name: tm_obj - type_python: "TimeMapping" - decision_log: "Exact match. Returns TimeMapping([1, 0])." + type_python: "ndi_time_timemapping" + decision_log: "Exact match. Returns ndi_time_timemapping([1, 0])." - name: linear kind: classmethod @@ -253,10 +253,10 @@ classes: default: "0.0" output_arguments: - name: tm_obj - type_python: "TimeMapping" + type_python: "ndi_time_timemapping" decision_log: > Python-specific convenience factory. Creates - TimeMapping([scale, shift]). + ndi_time_timemapping([scale, shift]). # ========================================================================= # ndi.time.timereference @@ -266,7 +266,7 @@ classes: matlab_path: "+ndi/+time/timereference.m" matlab_last_sync_hash: "fe64a9f5" python_path: "ndi/time/timereference.py" - python_class: "TimeReference" + python_class: "ndi_time_timereference" properties: - name: referent @@ -277,7 +277,7 @@ classes: - name: clocktype type_matlab: "ndi.time.clocktype" - type_python: "ClockType" + type_python: "ndi_time_clocktype" access: "GetAccess=public, SetAccess=protected" decision_log: "Implemented as @property in Python." @@ -312,7 +312,7 @@ classes: type_python: "Any" - name: clocktype type_matlab: "ndi.time.clocktype | char" - type_python: "ClockType | str" + type_python: "ndi_time_clocktype | str" - name: epoch type_matlab: "char" type_python: "str | None" @@ -327,7 +327,7 @@ classes: default: "None" output_arguments: - name: tr_obj - type_python: "TimeReference" + type_python: "ndi_time_timereference" decision_log: > Exact match. Accepts clocktype as string or enum. Raises ValueError if DEV_LOCAL_TIME without epoch. @@ -336,7 +336,7 @@ classes: input_arguments: [] output_arguments: - name: trs - type_python: "TimeReferenceStruct" + type_python: "ndi_time_timereference__struct" decision_log: > MATLAB: ndi_timereference2struct. Python uses to_struct(). Converts to a serializable struct without live objects. @@ -367,10 +367,10 @@ classes: type_matlab: "ndi.session" type_python: "Any" - name: struct - type_python: "TimeReferenceStruct" + type_python: "ndi_time_timereference__struct" output_arguments: - name: tr_obj - type_python: "TimeReference" + type_python: "ndi_time_timereference" decision_log: > MATLAB: ndi_timereferencestruct2timereference. Python uses classmethod from_struct(). Resolves referent from @@ -383,7 +383,7 @@ classes: type: class matlab_path: null python_path: "ndi/time/timereference.py" - python_class: "TimeReferenceStruct" + python_class: "ndi_time_timereference__struct" properties: - name: referent_epochsetname @@ -400,11 +400,11 @@ classes: - name: epoch type_python: "str | None" - decision_log: "Epoch identifier." + decision_log: "ndi_epoch_epoch identifier." - name: session_id type_python: "str" - decision_log: "Session identifier." + decision_log: "ndi_session identifier." - name: time type_python: "float | None" @@ -422,7 +422,7 @@ classes: matlab_path: "+ndi/+time/syncrule.m" matlab_last_sync_hash: "85b35859" python_path: "ndi/time/syncrule_base.py" - python_class: "SyncRule" + python_class: "ndi_time_syncrule" inherits: "ndi.ido" properties: @@ -447,7 +447,7 @@ classes: default: "None" output_arguments: - name: sr_obj - type_python: "SyncRule" + type_python: "ndi_time_syncrule" decision_log: > Abstract class. Validates parameters on construction. @@ -479,7 +479,7 @@ classes: input_arguments: [] output_arguments: - name: clocks - type_python: "list[ClockType]" + type_python: "list[ndi_time_clocktype]" decision_log: > Exact match. Returns empty list by default (no limits). @@ -487,9 +487,9 @@ classes: input_arguments: [] output_arguments: - name: clocks - type_python: "list[ClockType]" + type_python: "list[ndi_time_clocktype]" decision_log: > - Exact match. Base returns [ClockType.NO_TIME]. + Exact match. Base returns [ndi_time_clocktype.NO_TIME]. - name: eligible_epochsets input_arguments: [] @@ -520,7 +520,7 @@ classes: - name: cost type_python: "float | None" - name: mapping - type_python: "TimeMapping | None" + type_python: "ndi_time_timemapping | None" decision_log: > Exact match. Returns 2-tuple (cost, mapping). Base returns (None, None). @@ -549,7 +549,7 @@ classes: output_arguments: - name: q type_python: "Any" - decision_log: "Exact match. Returns Query matching base.id." + decision_log: "Exact match. Returns ndi_query matching base.id." - name: to_dict input_arguments: [] @@ -570,7 +570,7 @@ classes: type_python: "Any" output_arguments: - name: sr_obj - type_python: "SyncRule" + type_python: "ndi_time_syncrule" decision_log: > Factory method. Creates the appropriate subclass based on the document's syncrule.ndi_syncrule_class field. @@ -583,7 +583,7 @@ classes: matlab_path: "+ndi/+time/syncgraph.m" matlab_last_sync_hash: "dde9edf9" python_path: "ndi/time/syncgraph.py" - python_class: "SyncGraph" + python_class: "ndi_time_syncgraph" inherits: "ndi.ido" properties: @@ -595,7 +595,7 @@ classes: - name: rules type_matlab: "ndi.time.syncrule array" - type_python: "list[SyncRule]" + type_python: "list[ndi_time_syncrule]" access: "GetAccess=public, SetAccess=protected" decision_log: > Implemented as @property in Python. Returns a copy. @@ -618,7 +618,7 @@ classes: default: "None" output_arguments: - name: sg_obj - type_python: "SyncGraph" + type_python: "ndi_time_syncgraph" decision_log: > Exact match. Can load from document. Requires networkx. @@ -626,10 +626,10 @@ classes: input_arguments: - name: rule type_matlab: "ndi.time.syncrule" - type_python: "SyncRule" + type_python: "ndi_time_syncrule" output_arguments: - name: sg_obj - type_python: "SyncGraph" + type_python: "ndi_time_syncgraph" decision_log: > Exact match. Returns self for chaining. Checks for duplicates. Clears cached graph info. @@ -641,7 +641,7 @@ classes: type_python: "int" output_arguments: - name: sg_obj - type_python: "SyncGraph" + type_python: "ndi_time_syncgraph" decision_log: > Exact match. Returns self for chaining. Uses 0-based indexing (internal data structure access). @@ -650,16 +650,16 @@ classes: input_arguments: [] output_arguments: - name: ginfo - type_python: "GraphInfo" + type_python: "ndi_time_graphinfo" decision_log: > Exact match. Builds graph lazily and returns cached - GraphInfo with nodes, cost matrix, mappings. + ndi_time_graphinfo with nodes, cost matrix, mappings. - name: time_convert input_arguments: - name: timeref_in type_matlab: "ndi.time.timereference" - type_python: "TimeReference" + type_python: "ndi_time_timereference" - name: t_in type_matlab: "double" type_python: "float" @@ -668,12 +668,12 @@ classes: type_python: "Any" - name: clocktype_out type_matlab: "ndi.time.clocktype" - type_python: "ClockType" + type_python: "ndi_time_clocktype" output_arguments: - name: t_out type_python: "float | None" - name: timeref_out - type_python: "TimeReference | None" + type_python: "ndi_time_timereference | None" - name: msg type_python: "str" decision_log: > @@ -704,7 +704,7 @@ classes: output_arguments: - name: q type_python: "Any" - decision_log: "Exact match. Returns Query matching base.id and session_id." + decision_log: "Exact match. Returns ndi_query matching base.id and session_id." # ========================================================================= # ndi.time.syncgraph.epochnode (dataclass) @@ -713,23 +713,23 @@ classes: type: class matlab_path: null python_path: "ndi/time/syncgraph.py" - python_class: "EpochNode" + python_class: "ndi_time_epochnode" properties: - name: epoch_id type_python: "str" - decision_log: "Epoch identifier." + decision_log: "ndi_epoch_epoch identifier." - name: epoch_session_id type_python: "str" - decision_log: "Session identifier." + decision_log: "ndi_session identifier." - name: epochprobemap type_python: "Any" - decision_log: "Probe map for this epoch." + decision_log: "ndi_probe map for this epoch." - name: epoch_clock - type_python: "ClockType" + type_python: "ndi_time_clocktype" decision_log: "Clock type for this epoch." - name: t0_t1 @@ -763,7 +763,7 @@ classes: type_python: "dict[str, Any]" output_arguments: - name: node - type_python: "EpochNode" + type_python: "ndi_time_epochnode" decision_log: "Python-specific factory." decision_log: > @@ -777,11 +777,11 @@ classes: type: class matlab_path: null python_path: "ndi/time/syncgraph.py" - python_class: "GraphInfo" + python_class: "ndi_time_graphinfo" properties: - name: nodes - type_python: "list[EpochNode]" + type_python: "list[ndi_time_epochnode]" decision_log: "List of epoch nodes." - name: G @@ -789,8 +789,8 @@ classes: decision_log: "Cost matrix G[i,j] = cost from node i to j." - name: mapping - type_python: "list[list[TimeMapping | None]] | None" - decision_log: "Matrix of TimeMapping objects." + type_python: "list[list[ndi_time_timemapping | None]] | None" + decision_log: "Matrix of ndi_time_timemapping objects." - name: diG type_python: "Any" diff --git a/src/ndi/time/syncgraph.py b/src/ndi/time/syncgraph.py index e1efdf7..1af4f11 100644 --- a/src/ndi/time/syncgraph.py +++ b/src/ndi/time/syncgraph.py @@ -1,7 +1,7 @@ """ ndi.time.syncgraph - Synchronization graph for time conversion. -This module provides the SyncGraph class that manages time synchronization +This module provides the ndi_time_syncgraph class that manages time synchronization across epochs and devices using a graph-based approach. """ @@ -19,18 +19,18 @@ except ImportError: HAS_NETWORKX = False -from ..ido import Ido -from .clocktype import ClockType -from .syncrule_base import SyncRule -from .timemapping import TimeMapping +from ..ido import ndi_ido +from .clocktype import ndi_time_clocktype +from .syncrule_base import ndi_time_syncrule +from .timemapping import ndi_time_timemapping if TYPE_CHECKING: - from ..document import Document - from .timereference import TimeReference + from ..document import ndi_document + from .timereference import ndi_time_timereference @dataclass -class EpochNode: +class ndi_time_epochnode: """ Represents a node in the epoch graph. @@ -40,7 +40,7 @@ class EpochNode: epoch_id: str epoch_session_id: str epochprobemap: Any # The probe map for this epoch - epoch_clock: ClockType + epoch_clock: ndi_time_clocktype t0_t1: tuple[float, float] # Start and end times underlying_epochs: dict[str, Any] | None = None objectname: str = "" @@ -54,7 +54,7 @@ def to_dict(self) -> dict[str, Any]: "epochprobemap": self.epochprobemap, "epoch_clock": ( self.epoch_clock.value - if isinstance(self.epoch_clock, ClockType) + if isinstance(self.epoch_clock, ndi_time_clocktype) else str(self.epoch_clock) ), "t0_t1": list(self.t0_t1) if self.t0_t1 else None, @@ -64,11 +64,11 @@ def to_dict(self) -> dict[str, Any]: } @classmethod - def from_dict(cls, data: dict[str, Any]) -> EpochNode: + def from_dict(cls, data: dict[str, Any]) -> ndi_time_epochnode: """Create from dictionary.""" epoch_clock = data.get("epoch_clock") if isinstance(epoch_clock, str): - epoch_clock = ClockType.from_string(epoch_clock) + epoch_clock = ndi_time_clocktype.from_string(epoch_clock) t0_t1 = data.get("t0_t1") if isinstance(t0_t1, list): @@ -87,38 +87,38 @@ def from_dict(cls, data: dict[str, Any]) -> EpochNode: @dataclass -class GraphInfo: +class ndi_time_graphinfo: """ Container for sync graph information. Attributes: - nodes: List of EpochNode objects + nodes: List of ndi_time_epochnode objects G: Adjacency matrix (cost matrix) - G[i,j] is cost from node i to j - mapping: Matrix of TimeMapping objects - mapping[i,j] maps time from i to j + mapping: Matrix of ndi_time_timemapping objects - mapping[i,j] maps time from i to j diG: NetworkX DiGraph for path finding syncrule_ids: List of sync rule document IDs syncrule_G: Matrix indicating which sync rule created each edge """ - nodes: list[EpochNode] = field(default_factory=list) + nodes: list[ndi_time_epochnode] = field(default_factory=list) G: np.ndarray | None = None # Cost matrix - mapping: list[list[TimeMapping | None]] | None = None + mapping: list[list[ndi_time_timemapping | None]] | None = None diG: Any = None # NetworkX DiGraph syncrule_ids: list[str] = field(default_factory=list) syncrule_G: np.ndarray | None = None # Sync rule index matrix -class SyncGraph(Ido): +class ndi_time_syncgraph(ndi_ido): """ Synchronization graph for managing time conversion across epochs. - SyncGraph builds a graph where nodes are epochs and edges represent + ndi_time_syncgraph builds a graph where nodes are epochs and edges represent time mappings between them. It uses NetworkX to find shortest paths for time conversion. Example: - >>> sg = SyncGraph(session) - >>> sg.add_rule(FileMatch()) + >>> sg = ndi_time_syncgraph(session) + >>> sg.add_rule(ndi_time_syncrule_filematch()) >>> t_out, ref_out, msg = sg.time_convert( ... timeref_in, t_in, referent_out, clocktype_out ... ) @@ -127,11 +127,11 @@ class SyncGraph(Ido): def __init__( self, session: Any = None, - document: Document | None = None, + document: ndi_document | None = None, identifier: str | None = None, ): """ - Create a new SyncGraph. + Create a new ndi_time_syncgraph. Args: session: The NDI session object @@ -140,20 +140,20 @@ def __init__( """ if not HAS_NETWORKX: raise ImportError( - "networkx is required for SyncGraph. Install with: pip install networkx" + "networkx is required for ndi_time_syncgraph. Install with: pip install networkx" ) super().__init__(identifier) self._session = session - self._rules: list[SyncRule] = [] - self._cached_ginfo: GraphInfo | None = None + self._rules: list[ndi_time_syncrule] = [] + self._cached_ginfo: ndi_time_graphinfo | None = None # Load from document if provided if document is not None and session is not None: self._load_from_document(session, document) - def _load_from_document(self, session: Any, document: Document) -> None: + def _load_from_document(self, session: Any, document: ndi_document) -> None: """Load syncgraph state from a document.""" self._identifier = document.id @@ -162,12 +162,12 @@ def _load_from_document(self, session: Any, document: Document) -> None: if syncrule_ids: for rule_id in syncrule_ids: # Find and load the sync rule document - from ..query import Query + from ..query import ndi_query - q = Query("base.id") == rule_id + q = ndi_query("base.id") == rule_id docs = session.database_search(q) if docs: - rule = SyncRule.from_document(session, docs[0]) + rule = ndi_time_syncrule.from_document(session, docs[0]) self._rules.append(rule) @property @@ -176,22 +176,22 @@ def session(self) -> Any: return self._session @property - def rules(self) -> list[SyncRule]: + def rules(self) -> list[ndi_time_syncrule]: """Get the sync rules.""" return self._rules.copy() - def add_rule(self, rule: SyncRule) -> SyncGraph: + def add_rule(self, rule: ndi_time_syncrule) -> ndi_time_syncgraph: """ Add a sync rule to the graph. Args: - rule: SyncRule to add + rule: ndi_time_syncrule to add Returns: self for chaining """ - if not isinstance(rule, SyncRule): - raise TypeError("rule must be a SyncRule instance") + if not isinstance(rule, ndi_time_syncrule): + raise TypeError("rule must be a ndi_time_syncrule instance") # Check for duplicates for existing in self._rules: @@ -202,7 +202,7 @@ def add_rule(self, rule: SyncRule) -> SyncGraph: self._remove_cached_graphinfo() return self - def remove_rule(self, index: int) -> SyncGraph: + def remove_rule(self, index: int) -> ndi_time_syncgraph: """ Remove a sync rule by index. @@ -217,25 +217,25 @@ def remove_rule(self, index: int) -> SyncGraph: self._remove_cached_graphinfo() return self - def graphinfo(self) -> GraphInfo: + def graphinfo(self) -> ndi_time_graphinfo: """ Get the graph information, building if necessary. Returns: - GraphInfo object with nodes, cost matrix, mappings, etc. + ndi_time_graphinfo object with nodes, cost matrix, mappings, etc. """ if self._cached_ginfo is None: self._cached_ginfo = self._build_graphinfo() return self._cached_ginfo - def _build_graphinfo(self) -> GraphInfo: + def _build_graphinfo(self) -> ndi_time_graphinfo: """ Build the sync graph from scratch. Returns: - GraphInfo with all epoch nodes and mappings + ndi_time_graphinfo with all epoch nodes and mappings """ - ginfo = GraphInfo() + ginfo = ndi_time_graphinfo() ginfo.syncrule_ids = [rule.id for rule in self._rules] # Load all DAQ systems from session @@ -255,7 +255,7 @@ def _build_graphinfo(self) -> GraphInfo: return ginfo - def _add_epoch(self, daqsystem: Any, ginfo: GraphInfo) -> GraphInfo: + def _add_epoch(self, daqsystem: Any, ginfo: ndi_time_graphinfo) -> ndi_time_graphinfo: """ Add a DAQ system's epochs to the graph. @@ -264,12 +264,14 @@ def _add_epoch(self, daqsystem: Any, ginfo: GraphInfo) -> GraphInfo: ginfo: Current graph info Returns: - Updated GraphInfo + Updated ndi_time_graphinfo """ # Get epoch nodes from the DAQ system if hasattr(daqsystem, "epochnodes"): newnodes_data = daqsystem.epochnodes() - newnodes = [EpochNode.from_dict(n) if isinstance(n, dict) else n for n in newnodes_data] + newnodes = [ + ndi_time_epochnode.from_dict(n) if isinstance(n, dict) else n for n in newnodes_data + ] else: newnodes = [] @@ -351,7 +353,7 @@ def _add_epoch(self, daqsystem: Any, ginfo: GraphInfo) -> GraphInfo: return ginfo - def _apply_rules_to_edge(self, ginfo: GraphInfo, i: int, j: int) -> None: + def _apply_rules_to_edge(self, ginfo: ndi_time_graphinfo, i: int, j: int) -> None: """Apply sync rules to find the best edge between nodes i and j.""" best_cost = np.inf best_mapping = None @@ -390,11 +392,11 @@ def _remove_cached_graphinfo(self) -> None: def time_convert( self, - timeref_in: TimeReference, + timeref_in: ndi_time_timereference, t_in: float, referent_out: Any, - clocktype_out: ClockType, - ) -> tuple[float | None, TimeReference | None, str]: + clocktype_out: ndi_time_clocktype, + ) -> tuple[float | None, ndi_time_timereference | None, str]: """ Convert time from one reference to another. @@ -407,10 +409,10 @@ def time_convert( Returns: Tuple of (t_out, timeref_out, message) where: - t_out is the converted time (or None if failed) - - timeref_out is the output TimeReference (or None if failed) + - timeref_out is the output ndi_time_timereference (or None if failed) - message describes any error """ - from .timereference import TimeReference + from .timereference import ndi_time_timereference # Get graph info ginfo = self.graphinfo() @@ -467,7 +469,7 @@ def time_convert( # Create output time reference dest_node = ginfo.nodes[best_path[-1]] - timeref_out = TimeReference( + timeref_out = ndi_time_timereference( referent=referent_out, clocktype=dest_node.epoch_clock, epoch=dest_node.epoch_id, @@ -478,9 +480,9 @@ def time_convert( def _find_epoch_node( self, - nodes: list[EpochNode], + nodes: list[ndi_time_epochnode], referent: Any, - clocktype: ClockType, + clocktype: ndi_time_clocktype, epoch_id: str | None, ) -> int | None: """Find the index of a matching epoch node.""" @@ -509,9 +511,9 @@ def _find_epoch_node( def _find_destination_nodes( self, - nodes: list[EpochNode], + nodes: list[ndi_time_epochnode], referent: Any, - clocktype: ClockType, + clocktype: ndi_time_clocktype, ) -> list[int]: """Find indices of all nodes matching the destination criteria.""" # Get referent name @@ -538,7 +540,7 @@ def _find_destination_nodes( def __eq__(self, other: object) -> bool: """Check equality of two sync graphs.""" - if not isinstance(other, SyncGraph): + if not isinstance(other, ndi_time_syncgraph): return NotImplemented if self._session != other._session: @@ -553,19 +555,19 @@ def __eq__(self, other: object) -> bool: return True - def new_document(self) -> list[Document]: + def new_document(self) -> list[ndi_document]: """ Create documents for this sync graph and its rules. Returns: - List of Document objects + List of ndi_document objects """ - from ..document import Document + from ..document import ndi_document docs = [] # Create syncgraph document - sg_doc = Document( + sg_doc = ndi_document( document_type="daq/syncgraph", **{ "syncgraph.ndi_syncgraph_class": type(self).__name__, @@ -585,8 +587,8 @@ def new_document(self) -> list[Document]: def search_query(self) -> Any: """Create a search query for this sync graph.""" - from ..query import Query + from ..query import ndi_query - return (Query("base.id") == self.id) & ( - Query("base.session_id") == (self._session.id() if self._session else "") + return (ndi_query("base.id") == self.id) & ( + ndi_query("base.session_id") == (self._session.id() if self._session else "") ) diff --git a/src/ndi/time/syncrule/__init__.py b/src/ndi/time/syncrule/__init__.py index 083f2b4..cb04750 100644 --- a/src/ndi/time/syncrule/__init__.py +++ b/src/ndi/time/syncrule/__init__.py @@ -1,18 +1,18 @@ """ ndi.time.syncrule - Synchronization rule implementations. -This module provides concrete implementations of SyncRule for +This module provides concrete implementations of ndi_time_syncrule for different synchronization strategies. """ -from .common_triggers_overlapping_epochs import CommonTriggersOverlappingEpochs -from .filefind import FileFind -from .filematch import FileMatch -from .random_pulses import RandomPulses +from .common_triggers_overlapping_epochs import ndi_time_syncrule_commonTriggersOverlappingEpochs +from .filefind import ndi_time_syncrule_filefind +from .filematch import ndi_time_syncrule_filematch +from .random_pulses import ndi_time_syncrule_randomPulses __all__ = [ - "CommonTriggersOverlappingEpochs", - "FileFind", - "FileMatch", - "RandomPulses", + "ndi_time_syncrule_commonTriggersOverlappingEpochs", + "ndi_time_syncrule_filefind", + "ndi_time_syncrule_filematch", + "ndi_time_syncrule_randomPulses", ] diff --git a/src/ndi/time/syncrule/common_triggers_overlapping_epochs.py b/src/ndi/time/syncrule/common_triggers_overlapping_epochs.py index fb1dc7f..8cbd697 100644 --- a/src/ndi/time/syncrule/common_triggers_overlapping_epochs.py +++ b/src/ndi/time/syncrule/common_triggers_overlapping_epochs.py @@ -1,7 +1,7 @@ """ ndi.time.syncrule.commonTriggersOverlappingEpochs - Sync rule for common triggers. -This module provides the CommonTriggersOverlappingEpochs sync rule that +This module provides the ndi_time_syncrule_commonTriggersOverlappingEpochs sync rule that synchronizes two DAQ systems by finding a linear time mapping from shared trigger events recorded on overlapping (embedded) epochs. @@ -16,8 +16,8 @@ import numpy as np -from ..syncrule_base import SyncRule -from ..timemapping import TimeMapping +from ..syncrule_base import ndi_time_syncrule +from ..timemapping import ndi_time_timemapping def _parse_channel(ch_str: str) -> tuple[str, int]: @@ -85,7 +85,7 @@ def _get_underlying_files(epochnode: dict[str, Any]) -> list[str]: return [] -class CommonTriggersOverlappingEpochs(SyncRule): +class ndi_time_syncrule_commonTriggersOverlappingEpochs(ndi_time_syncrule): """ Synchronization rule based on common triggers in overlapping embedded epochs. @@ -103,7 +103,7 @@ class CommonTriggersOverlappingEpochs(SyncRule): errorOnFailure (bool): If True, raise on failure. Example: - >>> rule = CommonTriggersOverlappingEpochs({ + >>> rule = ndi_time_syncrule_commonTriggersOverlappingEpochs({ ... 'daqsystem1_name': 'daq1', ... 'daqsystem2_name': 'daq2', ... 'daqsystem_ch1': 'dep1', @@ -168,7 +168,7 @@ def is_valid_parameters(self, parameters: dict[str, Any]) -> tuple[bool, str]: def eligible_epochsets(self) -> list[str]: """Return eligible epochset class names.""" - return ["ndi.daq.system", "DAQSystem"] + return ["ndi.daq.system", "ndi_daq_system"] def ineligible_epochsets(self) -> list[str]: """Return ineligible epochset class names.""" @@ -184,7 +184,7 @@ def apply( epochnode_a: dict[str, Any], epochnode_b: dict[str, Any], daqsystem_a: Any = None, - ) -> tuple[float | None, TimeMapping | None]: + ) -> tuple[float | None, ndi_time_timemapping | None]: """ Apply the sync rule to obtain a cost and mapping between two epoch nodes. @@ -249,26 +249,26 @@ def apply( # 2. Check for existing syncrule_mapping in database try: - from ndi.query import Query + from ndi.query import ndi_query q_existing = ( - Query("").isa("syncrule_mapping") - & Query( + ndi_query("").isa("syncrule_mapping") + & ndi_query( "syncrule_mapping.epochnode_a.epoch_id", "exact_string", epochnode_a.get("epoch_id", ""), ) - & Query( + & ndi_query( "syncrule_mapping.epochnode_b.epoch_id", "exact_string", epochnode_b.get("epoch_id", ""), ) - & Query( + & ndi_query( "syncrule_mapping.epochnode_a.objectname", "exact_string", name_a, ) - & Query( + & ndi_query( "syncrule_mapping.epochnode_b.objectname", "exact_string", name_b, @@ -280,7 +280,7 @@ def apply( props = doc.document_properties sm = props.get("syncrule_mapping", {}) cost = sm.get("cost", 1.0) - mapping = TimeMapping(sm.get("mapping", [1, 0])) + mapping = ndi_time_timemapping(sm.get("mapping", [1, 0])) return cost, mapping except Exception: pass # No cached mapping, compute fresh @@ -434,10 +434,10 @@ def _get_epoch_id(epoch_table: list, idx: int) -> str: if node_a_is_1: # T2 = scale * T1 + shift -> map A(1) to B(2) - mapping = TimeMapping([scale, shift]) + mapping = ndi_time_timemapping([scale, shift]) else: # Want A(2)->B(1): T1 = (T2 - shift)/scale - mapping = TimeMapping([1.0 / scale, -shift / scale]) + mapping = ndi_time_timemapping([1.0 / scale, -shift / scale]) return 1.0, mapping diff --git a/src/ndi/time/syncrule/filefind.py b/src/ndi/time/syncrule/filefind.py index 23a4aab..624d284 100644 --- a/src/ndi/time/syncrule/filefind.py +++ b/src/ndi/time/syncrule/filefind.py @@ -1,7 +1,7 @@ """ ndi.time.syncrule.filefind - File find synchronization rule. -This module provides the FileFind sync rule that synchronizes epochs +This module provides the ndi_time_syncrule_filefind sync rule that synchronizes epochs based on a synchronization text file shared between two named DAQ systems. The sync file contains two numbers (shift and scale) defining: @@ -15,15 +15,15 @@ import os from typing import Any -from ..syncrule_base import SyncRule -from ..timemapping import TimeMapping +from ..syncrule_base import ndi_time_syncrule +from ..timemapping import ndi_time_timemapping -class FileFind(SyncRule): +class ndi_time_syncrule_filefind(ndi_time_syncrule): """ Synchronization rule based on finding a sync file between two DAQ systems. - FileFind looks for a specific synchronization text file in the epoch's + ndi_time_syncrule_filefind looks for a specific synchronization text file in the epoch's underlying files. The file should contain two numbers: a shift and a scale, defining the time relationship between two named DAQ systems: @@ -37,7 +37,7 @@ class FileFind(SyncRule): daqsystem2 (str): Name of the second DAQ system. Default: 'mydaq2'. Example: - >>> rule = FileFind({ + >>> rule = ndi_time_syncrule_filefind({ ... 'number_fullpath_matches': 1, ... 'syncfilename': 'syncfile.txt', ... 'daqsystem1': 'daq_vis', @@ -51,7 +51,7 @@ def __init__( identifier: str | None = None, ): """ - Create a new FileFind sync rule. + Create a new ndi_time_syncrule_filefind sync rule. Args: parameters: Dict with sync file matching parameters @@ -69,7 +69,7 @@ def __init__( def is_valid_parameters(self, parameters: dict[str, Any]) -> tuple[bool, str]: """ - Validate parameters for FileFind. + Validate parameters for ndi_time_syncrule_filefind. Args: parameters: Dict to validate @@ -101,7 +101,7 @@ def is_valid_parameters(self, parameters: dict[str, Any]) -> tuple[bool, str]: def eligible_epochsets(self) -> list[str]: """Return eligible epochset class names.""" - return ["ndi.daq.system", "DAQSystem"] + return ["ndi.daq.system", "ndi_daq_system"] def ineligible_epochsets(self) -> list[str]: """Return ineligible epochset class names.""" @@ -113,13 +113,13 @@ def apply( epochnode_a: dict[str, Any], epochnode_b: dict[str, Any], daqsystem1: Any = None, - ) -> tuple[float | None, TimeMapping | None]: + ) -> tuple[float | None, ndi_time_timemapping | None]: """ - Apply FileFind rule to determine if epochs can be synchronized. + Apply ndi_time_syncrule_filefind rule to determine if epochs can be synchronized. Checks if the two epoch nodes come from the configured DAQ systems, share enough underlying files, and contain the sync file. If so, - reads shift and scale from the sync file and returns a TimeMapping. + reads shift and scale from the sync file and returns a ndi_time_timemapping. Args: epochnode_a: First epoch node @@ -163,7 +163,7 @@ def apply( if syncdata is None: raise FileNotFoundError(f"No file matched {syncfilename}.") shift, scale = syncdata - return 1.0, TimeMapping([scale, shift]) + return 1.0, ndi_time_timemapping([scale, shift]) if backward: # Look for sync file in epochnode_b's files @@ -175,7 +175,7 @@ def apply( # then T1 = -shift/scale + (1/scale)*T2 scale_reverse = 1.0 / scale shift_reverse = -shift / scale - return 1.0, TimeMapping([scale_reverse, shift_reverse]) + return 1.0, ndi_time_timemapping([scale_reverse, shift_reverse]) return None, None diff --git a/src/ndi/time/syncrule/filematch.py b/src/ndi/time/syncrule/filematch.py index 9f3a472..a35a700 100644 --- a/src/ndi/time/syncrule/filematch.py +++ b/src/ndi/time/syncrule/filematch.py @@ -1,7 +1,7 @@ """ ndi.time.syncrule.filematch - File match synchronization rule. -This module provides the FileMatch sync rule that synchronizes epochs +This module provides the ndi_time_syncrule_filematch sync rule that synchronizes epochs based on shared underlying files. """ @@ -9,15 +9,15 @@ from typing import Any -from ..syncrule_base import SyncRule -from ..timemapping import TimeMapping +from ..syncrule_base import ndi_time_syncrule +from ..timemapping import ndi_time_timemapping -class FileMatch(SyncRule): +class ndi_time_syncrule_filematch(ndi_time_syncrule): """ Synchronization rule based on matching underlying files. - FileMatch identifies epochs that share underlying files and creates + ndi_time_syncrule_filematch identifies epochs that share underlying files and creates a synchronization mapping between them. This is useful when multiple DAQ systems record to a shared file system. @@ -27,8 +27,8 @@ class FileMatch(SyncRule): Default is 2. Example: - >>> rule = FileMatch() # Default: require 2 matching files - >>> rule = FileMatch({'number_fullpath_matches': 3}) # Require 3 matches + >>> rule = ndi_time_syncrule_filematch() # Default: require 2 matching files + >>> rule = ndi_time_syncrule_filematch({'number_fullpath_matches': 3}) # Require 3 matches """ def __init__( @@ -37,7 +37,7 @@ def __init__( identifier: str | None = None, ): """ - Create a new FileMatch sync rule. + Create a new ndi_time_syncrule_filematch sync rule. Args: parameters: Dict with 'number_fullpath_matches' key (default 2) @@ -50,7 +50,7 @@ def __init__( def is_valid_parameters(self, parameters: dict[str, Any]) -> tuple[bool, str]: """ - Validate parameters for FileMatch. + Validate parameters for ndi_time_syncrule_filematch. Args: parameters: Dict to validate @@ -75,7 +75,7 @@ def is_valid_parameters(self, parameters: dict[str, Any]) -> tuple[bool, str]: def eligible_epochsets(self) -> list[str]: """Return eligible epochset class names.""" - return ["ndi.daq.system", "DAQSystem"] + return ["ndi.daq.system", "ndi_daq_system"] def ineligible_epochsets(self) -> list[str]: """Return ineligible epochset class names.""" @@ -91,9 +91,9 @@ def apply( epochnode_a: dict[str, Any], epochnode_b: dict[str, Any], daqsystem1: Any = None, - ) -> tuple[float | None, TimeMapping | None]: + ) -> tuple[float | None, ndi_time_timemapping | None]: """ - Apply FileMatch rule to determine if epochs can be synchronized. + Apply ndi_time_syncrule_filematch rule to determine if epochs can be synchronized. Checks if the epochs share enough underlying files to establish a synchronization. If they do, returns cost=1 and identity mapping. @@ -133,7 +133,7 @@ def apply( required_matches = self._parameters.get("number_fullpath_matches", 2) if len(common) >= required_matches: # Epochs match - return cost=1 and identity mapping - return 1.0, TimeMapping.identity() + return 1.0, ndi_time_timemapping.identity() return None, None @@ -142,7 +142,7 @@ def _is_daq_system(classname: str) -> bool: """Check if a classname represents a DAQ system.""" daq_classes = [ "ndi.daq.system", - "DAQSystem", + "ndi_daq_system", "daq.system", ] return any(c in classname for c in daq_classes) diff --git a/src/ndi/time/syncrule/ndi_matlab_python_bridge.yaml b/src/ndi/time/syncrule/ndi_matlab_python_bridge.yaml index e3b0c31..f015097 100644 --- a/src/ndi/time/syncrule/ndi_matlab_python_bridge.yaml +++ b/src/ndi/time/syncrule/ndi_matlab_python_bridge.yaml @@ -19,7 +19,7 @@ classes: matlab_path: "+ndi/+time/+syncrule/filematch.m" matlab_last_sync_hash: "85b35859" python_path: "ndi/time/syncrule/filematch.py" - python_class: "FileMatch" + python_class: "ndi_time_syncrule_filematch" inherits: "ndi.time.syncrule" methods: @@ -36,7 +36,7 @@ classes: default: "None" output_arguments: - name: fm_obj - type_python: "FileMatch" + type_python: "ndi_time_syncrule_filematch" decision_log: > Exact match. Default parameters: {'number_fullpath_matches': 2}. @@ -61,7 +61,7 @@ classes: - name: classes type_python: "list[str]" decision_log: > - Exact match. Returns ['ndi.daq.system', 'DAQSystem']. + Exact match. Returns ['ndi.daq.system', 'ndi_daq_system']. - name: ineligible_epochsets input_arguments: [] @@ -85,7 +85,7 @@ classes: - name: cost type_python: "float | None" - name: mapping - type_python: "TimeMapping | None" + type_python: "ndi_time_timemapping | None" decision_log: > Exact match. Returns (1.0, identity_mapping) if epochs share >= number_fullpath_matches underlying files. @@ -99,7 +99,7 @@ classes: matlab_path: "+ndi/+time/+syncrule/filefind.m" matlab_last_sync_hash: "85b35859" python_path: "ndi/time/syncrule/filefind.py" - python_class: "FileFind" + python_class: "ndi_time_syncrule_filefind" inherits: "ndi.time.syncrule" methods: @@ -116,7 +116,7 @@ classes: default: "None" output_arguments: - name: ff_obj - type_python: "FileFind" + type_python: "ndi_time_syncrule_filefind" decision_log: > Exact match. Default parameters: {'file_patterns': [], 'match_type': 'exact'}. @@ -141,7 +141,7 @@ classes: - name: classes type_python: "list[str]" decision_log: > - Exact match. Returns ['ndi.daq.system', 'DAQSystem']. + Exact match. Returns ['ndi.daq.system', 'ndi_daq_system']. - name: apply input_arguments: @@ -155,7 +155,7 @@ classes: - name: cost type_python: "float | None" - name: mapping - type_python: "TimeMapping | None" + type_python: "ndi_time_timemapping | None" decision_log: > Exact match. Returns (1.0, identity_mapping) if all file patterns match in both epochs. Returns (None, None) @@ -170,7 +170,7 @@ classes: matlab_path: "+ndi/+time/+syncrule/commonTriggersOverlappingEpochs.m" matlab_last_sync_hash: "ca27911b" python_path: "ndi/time/syncrule/common_triggers_overlapping_epochs.py" - python_class: "CommonTriggersOverlappingEpochs" + python_class: "ndi_time_syncrule_commonTriggersOverlappingEpochs" inherits: "ndi.time.syncrule" methods: @@ -187,7 +187,7 @@ classes: default: "None" output_arguments: - name: ctoe_obj - type_python: "CommonTriggersOverlappingEpochs" + type_python: "ndi_time_syncrule_commonTriggersOverlappingEpochs" decision_log: > Exact match. Default parameters: daqsystem1_name='', daqsystem2_name='', @@ -215,7 +215,7 @@ classes: - name: classes type_python: "list[str]" decision_log: > - Exact match. Returns ['ndi.daq.system', 'DAQSystem']. + Exact match. Returns ['ndi.daq.system', 'ndi_daq_system']. - name: ineligible_epochsets input_arguments: [] @@ -243,9 +243,9 @@ classes: - name: cost type_python: "float | None" - name: mapping - type_python: "TimeMapping | None" + type_python: "ndi_time_timemapping | None" decision_log: > - Exact match. Extended signature vs FileMatch/FileFind: + Exact match. Extended signature vs ndi_time_syncrule_filematch/ndi_time_syncrule_filefind: accepts daqsystem_a as third argument. Checks embedded file overlap (grandparent/parent matching), expands epoch group iteratively, reads triggers from all @@ -264,7 +264,7 @@ classes: matlab_path: "+ndi/+time/+syncrule/randomPulses.m" matlab_last_sync_hash: "a009ea96" python_path: "ndi/time/syncrule/random_pulses.py" - python_class: "RandomPulses" + python_class: "ndi_time_syncrule_randomPulses" inherits: "ndi.time.syncrule" methods: @@ -281,7 +281,7 @@ classes: default: "None" output_arguments: - name: rp_obj - type_python: "RandomPulses" + type_python: "ndi_time_syncrule_randomPulses" decision_log: > Exact match. Default parameters: daqsystem1_name='', daqsystem2_name='', @@ -309,7 +309,7 @@ classes: - name: classes type_python: "list[str]" decision_log: > - Exact match. Returns ['ndi.daq.system.mfdaq', 'DAQSystem']. + Exact match. Returns ['ndi.daq.system.mfdaq', 'ndi_daq_system']. Note: MATLAB returns {'ndi.daq.system.mfdaq'} - more restrictive than other syncrules. @@ -339,9 +339,9 @@ classes: - name: cost type_python: "float | None" - name: mapping - type_python: "TimeMapping | None" + type_python: "ndi_time_timemapping | None" decision_log: > - Exact match. Extended signature vs FileMatch/FileFind: + Exact match. Extended signature vs ndi_time_syncrule_filematch/ndi_time_syncrule_filefind: accepts daqsystem_a as third argument. Reads random pulse trigger events from each epoch, uses inter-pulse-interval cross-correlation to align diff --git a/src/ndi/time/syncrule/random_pulses.py b/src/ndi/time/syncrule/random_pulses.py index dc661dc..a573eac 100644 --- a/src/ndi/time/syncrule/random_pulses.py +++ b/src/ndi/time/syncrule/random_pulses.py @@ -1,7 +1,7 @@ """ ndi.time.syncrule.randomPulses - Sync rule for random pulse sequences. -This module provides the RandomPulses sync rule that synchronizes two DAQ +This module provides the ndi_time_syncrule_randomPulses sync rule that synchronizes two DAQ systems that recorded a shared random pulse sequence. MATLAB path: +ndi/+time/+syncrule/randomPulses.m @@ -14,8 +14,8 @@ import numpy as np -from ..syncrule_base import SyncRule -from ..timemapping import TimeMapping +from ..syncrule_base import ndi_time_syncrule +from ..timemapping import ndi_time_timemapping def _parse_channel(ch_str: str) -> tuple[str, int]: @@ -83,7 +83,7 @@ def _sync_random_triggers(t1: np.ndarray, t2: np.ndarray) -> tuple[float, float] return shift, scale -class RandomPulses(SyncRule): +class ndi_time_syncrule_randomPulses(ndi_time_syncrule): """ Synchronization rule based on random pulse sequences on a shared channel. @@ -100,7 +100,7 @@ class RandomPulses(SyncRule): errorOnFailure (bool): If True, raise on failure. Example: - >>> rule = RandomPulses({ + >>> rule = ndi_time_syncrule_randomPulses({ ... 'daqsystem1_name': 'daq1', ... 'daqsystem2_name': 'daq2', ... 'daqsystem_ch1': 'dep1', @@ -160,7 +160,7 @@ def is_valid_parameters(self, parameters: dict[str, Any]) -> tuple[bool, str]: def eligible_epochsets(self) -> list[str]: """Return eligible epochset class names.""" - return ["ndi.daq.system.mfdaq", "DAQSystem"] + return ["ndi.daq.system.mfdaq", "ndi_daq_system"] def ineligible_epochsets(self) -> list[str]: """Return ineligible epochset class names.""" @@ -176,7 +176,7 @@ def apply( epochnode_a: dict[str, Any], epochnode_b: dict[str, Any], daqsystem_a: Any = None, - ) -> tuple[float | None, TimeMapping | None]: + ) -> tuple[float | None, ndi_time_timemapping | None]: """ Apply the sync rule to obtain a cost and mapping between two epoch nodes. @@ -241,26 +241,26 @@ def apply( # 2. Check for existing syncrule_mapping in database try: - from ndi.query import Query + from ndi.query import ndi_query q_existing = ( - Query("").isa("syncrule_mapping") - & Query( + ndi_query("").isa("syncrule_mapping") + & ndi_query( "syncrule_mapping.epochnode_a.epoch_id", "exact_string", epochnode_a.get("epoch_id", ""), ) - & Query( + & ndi_query( "syncrule_mapping.epochnode_b.epoch_id", "exact_string", epochnode_b.get("epoch_id", ""), ) - & Query( + & ndi_query( "syncrule_mapping.epochnode_a.objectname", "exact_string", name_a, ) - & Query( + & ndi_query( "syncrule_mapping.epochnode_b.objectname", "exact_string", name_b, @@ -272,7 +272,7 @@ def apply( props = doc.document_properties sm = props.get("syncrule_mapping", {}) cost = sm.get("cost", 1.0) - mapping = TimeMapping(sm.get("mapping", [1, 0])) + mapping = ndi_time_timemapping(sm.get("mapping", [1, 0])) return cost, mapping except Exception: pass # No cached mapping, compute fresh @@ -319,11 +319,11 @@ def apply( if node_a_is_1: # Want A->B, i.e. T1->T2 # T1 = scale * T2 + shift -> T2 = (T1 - shift) / scale - mapping = TimeMapping([1.0 / scale, -shift / scale]) + mapping = ndi_time_timemapping([1.0 / scale, -shift / scale]) else: # Want A->B, i.e. T2->T1 # T1 = scale * T2 + shift - mapping = TimeMapping([scale, shift]) + mapping = ndi_time_timemapping([scale, shift]) return 1.0, mapping diff --git a/src/ndi/time/syncrule_base.py b/src/ndi/time/syncrule_base.py index eed3bc9..aefc27d 100644 --- a/src/ndi/time/syncrule_base.py +++ b/src/ndi/time/syncrule_base.py @@ -1,7 +1,7 @@ """ ndi.time.syncrule - Base class for synchronization rules. -This module provides the SyncRule abstract base class for managing +This module provides the ndi_time_syncrule abstract base class for managing synchronization between epochs and devices. """ @@ -10,19 +10,19 @@ from abc import ABC from typing import TYPE_CHECKING, Any -from ..ido import Ido -from .clocktype import ClockType -from .timemapping import TimeMapping +from ..ido import ndi_ido +from .clocktype import ndi_time_clocktype +from .timemapping import ndi_time_timemapping if TYPE_CHECKING: - from ..document import Document + from ..document import ndi_document -class SyncRule(Ido, ABC): +class ndi_time_syncrule(ndi_ido, ABC): """ Abstract base class for synchronization rules. - SyncRule objects define how to synchronize time between different + ndi_time_syncrule objects define how to synchronize time between different epochs and devices. Subclasses implement specific synchronization strategies. @@ -36,7 +36,7 @@ def __init__( identifier: str | None = None, ): """ - Create a new SyncRule. + Create a new ndi_time_syncrule. Args: parameters: Parameters for the sync rule (must be valid) @@ -87,7 +87,7 @@ def is_valid_parameters(self, parameters: dict[str, Any]) -> tuple[bool, str]: """ return True, "" - def eligible_clocks(self) -> list[ClockType]: + def eligible_clocks(self) -> list[ndi_time_clocktype]: """ Return eligible clock types that can be used with this sync rule. @@ -97,23 +97,23 @@ def eligible_clocks(self) -> list[ClockType]: Override in subclasses to restrict eligible clocks. Returns: - List of eligible ClockType values + List of eligible ndi_time_clocktype values """ return [] - def ineligible_clocks(self) -> list[ClockType]: + def ineligible_clocks(self) -> list[ndi_time_clocktype]: """ Return ineligible clock types that cannot be used with this sync rule. If empty, no information is conveyed about which clock types are invalid (i.e., no specific limits). - The base class returns [ClockType.NO_TIME]. + The base class returns [ndi_time_clocktype.NO_TIME]. Returns: - List of ineligible ClockType values + List of ineligible ndi_time_clocktype values """ - return [ClockType.NO_TIME] + return [ndi_time_clocktype.NO_TIME] def eligible_epochsets(self) -> list[str]: """ @@ -148,7 +148,7 @@ def apply( epochnode_a: dict[str, Any], epochnode_b: dict[str, Any], daqsystem1: Any = None, - ) -> tuple[float | None, TimeMapping | None]: + ) -> tuple[float | None, ndi_time_timemapping | None]: """ Apply the sync rule to obtain cost and mapping between two epoch nodes. @@ -162,26 +162,26 @@ def apply( Returns: Tuple of (cost, mapping) where: - cost is the synchronization cost (float or None if no sync possible) - - mapping is the TimeMapping object (or None if no sync possible) + - mapping is the ndi_time_timemapping object (or None if no sync possible) """ return None, None def __eq__(self, other: object) -> bool: """Check equality of two sync rules.""" - if not isinstance(other, SyncRule): + if not isinstance(other, ndi_time_syncrule): return NotImplemented return self._parameters == other._parameters - def new_document(self) -> Document: + def new_document(self) -> ndi_document: """ - Create a new ndi.Document for this sync rule. + Create a new ndi.ndi_document for this sync rule. Returns: - Document representing this sync rule + ndi_document representing this sync rule """ - from ..document import Document + from ..document import ndi_document - doc = Document( + doc = ndi_document( document_type="daq/syncrule", **{ "syncrule.ndi_syncrule_class": type(self).__name__, @@ -196,11 +196,11 @@ def search_query(self) -> Any: Create a search query for this sync rule object. Returns: - Query object to find this sync rule + ndi_query object to find this sync rule """ - from ..query import Query + from ..query import ndi_query - return Query("base.id") == self.id + return ndi_query("base.id") == self.id def to_dict(self) -> dict[str, Any]: """ @@ -216,9 +216,9 @@ def to_dict(self) -> dict[str, Any]: } @classmethod - def from_document(cls, session: Any, doc: Document) -> SyncRule: + def from_document(cls, session: Any, doc: ndi_document) -> ndi_time_syncrule: """ - Create a SyncRule from a document. + Create a ndi_time_syncrule from a document. This factory method creates the appropriate subclass based on the document's syncrule.ndi_syncrule_class field. @@ -228,11 +228,11 @@ def from_document(cls, session: Any, doc: Document) -> SyncRule: doc: The document to load from Returns: - SyncRule instance of the appropriate subclass + ndi_time_syncrule instance of the appropriate subclass """ # Get class name from document props = doc.document_properties - class_name = props.get("syncrule", {}).get("ndi_syncrule_class", "SyncRule") + class_name = props.get("syncrule", {}).get("ndi_syncrule_class", "ndi_time_syncrule") parameters = props.get("syncrule", {}).get("parameters", {}) identifier = props.get("base", {}).get("id") @@ -241,17 +241,17 @@ def from_document(cls, session: Any, doc: Document) -> SyncRule: # Map class names to classes class_map = { - "SyncRule": SyncRule, - "FileMatch": syncrule_module.FileMatch, - "FileFind": syncrule_module.FileFind, - "CommonTriggersOverlappingEpochs": syncrule_module.CommonTriggersOverlappingEpochs, - "RandomPulses": syncrule_module.RandomPulses, + "ndi_time_syncrule": ndi_time_syncrule, + "ndi_time_syncrule_filematch": syncrule_module.ndi_time_syncrule_filematch, + "ndi_time_syncrule_filefind": syncrule_module.ndi_time_syncrule_filefind, + "ndi_time_syncrule_commonTriggersOverlappingEpochs": syncrule_module.ndi_time_syncrule_commonTriggersOverlappingEpochs, + "ndi_time_syncrule_randomPulses": syncrule_module.ndi_time_syncrule_randomPulses, } rule_class = class_map.get(class_name, cls) # Abstract classes can't be instantiated directly - if rule_class is SyncRule: - raise ValueError("Cannot instantiate abstract SyncRule directly") + if rule_class is ndi_time_syncrule: + raise ValueError("Cannot instantiate abstract ndi_time_syncrule directly") return rule_class(parameters=parameters, identifier=identifier) diff --git a/src/ndi/time/timemapping.py b/src/ndi/time/timemapping.py index 5057540..37de2ca 100644 --- a/src/ndi/time/timemapping.py +++ b/src/ndi/time/timemapping.py @@ -1,7 +1,7 @@ """ ndi.time.timemapping - Time mapping class for NDI framework. -This module provides the TimeMapping class for managing mapping of time +This module provides the ndi_time_timemapping class for managing mapping of time across epochs and devices using polynomial transformations. """ @@ -10,7 +10,7 @@ import numpy as np -class TimeMapping: +class ndi_time_timemapping: """ Class for managing mapping of time across epochs and devices. @@ -26,17 +26,17 @@ class TimeMapping: t_out = scale * t_in + shift Example: - >>> tm = TimeMapping([1, 0]) # Identity mapping + >>> tm = ndi_time_timemapping([1, 0]) # Identity mapping >>> tm.map(5.0) 5.0 - >>> tm = TimeMapping([2, 10]) # t_out = 2*t_in + 10 + >>> tm = ndi_time_timemapping([2, 10]) # t_out = 2*t_in + 10 >>> tm.map(5.0) 20.0 """ def __init__(self, mapping: list[float] | np.ndarray | None = None): """ - Create a new TimeMapping object. + Create a new ndi_time_timemapping object. Args: mapping: Polynomial coefficients [a_n, a_{n-1}, ..., a_1, a_0] @@ -99,17 +99,17 @@ def __call__(self, t_in: float | np.ndarray) -> float | np.ndarray: return self.map(t_in) @classmethod - def identity(cls) -> TimeMapping: + def identity(cls) -> ndi_time_timemapping: """ Create an identity mapping (t_out = t_in). Returns: - TimeMapping with [1, 0] coefficients + ndi_time_timemapping with [1, 0] coefficients """ return cls([1.0, 0.0]) @classmethod - def linear(cls, scale: float = 1.0, shift: float = 0.0) -> TimeMapping: + def linear(cls, scale: float = 1.0, shift: float = 0.0) -> ndi_time_timemapping: """ Create a linear mapping: t_out = scale * t_in + shift. @@ -118,11 +118,11 @@ def linear(cls, scale: float = 1.0, shift: float = 0.0) -> TimeMapping: shift: Offset/shift (default 0.0) Returns: - TimeMapping with [scale, shift] coefficients + ndi_time_timemapping with [scale, shift] coefficients """ return cls([scale, shift]) - def inverse(self) -> TimeMapping: + def inverse(self) -> ndi_time_timemapping: """ Compute the inverse mapping (for linear mappings only). @@ -130,7 +130,7 @@ def inverse(self) -> TimeMapping: t_in = (1/scale) * t_out - shift/scale Returns: - TimeMapping representing the inverse transformation + ndi_time_timemapping representing the inverse transformation Raises: ValueError: If mapping is not linear or scale is zero @@ -142,9 +142,9 @@ def inverse(self) -> TimeMapping: inv_scale = 1.0 / self._mapping[0] inv_shift = -self._mapping[1] / self._mapping[0] - return TimeMapping([inv_scale, inv_shift]) + return ndi_time_timemapping([inv_scale, inv_shift]) - def compose(self, other: TimeMapping) -> TimeMapping: + def compose(self, other: ndi_time_timemapping) -> ndi_time_timemapping: """ Compose two mappings: apply self first, then other. @@ -158,7 +158,7 @@ def compose(self, other: TimeMapping) -> TimeMapping: other: The mapping to apply after this one Returns: - Composed TimeMapping + Composed ndi_time_timemapping Raises: ValueError: If either mapping is not linear @@ -172,19 +172,19 @@ def compose(self, other: TimeMapping) -> TimeMapping: new_scale = c * a new_shift = c * b + d - return TimeMapping([new_scale, new_shift]) + return ndi_time_timemapping([new_scale, new_shift]) def __eq__(self, other: object) -> bool: """Check equality of two time mappings.""" - if not isinstance(other, TimeMapping): + if not isinstance(other, ndi_time_timemapping): return NotImplemented return np.allclose(self._mapping, other._mapping) def __repr__(self) -> str: """Return string representation.""" if len(self._mapping) == 2: - return f"TimeMapping(scale={self.scale}, shift={self.shift})" - return f"TimeMapping({list(self._mapping)})" + return f"ndi_time_timemapping(scale={self.scale}, shift={self.shift})" + return f"ndi_time_timemapping({list(self._mapping)})" def to_dict(self) -> dict: """ @@ -196,14 +196,14 @@ def to_dict(self) -> dict: return {"mapping": self._mapping.tolist()} @classmethod - def from_dict(cls, data: dict) -> TimeMapping: + def from_dict(cls, data: dict) -> ndi_time_timemapping: """ - Create TimeMapping from dictionary. + Create ndi_time_timemapping from dictionary. Args: data: Dictionary with 'mapping' key Returns: - TimeMapping instance + ndi_time_timemapping instance """ return cls(data["mapping"]) diff --git a/src/ndi/time/timereference.py b/src/ndi/time/timereference.py index 750d902..259827a 100644 --- a/src/ndi/time/timereference.py +++ b/src/ndi/time/timereference.py @@ -1,7 +1,7 @@ """ ndi.time.timereference - Time reference class for NDI framework. -This module provides the TimeReference class for specifying time +This module provides the ndi_time_timereference class for specifying time relative to an NDI clock type within a specific epoch. """ @@ -10,16 +10,16 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Any -from .clocktype import ClockType +from .clocktype import ndi_time_clocktype if TYPE_CHECKING: pass # Future imports for type checking @dataclass -class TimeReferenceStruct: +class ndi_time_timereference__struct: """ - Structure representation of a TimeReference without live objects. + Structure representation of a ndi_time_timereference without live objects. Used for serialization and database storage. """ @@ -32,11 +32,11 @@ class TimeReferenceStruct: time: float | None -class TimeReference: +class ndi_time_timereference: """ A class for specifying time relative to an NDI clock. - TimeReference describes a specific time point in the context of: + ndi_time_timereference describes a specific time point in the context of: - A referent (the object keeping time, e.g., DAQ system, element) - A clock type (utc, exp_global_time, dev_local_time, etc.) - An epoch (required for dev_local_time) @@ -44,17 +44,17 @@ class TimeReference: Example: >>> # Create a time reference for UTC time - >>> tr = TimeReference( + >>> tr = ndi_time_timereference( ... referent=my_daq_system, - ... clocktype=ClockType.UTC, + ... clocktype=ndi_time_clocktype.UTC, ... epoch=None, ... time=1234567890.0 ... ) >>> # Create a local time reference (requires epoch) - >>> tr = TimeReference( + >>> tr = ndi_time_timereference( ... referent=my_daq_system, - ... clocktype=ClockType.DEV_LOCAL_TIME, + ... clocktype=ndi_time_clocktype.DEV_LOCAL_TIME, ... epoch="epoch_001", ... time=0.5 ... ) @@ -63,7 +63,7 @@ class TimeReference: def __init__( self, referent: Any, - clocktype: ClockType | str, + clocktype: ndi_time_clocktype | str, epoch: str | None = None, time: float | None = None, session_id: str | None = None, @@ -74,21 +74,21 @@ def __init__( Args: referent: The object that serves as the time reference source. Must have a 'session' property with a valid id. - clocktype: The clock type (ClockType enum or string) + clocktype: The clock type (ndi_time_clocktype enum or string) epoch: The epoch identifier (required if clocktype is DEV_LOCAL_TIME) time: The time value at the reference point session_id: Optional session ID (extracted from referent if not provided) Raises: - TypeError: If clocktype is not a ClockType + TypeError: If clocktype is not a ndi_time_clocktype ValueError: If epoch is required but not provided """ # Handle clocktype as string or enum if isinstance(clocktype, str): - clocktype = ClockType.from_string(clocktype) + clocktype = ndi_time_clocktype.from_string(clocktype) - if not isinstance(clocktype, ClockType): - raise TypeError("clocktype must be a ClockType instance or valid string") + if not isinstance(clocktype, ndi_time_clocktype): + raise TypeError("clocktype must be a ndi_time_clocktype instance or valid string") # Validate epoch requirement if clocktype.needs_epoch() and epoch is None: @@ -113,7 +113,7 @@ def _extract_session_id(referent: Any) -> str: referent: Object with session property Returns: - Session ID string + ndi_session ID string Raises: ValueError: If session ID cannot be extracted @@ -139,7 +139,7 @@ def referent(self) -> Any: return self._referent @property - def clocktype(self) -> ClockType: + def clocktype(self) -> ndi_time_clocktype: """Get the clock type.""" return self._clocktype @@ -158,14 +158,14 @@ def session_id(self) -> str: """Get the session ID.""" return self._session_id - def to_struct(self) -> TimeReferenceStruct: + def to_struct(self) -> ndi_time_timereference__struct: """ Convert to a structure that lacks live Matlab/Python objects. Useful for serialization and database storage. Returns: - TimeReferenceStruct with string-based representation + ndi_time_timereference__struct with string-based representation """ # Get epochsetname if hasattr(self._referent, "epochsetname"): @@ -181,7 +181,7 @@ def to_struct(self) -> TimeReferenceStruct: # Get class name referent_classname = type(self._referent).__name__ - return TimeReferenceStruct( + return ndi_time_timereference__struct( referent_epochsetname=epochsetname, referent_classname=referent_classname, clocktypestring=self._clocktype.value, @@ -194,17 +194,17 @@ def to_struct(self) -> TimeReferenceStruct: def from_struct( cls, session: Any, - struct: TimeReferenceStruct, - ) -> TimeReference: + struct: ndi_time_timereference__struct, + ) -> ndi_time_timereference: """ - Create a TimeReference from a struct and session. + Create a ndi_time_timereference from a struct and session. Args: session: The session object to search for the referent - struct: TimeReferenceStruct with serialized data + struct: ndi_time_timereference__struct with serialized data Returns: - TimeReference with live referent object + ndi_time_timereference with live referent object Raises: ValueError: If referent cannot be found in session @@ -213,9 +213,9 @@ def from_struct( if hasattr(session, "findexpobj"): referent = session.findexpobj(struct.referent_epochsetname, struct.referent_classname) else: - raise ValueError("Session does not support finding experiment objects") + raise ValueError("ndi_session does not support finding experiment objects") - clocktype = ClockType.from_string(struct.clocktypestring) + clocktype = ndi_time_clocktype.from_string(struct.clocktypestring) return cls( referent=referent, @@ -244,7 +244,7 @@ def to_dict(self) -> dict: def __eq__(self, other: object) -> bool: """Check equality of time references.""" - if not isinstance(other, TimeReference): + if not isinstance(other, ndi_time_timereference): return NotImplemented return ( @@ -262,4 +262,4 @@ def __repr__(self) -> str: f"epoch={self._epoch!r}", f"time={self._time}", ] - return f"TimeReference({', '.join(parts)})" + return f"ndi_time_timereference({', '.join(parts)})" diff --git a/src/ndi/util/compare_session_summary.py b/src/ndi/util/compare_session_summary.py index 3b80369..0cfa213 100644 --- a/src/ndi/util/compare_session_summary.py +++ b/src/ndi/util/compare_session_summary.py @@ -19,6 +19,7 @@ def compareSessionSummary( summary2: dict[str, Any], *, excludeFiles: list[str] | None = None, + excludeFields: list[str] | None = None, ) -> list[str]: """Compare two session summaries and return a report. @@ -29,12 +30,15 @@ def compareSessionSummary( summary2: Second session summary dict. excludeFiles: Filenames to ignore when comparing ``files`` and ``filesInDotNDI`` fields. + excludeFields: Field names to skip entirely during comparison. Returns: List of difference strings. Empty list means summaries match. """ if excludeFiles is None: excludeFiles = [] + if excludeFields is None: + excludeFields = [] report: list[str] = [] @@ -51,6 +55,8 @@ def compareSessionSummary( # 2. Compare common fields for field in common_fields: + if field in excludeFields: + continue val1 = summary1[field] val2 = summary2[field] @@ -92,7 +98,7 @@ def compareSessionSummary( if not (n1 and n1 == n2): report.append(f'Field {field}[{j}] differs: "{s1}" vs "{s2}"') elif isinstance(item1, dict) and isinstance(item2, dict): - sub = compareSessionSummary(item1, item2) + sub = compareSessionSummary(item1, item2, excludeFields=excludeFields) for s in sub: report.append(f"Field {field}[{j}] struct diff: {s}") else: @@ -101,7 +107,7 @@ def compareSessionSummary( elif isinstance(val1, dict) and isinstance(val2, dict): # Both are dicts — could be a single struct or a struct array - sub = compareSessionSummary(val1, val2) + sub = compareSessionSummary(val1, val2, excludeFields=excludeFields) for s in sub: report.append(f"Field {field} struct diff: {s}") diff --git a/src/ndi/util/downsampleTimeseries.py b/src/ndi/util/downsampleTimeseries.py index 3face52..855ecf9 100644 --- a/src/ndi/util/downsampleTimeseries.py +++ b/src/ndi/util/downsampleTimeseries.py @@ -37,7 +37,7 @@ def downsampleTimeseries( 1-D array of time values (seconds). Samples are assumed to be equally spaced. d_in : numpy.ndarray - Data matrix. Each column is a channel; rows correspond to samples + ndi_gui_Data matrix. Each column is a channel; rows correspond to samples in *t_in*. LP : float Low-pass cutoff frequency in Hz. Must be positive. diff --git a/src/ndi/util/ndi_matlab_python_bridge.yaml b/src/ndi/util/ndi_matlab_python_bridge.yaml index 67afef4..f257165 100644 --- a/src/ndi/util/ndi_matlab_python_bridge.yaml +++ b/src/ndi/util/ndi_matlab_python_bridge.yaml @@ -210,7 +210,7 @@ not_applicable: status: not_applicable decision_log: > MATLAB-specific path resolution using mfilename('fullpath'). - Python uses ndi.common.PathConstants instead. + Python uses ndi.common.ndi_common_PathConstants instead. - name: "+openminds sub-package" matlab_path: "+ndi/+util/+openminds/" diff --git a/src/ndi/util/session_summary.py b/src/ndi/util/session_summary.py index 2b6fd86..4ad2d5d 100644 --- a/src/ndi/util/session_summary.py +++ b/src/ndi/util/session_summary.py @@ -1,4 +1,4 @@ -"""Session summary utility for symmetry testing. +"""ndi_session summary utility for symmetry testing. MATLAB equivalent: ``ndi.util.sessionSummary`` @@ -27,7 +27,7 @@ def sessionSummary(session_obj: Any) -> dict[str, Any]: """ summary: dict[str, Any] = {} - # 1. Session basics + # 1. ndi_session basics summary["reference"] = session_obj.reference summary["sessionId"] = session_obj.id() @@ -65,7 +65,9 @@ def sessionSummary(session_obj: Any) -> dict[str, Any]: # Get filenavigator class fn = getattr(sys, "filenavigator", None) if fn is not None: - details["filenavigator_class"] = type(fn).__qualname__ + details["filenavigator_class"] = getattr( + fn, "NDI_FILENAVIGATOR_CLASS", type(fn).__qualname__ + ) try: details["epochNodes_filenavigator"] = fn.epochnodes() except Exception: @@ -77,7 +79,7 @@ def sessionSummary(session_obj: Any) -> dict[str, Any]: # Get daqreader class dr = getattr(sys, "daqreader", None) if dr is not None: - details["daqreader_class"] = type(dr).__qualname__ + details["daqreader_class"] = getattr(dr, "NDI_DAQREADER_CLASS", type(dr).__qualname__) else: details["daqreader_class"] = "" diff --git a/src/ndi/validate.py b/src/ndi/validate.py index 84934c0..f5684c7 100644 --- a/src/ndi/validate.py +++ b/src/ndi/validate.py @@ -1,5 +1,5 @@ """ -ndi.validate - Document schema validation for NDI. +ndi.validate - ndi_document schema validation for NDI. MATLAB equivalent: +ndi/validate.m @@ -17,11 +17,11 @@ from pathlib import Path from typing import TYPE_CHECKING, Any -from .common import PathConstants +from .common import ndi_common_PathConstants if TYPE_CHECKING: - from .document import Document - from .session.session_base import Session + from .document import ndi_document + from .session.session_base import ndi_session # --------------------------------------------------------------------------- @@ -79,7 +79,7 @@ def _check_did_uid_params(value: str, params: Any) -> str | None: def _load_schema(schema_name: str) -> dict | None: """Load a schema JSON file by name. - Searches in PathConstants.SCHEMA_PATH for ``{name}_schema.json``. + Searches in ndi_common_PathConstants.SCHEMA_PATH for ``{name}_schema.json``. Handles subdirectory paths (e.g. 'apps/calculations/simple_calc'). Args: @@ -92,7 +92,7 @@ def _load_schema(schema_name: str) -> dict | None: return _schema_cache[schema_name] try: - schema_path = PathConstants.SCHEMA_PATH + schema_path = ndi_common_PathConstants.SCHEMA_PATH except (ValueError, AttributeError): return None @@ -117,14 +117,14 @@ def _load_schema(schema_name: str) -> dict | None: return None -def _get_schema_for_document(doc: Document) -> dict | None: +def _get_schema_for_document(doc: ndi_document) -> dict | None: """Get the schema for a document based on its document_class. Reads the document's ``document_class.definition`` path to find the corresponding schema file. Args: - doc: NDI Document object. + doc: NDI ndi_document object. Returns: Schema dict or None. @@ -275,12 +275,12 @@ def _validate_properties( def _validate_depends_on( doc_props: dict, schema: dict, - session: Session | None = None, + session: ndi_session | None = None, ) -> dict[str, str]: """Validate dependency references. Args: - doc_props: Document properties dict. + doc_props: ndi_document properties dict. schema: Schema dict with depends_on definitions. session: Optional session for database lookups. @@ -320,9 +320,9 @@ def _validate_depends_on( # If session available, verify the referenced document exists if session is not None: try: - from .query import Query + from .query import ndi_query - found = session.database_search(Query("base.id") == dep_value) + found = session.database_search(ndi_query("base.id") == dep_value) if found: results[dep_name] = "ok" else: @@ -336,8 +336,8 @@ def _validate_depends_on( def validate( - doc: Document, - session: Session | None = None, + doc: ndi_document, + session: ndi_session | None = None, ) -> ValidationResult: """Validate an NDI document against its schema. @@ -347,7 +347,7 @@ def validate( 3. Dependency references: check depends_on targets exist Args: - doc: NDI Document to validate. + doc: NDI ndi_document to validate. session: Optional session for dependency checking. Returns: diff --git a/src/ndi/validators/mustBeCellArrayOfClass.py b/src/ndi/validators/mustBeCellArrayOfClass.py index d86f594..f6a9766 100644 --- a/src/ndi/validators/mustBeCellArrayOfClass.py +++ b/src/ndi/validators/mustBeCellArrayOfClass.py @@ -15,7 +15,7 @@ def mustBeCellArrayOfClass(c: list | tuple, className: type) -> None: MATLAB equivalent: ``ndi.validators.mustBeCellArrayOfClass(c, className)`` In MATLAB, *className* is a string naming a class. In Python it is - the class object itself (e.g. ``ndi.session.DirSession``). + the class object itself (e.g. ``ndi.session.ndi_session_dir``). Parameters ---------- @@ -36,5 +36,5 @@ def mustBeCellArrayOfClass(c: list | tuple, className: type) -> None: if not isinstance(item, className): raise TypeError( f"All elements must be of class {className.__name__}. " - f"Element {i} is of class {type(item).__name__}." + f"ndi_element {i} is of class {type(item).__name__}." ) diff --git a/src/ndi/validators/mustBeCellArrayOfNdiSessions.py b/src/ndi/validators/mustBeCellArrayOfNdiSessions.py index c793ac0..9b205f2 100644 --- a/src/ndi/validators/mustBeCellArrayOfNdiSessions.py +++ b/src/ndi/validators/mustBeCellArrayOfNdiSessions.py @@ -3,7 +3,7 @@ MATLAB equivalent: +ndi/+validators/mustBeCellArrayOfNdiSessions.m -Validates that the input is a list of ``ndi.session.DirSession`` objects. +Validates that the input is a list of ``ndi.session.ndi_session_dir`` objects. """ from __future__ import annotations @@ -15,7 +15,7 @@ def mustBeCellArrayOfNdiSessions(value: Sequence) -> None: - """Validate that every element is an ``ndi.session.DirSession``. + """Validate that every element is an ``ndi.session.ndi_session_dir``. MATLAB equivalent: ``ndi.validators.mustBeCellArrayOfNdiSessions(value)`` @@ -28,17 +28,17 @@ def mustBeCellArrayOfNdiSessions(value: Sequence) -> None: ------ TypeError If *value* is not a list/tuple, or any element is not a - ``DirSession``. + ``ndi_session_dir``. """ # Import lazily to avoid circular imports. - from ndi.session import DirSession + from ndi.session import ndi_session_dir if not isinstance(value, (list, tuple)): raise TypeError("Input must be a list or tuple.") for i, item in enumerate(value): - if not isinstance(item, DirSession): + if not isinstance(item, ndi_session_dir): raise TypeError( - f"All elements must be ndi.session.DirSession objects. " - f"Element {i} is of class {type(item).__name__!r}." + f"All elements must be ndi.session.ndi_session_dir objects. " + f"ndi_element {i} is of class {type(item).__name__!r}." ) diff --git a/src/ndi/validators/mustBeCellArrayOfNonEmptyCharacterArrays.py b/src/ndi/validators/mustBeCellArrayOfNonEmptyCharacterArrays.py index dbeaf3e..4b7974b 100644 --- a/src/ndi/validators/mustBeCellArrayOfNonEmptyCharacterArrays.py +++ b/src/ndi/validators/mustBeCellArrayOfNonEmptyCharacterArrays.py @@ -39,7 +39,7 @@ def mustBeCellArrayOfNonEmptyCharacterArrays(value: Sequence) -> None: if not isinstance(item, str): raise TypeError( f"All elements must be non-empty strings. " - f"Element {i} is of type {type(item).__name__!r}." + f"ndi_element {i} is of type {type(item).__name__!r}." ) if not item: - raise ValueError(f"All elements must be non-empty strings. Element {i} is empty.") + raise ValueError(f"All elements must be non-empty strings. ndi_element {i} is empty.") diff --git a/src/ndi/validators/mustBeClassnameOfType.py b/src/ndi/validators/mustBeClassnameOfType.py index 47f3dbe..7fa669f 100644 --- a/src/ndi/validators/mustBeClassnameOfType.py +++ b/src/ndi/validators/mustBeClassnameOfType.py @@ -19,7 +19,7 @@ def mustBeClassnameOfType(classname: str, requiredType: type) -> None: ``ndi.validators.mustBeClassnameOfType(classname, requiredType)`` *classname* is a fully-qualified Python class name - (e.g. ``"ndi.session.DirSession"``). The function dynamically imports + (e.g. ``"ndi.session.ndi_session_dir"``). The function dynamically imports the module, looks up the class, and checks ``issubclass``. Parameters diff --git a/src/ndi/validators/mustBeEpochInput.py b/src/ndi/validators/mustBeEpochInput.py index 8d8e85a..4d2bec6 100644 --- a/src/ndi/validators/mustBeEpochInput.py +++ b/src/ndi/validators/mustBeEpochInput.py @@ -31,12 +31,12 @@ def mustBeEpochInput(v: str | int) -> None: """ if isinstance(v, str): if not v: - raise ValueError("Epoch input string must not be empty.") + raise ValueError("ndi_epoch_epoch input string must not be empty.") return if isinstance(v, (int,)) and not isinstance(v, bool): if v < 1: - raise ValueError("Epoch input integer must be positive.") + raise ValueError("ndi_epoch_epoch input integer must be positive.") return raise TypeError("Value must be a string or positive integer scalar.") diff --git a/src/ndi/validators/ndi_matlab_python_bridge.yaml b/src/ndi/validators/ndi_matlab_python_bridge.yaml index 12e4817..d11e11b 100644 --- a/src/ndi/validators/ndi_matlab_python_bridge.yaml +++ b/src/ndi/validators/ndi_matlab_python_bridge.yaml @@ -71,7 +71,7 @@ functions: type_python: "Sequence" output_arguments: [] decision_log: > - Exact match. Validates every element is an ndi.session.DirSession. + Exact match. Validates every element is an ndi.session.ndi_session_dir. Uses lazy import to avoid circular imports. Raises TypeError if validation fails. diff --git a/tests/matlab_tests/conftest.py b/tests/matlab_tests/conftest.py index 5b20507..d219a72 100644 --- a/tests/matlab_tests/conftest.py +++ b/tests/matlab_tests/conftest.py @@ -11,9 +11,9 @@ import pytest -from ndi.dataset import Dataset -from ndi.document import Document -from ndi.session.dir import DirSession +from ndi.dataset import ndi_dataset +from ndi.document import ndi_document +from ndi.session.dir import ndi_session_dir # --------------------------------------------------------------------------- # Marker for tests requiring live NDI Cloud credentials @@ -27,11 +27,11 @@ # --------------------------------------------------------------------------- # session_with_docs_and_files # Mirrors: ndi.unittest.session.buildSession.withDocsAndFiles() -# Creates a DirSession with 5 demoNDI documents, each with a file attachment. +# Creates a ndi_session_dir with 5 demoNDI documents, each with a file attachment. # --------------------------------------------------------------------------- -def _add_doc_with_file(session: DirSession, doc_number: int) -> None: +def _add_doc_with_file(session: ndi_session_dir, doc_number: int) -> None: """Add a demoNDI document with a file attachment to the session. Mirrors: ndi.unittest.session.buildSession.addDocsWithFiles() @@ -43,12 +43,12 @@ def _add_doc_with_file(session: DirSession, doc_number: int) -> None: filepath.write_text(docname) # Create document — merge demoNDI schema onto a session doc - doc = Document("demoNDI") + doc = ndi_document("demoNDI") props = doc.document_properties props["base"]["name"] = docname props["demoNDI"]["value"] = doc_number props["base"]["session_id"] = session.id() - doc = Document(props) + doc = ndi_document(props) # Attach file doc = doc.add_file("filename1.ext", str(filepath)) @@ -58,7 +58,7 @@ def _add_doc_with_file(session: DirSession, doc_number: int) -> None: @pytest.fixture def session_with_docs(tmp_path): - """Create a DirSession with 5 demoNDI documents + file attachments. + """Create a ndi_session_dir with 5 demoNDI documents + file attachments. Mirrors: ndi.unittest.session.buildSession.withDocsAndFiles() @@ -66,7 +66,7 @@ def session_with_docs(tmp_path): """ session_dir = tmp_path / "exp_demo" session_dir.mkdir() - session = DirSession("exp_demo", session_dir) + session = ndi_session_dir("exp_demo", session_dir) for i in range(1, 6): _add_doc_with_file(session, i) @@ -78,7 +78,7 @@ def session_with_docs(tmp_path): @pytest.fixture def build_dataset(tmp_path): - """Create a Dataset with an ingested session containing 5 demoNDI docs. + """Create a ndi_dataset with an ingested session containing 5 demoNDI docs. Mirrors: ndi.unittest.dataset.buildDataset.sessionWithIngestedDocsAndFiles() @@ -87,7 +87,7 @@ def build_dataset(tmp_path): # Create the session first session_dir = tmp_path / "session_src" session_dir.mkdir() - session = DirSession("exp_demo", session_dir) + session = ndi_session_dir("exp_demo", session_dir) for i in range(1, 6): _add_doc_with_file(session, i) @@ -95,7 +95,7 @@ def build_dataset(tmp_path): # Create the dataset dataset_dir = tmp_path / "ds_demo" dataset_dir.mkdir() - dataset = Dataset(dataset_dir, "ds_demo") + dataset = ndi_dataset(dataset_dir, "ds_demo") # Ingest the session into the dataset dataset.add_ingested_session(session) diff --git a/tests/matlab_tests/test_app.py b/tests/matlab_tests/test_app.py index f3021b1..4b9639b 100644 --- a/tests/matlab_tests/test_app.py +++ b/tests/matlab_tests/test_app.py @@ -4,21 +4,21 @@ MATLAB source files: +app/TestMarkGarbage.m → TestMarkGarbage -Tests the MarkGarbage app for marking, loading, and clearing +Tests the ndi_app_markgarbage app for marking, loading, and clearing valid time intervals on recording elements. The MATLAB tests require a full session with Intan data and probes. We provide: - Tests with mocked session (database_add/search/remove) but REAL - Document creation — ensuring the schema path and Document API work. - - Integration tests (skip if DirSession unavailable) for full flow. + ndi_document creation — ensuring the schema path and ndi_document API work. + - Integration tests (skip if ndi_session_dir unavailable) for full flow. """ from unittest.mock import MagicMock import pytest -from ndi.document import Document +from ndi.document import ndi_document # =========================================================================== # TestMarkGarbageInstantiation — Basic API tests @@ -29,51 +29,51 @@ class TestMarkGarbageInstantiation: """Port of ndi.unittest.app.TestMarkGarbage — instantiation tests.""" def test_create_without_session(self): - """MarkGarbage can be created without a session.""" - from ndi.app.markgarbage import MarkGarbage + """ndi_app_markgarbage can be created without a session.""" + from ndi.app.markgarbage import ndi_app_markgarbage - app = MarkGarbage() + app = ndi_app_markgarbage() assert app is not None assert app.session is None assert app.name == "ndi_app_markgarbage" def test_create_with_session(self): - """MarkGarbage can be created with a session.""" - from ndi.app.markgarbage import MarkGarbage + """ndi_app_markgarbage can be created with a session.""" + from ndi.app.markgarbage import ndi_app_markgarbage session = MagicMock() session.id.return_value = "session-123" - app = MarkGarbage(session) + app = ndi_app_markgarbage(session) assert app.session is session def test_repr(self): - """MarkGarbage has a useful repr.""" - from ndi.app.markgarbage import MarkGarbage + """ndi_app_markgarbage has a useful repr.""" + from ndi.app.markgarbage import ndi_app_markgarbage - app = MarkGarbage() - assert "MarkGarbage" in repr(app) + app = ndi_app_markgarbage() + assert "ndi_app_markgarbage" in repr(app) def test_repr_with_session(self): - """MarkGarbage repr reflects session state.""" - from ndi.app.markgarbage import MarkGarbage + """ndi_app_markgarbage repr reflects session state.""" + from ndi.app.markgarbage import ndi_app_markgarbage session = MagicMock() - app = MarkGarbage(session) + app = ndi_app_markgarbage(session) r = repr(app) - assert "MarkGarbage" in r + assert "ndi_app_markgarbage" in r assert "True" in r # session=True def test_inherits_from_app(self): - """MarkGarbage inherits from App.""" - from ndi.app import App - from ndi.app.markgarbage import MarkGarbage + """ndi_app_markgarbage inherits from ndi_app.""" + from ndi.app import ndi_app + from ndi.app.markgarbage import ndi_app_markgarbage - app = MarkGarbage() - assert isinstance(app, App) + app = ndi_app_markgarbage() + assert isinstance(app, ndi_app) # =========================================================================== -# TestMarkGarbageMocked — Tests with real Document, mocked session +# TestMarkGarbageMocked — Tests with real ndi_document, mocked session # =========================================================================== @@ -81,15 +81,15 @@ class TestMarkGarbageMocked: """Port of ndi.unittest.app.TestMarkGarbage — mocked database tests. These tests use a mock session (database_add/search/remove) but let - markvalidinterval create REAL Document objects from the actual + markvalidinterval create REAL ndi_document objects from the actual apps/markgarbage/valid_interval schema. This ensures the schema - path, Document constructor, set_session_id, and set_dependency_value + path, ndi_document constructor, set_session_id, and set_dependency_value all work correctly. """ def _make_app_and_probe(self): - """Create a MarkGarbage app with mocked session and probe.""" - from ndi.app.markgarbage import MarkGarbage + """Create a ndi_app_markgarbage app with mocked session and probe.""" + from ndi.app.markgarbage import ndi_app_markgarbage session = MagicMock() session.id.return_value = "session-test-123" @@ -105,22 +105,22 @@ def _make_app_and_probe(self): timeref = MagicMock() timeref.__str__ = MagicMock(return_value="timeref-epoch1") - app = MarkGarbage(session) + app = ndi_app_markgarbage(session) return app, session, probe, timeref def test_markvalidinterval_calls_database_add(self): - """markvalidinterval creates a real Document and adds to database.""" + """markvalidinterval creates a real ndi_document and adds to database.""" app, session, probe, timeref = self._make_app_and_probe() app.markvalidinterval(probe, 1.0, timeref, 3.0, timeref) session.database_add.assert_called_once() doc = session.database_add.call_args[0][0] - # Verify it's a real Document, not a mock - assert isinstance(doc, Document) + # Verify it's a real ndi_document, not a mock + assert isinstance(doc, ndi_document) def test_markvalidinterval_creates_correct_document(self): - """markvalidinterval creates Document with correct schema and properties.""" + """markvalidinterval creates ndi_document with correct schema and properties.""" app, session, probe, timeref = self._make_app_and_probe() app.markvalidinterval(probe, 1.0, timeref, 3.0, timeref) @@ -139,7 +139,7 @@ def test_markvalidinterval_creates_correct_document(self): assert vi["t1"] == 3.0 def test_markvalidinterval_sets_session_id(self): - """markvalidinterval sets the session ID on the Document.""" + """markvalidinterval sets the session ID on the ndi_document.""" app, session, probe, timeref = self._make_app_and_probe() app.markvalidinterval(probe, 1.0, timeref, 3.0, timeref) @@ -148,7 +148,7 @@ def test_markvalidinterval_sets_session_id(self): assert doc.session_id == "session-test-123" def test_markvalidinterval_sets_dependency(self): - """markvalidinterval sets element_id dependency on the Document.""" + """markvalidinterval sets element_id dependency on the ndi_document.""" app, session, probe, timeref = self._make_app_and_probe() app.markvalidinterval(probe, 1.0, timeref, 3.0, timeref) @@ -193,9 +193,9 @@ def test_loadvalidinterval_with_real_documents(self): """loadvalidinterval correctly reads intervals from real Documents.""" app, session, probe, timeref = self._make_app_and_probe() - # Create a real Document (as markvalidinterval would) + # Create a real ndi_document (as markvalidinterval would) interval = {"t0": 1.0, "timeref_t0": "tr", "t1": 3.0, "timeref_t1": "tr"} - real_doc = Document( + real_doc = ndi_document( "apps/markgarbage/valid_interval", valid_interval=interval, ) @@ -209,9 +209,9 @@ def test_loadvalidinterval_with_real_documents(self): def test_markvalidinterval_no_session_raises(self): """markvalidinterval without session raises RuntimeError.""" - from ndi.app.markgarbage import MarkGarbage + from ndi.app.markgarbage import ndi_app_markgarbage - app = MarkGarbage() # No session + app = ndi_app_markgarbage() # No session probe = MagicMock() probe.id = "probe-001" timeref = MagicMock() @@ -221,17 +221,17 @@ def test_markvalidinterval_no_session_raises(self): def test_clearvalidinterval_no_session_noop(self): """clearvalidinterval without session is a no-op.""" - from ndi.app.markgarbage import MarkGarbage + from ndi.app.markgarbage import ndi_app_markgarbage - app = MarkGarbage() + app = ndi_app_markgarbage() probe = MagicMock() app.clearvalidinterval(probe) def test_loadvalidinterval_no_session_returns_empty(self): """loadvalidinterval without session returns empty tuple.""" - from ndi.app.markgarbage import MarkGarbage + from ndi.app.markgarbage import ndi_app_markgarbage - app = MarkGarbage() + app = ndi_app_markgarbage() probe = MagicMock() intervals, docs = app.loadvalidinterval(probe) @@ -242,15 +242,15 @@ def test_mark_then_load_workflow(self): """Full mark → load workflow using real Documents.""" app, session, probe, timeref = self._make_app_and_probe() - # Mark an interval (creates real Document internally) + # Mark an interval (creates real ndi_document internally) app.markvalidinterval(probe, 2.0, timeref, 5.0, timeref) session.database_add.assert_called_once() - # Capture the real Document that was added + # Capture the real ndi_document that was added added_doc = session.database_add.call_args[0][0] - assert isinstance(added_doc, Document) + assert isinstance(added_doc, ndi_document) - # Mock the search to return that same real Document + # Mock the search to return that same real ndi_document session.database_search.return_value = [added_doc] intervals, docs = app.loadvalidinterval(probe) @@ -288,8 +288,8 @@ def test_multiple_intervals_workflow(self): # Capture both real Documents doc1 = session.database_add.call_args_list[0][0][0] doc2 = session.database_add.call_args_list[1][0][0] - assert isinstance(doc1, Document) - assert isinstance(doc2, Document) + assert isinstance(doc1, ndi_document) + assert isinstance(doc2, ndi_document) # Mock search returning both real docs session.database_search.return_value = [doc1, doc2] diff --git a/tests/matlab_tests/test_calculator.py b/tests/matlab_tests/test_calculator.py index 6510582..51c5778 100644 --- a/tests/matlab_tests/test_calculator.py +++ b/tests/matlab_tests/test_calculator.py @@ -6,15 +6,15 @@ +calc/+stimulus/testCalcTuningCurve.m -> TestTuningCurveCalc Tests for: -- ndi.calc.example.simple.SimpleCalc -- ndi.calc.stimulus.tuningcurve.TuningCurveCalc +- ndi.calc.example.simple.ndi_calc_example_simple +- ndi.calc.stimulus.tuningcurve.ndi_calc_stimulus_tuningcurve These calculators require a session. Tests use the session_with_docs fixture from conftest.py and also test sessionless instantiation. """ -from ndi.calc.example.simple import SimpleCalc -from ndi.calc.stimulus.tuningcurve import TuningCurveCalc +from ndi.calc.example.simple import ndi_calc_example_simple +from ndi.calc.stimulus.tuningcurve import ndi_calc_stimulus_tuningcurve # =========================================================================== # TestSimpleCalc @@ -25,66 +25,66 @@ class TestSimpleCalc: """Port of ndi.unittest.calc.example.testSimple. - Verifies SimpleCalc instantiation, interface properties, and + Verifies ndi_calc_example_simple instantiation, interface properties, and basic calculation output. """ def test_simple_calc_instantiation_no_session(self): - """SimpleCalc can be created without a session. + """ndi_calc_example_simple can be created without a session. MATLAB equivalent: testSimple (setup) """ - calc = SimpleCalc() + calc = ndi_calc_example_simple() assert calc is not None assert calc._session is None def test_simple_calc_instantiation(self, session_with_docs): - """SimpleCalc can be created with a session. + """ndi_calc_example_simple can be created with a session. MATLAB equivalent: testSimple.testCalcSimple """ session, _ = session_with_docs - calc = SimpleCalc(session=session) + calc = ndi_calc_example_simple(session=session) assert calc is not None assert calc._session is session def test_simple_calc_doc_types(self): - """SimpleCalc has doc_types=['simple_calc']. + """ndi_calc_example_simple has doc_types=['simple_calc']. MATLAB equivalent: testSimple (property check) """ - calc = SimpleCalc() + calc = ndi_calc_example_simple() assert hasattr(calc, "doc_types") assert isinstance(calc.doc_types, list) assert len(calc.doc_types) == 1 assert calc.doc_types[0] == "simple_calc" def test_simple_calc_doc_document_types(self): - """SimpleCalc has doc_document_types=['apps/calculators/simple_calc']. + """ndi_calc_example_simple has doc_document_types=['apps/calculators/simple_calc']. MATLAB equivalent: testSimple (property check) """ - calc = SimpleCalc() + calc = ndi_calc_example_simple() assert hasattr(calc, "doc_document_types") assert isinstance(calc.doc_document_types, list) assert len(calc.doc_document_types) == 1 assert calc.doc_document_types[0] == "apps/calculators/simple_calc" def test_simple_calc_has_run_method(self): - """SimpleCalc inherits run() from Calculator. + """ndi_calc_example_simple inherits run() from ndi_calculator. MATLAB equivalent: testSimple (interface check) """ - calc = SimpleCalc() + calc = ndi_calc_example_simple() assert hasattr(calc, "run") assert callable(calc.run) def test_simple_calc_has_calculate_method(self): - """SimpleCalc implements calculate(). + """ndi_calc_example_simple implements calculate(). MATLAB equivalent: testSimple (interface check) """ - calc = SimpleCalc() + calc = ndi_calc_example_simple() assert hasattr(calc, "calculate") assert callable(calc.calculate) @@ -93,7 +93,7 @@ def test_simple_calc_default_search_params(self): MATLAB equivalent: testSimple (default parameters) """ - calc = SimpleCalc() + calc = ndi_calc_example_simple() params = calc.default_search_for_input_parameters() assert isinstance(params, dict) assert "input_parameters" in params @@ -105,7 +105,7 @@ def test_simple_calc_calculate(self): MATLAB equivalent: testSimple.testCalcSimple (calculation) """ - calc = SimpleCalc() + calc = ndi_calc_example_simple() parameters = { "input_parameters": {"answer": 42}, "depends_on": [], @@ -121,13 +121,13 @@ def test_simple_calc_calculate(self): assert props["simple_calc"]["answer"] == 42 def test_simple_calc_repr(self): - """SimpleCalc has a useful repr. + """ndi_calc_example_simple has a useful repr. MATLAB equivalent: testSimple (implicit) """ - calc = SimpleCalc() + calc = ndi_calc_example_simple() r = repr(calc) - assert "SimpleCalc" in r + assert "ndi_calc_example_simple" in r # =========================================================================== @@ -139,66 +139,66 @@ def test_simple_calc_repr(self): class TestTuningCurveCalc: """Port of ndi.unittest.calc.stimulus.testCalcTuningCurve. - Verifies TuningCurveCalc instantiation, interface properties, + Verifies ndi_calc_stimulus_tuningcurve instantiation, interface properties, and mock doc generation. """ def test_tuning_curve_instantiation_no_session(self): - """TuningCurveCalc can be created without a session. + """ndi_calc_stimulus_tuningcurve can be created without a session. MATLAB equivalent: testCalcTuningCurve (setup) """ - calc = TuningCurveCalc() + calc = ndi_calc_stimulus_tuningcurve() assert calc is not None assert calc._session is None def test_tuning_curve_instantiation(self, session_with_docs): - """TuningCurveCalc can be created with a session. + """ndi_calc_stimulus_tuningcurve can be created with a session. MATLAB equivalent: testCalcTuningCurve.testCalcTuningCurve """ session, _ = session_with_docs - calc = TuningCurveCalc(session=session) + calc = ndi_calc_stimulus_tuningcurve(session=session) assert calc is not None assert calc._session is session def test_tuning_curve_doc_types(self): - """TuningCurveCalc has doc_types=['tuningcurve_calc']. + """ndi_calc_stimulus_tuningcurve has doc_types=['tuningcurve_calc']. MATLAB equivalent: testCalcTuningCurve (property check) """ - calc = TuningCurveCalc() + calc = ndi_calc_stimulus_tuningcurve() assert hasattr(calc, "doc_types") assert isinstance(calc.doc_types, list) assert len(calc.doc_types) == 1 assert calc.doc_types[0] == "tuningcurve_calc" def test_tuning_curve_doc_document_types(self): - """TuningCurveCalc has doc_document_types for schema path. + """ndi_calc_stimulus_tuningcurve has doc_document_types for schema path. MATLAB equivalent: testCalcTuningCurve (property check) """ - calc = TuningCurveCalc() + calc = ndi_calc_stimulus_tuningcurve() assert hasattr(calc, "doc_document_types") assert isinstance(calc.doc_document_types, list) assert len(calc.doc_document_types) == 1 assert calc.doc_document_types[0] == "apps/calculators/tuningcurve_calc" def test_tuning_curve_has_run_method(self): - """TuningCurveCalc inherits run() from Calculator. + """ndi_calc_stimulus_tuningcurve inherits run() from ndi_calculator. MATLAB equivalent: testCalcTuningCurve (interface check) """ - calc = TuningCurveCalc() + calc = ndi_calc_stimulus_tuningcurve() assert hasattr(calc, "run") assert callable(calc.run) def test_tuning_curve_has_calculate_method(self): - """TuningCurveCalc implements calculate(). + """ndi_calc_stimulus_tuningcurve implements calculate(). MATLAB equivalent: testCalcTuningCurve (interface check) """ - calc = TuningCurveCalc() + calc = ndi_calc_stimulus_tuningcurve() assert hasattr(calc, "calculate") assert callable(calc.calculate) @@ -207,7 +207,7 @@ def test_tuning_curve_default_search_params(self): MATLAB equivalent: testCalcTuningCurve (default parameters) """ - calc = TuningCurveCalc() + calc = ndi_calc_stimulus_tuningcurve() params = calc.default_search_for_input_parameters() assert isinstance(params, dict) assert "input_parameters" in params @@ -221,7 +221,7 @@ def test_tuning_curve_calculate(self): MATLAB equivalent: testCalcTuningCurve (calculation) """ - calc = TuningCurveCalc() + calc = ndi_calc_stimulus_tuningcurve() parameters = { "input_parameters": { "independent_label": "angle", @@ -242,7 +242,7 @@ def test_tuning_curve_generate_mock_docs(self): MATLAB equivalent: testCalcTuningCurve.testGenerateMockDocs """ - calc = TuningCurveCalc() + calc = ndi_calc_stimulus_tuningcurve() docs, doc_output, doc_expected = calc.generate_mock_docs( scope="standard", number_of_tests=4, @@ -260,10 +260,10 @@ def test_tuning_curve_generate_mock_docs(self): assert "independent_variables" in docs[0] def test_tuning_curve_repr(self): - """TuningCurveCalc has a useful repr. + """ndi_calc_stimulus_tuningcurve has a useful repr. MATLAB equivalent: testCalcTuningCurve (implicit) """ - calc = TuningCurveCalc() + calc = ndi_calc_stimulus_tuningcurve() r = repr(calc) - assert "TuningCurveCalc" in r + assert "ndi_calc_stimulus_tuningcurve" in r diff --git a/tests/matlab_tests/test_carbon_fiber.py b/tests/matlab_tests/test_carbon_fiber.py index bd7a0e1..b1b6a69 100644 --- a/tests/matlab_tests/test_carbon_fiber.py +++ b/tests/matlab_tests/test_carbon_fiber.py @@ -1,7 +1,7 @@ """ Tests for NDI-python against the Carbon fiber microelectrode dataset. -Dataset: 743 JSON documents + 66 element_epoch binary files (9.7 GB) +ndi_dataset: 743 JSON documents + 66 element_epoch binary files (9.7 GB) Source: NDI Cloud dataset 668b0539f13096e04f1feccd This is an extracellular electrophysiology dataset with: @@ -17,7 +17,7 @@ Mirrors MATLAB analysis workflow: 1. Load dataset 2. Inspect sessions and elements - 3. Query neurons and waveforms + 3. ndi_query neurons and waveforms 4. Examine stimulus response scalars 5. Verify tuning curves (orientation, spatial freq, temporal freq) 6. Check cross-document referential integrity @@ -33,7 +33,7 @@ import pytest # --------------------------------------------------------------------------- -# Dataset paths — skip entire file if not downloaded locally +# ndi_dataset paths — skip entire file if not downloaded locally # --------------------------------------------------------------------------- CARBON_FIBER_DOCS = Path( @@ -81,13 +81,13 @@ # --------------------------------------------------------------------------- -# Session-scoped fixtures +# ndi_session-scoped fixtures # --------------------------------------------------------------------------- @pytest.fixture(scope="session") def carbon_fiber_dataset(tmp_path_factory): - """Load 743 JSON docs into a Dataset object (once per session).""" + """Load 743 JSON docs into a ndi_dataset object (once per session).""" from ndi.cloud.orchestration import load_dataset_from_json_dir target = tmp_path_factory.mktemp("carbon_fiber_ds") @@ -101,7 +101,7 @@ def carbon_fiber_dataset(tmp_path_factory): @pytest.fixture(scope="session") def all_docs_raw(): - """Load all raw JSON dicts (no Dataset overhead).""" + """Load all raw JSON dicts (no ndi_dataset overhead).""" docs = [] for f in sorted(CARBON_FIBER_DOCS.glob("*.json")): with open(f) as fh: @@ -118,10 +118,10 @@ class TestDatasetLoading: """Validate the bulk load of 743 documents.""" def test_load_document_count(self, carbon_fiber_dataset): - """At least 743 documents load (Dataset init adds 1 session doc).""" - from ndi.query import Query + """At least 743 documents load (ndi_dataset init adds 1 session doc).""" + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("base")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("base")) assert len(docs) >= TOTAL_JSON_DOCS, f"Expected >= {TOTAL_JSON_DOCS}, got {len(docs)}" def test_document_type_counts(self, carbon_fiber_dataset): @@ -133,7 +133,7 @@ def test_document_type_counts(self, carbon_fiber_dataset): for dtype, expected in EXPECTED_TYPE_COUNTS.items(): actual_count = actual.get(dtype, 0) if dtype == "session": - # Dataset init may add 1 extra session doc + # ndi_dataset init may add 1 extra session doc assert ( actual_count >= expected ), f"{dtype}: expected >= {expected}, got {actual_count}" @@ -146,7 +146,7 @@ def test_getDocTypes(self, carbon_fiber_dataset): doc_types, doc_counts = getDocTypes(carbon_fiber_dataset) assert doc_types == sorted(doc_types) - # 27 from JSON + session doc auto-created by Dataset init = 28 + # 27 from JSON + session doc auto-created by ndi_dataset init = 28 assert len(doc_types) >= 27 assert sum(doc_counts) >= TOTAL_JSON_DOCS @@ -170,31 +170,31 @@ class TestSessionDiscovery: def test_session_count(self, carbon_fiber_dataset): """At least 2 session documents (recording + publication).""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("session")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("session")) assert len(docs) >= 2 def test_session_references(self, carbon_fiber_dataset): - """Session references include the recording date.""" - from ndi.query import Query + """ndi_session references include the recording date.""" + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("session")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("session")) refs = [doc.document_properties.get("session", {}).get("reference", "") for doc in docs] assert "2019-11-19" in refs, f"Recording session not found in {refs}" def test_subject_exists(self, carbon_fiber_dataset): """Single subject document exists.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("subject")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("subject")) assert len(docs) == 1 def test_subject_local_identifier(self, carbon_fiber_dataset): - """Subject has a local identifier from vhlab.org.""" - from ndi.query import Query + """ndi_subject has a local identifier from vhlab.org.""" + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("subject")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("subject")) lid = docs[0].document_properties.get("subject", {}).get("local_identifier", "") assert "vhlab.org" in lid, f"Expected vhlab.org in identifier, got '{lid}'" @@ -205,28 +205,28 @@ def test_subject_local_identifier(self, carbon_fiber_dataset): class TestElements: - """Element document structure and types.""" + """ndi_element document structure and types.""" def test_element_count(self, carbon_fiber_dataset): """20 element documents.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("element")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("element")) assert len(docs) == 20 def test_element_types(self, carbon_fiber_dataset): """Three element types: n-trode, spikes, stimulator.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("element")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("element")) types = {doc.document_properties.get("element", {}).get("type", "") for doc in docs} assert types == {"n-trode", "spikes", "stimulator"} def test_ntrode_elements(self, carbon_fiber_dataset): """2 n-trode (carbonfiber) elements.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("element")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("element")) ntrodes = [ doc for doc in docs @@ -238,9 +238,9 @@ def test_ntrode_elements(self, carbon_fiber_dataset): def test_spike_elements(self, carbon_fiber_dataset): """17 spike-sorted elements (one per neuron).""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("element")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("element")) spikes = [ doc for doc in docs @@ -250,9 +250,9 @@ def test_spike_elements(self, carbon_fiber_dataset): def test_stimulator_element(self, carbon_fiber_dataset): """1 visual stimulator element.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("element")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("element")) stims = [ doc for doc in docs @@ -263,10 +263,10 @@ def test_stimulator_element(self, carbon_fiber_dataset): def test_element_depends_on_subject(self, carbon_fiber_dataset): """All elements depend on the subject document.""" - from ndi.query import Query + from ndi.query import ndi_query - elements = carbon_fiber_dataset.database_search(Query("").isa("element")) - subjects = carbon_fiber_dataset.database_search(Query("").isa("subject")) + elements = carbon_fiber_dataset.database_search(ndi_query("").isa("element")) + subjects = carbon_fiber_dataset.database_search(ndi_query("").isa("subject")) subject_id = subjects[0].document_properties.get("base", {}).get("id", "") for elem in elements: @@ -274,7 +274,7 @@ def test_element_depends_on_subject(self, carbon_fiber_dataset): if isinstance(deps, dict): deps = [deps] subject_deps = [d for d in deps if d.get("name") == "subject_id"] - assert len(subject_deps) == 1, "Element missing subject_id dependency" + assert len(subject_deps) == 1, "ndi_element missing subject_id dependency" assert subject_deps[0]["value"] == subject_id @@ -284,20 +284,20 @@ def test_element_depends_on_subject(self, carbon_fiber_dataset): class TestNeurons: - """Neuron extracellular document structure.""" + """ndi_neuron extracellular document structure.""" def test_neuron_count(self, carbon_fiber_dataset): """17 neuron_extracellular documents.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("neuron_extracellular")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("neuron_extracellular")) assert len(docs) == 17 def test_neuron_waveform_shape(self, carbon_fiber_dataset): """All neurons have 21 samples x 16 channels waveforms.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("neuron_extracellular")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("neuron_extracellular")) for doc in docs: ne = doc.document_properties.get("neuron_extracellular", {}) assert ne.get("number_of_samples_per_channel") == 21 @@ -305,19 +305,19 @@ def test_neuron_waveform_shape(self, carbon_fiber_dataset): def test_neuron_has_mean_waveform(self, carbon_fiber_dataset): """All neurons have non-empty mean waveform arrays.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("neuron_extracellular")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("neuron_extracellular")) for doc in docs: ne = doc.document_properties.get("neuron_extracellular", {}) waveform = ne.get("mean_waveform", []) - assert len(waveform) > 0, "Neuron missing mean_waveform" + assert len(waveform) > 0, "ndi_neuron missing mean_waveform" def test_neuron_depends_on_element_and_clusters(self, carbon_fiber_dataset): """Every neuron depends on element_id and spike_clusters_id.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("neuron_extracellular")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("neuron_extracellular")) for doc in docs: deps = doc.document_properties.get("depends_on", []) if isinstance(deps, dict): @@ -328,9 +328,9 @@ def test_neuron_depends_on_element_and_clusters(self, carbon_fiber_dataset): def test_neuron_app_is_jrclust(self, carbon_fiber_dataset): """All neurons were sorted with JRCLUST.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("neuron_extracellular")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("neuron_extracellular")) for doc in docs: app = doc.document_properties.get("app", {}) assert app.get("name") == "JRCLUST" @@ -346,16 +346,16 @@ class TestStimulusResponses: def test_stimulus_response_count(self, carbon_fiber_dataset): """138 stimulus_response_scalar documents.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("stimulus_response_scalar")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("stimulus_response_scalar")) assert len(docs) == 138 def test_response_types(self, carbon_fiber_dataset): """Three response types: mean, F1, F2 (46 each).""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("stimulus_response_scalar")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("stimulus_response_scalar")) type_counts: Counter[str] = Counter() for doc in docs: rt = doc.document_properties.get("stimulus_response_scalar", {}).get( @@ -369,9 +369,9 @@ def test_response_types(self, carbon_fiber_dataset): def test_stimulus_response_has_responses(self, carbon_fiber_dataset): """Each stimulus_response_scalar has stimid and response arrays.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("stimulus_response_scalar")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("stimulus_response_scalar")) for doc in docs[:10]: # sample first 10 srs = doc.document_properties.get("stimulus_response_scalar", {}) responses = srs.get("responses", {}) @@ -380,9 +380,9 @@ def test_stimulus_response_has_responses(self, carbon_fiber_dataset): def test_stimulus_response_depends_on_five_docs(self, carbon_fiber_dataset): """Each response depends on element, stimulator, control, presentation, and parameters.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("stimulus_response_scalar")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("stimulus_response_scalar")) expected_deps = { "element_id", "stimulator_id", @@ -408,16 +408,16 @@ class TestTuningCurves: def test_tuningcurve_count(self, carbon_fiber_dataset): """126 tuningcurve_calc documents total.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("tuningcurve_calc")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("tuningcurve_calc")) assert len(docs) == 126 def test_tuningcurve_types(self, carbon_fiber_dataset): """Three tuning curve types: Orientation (34), Spatial (34), Temporal (58).""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("tuningcurve_calc")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("tuningcurve_calc")) label_counts: Counter[str] = Counter() for doc in docs: tc = doc.document_properties.get("stimulus_tuningcurve", {}) @@ -430,9 +430,9 @@ def test_tuningcurve_types(self, carbon_fiber_dataset): def test_orientation_tuning_has_12_directions(self, carbon_fiber_dataset): """Orientation tuning curves sample 12 directions (0-330 deg, 30 deg steps).""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("tuningcurve_calc")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("tuningcurve_calc")) for doc in docs: tc = doc.document_properties.get("stimulus_tuningcurve", {}) if tc.get("independent_variable_label") == ["Orientation"]: @@ -444,25 +444,25 @@ def test_orientation_tuning_has_12_directions(self, carbon_fiber_dataset): def test_tuningcurve_has_control_response(self, carbon_fiber_dataset): """Tuning curves include control response mean.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("tuningcurve_calc")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("tuningcurve_calc")) for doc in docs[:10]: tc = doc.document_properties.get("stimulus_tuningcurve", {}) assert "control_response_mean" in tc, "Missing control_response_mean" def test_oridirtuning_count(self, carbon_fiber_dataset): """34 orientation/direction tuning analysis documents.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("oridirtuning_calc")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("oridirtuning_calc")) assert len(docs) == 34 def test_oridirtuning_has_compass_coordinates(self, carbon_fiber_dataset): """Ori/dir tuning uses compass coordinates.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("oridirtuning_calc")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("oridirtuning_calc")) for doc in docs: odt = doc.document_properties.get("orientation_direction_tuning", {}) props = odt.get("properties", {}) @@ -470,23 +470,29 @@ def test_oridirtuning_has_compass_coordinates(self, carbon_fiber_dataset): def test_spatial_frequency_tuning_count(self, carbon_fiber_dataset): """34 spatial frequency tuning documents.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("spatial_frequency_tuning_calc")) + docs = carbon_fiber_dataset.database_search( + ndi_query("").isa("spatial_frequency_tuning_calc") + ) assert len(docs) == 34 def test_temporal_frequency_tuning_count(self, carbon_fiber_dataset): """58 temporal frequency tuning documents.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("temporal_frequency_tuning_calc")) + docs = carbon_fiber_dataset.database_search( + ndi_query("").isa("temporal_frequency_tuning_calc") + ) assert len(docs) == 58 def test_temporal_frequency_has_5_frequencies(self, carbon_fiber_dataset): """Temporal frequency tuning samples 5 frequencies (1,2,4,8,16 Hz).""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("temporal_frequency_tuning_calc")) + docs = carbon_fiber_dataset.database_search( + ndi_query("").isa("temporal_frequency_tuning_calc") + ) for doc in docs[:5]: tft = doc.document_properties.get("temporal_frequency_tuning", {}) tc = tft.get("tuning_curve", {}) @@ -500,13 +506,13 @@ def test_temporal_frequency_has_5_frequencies(self, carbon_fiber_dataset): class TestEpochStructure: - """Epoch and DAQ infrastructure documents.""" + """ndi_epoch_epoch and DAQ infrastructure documents.""" def test_element_epoch_count(self, carbon_fiber_dataset): """46 element_epoch documents.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("element_epoch")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("element_epoch")) assert len(docs) == 46 def test_element_epoch_has_file_info(self, all_docs_raw): @@ -533,16 +539,16 @@ def test_element_epoch_has_file_info(self, all_docs_raw): def test_epochfiles_ingested_count(self, carbon_fiber_dataset): """10 epochfiles_ingested documents.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("epochfiles_ingested")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("epochfiles_ingested")) assert len(docs) == 10 def test_stimulus_presentation_count(self, carbon_fiber_dataset): """5 stimulus_presentation documents.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("stimulus_presentation")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("stimulus_presentation")) assert len(docs) == 5 @@ -556,17 +562,17 @@ class TestOpenMinds: def test_openminds_count(self, carbon_fiber_dataset): """57 openminds + 10 openminds_stimulus = 67 total (isa matches subclasses).""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("openminds")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("openminds")) # isa("openminds") matches both openminds (57) and openminds_stimulus (10) assert len(docs) == 67 def test_openminds_stimulus_count(self, carbon_fiber_dataset): """10 openminds_stimulus documents.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("openminds_stimulus")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("openminds_stimulus")) assert len(docs) == 10 @@ -580,9 +586,9 @@ class TestCrossDocumentRelationships: def test_depends_on_references_exist(self, carbon_fiber_dataset): """All depends_on values point to existing document IDs.""" - from ndi.query import Query + from ndi.query import ndi_query - all_docs = carbon_fiber_dataset.database_search(Query("").isa("base")) + all_docs = carbon_fiber_dataset.database_search(ndi_query("").isa("base")) all_ids = {doc.document_properties.get("base", {}).get("id", "") for doc in all_docs} missing = 0 @@ -605,24 +611,24 @@ def test_depends_on_references_exist(self, carbon_fiber_dataset): def test_session_id_consistency(self, carbon_fiber_dataset): """All docs share one of 2 session_ids.""" - from ndi.query import Query + from ndi.query import ndi_query - all_docs = carbon_fiber_dataset.database_search(Query("").isa("base")) + all_docs = carbon_fiber_dataset.database_search(ndi_query("").isa("base")) session_ids: set[str] = set() for doc in all_docs: sid = doc.document_properties.get("base", {}).get("session_id", "") if sid: session_ids.add(sid) - # 2 original + 1 from Dataset init + # 2 original + 1 from ndi_dataset init assert len(session_ids) >= 2, f"Expected >= 2 session_ids, got {session_ids}" assert len(session_ids) <= 5, f"Too many session_ids ({len(session_ids)})" def test_neuron_element_chain(self, carbon_fiber_dataset): """Each neuron -> element -> subject chain is valid.""" - from ndi.query import Query + from ndi.query import ndi_query - neurons = carbon_fiber_dataset.database_search(Query("").isa("neuron_extracellular")) - elements = carbon_fiber_dataset.database_search(Query("").isa("element")) + neurons = carbon_fiber_dataset.database_search(ndi_query("").isa("neuron_extracellular")) + elements = carbon_fiber_dataset.database_search(ndi_query("").isa("element")) element_ids = {doc.document_properties.get("base", {}).get("id", "") for doc in elements} for neuron in neurons: @@ -630,15 +636,17 @@ def test_neuron_element_chain(self, carbon_fiber_dataset): if isinstance(deps, dict): deps = [deps] elem_dep = [d for d in deps if d.get("name") == "element_id"] - assert len(elem_dep) == 1, "Neuron missing element_id dependency" - assert elem_dep[0]["value"] in element_ids, "Neuron points to non-existent element" + assert len(elem_dep) == 1, "ndi_neuron missing element_id dependency" + assert elem_dep[0]["value"] in element_ids, "ndi_neuron points to non-existent element" def test_tuningcurve_depends_on_stimulus_response(self, carbon_fiber_dataset): """Each tuning curve depends on a stimulus_response_scalar document.""" - from ndi.query import Query + from ndi.query import ndi_query - tcs = carbon_fiber_dataset.database_search(Query("").isa("tuningcurve_calc")) - sr_docs = carbon_fiber_dataset.database_search(Query("").isa("stimulus_response_scalar")) + tcs = carbon_fiber_dataset.database_search(ndi_query("").isa("tuningcurve_calc")) + sr_docs = carbon_fiber_dataset.database_search( + ndi_query("").isa("stimulus_response_scalar") + ) sr_ids = {doc.document_properties.get("base", {}).get("id", "") for doc in sr_docs} for tc in tcs: @@ -660,35 +668,35 @@ class TestDAQInfrastructure: def test_daqsystem_count(self, carbon_fiber_dataset): """3 daqsystem documents.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("daqsystem")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("daqsystem")) assert len(docs) == 3 def test_daqreader_count(self, carbon_fiber_dataset): """3 daqreader documents.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("daqreader")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("daqreader")) assert len(docs) == 3 def test_filenavigator_count(self, carbon_fiber_dataset): """3 filenavigator documents.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("filenavigator")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("filenavigator")) assert len(docs) == 3 def test_syncgraph_exists(self, carbon_fiber_dataset): """Single syncgraph document.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("syncgraph")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("syncgraph")) assert len(docs) == 1 def test_syncrule_count(self, carbon_fiber_dataset): """2 syncrule documents.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = carbon_fiber_dataset.database_search(Query("").isa("syncrule")) + docs = carbon_fiber_dataset.database_search(ndi_query("").isa("syncrule")) assert len(docs) == 2 diff --git a/tests/matlab_tests/test_cloud_compute.py b/tests/matlab_tests/test_cloud_compute.py index 44acb32..56a32e2 100644 --- a/tests/matlab_tests/test_cloud_compute.py +++ b/tests/matlab_tests/test_cloud_compute.py @@ -178,7 +178,7 @@ def test_hello_world_flow_live(self): for s in sessions: sid = s.get("sessionId") or s.get("id", "") session_ids.append(sid) - assert session_id in session_ids, f"Session {session_id} not in list: {session_ids}" + assert session_id in session_ids, f"ndi_session {session_id} not in list: {session_ids}" # 4. Abort session (cleanup) try: @@ -284,7 +284,7 @@ def test_zombie_flow_live(self): # 3. Verify session in list sessions = listSessions(client=client).data session_ids = [s.get("sessionId") or s.get("id", "") for s in sessions] - assert session_id in session_ids, f"Session {session_id} not in list: {session_ids}" + assert session_id in session_ids, f"ndi_session {session_id} not in list: {session_ids}" # 4. Polling loop — wait for final status max_iterations = 60 # 60 * 10s = 600s = 10 min diff --git a/tests/matlab_tests/test_core.py b/tests/matlab_tests/test_core.py index 17eacaa..286cdc8 100644 --- a/tests/matlab_tests/test_core.py +++ b/tests/matlab_tests/test_core.py @@ -12,10 +12,10 @@ import pytest -from ndi.cache import Cache -from ndi.document import Document -from ndi.query import Query -from ndi.session.dir import DirSession +from ndi.cache import ndi_cache +from ndi.document import ndi_document +from ndi.query import ndi_query +from ndi.session.dir import ndi_session_dir # =========================================================================== # TestCache @@ -31,13 +31,13 @@ def test_cache_creation(self): MATLAB equivalent: CacheTest.testCacheCreation """ - c = Cache() + c = ndi_cache() assert c.max_memory == 10e9 assert c.replacement_rule == "fifo" def test_cache_creation_custom(self): """Test creating a cache with custom parameters.""" - c = Cache(max_memory=5e6, replacement_rule="lifo") + c = ndi_cache(max_memory=5e6, replacement_rule="lifo") assert c.max_memory == 5e6 assert c.replacement_rule == "lifo" @@ -46,7 +46,7 @@ def test_add_and_lookup(self): MATLAB equivalent: CacheTest.testAddAndLookup """ - c = Cache(max_memory=1e6) + c = ndi_cache(max_memory=1e6) test_data = list(range(100)) c.add("mykey", "mytype", test_data) @@ -60,7 +60,7 @@ def test_remove(self): MATLAB equivalent: CacheTest.testRemove """ - c = Cache(max_memory=1e6) + c = ndi_cache(max_memory=1e6) test_data = [1, 2, 3] c.add("mykey", "mytype", test_data) @@ -74,7 +74,7 @@ def test_clear(self): MATLAB equivalent: CacheTest.testClear """ - c = Cache(max_memory=1e6) + c = ndi_cache(max_memory=1e6) c.add("key1", "mytype", [1, 2, 3]) c.add("key2", "mytype", [4, 5, 6]) c.clear() @@ -87,7 +87,7 @@ def test_fifo_replacement(self): MATLAB equivalent: CacheTest.testFifoReplacement """ # Use a small max_memory that will force eviction - c = Cache(max_memory=1000, replacement_rule="fifo") + c = ndi_cache(max_memory=1000, replacement_rule="fifo") # Add first item c.add("key1", "type1", b"x" * 800) @@ -105,7 +105,7 @@ def test_lifo_replacement(self): MATLAB equivalent: CacheTest.testLifoReplacement """ - c = Cache(max_memory=1000, replacement_rule="lifo") + c = ndi_cache(max_memory=1000, replacement_rule="lifo") c.add("key1", "type1", b"x" * 800) c.add("key2", "type2", b"y" * 800) @@ -123,7 +123,7 @@ def test_error_replacement(self): MATLAB equivalent: CacheTest.testErrorReplacement """ - c = Cache(max_memory=1000, replacement_rule="error") + c = ndi_cache(max_memory=1000, replacement_rule="error") c.add("key1", "type1", b"x" * 800) # Adding another item that exceeds capacity should raise @@ -132,7 +132,7 @@ def test_error_replacement(self): def test_lookup_miss(self): """Looking up a non-existent key returns None.""" - c = Cache(max_memory=1e6) + c = ndi_cache(max_memory=1e6) assert c.lookup("nonexistent", "type") is None @@ -146,11 +146,11 @@ class TestQuery: """Test query construction.""" def test_all_query(self): - """Query.all() matches all base documents. + """ndi_query.all() matches all base documents. MATLAB equivalent: QueryTest.test_all_query """ - q = Query.all() + q = ndi_query.all() assert q is not None # Verify the query searches for isa('base') @@ -159,11 +159,11 @@ def test_all_query(self): assert ss["param1"] == "base" def test_none_query(self): - """Query.none() matches nothing. + """ndi_query.none() matches nothing. MATLAB equivalent: QueryTest.test_none_query """ - q = Query.none() + q = ndi_query.none() assert q is not None # Should search for a nonsensical type @@ -172,16 +172,16 @@ def test_none_query(self): # param1 should be a nonsense string that no document matches def test_exact_string_query(self): - """Query for exact string match.""" - q = Query("base.name") == "test_doc" + """ndi_query for exact string match.""" + q = ndi_query("base.name") == "test_doc" ss = q.to_search_structure() assert ss["field"] == "base.name" assert ss["param1"] == "test_doc" def test_isa_query(self): - """Query.isa() searches by document class.""" - q = Query("").isa("demoNDI") + """ndi_query.isa() searches by document class.""" + q = ndi_query("").isa("demoNDI") ss = q.to_search_structure() assert ss["operation"] == "isa" @@ -189,16 +189,16 @@ def test_isa_query(self): def test_and_query(self): """Combining queries with & creates AND query.""" - q1 = Query("base.name") == "test" - q2 = Query("").isa("demoNDI") + q1 = ndi_query("base.name") == "test" + q2 = ndi_query("").isa("demoNDI") combined = q1 & q2 assert combined is not None def test_or_query(self): """Combining queries with | creates OR query.""" - q1 = Query("base.name") == "alpha" - q2 = Query("base.name") == "beta" + q1 = ndi_query("base.name") == "alpha" + q2 = ndi_query("base.name") == "beta" combined = q1 | q2 assert combined is not None @@ -207,7 +207,7 @@ def test_query_integration(self, tmp_path): """Queries work against a real session database.""" session_dir = tmp_path / "query_test" session_dir.mkdir() - session = DirSession("q_test", session_dir) + session = ndi_session_dir("q_test", session_dir) # Add documents doc1 = session.newdocument( @@ -227,17 +227,17 @@ def test_query_integration(self, tmp_path): session.database_add(doc1) session.database_add(doc2) - # Query by name - results = session.database_search(Query("base.name") == "alpha") + # ndi_query by name + results = session.database_search(ndi_query("base.name") == "alpha") assert len(results) == 1 assert results[0].document_properties["base"]["name"] == "alpha" - # Query by isa - results = session.database_search(Query("").isa("demoNDI")) + # ndi_query by isa + results = session.database_search(ndi_query("").isa("demoNDI")) assert len(results) == 2 - # Query.all() should find demoNDI docs + session doc - all_docs = session.database_search(Query.all()) + # ndi_query.all() should find demoNDI docs + session doc + all_docs = session.database_search(ndi_query.all()) assert len(all_docs) >= 3 # 2 demoNDI + 1 session doc @@ -255,10 +255,10 @@ def test_write_json(self, tmp_path): MATLAB equivalent: DocumentWriteTest.testWriteJSON """ - doc = Document("base") + doc = ndi_document("base") props = doc.document_properties props["base"]["name"] = "test_doc" - doc = Document(props) + doc = ndi_document(props) output_file = tmp_path / "test_output.json" doc.write(str(output_file)) @@ -271,7 +271,7 @@ def test_write_json(self, tmp_path): def test_write_preserves_id(self, tmp_path): """Written document retains its original ID.""" - doc = Document("demoNDI") + doc = ndi_document("demoNDI") original_id = doc.id output_file = tmp_path / "test_id.json" @@ -286,7 +286,7 @@ def test_write_preserves_id(self, tmp_path): # Port of: ndi.unittest.NDIFileNavigatorTest (adapted) # # NOTE: The MATLAB test relies on creating specific file structures -# with epoch patterns. We test the FileNavigator with a minimal +# with epoch patterns. We test the ndi_file_navigator with a minimal # synthetic directory structure. # =========================================================================== @@ -295,38 +295,38 @@ class TestFileNavigator: """Test file navigator epoch discovery.""" def test_navigator_creation(self, tmp_path): - """FileNavigator can be created for a session. + """ndi_file_navigator can be created for a session. MATLAB equivalent: NDIFileNavigatorTest.testFileNavigatorFields """ - from ndi.file.navigator import FileNavigator + from ndi.file.navigator import ndi_file_navigator session_dir = tmp_path / "nav_session" session_dir.mkdir() - session = DirSession("nav_test", session_dir) + session = ndi_session_dir("nav_test", session_dir) - fn = FileNavigator(session, ["myfile_#.ext1", "myfile_#.ext2"]) + fn = ndi_file_navigator(session, ["myfile_#.ext1", "myfile_#.ext2"]) assert fn is not None def test_navigator_no_epochs_in_empty_dir(self, tmp_path): - """FileNavigator finds no epochs in an empty directory.""" + """ndi_file_navigator finds no epochs in an empty directory.""" session_dir = tmp_path / "empty_session" session_dir.mkdir() - session = DirSession("empty_test", session_dir) + session = ndi_session_dir("empty_test", session_dir) - from ndi.file.navigator import FileNavigator + from ndi.file.navigator import ndi_file_navigator - fn = FileNavigator(session, ["data_#.bin"]) + fn = ndi_file_navigator(session, ["data_#.bin"]) et = fn.epochtable() assert isinstance(et, list) def test_navigator_finds_epochs(self, tmp_path): - """FileNavigator discovers epochs from matching file patterns. + """ndi_file_navigator discovers epochs from matching file patterns. MATLAB equivalent: NDIFileNavigatorTest.testNumberOfEpochs """ - from ndi.file.navigator import FileNavigator + from ndi.file.navigator import ndi_file_navigator session_dir = tmp_path / "epoch_session" session_dir.mkdir() @@ -338,8 +338,8 @@ def test_navigator_finds_epochs(self, tmp_path): (subdir / f"data_{i}.ext1").write_text(f"data{i}") (subdir / f"data_{i}.ext2").write_text(f"meta{i}") - session = DirSession("epoch_test", session_dir) - fn = FileNavigator(session, ["data_#.ext1", "data_#.ext2"]) + session = ndi_session_dir("epoch_test", session_dir) + fn = ndi_file_navigator(session, ["data_#.ext1", "data_#.ext2"]) et = fn.epochtable() # Should discover some epochs diff --git a/tests/matlab_tests/test_dabrowska.py b/tests/matlab_tests/test_dabrowska.py index 23a7b14..49681b0 100644 --- a/tests/matlab_tests/test_dabrowska.py +++ b/tests/matlab_tests/test_dabrowska.py @@ -4,7 +4,7 @@ Mirrors the MATLAB tutorial workflow from: ndi.setup.conv.dabrowska.tutorial_67f723d574f5f79c6062389d.mlx -Dataset: 14,646 documents (SQLite), 215 subjects, 606 probes, ~4800 epochs +ndi_dataset: 14,646 documents (SQLite), 215 subjects, 606 probes, ~4800 epochs Source: NDI Cloud dataset 67f723d574f5f79c6062389d Dabrowska dataset contains: @@ -17,10 +17,10 @@ MATLAB tutorial steps tested: 1. Load dataset + document types - 2. Subject summary (docTable.subject) — 215 subjects, dynamic treatments + 2. ndi_subject summary (docTable.subject) — 215 subjects, dynamic treatments 3. Filter subjects by strain (identifyMatchingRows) - 4. Probe summary (docTable.probe) — 606 probes, 9 columns - 5. Epoch summary (docTable.epoch) — ~4800 epochs, 8 columns + 4. ndi_probe summary (docTable.probe) — 606 probes, 9 columns + 5. ndi_epoch_epoch summary (docTable.epoch) — ~4800 epochs, 8 columns 6. Combined table + filtering 7. Electrophysiology data exploration 8. Elevated Plus Maze analysis @@ -35,7 +35,7 @@ import pytest # --------------------------------------------------------------------------- -# Dataset paths — skip entire file if not downloaded locally +# ndi_dataset paths — skip entire file if not downloaded locally # --------------------------------------------------------------------------- DABROWSKA_PATH = Path(os.path.expanduser("~/Documents/ndi-projects/datasets/dabrowska")) @@ -63,7 +63,7 @@ # --------------------------------------------------------------------------- -# Session-scoped fixtures — loaded once for all tests +# ndi_session-scoped fixtures — loaded once for all tests # --------------------------------------------------------------------------- @@ -72,7 +72,7 @@ def dabrowska_dataset(): """Load the Dabrowska dataset from SQLite.""" import ndi.dataset - return ndi.dataset.Dataset(DABROWSKA_PATH) + return ndi.dataset.ndi_dataset(DABROWSKA_PATH) @pytest.fixture(scope="session") @@ -101,11 +101,11 @@ def epoch_summary(dabrowska_dataset): @pytest.fixture(scope="session") def epm_table(dabrowska_dataset): - """Query and convert EPM OTR docs to table.""" + """ndi_query and convert EPM OTR docs to table.""" from ndi.fun.doc_table import ontologyTableRowDoc2Table - from ndi.query import Query + from ndi.query import ndi_query - query = Query("ontologyTableRow.variableNames").contains("ElevatedPlusMaze") + query = ndi_query("ontologyTableRow.variableNames").contains("ElevatedPlusMaze") docs = dabrowska_dataset.database_search(query) tables, _ = ontologyTableRowDoc2Table(docs) return tables[0] @@ -113,11 +113,11 @@ def epm_table(dabrowska_dataset): @pytest.fixture(scope="session") def fps_table(dabrowska_dataset): - """Query and convert FPS OTR docs to table.""" + """ndi_query and convert FPS OTR docs to table.""" from ndi.fun.doc_table import ontologyTableRowDoc2Table - from ndi.query import Query + from ndi.query import ndi_query - query = Query("ontologyTableRow.variableNames").contains("Fear_potentiatedStartle") + query = ndi_query("ontologyTableRow.variableNames").contains("Fear_potentiatedStartle") docs = dabrowska_dataset.database_search(query) tables, _ = ontologyTableRowDoc2Table(docs) return tables[0] @@ -132,7 +132,7 @@ class TestDatasetLoading: """Validate dataset loading and document type counts.""" def test_dataset_loads(self, dabrowska_dataset): - """Dataset object is created successfully.""" + """ndi_dataset object is created successfully.""" assert dabrowska_dataset is not None def test_document_type_counts(self, dabrowska_dataset): @@ -152,16 +152,16 @@ def test_document_type_counts(self, dabrowska_dataset): def test_total_document_count(self, dabrowska_dataset): """Total documents >= 14,646.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = dabrowska_dataset.database_search(Query("").isa("base")) + docs = dabrowska_dataset.database_search(ndi_query("").isa("base")) assert len(docs) >= 14646, f"Expected >= 14646, got {len(docs)}" def test_session_docs_exist(self, dabrowska_dataset): """At least 3 session documents exist.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = dabrowska_dataset.database_search(Query("").isa("session")) + docs = dabrowska_dataset.database_search(ndi_query("").isa("session")) assert len(docs) >= 3 @@ -392,9 +392,9 @@ class TestEPMAnalysis: def test_epm_doc_count(self, dabrowska_dataset): """45 EPM OTR documents.""" - from ndi.query import Query + from ndi.query import ndi_query - query = Query("ontologyTableRow.variableNames").contains("ElevatedPlusMaze") + query = ndi_query("ontologyTableRow.variableNames").contains("ElevatedPlusMaze") docs = dabrowska_dataset.database_search(query) assert len(docs) == 45 @@ -444,9 +444,9 @@ class TestFPSAnalysis: def test_fps_doc_count(self, dabrowska_dataset): """6160 FPS OTR documents.""" - from ndi.query import Query + from ndi.query import ndi_query - query = Query("ontologyTableRow.variableNames").contains("Fear_potentiatedStartle") + query = ndi_query("ontologyTableRow.variableNames").contains("Fear_potentiatedStartle") docs = dabrowska_dataset.database_search(query) assert len(docs) == 6160 diff --git a/tests/matlab_tests/test_daq.py b/tests/matlab_tests/test_daq.py index 399a08b..9cfcde5 100644 --- a/tests/matlab_tests/test_daq.py +++ b/tests/matlab_tests/test_daq.py @@ -7,9 +7,9 @@ mfdaqNDRIntanTest.m -> skipped (NDR Intan reader not ported; uses SpikeInterface) Python replacement modules: - ndi.daq.reader.mfdaq.intan.IntanReader (wraps SpikeInterface) - ndi.daq.reader.spikeinterface_adapter.SpikeInterfaceReader - ndi.daq.mfdaq.MFDAQReader (base with epochsamples2times / epochtimes2samples) + ndi.daq.reader.mfdaq.intan.ndi_daq_reader_mfdaq_intan (wraps SpikeInterface) + ndi.daq.reader.spikeinterface_adapter.ndi_daq_reader_SpikeInterfaceReader + ndi.daq.mfdaq.ndi_daq_reader_mfdaq (base with epochsamples2times / epochtimes2samples) ndi.fun.utils.channelname2prefixnumber """ @@ -20,8 +20,8 @@ import numpy as np import pytest -from ndi.daq.mfdaq import ChannelInfo, ChannelType, MFDAQReader, standardize_channel_type -from ndi.daq.reader.mfdaq.intan import IntanReader +from ndi.daq.mfdaq import ChannelInfo, ChannelType, ndi_daq_reader_mfdaq, standardize_channel_type +from ndi.daq.reader.mfdaq.intan import ndi_daq_reader_mfdaq_intan from ndi.fun.utils import channelname2prefixnumber # --------------------------------------------------------------------------- @@ -71,50 +71,50 @@ def _find_example_rhd() -> str: class TestIntanReader: """Port of ndi.unittest.daq.reader.mfdaqIntanTest. - The MATLAB test creates an IntanReader, points it at real .rhd files, + The MATLAB test creates an ndi_daq_reader_mfdaq_intan, points it at real .rhd files, and checks channel discovery, epochsamples2times, epochtimes2samples. The Python reader delegates to SpikeInterface under the hood. """ def test_intan_reader_instantiation(self): - """IntanReader can be created with no arguments. + """ndi_daq_reader_mfdaq_intan can be created with no arguments. MATLAB equivalent: mfdaqIntanTest - reader construction """ - reader = IntanReader() + reader = ndi_daq_reader_mfdaq_intan() assert reader is not None - assert isinstance(reader, IntanReader) - assert isinstance(reader, MFDAQReader) + assert isinstance(reader, ndi_daq_reader_mfdaq_intan) + assert isinstance(reader, ndi_daq_reader_mfdaq) assert reader.NDI_DAQREADER_CLASS == "ndi.daq.reader.mfdaq.intan" assert reader.FILE_EXTENSIONS == [".rhd", ".rhs"] - assert reader.id # Ido gives a non-empty id string + assert reader.id # ndi_ido gives a non-empty id string assert isinstance(reader.id, str) def test_intan_reader_with_identifier(self): - """IntanReader can be created with an explicit identifier. + """ndi_daq_reader_mfdaq_intan can be created with an explicit identifier. MATLAB equivalent: mfdaqIntanTest - reader construction with identifier """ - reader = IntanReader(identifier="test_reader_id") - # The identifier is stored via Ido but may not be directly the same - # because Ido might override it; just verify we got a valid object - assert isinstance(reader, IntanReader) + reader = ndi_daq_reader_mfdaq_intan(identifier="test_reader_id") + # The identifier is stored via ndi_ido but may not be directly the same + # because ndi_ido might override it; just verify we got a valid object + assert isinstance(reader, ndi_daq_reader_mfdaq_intan) def test_intan_reader_ndi_class(self): - """IntanReader reports the correct NDI DAQ reader class string. + """ndi_daq_reader_mfdaq_intan reports the correct NDI DAQ reader class string. MATLAB equivalent: mfdaqIntanTest - class property check """ - reader = IntanReader() + reader = ndi_daq_reader_mfdaq_intan() assert reader._ndi_daqreader_class == "ndi.daq.reader.mfdaq.intan" def test_intan_reader_channel_types(self): - """MFDAQReader.channel_types() returns known channel types. + """ndi_daq_reader_mfdaq.channel_types() returns known channel types. MATLAB equivalent: mfdaqIntanTest - channel type discovery """ - types, abbrevs = MFDAQReader.channel_types() + types, abbrevs = ndi_daq_reader_mfdaq.channel_types() assert "analog_in" in types assert "digital_in" in types @@ -125,13 +125,13 @@ def test_intan_reader_channel_types(self): assert len(types) == len(abbrevs) def test_intan_reader_mocked_getchannels(self): - """IntanReader.getchannelsepoch works via mocked SpikeInterface. + """ndi_daq_reader_mfdaq_intan.getchannelsepoch works via mocked SpikeInterface. - The IntanReader delegates to SpikeInterfaceReader internally. + The ndi_daq_reader_mfdaq_intan delegates to ndi_daq_reader_SpikeInterfaceReader internally. We mock that to simulate a successful channel discovery without needing real data files. """ - reader = IntanReader() + reader = ndi_daq_reader_mfdaq_intan() mock_channels = [ ChannelInfo( @@ -159,11 +159,11 @@ def test_intan_reader_mocked_getchannels(self): assert channels[2].type == "time" def test_intan_reader_no_spikeinterface(self): - """IntanReader.getchannelsepoch returns [] when SI is unavailable. + """ndi_daq_reader_mfdaq_intan.getchannelsepoch returns [] when SI is unavailable. MATLAB tests always had NDR; Python gracefully degrades. """ - reader = IntanReader() + reader = ndi_daq_reader_mfdaq_intan() # Mock _get_si_reader to return None (no spikeinterface) with patch.object(reader, "_get_si_reader", return_value=None): @@ -172,26 +172,26 @@ def test_intan_reader_no_spikeinterface(self): assert channels == [] def test_intan_reader_readchannels_no_si_raises(self): - """IntanReader.readchannels_epochsamples raises ImportError without SI.""" - reader = IntanReader() + """ndi_daq_reader_mfdaq_intan.readchannels_epochsamples raises ImportError without SI.""" + reader = ndi_daq_reader_mfdaq_intan() with patch.object(reader, "_get_si_reader", return_value=None): with pytest.raises(ImportError, match="spikeinterface"): reader.readchannels_epochsamples("ai", [1], ["fake.rhd"], 1, 100) def test_intan_reader_samplerate_no_si_raises(self): - """IntanReader.samplerate raises ImportError without SI.""" - reader = IntanReader() + """ndi_daq_reader_mfdaq_intan.samplerate raises ImportError without SI.""" + reader = ndi_daq_reader_mfdaq_intan() with patch.object(reader, "_get_si_reader", return_value=None): with pytest.raises(ImportError, match="spikeinterface"): reader.samplerate(["fake.rhd"], "ai", [1]) def test_intan_reader_repr(self): - """IntanReader has a useful repr.""" - reader = IntanReader() + """ndi_daq_reader_mfdaq_intan has a useful repr.""" + reader = ndi_daq_reader_mfdaq_intan() r = repr(reader) - assert "IntanReader" in r + assert "ndi_daq_reader_mfdaq_intan" in r @requires_data def test_intan_reader_live(self): @@ -204,7 +204,7 @@ def test_intan_reader_live(self): rhd_path = _find_example_rhd() assert rhd_path, "Should have found an .rhd file" - reader = IntanReader() + reader = ndi_daq_reader_mfdaq_intan() channels = reader.getchannelsepoch([rhd_path]) # Should discover at least one analog input channel @@ -222,12 +222,12 @@ def test_intan_reader_live(self): # TestEpochSampleTimeConversion # Port of: mfdaqIntanTest - epochsamples2times / epochtimes2samples # -# These methods live on MFDAQReader. We test them with a concrete mock +# These methods live on ndi_daq_reader_mfdaq. We test them with a concrete mock # subclass rather than needing real data. # =========================================================================== -class _MockMFDAQReader(MFDAQReader): +class _MockMFDAQReader(ndi_daq_reader_mfdaq): """Concrete MFDAQ reader for testing time/sample conversions.""" def __init__(self, sample_rate=30000.0, t0=0.0, num_samples=300000): @@ -260,7 +260,7 @@ def t0_t1(self, epochfiles): class TestEpochSampleTimeConversion: """Port of mfdaqIntanTest - epochsamples2times / epochtimes2samples. - Tests the MFDAQReader methods for converting between sample indices + Tests the ndi_daq_reader_mfdaq methods for converting between sample indices and time values. Uses a mock reader with known sample rate and t0. """ @@ -471,7 +471,7 @@ def test_channel_type_abbreviation_property(self): class TestMFDAQReaderBase: - """Test MFDAQReader base class methods that don't need real data.""" + """Test ndi_daq_reader_mfdaq base class methods that don't need real data.""" def test_underlying_datatype_analog(self): """underlying_datatype returns float64 for analog_in channels.""" @@ -496,7 +496,7 @@ def test_underlying_datatype_unknown_raises(self): reader.underlying_datatype(["dummy.rhd"], "bogus_type", [1]) def test_epochclock_returns_dev_local_time(self): - """MFDAQReader.epochclock returns DEV_LOCAL_TIME.""" + """ndi_daq_reader_mfdaq.epochclock returns DEV_LOCAL_TIME.""" from ndi.time import DEV_LOCAL_TIME reader = _MockMFDAQReader() diff --git a/tests/matlab_tests/test_database.py b/tests/matlab_tests/test_database.py index f96e1f1..21c6bb1 100644 --- a/tests/matlab_tests/test_database.py +++ b/tests/matlab_tests/test_database.py @@ -14,10 +14,10 @@ import pytest -from ndi.common import PathConstants -from ndi.document import Document -from ndi.query import Query -from ndi.session.dir import DirSession +from ndi.common import ndi_common_PathConstants +from ndi.document import ndi_document +from ndi.query import ndi_query +from ndi.session.dir import ndi_session_dir # =========================================================================== # TestNDIDocument @@ -36,7 +36,7 @@ def test_document_creation_and_io(self, tmp_path): # Create session session_dir = tmp_path / "doc_session" session_dir.mkdir() - session = DirSession("doc_test", session_dir) + session = ndi_session_dir("doc_test", session_dir) # Create document with custom fields doc = session.newdocument( @@ -49,7 +49,7 @@ def test_document_creation_and_io(self, tmp_path): # Write a binary file binary_file = tmp_path / "test_binary.dat" - binary_file.write_bytes(b"Hello NDI Binary Data") + binary_file.write_bytes(b"Hello NDI Binary ndi_gui_Data") # Attach file to document doc = doc.add_file("filename1.ext", str(binary_file)) @@ -58,13 +58,13 @@ def test_document_creation_and_io(self, tmp_path): session.database_add(doc) # Search by name - q_name = Query("base.name") == "my_demo_doc" + q_name = ndi_query("base.name") == "my_demo_doc" results = session.database_search(q_name) assert len(results) == 1, "Should find 1 document by name" assert results[0].id == doc.id # Search by isa - q_type = Query("").isa("demoNDI") + q_type = ndi_query("").isa("demoNDI") results_isa = session.database_search(q_type) assert len(results_isa) == 1, "Should find 1 demoNDI document" @@ -72,14 +72,16 @@ def test_document_creation_and_io(self, tmp_path): fid = session.database_openbinarydoc(doc, "filename1.ext") content = fid.read() session.database_closebinarydoc(fid) - assert content == b"Hello NDI Binary Data", "Binary content should match what was written" + assert ( + content == b"Hello NDI Binary ndi_gui_Data" + ), "Binary content should match what was written" # Remove document session.database_rm(doc) # Verify removal results_after = session.database_search(q_name) - assert len(results_after) == 0, "Document should be removed" + assert len(results_after) == 0, "ndi_document should be removed" # =========================================================================== @@ -97,7 +99,7 @@ def test_field_discovery(self): MATLAB equivalent: TestNDIDocumentFields.testFieldDiscoveryAndValidation """ # Locate schema files - doc_folder = PathConstants.COMMON_FOLDER / "database_documents" + doc_folder = ndi_common_PathConstants.COMMON_FOLDER / "database_documents" if not doc_folder.exists(): pytest.skip(f"Schema folder not found: {doc_folder}") @@ -135,7 +137,7 @@ def test_field_discovery(self): def _discover_document_types(): """Discover all document types from JSON schema files.""" - doc_folder = PathConstants.COMMON_FOLDER / "database_documents" + doc_folder = ndi_common_PathConstants.COMMON_FOLDER / "database_documents" if not doc_folder.exists(): return [] @@ -156,11 +158,11 @@ class TestNDIDocumentJSON: @pytest.mark.parametrize("doc_type", _DOC_TYPES, ids=_DOC_TYPES) def test_single_json_definition(self, doc_type): - """Verify Document(doc_type) succeeds for each schema. + """Verify ndi_document(doc_type) succeeds for each schema. MATLAB equivalent: TestNDIDocumentJSON.testSingleJsonDefinition """ - doc = Document(doc_type) + doc = ndi_document(doc_type) assert doc is not None assert doc.document_properties is not None @@ -190,7 +192,7 @@ def test_document_round_trip(self, tmp_path): """ session_dir = tmp_path / "persist" session_dir.mkdir() - session = DirSession("persist_test", session_dir) + session = ndi_session_dir("persist_test", session_dir) # Create document doc = session.newdocument( @@ -206,7 +208,7 @@ def test_document_round_trip(self, tmp_path): session.database_add(doc) # Retrieve - q = Query("base.id") == original_id + q = ndi_query("base.id") == original_id results = session.database_search(q) assert len(results) == 1 @@ -219,7 +221,7 @@ def test_multiple_document_types_persist(self, tmp_path): """Multiple document types can coexist in the same database.""" session_dir = tmp_path / "multi" session_dir.mkdir() - session = DirSession("multi_test", session_dir) + session = ndi_session_dir("multi_test", session_dir) # Add several document types doc_types = ["base", "demoNDI", "subject"] @@ -231,7 +233,7 @@ def test_multiple_document_types_persist(self, tmp_path): pass # Some types may not be available # Search for all - all_docs = session.database_search(Query("").isa("base")) + all_docs = session.database_search(ndi_query("").isa("base")) # At minimum, base docs + session doc assert len(all_docs) >= 1 @@ -250,7 +252,7 @@ def test_document_discovery(self): MATLAB equivalent: TestNDIDocumentDiscovery.testDocumentDiscoveryAndValidation """ - doc_folder = PathConstants.COMMON_FOLDER / "database_documents" + doc_folder = ndi_common_PathConstants.COMMON_FOLDER / "database_documents" if not doc_folder.exists(): pytest.skip(f"Schema folder not found: {doc_folder}") @@ -266,7 +268,7 @@ def test_document_discovery(self): def test_schema_count(self): """Verify we have a reasonable number of schemas.""" - doc_folder = PathConstants.COMMON_FOLDER / "database_documents" + doc_folder = ndi_common_PathConstants.COMMON_FOLDER / "database_documents" if not doc_folder.exists(): pytest.skip(f"Schema folder not found: {doc_folder}") @@ -333,17 +335,17 @@ def test_compare_documents(self): tolerance=0.5, ) - doc1 = Document("demoNDI") + doc1 = ndi_document("demoNDI") props1 = doc1.document_properties props1["base"]["name"] = "test" props1["demoNDI"]["value"] = 10 - doc1 = Document(props1) + doc1 = ndi_document(props1) - doc2 = Document("demoNDI") + doc2 = ndi_document("demoNDI") props2 = doc2.document_properties props2["base"]["name"] = "test" props2["demoNDI"]["value"] = 10.3 - doc2 = Document(props2) + doc2 = ndi_document(props2) result = dc.compare(doc1, doc2) assert result is not None @@ -361,15 +363,15 @@ def test_compare_mismatched_names(self): method="character_exact", ) - doc1 = Document("demoNDI") + doc1 = ndi_document("demoNDI") props1 = doc1.document_properties props1["base"]["name"] = "alpha" - doc1 = Document(props1) + doc1 = ndi_document(props1) - doc2 = Document("demoNDI") + doc2 = ndi_document("demoNDI") props2 = doc2.document_properties props2["base"]["name"] = "beta" - doc2 = Document(props2) + doc2 = ndi_document(props2) result = dc.compare(doc1, doc2) # Result should indicate mismatch diff --git a/tests/matlab_tests/test_dataset.py b/tests/matlab_tests/test_dataset.py index edad3b8..10f3520 100644 --- a/tests/matlab_tests/test_dataset.py +++ b/tests/matlab_tests/test_dataset.py @@ -13,28 +13,28 @@ import pytest -from ndi.dataset import Dataset -from ndi.document import Document -from ndi.query import Query -from ndi.session.dir import DirSession +from ndi.dataset import ndi_dataset +from ndi.document import ndi_document +from ndi.query import ndi_query +from ndi.session.dir import ndi_session_dir # --------------------------------------------------------------------------- # Helper — mirrors ndi.unittest.session.buildSession.addDocsWithFiles() # --------------------------------------------------------------------------- -def _add_doc_with_file(session: DirSession, doc_number: int) -> None: +def _add_doc_with_file(session: ndi_session_dir, doc_number: int) -> None: """Add a demoNDI document with a file attachment to the session.""" docname = f"doc_{doc_number}" filepath = session.path / docname filepath.write_text(docname) - doc = Document("demoNDI") + doc = ndi_document("demoNDI") props = doc.document_properties props["base"]["name"] = docname props["demoNDI"]["value"] = doc_number props["base"]["session_id"] = session.id() - doc = Document(props) + doc = ndi_document(props) doc = doc.add_file("filename1.ext", str(filepath)) session.database_add(doc) @@ -46,20 +46,20 @@ def _add_doc_with_file(session: DirSession, doc_number: int) -> None: class TestDatasetConstructor: - """Test the constructor of Dataset.""" + """Test the constructor of ndi_dataset.""" def test_constructor_with_reference(self, tmp_path): - """Test Dataset(path, reference) — 2-arg form. + """Test ndi_dataset(path, reference) — 2-arg form. MATLAB equivalent: testConstructorWithEmptyDocs (MATLAB tests 2-arg and 3-arg; Python only has 2-arg and 1-arg.) """ ds_path = tmp_path / "test_dataset" ds_path.mkdir() - ds = Dataset(ds_path, "test_ref") + ds = ndi_dataset(ds_path, "test_ref") - # Verify it's a valid Dataset - assert isinstance(ds, Dataset) + # Verify it's a valid ndi_dataset + assert isinstance(ds, ndi_dataset) assert ds.getpath() == ds_path # Verify ID is non-empty string @@ -68,12 +68,12 @@ def test_constructor_with_reference(self, tmp_path): assert isinstance(ds_id, str) def test_constructor_path_only(self, tmp_path): - """Test Dataset(path) — reference derived from directory name.""" + """Test ndi_dataset(path) — reference derived from directory name.""" ds_path = tmp_path / "my_dataset" ds_path.mkdir() - ds = Dataset(ds_path) + ds = ndi_dataset(ds_path) - assert isinstance(ds, Dataset) + assert isinstance(ds, ndi_dataset) assert ds.reference == "my_dataset" assert ds.getpath() == ds_path @@ -85,11 +85,11 @@ def test_two_datasets_have_different_ids(self, tmp_path): """Two separate datasets should have unique IDs.""" ds1_path = tmp_path / "ds1" ds1_path.mkdir() - ds1 = Dataset(ds1_path, "ref1") + ds1 = ndi_dataset(ds1_path, "ref1") ds2_path = tmp_path / "ds2" ds2_path.mkdir() - ds2 = Dataset(ds2_path, "ref2") + ds2 = ndi_dataset(ds2_path, "ref2") assert ds1.id() != ds2.id() @@ -101,33 +101,33 @@ def test_two_datasets_have_different_ids(self, tmp_path): class TestDatasetBuild: - """Test that buildDataset fixture creates a valid Dataset + Session.""" + """Test that buildDataset fixture creates a valid ndi_dataset + ndi_session.""" def test_setup(self, build_dataset): - """Verify Dataset and Session are created and populated. + """Verify ndi_dataset and ndi_session are created and populated. MATLAB equivalent: testDatasetBuild.testSetup - - Dataset is ndi.dataset.dir - - Session is ndi.session.dir - - Session appears in dataset.session_list() + - ndi_dataset is ndi.dataset.dir + - ndi_session is ndi.session.dir + - ndi_session appears in dataset.session_list() - 5 demoNDI documents are present, each with readable file content """ dataset, session = build_dataset - # Check Dataset existence and type + # Check ndi_dataset existence and type assert dataset is not None - assert isinstance(dataset, Dataset) + assert isinstance(dataset, ndi_dataset) - # Check Session existence and type + # Check ndi_session existence and type assert session is not None - assert isinstance(session, DirSession) + assert isinstance(session, ndi_session_dir) - # Session should be in dataset's session list + # ndi_session should be in dataset's session list refs, session_ids, *_ = dataset.session_list() - assert session.id() in session_ids, "Session ID should be in dataset session list" + assert session.id() in session_ids, "ndi_session ID should be in dataset session list" # Should find exactly 5 demoNDI documents - q = Query("").isa("demoNDI") + q = ndi_query("").isa("demoNDI") docs = dataset.database_search(q) assert len(docs) == 5, "Should find 5 demoNDI documents in the dataset" @@ -148,7 +148,7 @@ def test_setup(self, build_dataset): content = content.decode("utf-8") assert content == docname, f"Content of {docname} should match" break - assert found, f"Document {docname} should be found" + assert found, f"ndi_document {docname} should be found" # =========================================================================== @@ -161,7 +161,7 @@ def test_setup(self, build_dataset): class TestSessionList: - """Test the session_list method of Dataset.""" + """Test the session_list method of ndi_dataset.""" def test_session_list_outputs(self, build_dataset): """Verify session_list returns correct structure and values. @@ -177,13 +177,13 @@ def test_session_list_outputs(self, build_dataset): assert len(refs) == 1 # 1. Verify session_reference - assert refs[0] == "exp_demo", "Session reference should match expected value" + assert refs[0] == "exp_demo", "ndi_session reference should match expected value" # 2. Verify session_id - assert session_ids[0] == session.id(), "Session ID should match the ingested session ID" + assert session_ids[0] == session.id(), "ndi_session ID should match the ingested session ID" # 3. Verify the session_in_a_dataset document exists and is correct - q = Query("").isa("session_in_a_dataset") + q = ndi_query("").isa("session_in_a_dataset") found = dataset.database_search(q) assert len(found) == 1, "Should find exactly one session_in_a_dataset document" @@ -199,7 +199,9 @@ def test_session_list_outputs(self, build_dataset): assert props.get("session_reference") == "exp_demo", "session_reference should match" # Check session_creator - assert props.get("session_creator") == "DirSession", "session_creator should be DirSession" + assert ( + props.get("session_creator") == "ndi_session_dir" + ), "session_creator should be ndi_session_dir" # =========================================================================== @@ -215,7 +217,7 @@ def test_session_list_outputs(self, build_dataset): class TestDeleteIngestedSession: - """Test the deleteIngestedSession method of Dataset.""" + """Test the deleteIngestedSession method of ndi_dataset.""" def test_delete_success(self, build_dataset): """Delete ingested session and verify it's removed. @@ -227,23 +229,23 @@ def test_delete_success(self, build_dataset): # Verify session exists initially refs, session_ids, *_ = dataset.session_list() - assert session_id in session_ids, "Session ID should be in dataset" + assert session_id in session_ids, "ndi_session ID should be in dataset" # Verify documents exist - q = Query("base.session_id") == session_id + q = ndi_query("base.session_id") == session_id docs = dataset.database_search(q) - assert len(docs) > 0, "Session documents should exist" + assert len(docs) > 0, "ndi_session documents should exist" # Delete the session dataset.deleteIngestedSession(session_id, are_you_sure=True) # Verify session is removed from list refs_after, ids_after, *_ = dataset.session_list() - assert session_id not in ids_after, "Session ID should NOT be in dataset after deletion" + assert session_id not in ids_after, "ndi_session ID should NOT be in dataset after deletion" # Verify documents are removed docs_after = dataset.database_search(q) - assert len(docs_after) == 0, "Session documents should be gone after deletion" + assert len(docs_after) == 0, "ndi_session documents should be gone after deletion" def test_delete_not_confirmed(self, build_dataset): """Deleting without are_you_sure=True raises ValueError. @@ -261,7 +263,7 @@ def test_delete_not_confirmed(self, build_dataset): refs, session_ids, *_ = dataset.session_list() assert ( session_id in session_ids - ), "Session ID should still be in dataset after failed delete" + ), "ndi_session ID should still be in dataset after failed delete" def test_delete_linked_session_error(self, build_dataset, tmp_path): """Deleting a linked session raises ValueError. @@ -274,7 +276,7 @@ def test_delete_linked_session_error(self, build_dataset, tmp_path): # Create and link a separate session linked_dir = tmp_path / "linked_session" linked_dir.mkdir() - linked_session = DirSession("linked_ref", linked_dir) + linked_session = ndi_session_dir("linked_ref", linked_dir) dataset.add_linked_session(linked_session) @@ -308,7 +310,7 @@ def test_delete_nonexistent_session(self, build_dataset): class TestUnlinkSession: - """Test the unlink_session method of Dataset.""" + """Test the unlink_session method of ndi_dataset.""" def test_unlink_linked_session(self, tmp_path): """Unlink a linked session and verify it's removed from the dataset. @@ -318,13 +320,13 @@ def test_unlink_linked_session(self, tmp_path): # Create session with docs session_dir = tmp_path / "sess_unlink" session_dir.mkdir() - session = DirSession("exp_unlink", session_dir) + session = ndi_session_dir("exp_unlink", session_dir) _add_doc_with_file(session, 1) # Create dataset ds_dir = tmp_path / "ds_unlink" ds_dir.mkdir() - dataset = Dataset(ds_dir, "ds_unlink") + dataset = ndi_dataset(ds_dir, "ds_unlink") dataset.add_linked_session(session) # Verify session is linked @@ -337,12 +339,12 @@ def test_unlink_linked_session(self, tmp_path): # Verify session is gone from dataset refs_after, ids_after, *_ = dataset.session_list() - assert len(ids_after) == 0, "Session list should be empty after unlink" + assert len(ids_after) == 0, "ndi_session list should be empty after unlink" - # Session files should still exist + # ndi_session files should still exist assert ( session.path / ".ndi" - ).exists(), "Session .ndi directory should still exist after unlink" + ).exists(), "ndi_session .ndi directory should still exist after unlink" def test_unlink_ingested_session_error(self, tmp_path): """Unlinking an ingested session raises ValueError. @@ -352,13 +354,13 @@ def test_unlink_ingested_session_error(self, tmp_path): # Create session session_dir = tmp_path / "sess_ing" session_dir.mkdir() - session = DirSession("exp_ing", session_dir) + session = ndi_session_dir("exp_ing", session_dir) _add_doc_with_file(session, 1) # Create dataset, ingest ds_dir = tmp_path / "ds_ing" ds_dir.mkdir() - dataset = Dataset(ds_dir, "ds_ing") + dataset = ndi_dataset(ds_dir, "ds_ing") dataset.add_ingested_session(session) session_id = session.id() @@ -374,7 +376,7 @@ def test_unlink_nonexistent_session(self, tmp_path): """ ds_dir = tmp_path / "ds_empty" ds_dir.mkdir() - dataset = Dataset(ds_dir, "ds_empty") + dataset = ndi_dataset(ds_dir, "ds_empty") with pytest.raises(ValueError, match="not found"): dataset.unlink_session("nonexistent_id", are_you_sure=True) @@ -386,11 +388,11 @@ def test_unlink_not_confirmed(self, tmp_path): """ session_dir = tmp_path / "sess_conf" session_dir.mkdir() - session = DirSession("exp_conf", session_dir) + session = ndi_session_dir("exp_conf", session_dir) ds_dir = tmp_path / "ds_conf" ds_dir.mkdir() - dataset = Dataset(ds_dir, "ds_conf") + dataset = ndi_dataset(ds_dir, "ds_conf") dataset.add_linked_session(session) with pytest.raises(ValueError, match="are_you_sure"): @@ -419,13 +421,13 @@ def test_open_existing_dataset(self, tmp_path): # 1. Create a dataset with an ingested session session_dir = tmp_path / "old_session" session_dir.mkdir() - session = DirSession("old_exp", session_dir) + session = ndi_session_dir("old_exp", session_dir) _add_doc_with_file(session, 1) _add_doc_with_file(session, 2) ds_dir = tmp_path / "old_dataset" ds_dir.mkdir() - dataset = Dataset(ds_dir, "old_ds") + dataset = ndi_dataset(ds_dir, "old_ds") dataset.add_ingested_session(session) # Remember the IDs @@ -437,7 +439,7 @@ def test_open_existing_dataset(self, tmp_path): del session # 3. Reopen the dataset from path only - reopened = Dataset(ds_dir) + reopened = ndi_dataset(ds_dir) assert reopened.id() == original_ds_id, "Reopened dataset should have same ID" # 4. Verify session list @@ -448,6 +450,6 @@ def test_open_existing_dataset(self, tmp_path): ), "Original session should still be in the reopened dataset" # 5. Verify documents are still accessible - q = Query("").isa("demoNDI") + q = ndi_query("").isa("demoNDI") docs = reopened.database_search(q) assert len(docs) == 2, "Should find 2 demoNDI documents" diff --git a/tests/matlab_tests/test_element.py b/tests/matlab_tests/test_element.py index 2962ae9..69cd360 100644 --- a/tests/matlab_tests/test_element.py +++ b/tests/matlab_tests/test_element.py @@ -5,8 +5,8 @@ +element/OneEpochTest.m → TestOneEpoch Tests the element module including: - - Element instantiation and basic properties - - Element epoch table management + - ndi_element instantiation and basic properties + - ndi_element epoch table management - oneepoch() function for creating single-epoch concatenated elements - missingepochs() function for comparing epoch tables - downsample_timeseries() for signal processing @@ -21,7 +21,7 @@ import pytest # =========================================================================== -# TestElementInstantiation — Element creation and basic properties +# TestElementInstantiation — ndi_element creation and basic properties # =========================================================================== @@ -29,21 +29,21 @@ class TestElementInstantiation: """Port of ndi.unittest.element basic tests.""" def test_create_element_no_session(self): - """Element can be created without a session.""" - from ndi.element import Element + """ndi_element can be created without a session.""" + from ndi.element import ndi_element - elem = Element(name="electrode1", reference=1, type="n-trode") + elem = ndi_element(name="electrode1", reference=1, type="n-trode") assert elem is not None assert elem.name == "electrode1" assert elem.reference == 1 def test_create_element_with_session(self): - """Element can be created with a mocked session.""" - from ndi.element import Element + """ndi_element can be created with a mocked session.""" + from ndi.element import ndi_element session = MagicMock() session.id.return_value = "session-123" - elem = Element( + elem = ndi_element( session=session, name="cortex", reference=1, @@ -53,25 +53,25 @@ def test_create_element_with_session(self): assert elem.name == "cortex" def test_element_type_attribute(self): - """Element stores type correctly.""" - from ndi.element import Element + """ndi_element stores type correctly.""" + from ndi.element import ndi_element - elem = Element(name="stim", reference=0, type="stimulator") + elem = ndi_element(name="stim", reference=0, type="stimulator") assert elem._type == "stimulator" def test_element_default_values(self): - """Element has correct default values.""" - from ndi.element import Element + """ndi_element has correct default values.""" + from ndi.element import ndi_element - elem = Element() + elem = ndi_element() assert elem.name == "" assert elem.reference == 0 def test_element_subject_id(self): - """Element stores subject_id.""" - from ndi.element import Element + """ndi_element stores subject_id.""" + from ndi.element import ndi_element - elem = Element( + elem = ndi_element( name="e1", reference=1, type="n-trode", @@ -80,11 +80,11 @@ def test_element_subject_id(self): assert elem.subject_id == "subj-001" def test_element_underlying_element(self): - """Element stores underlying_element reference.""" - from ndi.element import Element + """ndi_element stores underlying_element reference.""" + from ndi.element import ndi_element - parent = Element(name="parent", reference=1, type="n-trode") - child = Element( + parent = ndi_element(name="parent", reference=1, type="n-trode") + child = ndi_element( name="child", reference=1, type="timeseries", @@ -93,39 +93,39 @@ def test_element_underlying_element(self): assert child.underlying_element is parent def test_element_direct_flag(self): - """Element stores direct flag.""" - from ndi.element import Element + """ndi_element stores direct flag.""" + from ndi.element import ndi_element - elem = Element(name="e1", reference=1, direct=False) + elem = ndi_element(name="e1", reference=1, direct=False) assert elem.direct is False def test_element_inherits_ido(self): - """Element inherits from Ido (has unique identifier).""" - from ndi.element import Element - from ndi.ido import Ido + """ndi_element inherits from ndi_ido (has unique identifier).""" + from ndi.element import ndi_element + from ndi.ido import ndi_ido - elem = Element(name="e1", reference=1) - assert isinstance(elem, Ido) + elem = ndi_element(name="e1", reference=1) + assert isinstance(elem, ndi_ido) def test_element_inherits_epochset(self): - """Element inherits from EpochSet.""" - from ndi.element import Element - from ndi.epoch.epochset import EpochSet + """ndi_element inherits from ndi_epoch_epochset.""" + from ndi.element import ndi_element + from ndi.epoch.epochset import ndi_epoch_epochset - elem = Element(name="e1", reference=1) - assert isinstance(elem, EpochSet) + elem = ndi_element(name="e1", reference=1) + assert isinstance(elem, ndi_epoch_epochset) def test_element_inherits_documentservice(self): - """Element inherits from DocumentService.""" - from ndi.documentservice import DocumentService - from ndi.element import Element + """ndi_element inherits from ndi_documentservice.""" + from ndi.documentservice import ndi_documentservice + from ndi.element import ndi_element - elem = Element(name="e1", reference=1) - assert isinstance(elem, DocumentService) + elem = ndi_element(name="e1", reference=1) + assert isinstance(elem, ndi_documentservice) # =========================================================================== -# TestElementEpochTable — Epoch table operations +# TestElementEpochTable — ndi_epoch_epoch table operations # =========================================================================== @@ -134,26 +134,26 @@ class TestElementEpochTable: def test_epochtable_no_session(self): """epochtable returns empty for element with no session.""" - from ndi.element import Element + from ndi.element import ndi_element - elem = Element(name="e1", reference=1) + elem = ndi_element(name="e1", reference=1) et = elem.buildepochtable() assert isinstance(et, list) assert len(et) == 0 def test_elementstring(self): """elementstring returns formatted string.""" - from ndi.element import Element + from ndi.element import ndi_element - elem = Element(name="cortex", reference=1) + elem = ndi_element(name="cortex", reference=1) s = elem.elementstring() assert "cortex" in s def test_epochsetname(self): """epochsetname returns name string.""" - from ndi.element import Element + from ndi.element import ndi_element - elem = Element(name="cortex", reference=1, type="n-trode") + elem = ndi_element(name="cortex", reference=1, type="n-trode") name = elem.epochsetname() assert isinstance(name, str) assert len(name) > 0 @@ -168,8 +168,8 @@ class TestOneEpoch: """Port of ndi.unittest.element.OneEpochTest — oneepoch function tests.""" def test_oneepoch_creates_element(self): - """oneepoch() creates a new Element.""" - from ndi.element import Element + """oneepoch() creates a new ndi_element.""" + from ndi.element import ndi_element from ndi.element.functions import oneepoch session = MagicMock() @@ -185,7 +185,7 @@ def test_oneepoch_creates_element(self): ] result = oneepoch(session, input_elem, "probe1_oneepoch", 1) - assert isinstance(result, Element) + assert isinstance(result, ndi_element) assert result.name == "probe1_oneepoch" assert result.reference == 1 @@ -216,7 +216,7 @@ def test_oneepoch_empty_epochs_raises(self): def test_oneepoch_multiple_epochs(self): """oneepoch() works with multiple input epochs.""" - from ndi.element import Element + from ndi.element import ndi_element from ndi.element.functions import oneepoch session = MagicMock() @@ -228,7 +228,7 @@ def test_oneepoch_multiple_epochs(self): ] result = oneepoch(session, input_elem, "combined", 1) - assert isinstance(result, Element) + assert isinstance(result, ndi_element) def test_oneepoch_with_list_epochs(self): """oneepoch() works when epoch table is provided as a list.""" @@ -331,7 +331,7 @@ class TestSpikesForProbe: def test_creates_spike_element(self): """spikesForProbe creates an element of type 'spikes'.""" - from ndi.element import Element + from ndi.element import ndi_element from ndi.element.functions import spikesForProbe session = MagicMock() @@ -345,7 +345,7 @@ def test_creates_spike_element(self): ] result = spikesForProbe(session, probe, "unit1", 1, spikedata) - assert isinstance(result, Element) + assert isinstance(result, ndi_element) assert result._type == "spikes" def test_invalid_epoch_raises(self): @@ -430,8 +430,8 @@ def test_downsample_multichannel(self): assert len(t_out) < len(t) def test_downsample_element_function(self): - """downsample() creates a new Element.""" - from ndi.element import Element + """downsample() creates a new ndi_element.""" + from ndi.element import ndi_element from ndi.element.functions import downsample session = MagicMock() @@ -442,7 +442,7 @@ def test_downsample_element_function(self): ] result = downsample(session, input_elem, 50.0, "ds_output", 1) - assert isinstance(result, Element) + assert isinstance(result, ndi_element) def test_downsample_invalid_frequency_raises(self): """downsample() raises for non-positive frequency.""" diff --git a/tests/matlab_tests/test_fun.py b/tests/matlab_tests/test_fun.py index 4ff1acc..21fae02 100644 --- a/tests/matlab_tests/test_fun.py +++ b/tests/matlab_tests/test_fun.py @@ -21,47 +21,49 @@ import numpy as np import pandas as pd -from ndi.dataset import Dataset -from ndi.document import Document +from ndi.dataset import ndi_dataset +from ndi.document import ndi_document from ndi.fun.dataset import diff as dataset_diff from ndi.fun.doc import allTypes, findFuid from ndi.fun.doc import diff as doc_diff from ndi.fun.session import diff as session_diff from ndi.fun.table import vstack -from ndi.query import Query -from ndi.session.dir import DirSession +from ndi.query import ndi_query +from ndi.session.dir import ndi_session_dir # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- -def _make_demo_doc(name: str = "test", value: int = 42, session_id: str = "") -> Document: +def _make_demo_doc(name: str = "test", value: int = 42, session_id: str = "") -> ndi_document: """Create a demoNDI document with given name and value.""" - doc = Document("demoNDI") + doc = ndi_document("demoNDI") props = doc.document_properties props["base"]["name"] = name props["demoNDI"]["value"] = value if session_id: props["base"]["session_id"] = session_id - return Document(props) + return ndi_document(props) def _make_session_with_docs(tmp_path, ref, doc_specs): - """Create a DirSession and add documents from a list of (name, value) tuples. + """Create a ndi_session_dir and add documents from a list of (name, value) tuples. Returns (session, list_of_added_docs). """ session_dir = tmp_path / ref session_dir.mkdir(parents=True, exist_ok=True) - session = DirSession(ref, session_dir) + session = ndi_session_dir(ref, session_dir) added = [] for name, value in doc_specs: doc = _make_demo_doc(name, value, session.id()) session.database_add(doc) # Re-fetch from DB so we have the stored version - results = session.database_search(Query("base.id") == doc.document_properties["base"]["id"]) + results = session.database_search( + ndi_query("base.id") == doc.document_properties["base"]["id"] + ) added.append(results[0] if results else doc) return session, added @@ -130,7 +132,7 @@ def test_find_known_fuid(self, tmp_path): """ session_dir = tmp_path / "fuid_sess" session_dir.mkdir() - session = DirSession("fuid_test", session_dir) + session = ndi_session_dir("fuid_test", session_dir) # Create a document with a file attachment filepath = session_dir / "testfile.dat" @@ -158,7 +160,7 @@ def test_findFuid_not_found(self, tmp_path): """ session_dir = tmp_path / "fuid_sess2" session_dir.mkdir() - session = DirSession("fuid_test2", session_dir) + session = ndi_session_dir("fuid_test2", session_dir) # Add a document (so the database is not empty) doc = _make_demo_doc("some_doc", 99, session.id()) @@ -176,7 +178,7 @@ def test_findFuid_in_populated_session(self, tmp_path): """ session_dir = tmp_path / "fuid_sess3" session_dir.mkdir() - session = DirSession("fuid_test3", session_dir) + session = ndi_session_dir("fuid_test3", session_dir) # Create multiple docs with files target_fuid = None @@ -220,7 +222,7 @@ def test_identical_docs(self): """ doc1 = _make_demo_doc("same_doc", 42) # Create a copy with same properties (same id, same everything) - doc2 = Document(copy.deepcopy(doc1.document_properties)) + doc2 = ndi_document(copy.deepcopy(doc1.document_properties)) result = doc_diff(doc1, doc2) @@ -233,7 +235,7 @@ def test_property_mismatch(self): MATLAB equivalent: testDiff.testPropertyMismatch """ doc1 = _make_demo_doc("doc_a", 10) - doc2 = Document(copy.deepcopy(doc1.document_properties)) + doc2 = ndi_document(copy.deepcopy(doc1.document_properties)) doc2.document_properties["demoNDI"]["value"] = 99 result = doc_diff(doc1, doc2) @@ -250,7 +252,7 @@ def test_ignore_fields(self): MATLAB equivalent: testDiff.testIgnoreFields """ doc1 = _make_demo_doc("doc_a", 10) - doc2 = Document(copy.deepcopy(doc1.document_properties)) + doc2 = ndi_document(copy.deepcopy(doc1.document_properties)) doc2.document_properties["demoNDI"]["value"] = 99 result = doc_diff(doc1, doc2, exclude_fields=["demoNDI.value"]) @@ -295,7 +297,7 @@ def test_dependencies_order_independence(self): MATLAB equivalent: testDiff.testDependenciesOrderIndependence """ doc1 = _make_demo_doc("doc_dep", 1) - doc2 = Document(copy.deepcopy(doc1.document_properties)) + doc2 = ndi_document(copy.deepcopy(doc1.document_properties)) # Add depends_on in different orders dep_a = {"name": "dep_a", "value": "id_aaa"} @@ -316,7 +318,7 @@ def test_file_lists_order_independence(self): MATLAB equivalent: testDiff.testFileListsOrderIndependence """ doc1 = _make_demo_doc("doc_files", 1) - doc2 = Document(copy.deepcopy(doc1.document_properties)) + doc2 = ndi_document(copy.deepcopy(doc1.document_properties)) fi_a = {"name": "file_a.dat", "locations": []} fi_b = {"name": "file_b.dat", "locations": []} @@ -348,14 +350,14 @@ def test_identical_sessions(self, tmp_path): """ session_dir = tmp_path / "identical_sess" session_dir.mkdir() - session = DirSession("identical", session_dir) + session = ndi_session_dir("identical", session_dir) for i in range(1, 4): doc = _make_demo_doc(f"doc_{i}", i, session.id()) session.database_add(doc) # Reopen same session (same path -> same data) - session2 = DirSession("identical", session_dir) + session2 = ndi_session_dir("identical", session_dir) result = session_diff(session, session2) @@ -367,17 +369,17 @@ def test_identical_sessions(self, tmp_path): assert len(result["mismatches"]) == 0 def test_docs_only_in_s1(self, tmp_path): - """Session 1 has extra docs that Session 2 does not. + """ndi_session 1 has extra docs that ndi_session 2 does not. MATLAB equivalent: diffTest.testDocsInAOnly """ s1_dir = tmp_path / "sess_a" s1_dir.mkdir() - session1 = DirSession("sess_a", s1_dir) + session1 = ndi_session_dir("sess_a", s1_dir) s2_dir = tmp_path / "sess_b" s2_dir.mkdir() - session2 = DirSession("sess_b", s2_dir) + session2 = ndi_session_dir("sess_b", s2_dir) # Add 3 docs to session1, 0 to session2 for i in range(1, 4): @@ -394,17 +396,17 @@ def test_docs_only_in_s1(self, tmp_path): assert result["common_count"] == 0 def test_docs_only_in_s2(self, tmp_path): - """Session 2 has extra docs that Session 1 does not. + """ndi_session 2 has extra docs that ndi_session 1 does not. MATLAB equivalent: diffTest.testDocsInBOnly """ s1_dir = tmp_path / "sess_a" s1_dir.mkdir() - session1 = DirSession("sess_a", s1_dir) + session1 = ndi_session_dir("sess_a", s1_dir) s2_dir = tmp_path / "sess_b" s2_dir.mkdir() - session2 = DirSession("sess_b", s2_dir) + session2 = ndi_session_dir("sess_b", s2_dir) # Add 2 docs to session2, 0 to session1 for i in range(1, 3): @@ -427,11 +429,11 @@ def test_mismatched_docs(self, tmp_path): """ s1_dir = tmp_path / "sess_a" s1_dir.mkdir() - session1 = DirSession("sess_a", s1_dir) + session1 = ndi_session_dir("sess_a", s1_dir) s2_dir = tmp_path / "sess_b" s2_dir.mkdir() - session2 = DirSession("sess_b", s2_dir) + session2 = ndi_session_dir("sess_b", s2_dir) # Create a document with a known ID doc = _make_demo_doc("shared_doc", 10, session1.id()) @@ -442,7 +444,7 @@ def test_mismatched_docs(self, tmp_path): doc2_props = copy.deepcopy(doc.document_properties) doc2_props["demoNDI"]["value"] = 99 doc2_props["base"]["session_id"] = session2.id() - doc2 = Document(doc2_props) + doc2 = ndi_document(doc2_props) session2.database_add(doc2) result = session_diff(session1, session2) @@ -470,7 +472,7 @@ def test_identical_datasets(self, tmp_path): # Create source session sess_dir = tmp_path / "src_sess" sess_dir.mkdir() - session = DirSession("src", sess_dir) + session = ndi_session_dir("src", sess_dir) for i in range(1, 3): doc = _make_demo_doc(f"doc_{i}", i, session.id()) session.database_add(doc) @@ -478,11 +480,11 @@ def test_identical_datasets(self, tmp_path): # Create dataset and ingest ds_dir = tmp_path / "ds1" ds_dir.mkdir() - dataset1 = Dataset(ds_dir, "ds1") + dataset1 = ndi_dataset(ds_dir, "ds1") dataset1.add_ingested_session(session) # Reopen the same dataset - dataset2 = Dataset(ds_dir, "ds1") + dataset2 = ndi_dataset(ds_dir, "ds1") result = dataset_diff(dataset1, dataset2) @@ -490,56 +492,56 @@ def test_identical_datasets(self, tmp_path): assert result["session_diff"]["equal"] is True def test_docs_only_in_dataset1(self, tmp_path): - """Dataset 1 has extra docs that Dataset 2 does not. + """ndi_dataset 1 has extra docs that ndi_dataset 2 does not. MATLAB equivalent: diffTest.testDocsInAOnly """ - # Dataset 1 with docs + # ndi_dataset 1 with docs sess1_dir = tmp_path / "sess1" sess1_dir.mkdir() - session1 = DirSession("sess1", sess1_dir) + session1 = ndi_session_dir("sess1", sess1_dir) for i in range(1, 4): doc = _make_demo_doc(f"doc_{i}", i, session1.id()) session1.database_add(doc) ds1_dir = tmp_path / "ds1" ds1_dir.mkdir() - dataset1 = Dataset(ds1_dir, "ds1") + dataset1 = ndi_dataset(ds1_dir, "ds1") dataset1.add_ingested_session(session1) - # Dataset 2 empty + # ndi_dataset 2 empty ds2_dir = tmp_path / "ds2" ds2_dir.mkdir() - dataset2 = Dataset(ds2_dir, "ds2") + dataset2 = ndi_dataset(ds2_dir, "ds2") result = dataset_diff(dataset1, dataset2) assert result["equal"] is False sd = result["session_diff"] - # Dataset 1 has documents that dataset 2 does not + # ndi_dataset 1 has documents that dataset 2 does not assert len(sd["only_in_s1"]) > 0 or len(sd["mismatches"]) > 0 def test_docs_only_in_dataset2(self, tmp_path): - """Dataset 2 has extra docs that Dataset 1 does not. + """ndi_dataset 2 has extra docs that ndi_dataset 1 does not. MATLAB equivalent: diffTest.testDocsInBOnly """ - # Dataset 1 empty + # ndi_dataset 1 empty ds1_dir = tmp_path / "ds1" ds1_dir.mkdir() - dataset1 = Dataset(ds1_dir, "ds1") + dataset1 = ndi_dataset(ds1_dir, "ds1") - # Dataset 2 with docs + # ndi_dataset 2 with docs sess2_dir = tmp_path / "sess2" sess2_dir.mkdir() - session2 = DirSession("sess2", sess2_dir) + session2 = ndi_session_dir("sess2", sess2_dir) for i in range(1, 4): doc = _make_demo_doc(f"doc_{i}", i, session2.id()) session2.database_add(doc) ds2_dir = tmp_path / "ds2" ds2_dir.mkdir() - dataset2 = Dataset(ds2_dir, "ds2") + dataset2 = ndi_dataset(ds2_dir, "ds2") dataset2.add_ingested_session(session2) result = dataset_diff(dataset1, dataset2) @@ -557,31 +559,31 @@ def test_mismatched_datasets(self, tmp_path): doc = _make_demo_doc("shared", 10) doc.document_properties["base"]["id"] - # Dataset 1 + # ndi_dataset 1 sess1_dir = tmp_path / "sess1" sess1_dir.mkdir() - session1 = DirSession("sess1", sess1_dir) + session1 = ndi_session_dir("sess1", sess1_dir) doc1_props = copy.deepcopy(doc.document_properties) doc1_props["base"]["session_id"] = session1.id() - session1.database_add(Document(doc1_props)) + session1.database_add(ndi_document(doc1_props)) ds1_dir = tmp_path / "ds1" ds1_dir.mkdir() - dataset1 = Dataset(ds1_dir, "ds1") + dataset1 = ndi_dataset(ds1_dir, "ds1") dataset1.add_ingested_session(session1) - # Dataset 2 — same doc ID but different value + # ndi_dataset 2 — same doc ID but different value sess2_dir = tmp_path / "sess2" sess2_dir.mkdir() - session2 = DirSession("sess2", sess2_dir) + session2 = ndi_session_dir("sess2", sess2_dir) doc2_props = copy.deepcopy(doc.document_properties) doc2_props["demoNDI"]["value"] = 99 doc2_props["base"]["session_id"] = session2.id() - session2.database_add(Document(doc2_props)) + session2.database_add(ndi_document(doc2_props)) ds2_dir = tmp_path / "ds2" ds2_dir.mkdir() - dataset2 = Dataset(ds2_dir, "ds2") + dataset2 = ndi_dataset(ds2_dir, "ds2") dataset2.add_ingested_session(session2) result = dataset_diff(dataset1, dataset2) diff --git a/tests/matlab_tests/test_ingestion.py b/tests/matlab_tests/test_ingestion.py index 848f9cf..dd3ce19 100644 --- a/tests/matlab_tests/test_ingestion.py +++ b/tests/matlab_tests/test_ingestion.py @@ -12,7 +12,7 @@ Since the MATLAB tests require real Intan (.rhd) / Axon (.abf) data files and a full DAQ system pipeline, we provide: - Mocked unit tests (always run) testing the ingestion functions - - Integration tests (skip if DirSession unavailable) for full flow + - Integration tests (skip if ndi_session_dir unavailable) for full flow """ from unittest.mock import MagicMock @@ -26,9 +26,9 @@ def _have_example_data() -> bool: """Check if example Intan data is available.""" - from ndi.common import PathConstants + from ndi.common import ndi_common_PathConstants - example = PathConstants.COMMON_FOLDER / "example_sessions" + example = ndi_common_PathConstants.COMMON_FOLDER / "example_sessions" return example.exists() and any(example.rglob("*.rhd")) diff --git a/tests/matlab_tests/test_jess_haley.py b/tests/matlab_tests/test_jess_haley.py index 033e581..51a9179 100644 --- a/tests/matlab_tests/test_jess_haley.py +++ b/tests/matlab_tests/test_jess_haley.py @@ -4,7 +4,7 @@ Mirrors the MATLAB tutorial workflow from: ndi.setup.conv.haley.tutorial_682e7772cdf3f24938176fac.mlx -Dataset: 78,687 JSON documents + 11,163 binary files (15 GB) +ndi_dataset: 78,687 JSON documents + 11,163 binary files (15 GB) Source: NDI Cloud dataset 682e7772cdf3f24938176fac MATLAB tutorial steps tested: @@ -13,10 +13,10 @@ 3. View document types (getDocTypes) 4. View ontology variables (ontologyTableRowVars) 5. Extract metadata tables (ontologyTableRowDoc2Table) - 6. Subject summary (docTable.subject) + 6. ndi_subject summary (docTable.subject) 7. Table join (table.join) 8. Filter subjects (identifyMatchingRows) - 9. Query elements (position/distance) + 9. ndi_query elements (position/distance) 10. Read images (readImageStack) 11. Plot image + position overlay """ @@ -30,7 +30,7 @@ import pytest # --------------------------------------------------------------------------- -# Dataset paths — skip entire file if not downloaded locally +# ndi_dataset paths — skip entire file if not downloaded locally # --------------------------------------------------------------------------- JESS_HALEY_DOCS = Path(os.path.expanduser("~/Documents/ndi-projects/datasets/jess-haley/documents")) @@ -61,7 +61,7 @@ "treatment": 56, } -# Total documents in JSON files (Dataset init adds 1 extra session doc) +# Total documents in JSON files (ndi_dataset init adds 1 extra session doc) TOTAL_JSON_DOCS = 78687 # Expected ontologyTableRow group sizes (sorted descending, order-independent) @@ -71,13 +71,13 @@ # --------------------------------------------------------------------------- -# Session-scoped fixture — loads dataset once for all tests +# ndi_session-scoped fixture — loads dataset once for all tests # --------------------------------------------------------------------------- @pytest.fixture(scope="session") def jess_haley_dataset(tmp_path_factory): - """Load 78K JSON docs into a Dataset object (once per session).""" + """Load 78K JSON docs into a ndi_dataset object (once per session).""" from ndi.cloud.orchestration import load_dataset_from_json_dir target = tmp_path_factory.mktemp("jess_haley_ds") @@ -91,7 +91,7 @@ def jess_haley_dataset(tmp_path_factory): @pytest.fixture(scope="session") def all_docs_raw(): - """Load all raw JSON dicts (no Dataset overhead).""" + """Load all raw JSON dicts (no ndi_dataset overhead).""" docs = [] for f in sorted(JESS_HALEY_DOCS.glob("*.json")): with open(f) as fh: @@ -101,10 +101,10 @@ def all_docs_raw(): @pytest.fixture(scope="session") def ontology_table_row_docs(jess_haley_dataset): - """Query all ontologyTableRow documents from the dataset.""" - from ndi.query import Query + """ndi_query all ontologyTableRow documents from the dataset.""" + from ndi.query import ndi_query - return jess_haley_dataset.database_search(Query("").isa("ontologyTableRow")) + return jess_haley_dataset.database_search(ndi_query("").isa("ontologyTableRow")) @pytest.fixture(scope="session") @@ -124,10 +124,10 @@ class TestDatasetLoading: """Validate the bulk load of 78K documents.""" def test_load_document_count(self, jess_haley_dataset): - """At least 78,687 documents load (Dataset init adds 1 session doc).""" - from ndi.query import Query + """At least 78,687 documents load (ndi_dataset init adds 1 session doc).""" + from ndi.query import ndi_query - docs = jess_haley_dataset.database_search(Query("").isa("base")) + docs = jess_haley_dataset.database_search(ndi_query("").isa("base")) assert len(docs) >= TOTAL_JSON_DOCS, f"Expected >= {TOTAL_JSON_DOCS}, got {len(docs)}" def test_document_type_counts(self, jess_haley_dataset): @@ -139,7 +139,7 @@ def test_document_type_counts(self, jess_haley_dataset): for dtype, expected in EXPECTED_TYPE_COUNTS.items(): actual_count = actual.get(dtype, 0) if dtype == "session": - # Dataset init adds 1 extra session doc + # ndi_dataset init adds 1 extra session doc assert ( actual_count >= expected ), f"{dtype}: expected >= {expected}, got {actual_count}" @@ -173,25 +173,25 @@ class TestSessionDiscovery: """MATLAB: dataset.session_list(), dataset.open_session().""" def test_session_docs_exist(self, jess_haley_dataset): - """Session documents exist in the dataset.""" - from ndi.query import Query + """ndi_session documents exist in the dataset.""" + from ndi.query import ndi_query - docs = jess_haley_dataset.database_search(Query("").isa("session")) - # 3 from JSON + 1 auto-created by Dataset init + docs = jess_haley_dataset.database_search(ndi_query("").isa("session")) + # 3 from JSON + 1 auto-created by ndi_dataset init assert len(docs) >= 3 def test_session_count(self, jess_haley_dataset): """At least 3 session documents from the Jess Haley dataset.""" - from ndi.query import Query + from ndi.query import ndi_query - docs = jess_haley_dataset.database_search(Query("").isa("session")) + docs = jess_haley_dataset.database_search(ndi_query("").isa("session")) assert len(docs) >= 3 def test_session_refs_contain_celegans_and_ecoli(self, jess_haley_dataset): - """Session documents have reference fields.""" - from ndi.query import Query + """ndi_session documents have reference fields.""" + from ndi.query import ndi_query - docs = jess_haley_dataset.database_search(Query("").isa("session")) + docs = jess_haley_dataset.database_search(ndi_query("").isa("session")) refs = [] for doc in docs: props = doc.document_properties @@ -313,9 +313,9 @@ class TestSubjectQueries: """MATLAB: ndi.fun.docTable.subject(session).""" def test_subject_count(self, jess_haley_dataset): - from ndi.query import Query + from ndi.query import ndi_query - docs = jess_haley_dataset.database_search(Query("").isa("subject")) + docs = jess_haley_dataset.database_search(ndi_query("").isa("subject")) assert len(docs) == 1656 def test_subject_table(self, jess_haley_dataset): @@ -415,15 +415,15 @@ class TestElementQueries: """MATLAB: query elements by type (position, distance).""" def test_element_count(self, jess_haley_dataset): - from ndi.query import Query + from ndi.query import ndi_query - docs = jess_haley_dataset.database_search(Query("").isa("element")) + docs = jess_haley_dataset.database_search(ndi_query("").isa("element")) assert len(docs) == 4156 def test_element_types_include_position_and_distance(self, jess_haley_dataset): - from ndi.query import Query + from ndi.query import ndi_query - docs = jess_haley_dataset.database_search(Query("").isa("element")) + docs = jess_haley_dataset.database_search(ndi_query("").isa("element")) types = set() for doc in docs: t = doc.document_properties.get("element", {}).get("type", "") @@ -432,9 +432,9 @@ def test_element_types_include_position_and_distance(self, jess_haley_dataset): assert "distance" in types, f"'distance' not in element types: {types}" def test_position_metadata_links_to_element(self, jess_haley_dataset): - from ndi.query import Query + from ndi.query import ndi_query - docs = jess_haley_dataset.database_search(Query("").isa("position_metadata")) + docs = jess_haley_dataset.database_search(ndi_query("").isa("position_metadata")) assert len(docs) > 0 # depends_on can be a dict or a list of dicts sample = docs[0] @@ -448,9 +448,9 @@ def test_position_metadata_links_to_element(self, jess_haley_dataset): assert "element_id" in dep_names, f"No element_id in depends_on: {dep_names}" def test_distance_metadata_links_to_element(self, jess_haley_dataset): - from ndi.query import Query + from ndi.query import ndi_query - docs = jess_haley_dataset.database_search(Query("").isa("distance_metadata")) + docs = jess_haley_dataset.database_search(ndi_query("").isa("distance_metadata")) assert len(docs) > 0 sample = docs[0] deps = sample.document_properties.get("depends_on", []) @@ -472,23 +472,23 @@ class TestImageStack: """MATLAB: readImageStack, imageStack document structure.""" def test_image_stack_count(self, jess_haley_dataset): - from ndi.query import Query + from ndi.query import ndi_query - docs = jess_haley_dataset.database_search(Query("").isa("imageStack")) + docs = jess_haley_dataset.database_search(ndi_query("").isa("imageStack")) assert len(docs) == 7007 def test_image_stack_has_parameters(self, jess_haley_dataset): - from ndi.query import Query + from ndi.query import ndi_query - docs = jess_haley_dataset.database_search(Query("").isa("imageStack")) + docs = jess_haley_dataset.database_search(ndi_query("").isa("imageStack")) sample = docs[0] assert "imageStack_parameters" in sample.document_properties def test_image_stack_types(self, jess_haley_dataset): - """Dataset has uint8 videos, logical masks, and uint16 fluorescence.""" - from ndi.query import Query + """ndi_dataset has uint8 videos, logical masks, and uint16 fluorescence.""" + from ndi.query import ndi_query - docs = jess_haley_dataset.database_search(Query("").isa("imageStack")) + docs = jess_haley_dataset.database_search(ndi_query("").isa("imageStack")) data_types = set() for doc in docs[:500]: # sample first 500 params = doc.document_properties.get("imageStack_parameters", {}) @@ -522,9 +522,9 @@ def test_image_stack_binary_files_exist(self, all_docs_raw): assert missing == 0, f"{missing}/{total} imageStack docs missing binary files" def test_ontology_label_count(self, jess_haley_dataset): - from ndi.query import Query + from ndi.query import ndi_query - docs = jess_haley_dataset.database_search(Query("").isa("ontologyLabel")) + docs = jess_haley_dataset.database_search(ndi_query("").isa("ontologyLabel")) assert len(docs) == 7007 @@ -534,36 +534,36 @@ def test_ontology_label_count(self, jess_haley_dataset): class TestEpochAndMetadata: - """Epoch/position/distance document relationships.""" + """ndi_epoch_epoch/position/distance document relationships.""" def test_element_epoch_count(self, jess_haley_dataset): - from ndi.query import Query + from ndi.query import ndi_query - docs = jess_haley_dataset.database_search(Query("").isa("element_epoch")) + docs = jess_haley_dataset.database_search(ndi_query("").isa("element_epoch")) assert len(docs) == 4156 def test_position_metadata_count(self, jess_haley_dataset): - from ndi.query import Query + from ndi.query import ndi_query - docs = jess_haley_dataset.database_search(Query("").isa("position_metadata")) + docs = jess_haley_dataset.database_search(ndi_query("").isa("position_metadata")) assert len(docs) == 2078 def test_distance_metadata_count(self, jess_haley_dataset): - from ndi.query import Query + from ndi.query import ndi_query - docs = jess_haley_dataset.database_search(Query("").isa("distance_metadata")) + docs = jess_haley_dataset.database_search(ndi_query("").isa("distance_metadata")) assert len(docs) == 2078 def test_metadata_depends_on_references_exist(self, jess_haley_dataset): """Position/distance metadata depends_on values reference existing elements.""" - from ndi.query import Query + from ndi.query import ndi_query # Build set of all element IDs - elements = jess_haley_dataset.database_search(Query("").isa("element")) + elements = jess_haley_dataset.database_search(ndi_query("").isa("element")) element_ids = {doc.document_properties.get("base", {}).get("id", "") for doc in elements} # Check first 50 position_metadata docs - pos_docs = jess_haley_dataset.database_search(Query("").isa("position_metadata")) + pos_docs = jess_haley_dataset.database_search(ndi_query("").isa("position_metadata")) missing = 0 for doc in pos_docs[:50]: deps = doc.document_properties.get("depends_on", []) @@ -589,9 +589,9 @@ class TestCrossDocumentRelationships: def test_depends_on_references_exist(self, jess_haley_dataset): """Sampled depends_on values point to existing doc IDs.""" - from ndi.query import Query + from ndi.query import ndi_query - all_docs = jess_haley_dataset.database_search(Query("").isa("base")) + all_docs = jess_haley_dataset.database_search(ndi_query("").isa("base")) all_ids = {doc.document_properties.get("base", {}).get("id", "") for doc in all_docs} # Sample 200 docs with depends_on @@ -616,9 +616,9 @@ def test_depends_on_references_exist(self, jess_haley_dataset): def test_session_id_consistency(self, jess_haley_dataset): """All docs share one of a small number of session_ids.""" - from ndi.query import Query + from ndi.query import ndi_query - all_docs = jess_haley_dataset.database_search(Query("").isa("base")) + all_docs = jess_haley_dataset.database_search(ndi_query("").isa("base")) # Collect all unique session_ids across the dataset session_ids: set[str] = set() for doc in all_docs: @@ -664,7 +664,7 @@ def test_plot_document_type_distribution(self, jess_haley_dataset): fig, ax = plt.subplots(figsize=(12, 6)) ax.barh(doc_types, doc_counts, color="steelblue") ax.set_xlabel("Count") - ax.set_title("Jess Haley Dataset: Document Type Distribution") + ax.set_title("Jess Haley ndi_dataset: ndi_document Type Distribution") for i, (_t, c) in enumerate(zip(doc_types, doc_counts)): ax.text(c + 100, i, str(c), va="center", fontsize=8) plt.tight_layout() @@ -703,8 +703,8 @@ def test_plot_subject_experiment_types(self, jess_haley_dataset): fig, ax = plt.subplots(figsize=(10, 5)) ax.bar(types, counts, color="coral") ax.set_xlabel("Experiment Type") - ax.set_ylabel("Subject Count") - ax.set_title("Jess Haley: Subject Experiment Types") + ax.set_ylabel("ndi_subject Count") + ax.set_title("Jess Haley: ndi_subject Experiment Types") plt.xticks(rotation=30, ha="right") plt.tight_layout() plt.savefig(out / "subject_experiment_types.png", dpi=150) @@ -982,14 +982,14 @@ def test_plot_summary_dashboard(self, jess_haley_dataset, all_docs_raw): out = self._ensure_output_dir() fig, axes = plt.subplots(2, 3, figsize=(20, 12)) - # (1) Document type distribution + # (1) ndi_document type distribution ax = axes[0, 0] doc_types, doc_counts = getDocTypes(jess_haley_dataset) ax.barh(doc_types, doc_counts, color="steelblue") ax.set_xlabel("Count") - ax.set_title("Document Types") + ax.set_title("ndi_document Types") - # (2) Subject experiment types + # (2) ndi_subject experiment types ax = axes[0, 1] from collections import Counter @@ -1001,7 +1001,7 @@ def test_plot_summary_dashboard(self, jess_haley_dataset, all_docs_raw): type_counter[parts[2]] += 1 types_sorted = sorted(type_counter.keys()) ax.bar(types_sorted, [type_counter[t] for t in types_sorted], color="coral") - ax.set_title("Subject Experiment Types") + ax.set_title("ndi_subject Experiment Types") ax.tick_params(axis="x", rotation=20) # (3) OTR group sizes @@ -1083,7 +1083,7 @@ def test_plot_summary_dashboard(self, jess_haley_dataset, all_docs_raw): ha="center", ) - plt.suptitle("Jess Haley Dataset: Summary Dashboard", fontsize=16) + plt.suptitle("Jess Haley ndi_dataset: Summary Dashboard", fontsize=16) plt.tight_layout() plt.savefig(out / "summary_dashboard.png", dpi=150) plt.close() diff --git a/tests/matlab_tests/test_probe.py b/tests/matlab_tests/test_probe.py index 06f8201..e2ec39b 100644 --- a/tests/matlab_tests/test_probe.py +++ b/tests/matlab_tests/test_probe.py @@ -7,10 +7,10 @@ Tests for: - ndi.probe.initProbeTypeMap() / getProbeTypeMap() -- ndi.probe.Probe instantiation and basic interface +- ndi.probe.ndi_probe instantiation and basic interface """ -from ndi.probe import Probe, getProbeTypeMap, initProbeTypeMap +from ndi.probe import getProbeTypeMap, initProbeTypeMap, ndi_probe # =========================================================================== # TestProbeMap @@ -32,7 +32,7 @@ def test_initProbeTypeMap(self): """ probe_map = initProbeTypeMap() assert isinstance(probe_map, dict) - assert len(probe_map) > 0, "Probe type map should be non-empty" + assert len(probe_map) > 0, "ndi_probe type map should be non-empty" def test_getProbeTypeMap(self): """getProbeTypeMap() returns the cached version, same result. @@ -87,17 +87,17 @@ def test_map_ntrode_class(self): class TestProbe: """Port of ndi.unittest.probe.ProbeTest. - Tests Probe instantiation and basic interface without a session + Tests ndi_probe instantiation and basic interface without a session (since getprobes/samplerate/read_epochsamples require a full DAQ system setup that is not available in unit tests). """ def test_probe_instantiation(self): - """Probe can be created without a session. + """ndi_probe can be created without a session. MATLAB equivalent: ProbeTest.testProbeCreate """ - probe = Probe( + probe = ndi_probe( name="electrode1", reference=1, type="n-trode", @@ -108,17 +108,17 @@ def test_probe_instantiation(self): assert probe._type == "n-trode" def test_probe_with_session(self, tmp_path): - """Probe can be created with a DirSession. + """ndi_probe can be created with a ndi_session_dir. MATLAB equivalent: ProbeTest.testGetprobes (setup phase) """ - from ndi.session.dir import DirSession + from ndi.session.dir import ndi_session_dir session_dir = tmp_path / "probe_sess" session_dir.mkdir() - session = DirSession("probe_test", session_dir) + session = ndi_session_dir("probe_test", session_dir) - probe = Probe( + probe = ndi_probe( session=session, name="electrode1", reference=1, @@ -133,7 +133,7 @@ def test_probe_issyncgraphroot(self): MATLAB equivalent: ProbeTest (implicit property) """ - probe = Probe(name="test", reference=0, type="n-trode") + probe = ndi_probe(name="test", reference=0, type="n-trode") assert probe.issyncgraphroot() is False def test_probe_epochsetname(self): @@ -141,7 +141,7 @@ def test_probe_epochsetname(self): MATLAB equivalent: ProbeTest (implicit property) """ - probe = Probe(name="electrode1", reference=1, type="n-trode") + probe = ndi_probe(name="electrode1", reference=1, type="n-trode") name = probe.epochsetname() assert "electrode1" in name assert "1" in name @@ -151,18 +151,18 @@ def test_probe_buildepochtable_no_session(self): MATLAB equivalent: ProbeTest (edge case) """ - probe = Probe(name="test", reference=0, type="n-trode") + probe = ndi_probe(name="test", reference=0, type="n-trode") et = probe.buildepochtable() assert et == [] def test_probe_repr(self): - """Probe has a useful repr string. + """ndi_probe has a useful repr string. MATLAB equivalent: ProbeTest (implicit) """ - probe = Probe(name="electrode1", reference=1, type="n-trode") + probe = ndi_probe(name="electrode1", reference=1, type="n-trode") r = repr(probe) - assert "Probe" in r + assert "ndi_probe" in r assert "electrode1" in r def test_probe_epochprobemapmatch(self): @@ -170,7 +170,7 @@ def test_probe_epochprobemapmatch(self): MATLAB equivalent: ProbeTest (functional) """ - probe = Probe(name="electrode1", reference=1, type="n-trode") + probe = ndi_probe(name="electrode1", reference=1, type="n-trode") matching_map = { "name": "electrode1", diff --git a/tests/matlab_tests/test_session.py b/tests/matlab_tests/test_session.py index 547727a..c4bde6c 100644 --- a/tests/matlab_tests/test_session.py +++ b/tests/matlab_tests/test_session.py @@ -11,10 +11,10 @@ import pytest -from ndi.dataset import Dataset -from ndi.document import Document -from ndi.query import Query -from ndi.session.dir import DirSession +from ndi.dataset import ndi_dataset +from ndi.document import ndi_document +from ndi.query import ndi_query +from ndi.session.dir import ndi_session_dir # =========================================================================== # TestDeleteSession @@ -32,7 +32,7 @@ def test_delete_no_confirm(self, tmp_path): """ session_dir = tmp_path / "del_session" session_dir.mkdir() - session = DirSession("del_test", session_dir) + session = ndi_session_dir("del_test", session_dir) ndi_dir = session_dir / ".ndi" assert ndi_dir.exists(), ".ndi directory should exist before delete" @@ -50,7 +50,7 @@ def test_delete_confirm(self, tmp_path): """ session_dir = tmp_path / "del_session" session_dir.mkdir() - session = DirSession("del_test", session_dir) + session = ndi_session_dir("del_test", session_dir) ndi_dir = session_dir / ".ndi" assert ndi_dir.exists(), ".ndi directory should exist before delete" @@ -66,7 +66,7 @@ def test_delete_preserves_data_files(self, tmp_path): """ session_dir = tmp_path / "del_session" session_dir.mkdir() - session = DirSession("del_test", session_dir) + session = ndi_session_dir("del_test", session_dir) # Create a user data file data_file = session_dir / "my_data.txt" @@ -98,12 +98,12 @@ def test_standalone_session_not_in_dataset(self, tmp_path): """ session_dir = tmp_path / "standalone" session_dir.mkdir() - session = DirSession("standalone", session_dir) + session = ndi_session_dir("standalone", session_dir) # Create a dataset without the session ds_dir = tmp_path / "ds_empty" ds_dir.mkdir() - dataset = Dataset(ds_dir, "ds_empty") + dataset = ndi_dataset(ds_dir, "ds_empty") refs, session_ids, *_ = dataset.session_list() assert session.id() not in session_ids, "Standalone session should not be in dataset" @@ -116,27 +116,27 @@ def test_ingested_session_in_dataset(self, tmp_path): # Create session with a doc session_dir = tmp_path / "sess" session_dir.mkdir() - session = DirSession("exp", session_dir) + session = ndi_session_dir("exp", session_dir) - doc = Document("demoNDI") + doc = ndi_document("demoNDI") props = doc.document_properties props["base"]["name"] = "test_doc" props["demoNDI"]["value"] = 42 props["base"]["session_id"] = session.id() - doc = Document(props) + doc = ndi_document(props) session.database_add(doc) # Create dataset and ingest ds_dir = tmp_path / "ds" ds_dir.mkdir() - dataset = Dataset(ds_dir, "ds") + dataset = ndi_dataset(ds_dir, "ds") dataset.add_ingested_session(session) refs, session_ids, *_ = dataset.session_list() assert session.id() in session_ids, "Ingested session should appear in dataset session list" # Also verify the document was ingested - q = Query("").isa("demoNDI") + q = ndi_query("").isa("demoNDI") docs = dataset.database_search(q) assert len(docs) == 1 assert docs[0].document_properties["base"]["name"] == "test_doc" @@ -145,11 +145,11 @@ def test_linked_session_in_dataset(self, tmp_path): """A linked session appears in the dataset session list as linked.""" session_dir = tmp_path / "linked_sess" session_dir.mkdir() - session = DirSession("linked", session_dir) + session = ndi_session_dir("linked", session_dir) ds_dir = tmp_path / "ds_link" ds_dir.mkdir() - dataset = Dataset(ds_dir, "ds_link") + dataset = ndi_dataset(ds_dir, "ds_link") dataset.add_linked_session(session) refs, session_ids, *_ = dataset.session_list() @@ -167,10 +167,10 @@ class TestSessionBasics: """Test basic session creation and properties.""" def test_session_creation(self, tmp_path): - """DirSession can be created with reference and path.""" + """ndi_session_dir can be created with reference and path.""" session_dir = tmp_path / "sess" session_dir.mkdir() - session = DirSession("my_session", session_dir) + session = ndi_session_dir("my_session", session_dir) assert session.reference == "my_session" assert session.path == session_dir @@ -178,10 +178,10 @@ def test_session_creation(self, tmp_path): assert isinstance(session.id(), str) def test_session_has_ndi_directory(self, tmp_path): - """DirSession creates a .ndi directory for database storage.""" + """ndi_session_dir creates a .ndi directory for database storage.""" session_dir = tmp_path / "sess" session_dir.mkdir() - DirSession("my_session", session_dir) + ndi_session_dir("my_session", session_dir) ndi_dir = session_dir / ".ndi" assert ndi_dir.exists(), ".ndi directory should be created" @@ -190,25 +190,25 @@ def test_session_reopens_with_same_id(self, tmp_path): """Reopening a session from the same path gives the same ID.""" session_dir = tmp_path / "sess" session_dir.mkdir() - s1 = DirSession("my_session", session_dir) + s1 = ndi_session_dir("my_session", session_dir) original_id = s1.id() # Reopen - s2 = DirSession("my_session", session_dir) + s2 = ndi_session_dir("my_session", session_dir) assert s2.id() == original_id, "Reopened session should have same ID" def test_session_requires_existing_directory(self, tmp_path): - """DirSession raises ValueError for non-existent path.""" + """ndi_session_dir raises ValueError for non-existent path.""" nonexistent = tmp_path / "does_not_exist" with pytest.raises(ValueError): - DirSession("bad_session", nonexistent) + ndi_session_dir("bad_session", nonexistent) def test_newdocument(self, tmp_path): - """Session.newdocument() creates a document with session_id set.""" + """ndi_session.newdocument() creates a document with session_id set.""" session_dir = tmp_path / "sess" session_dir.mkdir() - session = DirSession("my_session", session_dir) + session = ndi_session_dir("my_session", session_dir) doc = session.newdocument( "demoNDI", @@ -222,10 +222,10 @@ def test_newdocument(self, tmp_path): assert doc.document_properties["base"]["name"] == "test" def test_creator_args(self, tmp_path): - """DirSession.creator_args() returns the construction arguments.""" + """ndi_session_dir.creator_args() returns the construction arguments.""" session_dir = tmp_path / "sess" session_dir.mkdir() - session = DirSession("my_session", session_dir) + session = ndi_session_dir("my_session", session_dir) args = session.creator_args() assert len(args) >= 2 diff --git a/tests/matlab_tests/test_validators.py b/tests/matlab_tests/test_validators.py index 1455deb..cd858d2 100644 --- a/tests/matlab_tests/test_validators.py +++ b/tests/matlab_tests/test_validators.py @@ -11,16 +11,16 @@ These are MATLAB class/type validators with no direct Python equivalent. Tests for: -- ndi.ido.Ido ID generation and validation -- ndi.document.Document creation and type validation -- Session.database_add() session_id mismatch rejection +- ndi.ido.ndi_ido ID generation and validation +- ndi.document.ndi_document creation and type validation +- ndi_session.database_add() session_id mismatch rejection """ import pytest -from ndi.document import Document -from ndi.ido import Ido -from ndi.session.dir import DirSession +from ndi.document import ndi_document +from ndi.ido import ndi_ido +from ndi.session.dir import ndi_session_dir from ndi.session.session_base import empty_id # =========================================================================== @@ -32,90 +32,90 @@ class TestDocumentValidation: """Port of MATLAB validator tests -- document-level validation. - Tests that Document creation enforces schema constraints. + Tests that ndi_document creation enforces schema constraints. """ def test_document_type_validation(self): - """Document rejects unknown types with FileNotFoundError. + """ndi_document rejects unknown types with FileNotFoundError. MATLAB equivalent: validators (type checking) """ with pytest.raises(FileNotFoundError): - Document("completely_nonexistent_type_xyz") + ndi_document("completely_nonexistent_type_xyz") def test_document_valid_type(self): - """Document accepts known types like 'base' and 'demoNDI'. + """ndi_document accepts known types like 'base' and 'demoNDI'. MATLAB equivalent: validators (type checking) """ - doc = Document("base") + doc = ndi_document("base") assert doc is not None assert isinstance(doc.document_properties, dict) - doc2 = Document("demoNDI") + doc2 = ndi_document("demoNDI") assert doc2 is not None def test_session_id_validation(self, tmp_path): - """Session rejects documents with mismatched session_id. + """ndi_session rejects documents with mismatched session_id. MATLAB equivalent: validators (session_id checking) """ session_dir = tmp_path / "val_sess" session_dir.mkdir() - session = DirSession("validator_test", session_dir) + session = ndi_session_dir("validator_test", session_dir) # Create a doc with a different session_id - doc = Document("demoNDI") + doc = ndi_document("demoNDI") props = doc.document_properties props["base"]["session_id"] = "wrong_session_id_12345" props["demoNDI"]["value"] = 1 - doc = Document(props) + doc = ndi_document(props) with pytest.raises(ValueError, match="doesn't match"): session.database_add(doc) def test_session_id_empty_accepted(self, tmp_path): - """Session accepts documents with empty session_id (auto-assigned). + """ndi_session accepts documents with empty session_id (auto-assigned). MATLAB equivalent: validators (session_id auto-assignment) """ session_dir = tmp_path / "val_sess2" session_dir.mkdir() - session = DirSession("validator_test", session_dir) + session = ndi_session_dir("validator_test", session_dir) - doc = Document("demoNDI") + doc = ndi_document("demoNDI") props = doc.document_properties props["base"]["session_id"] = "" props["demoNDI"]["value"] = 99 - doc = Document(props) + doc = ndi_document(props) # Should not raise -- empty session_id is accepted and auto-assigned session.database_add(doc) def test_session_id_empty_id_accepted(self, tmp_path): - """Session accepts documents with empty_id() session_id. + """ndi_session accepts documents with empty_id() session_id. MATLAB equivalent: validators (session_id auto-assignment) """ session_dir = tmp_path / "val_sess3" session_dir.mkdir() - session = DirSession("validator_test", session_dir) + session = ndi_session_dir("validator_test", session_dir) - doc = Document("demoNDI") + doc = ndi_document("demoNDI") props = doc.document_properties props["base"]["session_id"] = empty_id() props["demoNDI"]["value"] = 88 - doc = Document(props) + doc = ndi_document(props) # Should not raise -- empty_id is accepted and auto-assigned session.database_add(doc) def test_document_properties_structure(self): - """Document has required base properties after creation. + """ndi_document has required base properties after creation. MATLAB equivalent: validators (structure check) """ - doc = Document("base") + doc = ndi_document("base") props = doc.document_properties assert "base" in props @@ -137,40 +137,40 @@ class TestIDValidation: """ def test_valid_id_format(self): - """IDs generated by Document have correct format. + """IDs generated by ndi_document have correct format. MATLAB equivalent: mustBeIDTest.testValidID """ - doc = Document("base") + doc = ndi_document("base") doc_id = doc.id assert isinstance(doc_id, str) assert len(doc_id) > 0 - assert Ido.is_valid(doc_id), f"Document ID '{doc_id}' should be a valid NDI ID" + assert ndi_ido.is_valid(doc_id), f"ndi_document ID '{doc_id}' should be a valid NDI ID" def test_id_is_string(self): """ID is always a string. MATLAB equivalent: mustBeIDTest.testIDIsString """ - doc = Document("base") + doc = ndi_document("base") assert isinstance(doc.id, str) def test_ido_generates_valid_ids(self): - """Ido.unique_id() generates valid IDs. + """ndi_ido.unique_id() generates valid IDs. MATLAB equivalent: mustBeIDTest (ID generation) """ for _ in range(10): - id_val = Ido.unique_id() - assert Ido.is_valid(id_val), f"Generated ID '{id_val}' should be valid" + id_val = ndi_ido.unique_id() + assert ndi_ido.is_valid(id_val), f"Generated ID '{id_val}' should be valid" def test_ido_id_contains_underscore(self): """NDI IDs contain an underscore separator. MATLAB equivalent: mustBeIDTest (format check) """ - ido = Ido() + ido = ndi_ido() assert "_" in ido.id, "NDI ID should contain underscore separator" def test_ido_id_hex_format(self): @@ -178,7 +178,7 @@ def test_ido_id_hex_format(self): MATLAB equivalent: mustBeIDTest (hex check) """ - ido = Ido() + ido = ndi_ido() parts = ido.id.split("_") assert len(parts) == 2, "NDI ID should have exactly two parts" @@ -189,11 +189,11 @@ def test_ido_id_hex_format(self): ), f"ID part '{part}' should be hexadecimal" def test_ido_ids_are_unique(self): - """Multiple Ido instances generate unique IDs. + """Multiple ndi_ido instances generate unique IDs. MATLAB equivalent: mustBeIDTest (uniqueness) """ - ids = [Ido().id for _ in range(20)] + ids = [ndi_ido().id for _ in range(20)] assert len(set(ids)) == len(ids), "All generated IDs should be unique" def test_ido_is_valid_accepts_ndi_format(self): @@ -201,23 +201,23 @@ def test_ido_is_valid_accepts_ndi_format(self): MATLAB equivalent: mustBeIDTest.testValidFormat """ - assert Ido.is_valid("1a2b3c_4d5e6f") is True + assert ndi_ido.is_valid("1a2b3c_4d5e6f") is True def test_ido_is_valid_accepts_uuid_format(self): """is_valid() accepts UUID format for compatibility. MATLAB equivalent: mustBeIDTest.testValidFormat """ - assert Ido.is_valid("12345678-1234-1234-1234-123456789abc") is True + assert ndi_ido.is_valid("12345678-1234-1234-1234-123456789abc") is True def test_ido_is_valid_rejects_invalid(self): """is_valid() rejects invalid ID strings. MATLAB equivalent: mustBeIDTest.testInvalidID """ - assert Ido.is_valid("") is False - assert Ido.is_valid("not-a-valid-id") is False - assert Ido.is_valid("xyz") is False + assert ndi_ido.is_valid("") is False + assert ndi_ido.is_valid("not-a-valid-id") is False + assert ndi_ido.is_valid("xyz") is False def test_document_session_id_is_property(self): """doc.session_id is a @property (not a method call). @@ -225,25 +225,25 @@ def test_document_session_id_is_property(self): This is a Python-specific concern: ensure session_id is accessed as a property, not called as session_id(). """ - doc = Document("base") + doc = ndi_document("base") # session_id is a property -- accessing it should return a string session_id = doc.session_id assert isinstance(session_id, str) def test_ido_equality(self): - """Ido objects with same ID are equal. + """ndi_ido objects with same ID are equal. MATLAB equivalent: mustBeIDTest (equality) """ - id_str = Ido.unique_id() - ido1 = Ido(id=id_str) - ido2 = Ido(id=id_str) + id_str = ndi_ido.unique_id() + ido1 = ndi_ido(id=id_str) + ido2 = ndi_ido(id=id_str) assert ido1 == ido2 def test_ido_string_equality(self): - """Ido compares equal to its string representation. + """ndi_ido compares equal to its string representation. MATLAB equivalent: mustBeIDTest (string comparison) """ - ido = Ido() + ido = ndi_ido() assert ido == ido.id diff --git a/tests/symmetry/make_artifacts/session/test_build_session.py b/tests/symmetry/make_artifacts/session/test_build_session.py index a31b1d6..efb8b32 100644 --- a/tests/symmetry/make_artifacts/session/test_build_session.py +++ b/tests/symmetry/make_artifacts/session/test_build_session.py @@ -3,7 +3,7 @@ Python equivalent of: tests/+ndi/+symmetry/+makeArtifacts/+session/buildSession.m -This test creates an NDI DirSession with a subject and a subjectmeasurement +This test creates an NDI ndi_session_dir with a subject and a subjectmeasurement document, then persists the session database, a ``sessionSummary.json`` manifest, and individual JSON representations of every document into: @@ -19,9 +19,9 @@ import pytest -from ndi.query import Query -from ndi.session.dir import DirSession -from ndi.subject import Subject +from ndi.query import ndi_query +from ndi.session.dir import ndi_session_dir +from ndi.subject import ndi_subject from ndi.util import sessionSummary from tests.symmetry.conftest import PYTHON_ARTIFACTS @@ -44,12 +44,12 @@ def _setup(self, tmp_path): session_dir = tmp_path / "exp1" session_dir.mkdir() - session = DirSession("exp1", session_dir) + session = ndi_session_dir("exp1", session_dir) session.database_clear("yes") session.cache.clear() # Add a subject - subject = Subject("anteater27@nosuchlab.org", "") + subject = ndi_subject("anteater27@nosuchlab.org", "") session.database_add(subject.newdocument()) # Add a subjectmeasurement document @@ -65,6 +65,44 @@ def _setup(self, tmp_path): doc = doc.set_dependency_value("subject_id", subject.id, error_if_not_found=False) session.database_add(doc) + # Add DAQ system documents (filenavigator, daqreader, daqsystem). + # MATLAB's buildSession creates an Intan DAQ system with actual + # data files. We create the same document structure without raw + # data files so the database search / load round-trip is tested. + fn_doc = session.newdocument( + "daq/filenavigator", + **{ + "base.name": "unknown", + "filenavigator.ndi_filenavigator_class": "ndi.file.navigator", + "filenavigator.fileparameters": "{ '#.rhd' }", + "filenavigator.epochprobemap_class": "ndi.epoch.epochprobemap_daqsystem", + "filenavigator.epochprobemap_fileparameters": "{ '#.epochprobemap.ndi' }", + }, + ) + session.database_add(fn_doc) + + dr_doc = session.newdocument( + "daq/daqreader", + **{ + "base.name": "intan_reader", + "daqreader.ndi_daqreader_class": "ndi.daq.reader.mfdaq.intan", + }, + ) + session.database_add(dr_doc) + + daq_doc = session.newdocument( + "daq/daqsystem", + **{ + "base.name": "intan1", + "daqsystem.ndi_daqsystem_class": "ndi.daq.system.mfdaq", + }, + ) + daq_doc = daq_doc.set_dependency_value( + "filenavigator_id", fn_doc.id, error_if_not_found=False + ) + daq_doc = daq_doc.set_dependency_value("daqreader_id", dr_doc.id, error_if_not_found=False) + session.database_add(daq_doc) + self.session = session # No teardown — artifacts must persist for readArtifacts. @@ -83,7 +121,7 @@ def test_build_session_artifacts(self): # Re-open the session to ensure all internal documents are flushed. session_path = self.session.path - self.session = DirSession("exp1", session_path) + self.session = ndi_session_dir("exp1", session_path) # Create comprehensive session summary summary = sessionSummary(self.session) @@ -96,7 +134,7 @@ def test_build_session_artifacts(self): json_docs_dir = artifact_dir / "jsonDocuments" json_docs_dir.mkdir(exist_ok=True) - docs = self.session.database_search(Query("base.id").match("(.*)")) + docs = self.session.database_search(ndi_query("base.id").match("(.*)")) for doc in docs: props = doc.document_properties doc_path = json_docs_dir / f"{doc.id}.json" diff --git a/tests/symmetry/read_artifacts/session/test_build_session.py b/tests/symmetry/read_artifacts/session/test_build_session.py index 723dcff..c422c26 100644 --- a/tests/symmetry/read_artifacts/session/test_build_session.py +++ b/tests/symmetry/read_artifacts/session/test_build_session.py @@ -18,8 +18,8 @@ import pytest -from ndi.query import Query -from ndi.session.dir import DirSession +from ndi.query import ndi_query +from ndi.session.dir import ndi_session_dir from ndi.util import compareSessionSummary, sessionSummary from tests.symmetry.conftest import SOURCE_TYPES, SYMMETRY_BASE @@ -45,7 +45,7 @@ def _open_session(self, source_type): f"Artifact directory from {source_type} does not exist. " f"Run the corresponding makeArtifacts suite first." ) - return artifact_dir, DirSession("exp1", artifact_dir) + return artifact_dir, ndi_session_dir("exp1", artifact_dir) # -- tests --------------------------------------------------------------- @@ -60,16 +60,30 @@ def test_build_session_summary(self, source_type): expected_summary = json.loads(summary_path.read_text(encoding="utf-8")) actual_summary = sessionSummary(session) + # ndi_epoch_epoch node details contain machine-specific paths and runtime-generated + # IDs that will differ when artifacts are read on a different machine or + # across language implementations. Exclude them from comparison. + exclude_fields = ["epochNodes_filenavigator", "epochNodes_daqsystem"] + + # Python may create .epochid.ndi files as a side effect of loading + # the session. Filter these runtime-generated hidden files from both + # summaries so they don't cause spurious mismatches. + def _filter_epochid(files: list[str]) -> list[str]: + return [f for f in files if not f.endswith(".epochid.ndi")] + + actual_summary["files"] = _filter_epochid(actual_summary.get("files", [])) + expected_summary["files"] = _filter_epochid(expected_summary.get("files", [])) + report = compareSessionSummary( actual_summary, expected_summary, excludeFiles=["sessionSummary.json", "jsonDocuments"], + excludeFields=exclude_fields, ) - assert ( - len(report) == 0 - ), f"Session summary mismatch against {source_type} generated artifacts:\n" + "\n".join( - report + assert len(report) == 0, ( + f"ndi_session summary mismatch against {source_type} generated artifacts:\n" + + "\n".join(report) ) def test_build_session_documents(self, source_type): @@ -82,7 +96,7 @@ def test_build_session_documents(self, source_type): json_files = list(json_docs_dir.glob("*.json")) - actual_docs = session.database_search(Query("base.id").match("(.*)")) + actual_docs = session.database_search(ndi_query("base.id").match("(.*)")) assert len(actual_docs) == len(json_files), ( f"Number of documents in session ({len(actual_docs)}) does not match " @@ -101,9 +115,9 @@ def test_build_session_documents(self, source_type): assert actual_props.get("document_class", {}).get( "class_name" ) == expected_doc.get("document_class", {}).get("class_name"), ( - f"Document class mismatch for id: {expected_id} " f"in {source_type}" + f"ndi_document class mismatch for id: {expected_id} " f"in {source_type}" ) break assert found, ( - f"Document from {source_type} artifact not found in session: " f"{expected_id}" + f"ndi_document from {source_type} artifact not found in session: " f"{expected_id}" ) diff --git a/tests/test_batch_a.py b/tests/test_batch_a.py index dfad132..8daede7 100644 --- a/tests/test_batch_a.py +++ b/tests/test_batch_a.py @@ -1,16 +1,16 @@ """ Tests for Batch A: Core infrastructure gap-fill. -Tests DAQSystemString, EpochProbeMapDAQSystem, epoch functions, -MFDAQEpochChannel, DAQSystemMFDAQ, EpochDirNavigator, -ProbeTimeseries, ProbeTimeseriesMFDAQ, and element utility functions. +Tests ndi_daq_daqsystemstring, ndi_epoch_epochprobemap__daqsystem, epoch functions, +ndi_file_type_mfdaq__epoch__channel, ndi_daq_system_mfdaq, ndi_file_navigator_epochdir, +ndi_probe_timeseries, ndi_probe_timeseries_mfdaq, and element utility functions. """ import numpy as np import pytest # ============================================================================ -# DAQSystemString Tests +# ndi_daq_daqsystemstring Tests # ============================================================================ @@ -18,116 +18,116 @@ class TestDAQSystemString: """Tests for ndi.daq.daqsystemstring.""" def test_import(self): - from ndi.daq.daqsystemstring import DAQSystemString + from ndi.daq.daqsystemstring import ndi_daq_daqsystemstring - assert DAQSystemString is not None + assert ndi_daq_daqsystemstring is not None def test_parse_simple(self): - from ndi.daq.daqsystemstring import DAQSystemString + from ndi.daq.daqsystemstring import ndi_daq_daqsystemstring - dss = DAQSystemString.parse("mydevice:ai1-5") + dss = ndi_daq_daqsystemstring.parse("mydevice:ai1-5") assert dss.devicename == "mydevice" assert len(dss.channels) == 1 assert dss.channels[0][0] == "ai" assert dss.channels[0][1] == [1, 2, 3, 4, 5] def test_parse_multiple_groups(self): - from ndi.daq.daqsystemstring import DAQSystemString + from ndi.daq.daqsystemstring import ndi_daq_daqsystemstring - dss = DAQSystemString.parse("intan1:ai1-3;di1,2") + dss = ndi_daq_daqsystemstring.parse("intan1:ai1-3;di1,2") assert dss.devicename == "intan1" assert len(dss.channels) == 2 assert dss.channels[0] == ("ai", [1, 2, 3]) assert dss.channels[1] == ("di", [1, 2]) def test_parse_comma_separated(self): - from ndi.daq.daqsystemstring import DAQSystemString + from ndi.daq.daqsystemstring import ndi_daq_daqsystemstring - dss = DAQSystemString.parse("dev:ai1,3,7") + dss = ndi_daq_daqsystemstring.parse("dev:ai1,3,7") assert dss.channel_list("ai") == [1, 3, 7] def test_parse_mixed_ranges_and_singles(self): - from ndi.daq.daqsystemstring import DAQSystemString + from ndi.daq.daqsystemstring import ndi_daq_daqsystemstring - dss = DAQSystemString.parse("dev:ai1-3,7,10-12") + dss = ndi_daq_daqsystemstring.parse("dev:ai1-3,7,10-12") assert dss.channel_list("ai") == [1, 2, 3, 7, 10, 11, 12] def test_parse_device_only(self): - from ndi.daq.daqsystemstring import DAQSystemString + from ndi.daq.daqsystemstring import ndi_daq_daqsystemstring - dss = DAQSystemString.parse("mydevice") + dss = ndi_daq_daqsystemstring.parse("mydevice") assert dss.devicename == "mydevice" assert dss.channels == [] def test_parse_empty(self): - from ndi.daq.daqsystemstring import DAQSystemString + from ndi.daq.daqsystemstring import ndi_daq_daqsystemstring - dss = DAQSystemString.parse("") + dss = ndi_daq_daqsystemstring.parse("") assert dss.devicename == "" assert dss.channels == [] def test_parse_device_colon_no_channels(self): - from ndi.daq.daqsystemstring import DAQSystemString + from ndi.daq.daqsystemstring import ndi_daq_daqsystemstring - dss = DAQSystemString.parse("dev:") + dss = ndi_daq_daqsystemstring.parse("dev:") assert dss.devicename == "dev" assert dss.channels == [] def test_devicestring_roundtrip(self): - from ndi.daq.daqsystemstring import DAQSystemString + from ndi.daq.daqsystemstring import ndi_daq_daqsystemstring original = "intan1:ai1-5;di1-3" - dss = DAQSystemString.parse(original) + dss = ndi_daq_daqsystemstring.parse(original) result = dss.devicestring() - dss2 = DAQSystemString.parse(result) + dss2 = ndi_daq_daqsystemstring.parse(result) assert dss2.devicename == dss.devicename assert dss2.channels == dss.channels def test_channel_types(self): - from ndi.daq.daqsystemstring import DAQSystemString + from ndi.daq.daqsystemstring import ndi_daq_daqsystemstring - dss = DAQSystemString.parse("dev:ai1-3;di1;ao2") + dss = ndi_daq_daqsystemstring.parse("dev:ai1-3;di1;ao2") assert set(dss.channel_types()) == {"ai", "di", "ao"} def test_channel_list_filtered(self): - from ndi.daq.daqsystemstring import DAQSystemString + from ndi.daq.daqsystemstring import ndi_daq_daqsystemstring - dss = DAQSystemString.parse("dev:ai1-3;di5,6") + dss = ndi_daq_daqsystemstring.parse("dev:ai1-3;di5,6") assert dss.channel_list("ai") == [1, 2, 3] assert dss.channel_list("di") == [5, 6] assert dss.channel_list("ao") == [] def test_channel_list_all(self): - from ndi.daq.daqsystemstring import DAQSystemString + from ndi.daq.daqsystemstring import ndi_daq_daqsystemstring - dss = DAQSystemString.parse("dev:ai1,2;di5") + dss = ndi_daq_daqsystemstring.parse("dev:ai1,2;di5") assert dss.channel_list() == [1, 2, 5] def test_str_repr(self): - from ndi.daq.daqsystemstring import DAQSystemString + from ndi.daq.daqsystemstring import ndi_daq_daqsystemstring - dss = DAQSystemString.parse("dev:ai1") + dss = ndi_daq_daqsystemstring.parse("dev:ai1") assert "dev" in str(dss) - assert "DAQSystemString" in repr(dss) + assert "ndi_daq_daqsystemstring" in repr(dss) def test_equality(self): - from ndi.daq.daqsystemstring import DAQSystemString + from ndi.daq.daqsystemstring import ndi_daq_daqsystemstring - dss1 = DAQSystemString.parse("dev:ai1-3") - dss2 = DAQSystemString.parse("dev:ai1-3") - dss3 = DAQSystemString.parse("dev:ai1-4") + dss1 = ndi_daq_daqsystemstring.parse("dev:ai1-3") + dss2 = ndi_daq_daqsystemstring.parse("dev:ai1-3") + dss3 = ndi_daq_daqsystemstring.parse("dev:ai1-4") assert dss1 == dss2 assert dss1 != dss3 def test_invalid_range(self): - from ndi.daq.daqsystemstring import DAQSystemString + from ndi.daq.daqsystemstring import ndi_daq_daqsystemstring with pytest.raises(ValueError): - DAQSystemString.parse("dev:aiabc-def") + ndi_daq_daqsystemstring.parse("dev:aiabc-def") # ============================================================================ -# EpochProbeMapDAQSystem Tests +# ndi_epoch_epochprobemap__daqsystem Tests # ============================================================================ @@ -135,14 +135,14 @@ class TestEpochProbeMapDAQSystem: """Tests for ndi.epoch.epochprobemap_daqsystem.""" def test_import(self): - from ndi.epoch.epochprobemap_daqsystem import EpochProbeMapDAQSystem + from ndi.epoch.epochprobemap_daqsystem import ndi_epoch_epochprobemap__daqsystem - assert EpochProbeMapDAQSystem is not None + assert ndi_epoch_epochprobemap__daqsystem is not None def test_construction(self): - from ndi.epoch.epochprobemap_daqsystem import EpochProbeMapDAQSystem + from ndi.epoch.epochprobemap_daqsystem import ndi_epoch_epochprobemap__daqsystem - epm = EpochProbeMapDAQSystem( + epm = ndi_epoch_epochprobemap__daqsystem( name="probe1", reference=1, type="n-trode", @@ -155,9 +155,9 @@ def test_construction(self): assert epm.devicestring == "intan1:ai1-4" def test_daqsystemstring_property(self): - from ndi.epoch.epochprobemap_daqsystem import EpochProbeMapDAQSystem + from ndi.epoch.epochprobemap_daqsystem import ndi_epoch_epochprobemap__daqsystem - epm = EpochProbeMapDAQSystem( + epm = ndi_epoch_epochprobemap__daqsystem( name="probe1", reference=1, type="n-trode", @@ -168,9 +168,9 @@ def test_daqsystemstring_property(self): assert dss.channel_list("ai") == [1, 2, 3, 4] def test_serialization(self): - from ndi.epoch.epochprobemap_daqsystem import EpochProbeMapDAQSystem + from ndi.epoch.epochprobemap_daqsystem import ndi_epoch_epochprobemap__daqsystem - epm = EpochProbeMapDAQSystem( + epm = ndi_epoch_epochprobemap__daqsystem( name="probe1", reference=1, type="n-trode", @@ -184,9 +184,9 @@ def test_serialization(self): assert parts[0] == "probe1" def test_decode_roundtrip(self): - from ndi.epoch.epochprobemap_daqsystem import EpochProbeMapDAQSystem + from ndi.epoch.epochprobemap_daqsystem import ndi_epoch_epochprobemap__daqsystem - epm = EpochProbeMapDAQSystem( + epm = ndi_epoch_epochprobemap__daqsystem( name="probe1", reference=1, type="n-trode", @@ -194,7 +194,7 @@ def test_decode_roundtrip(self): subjectstring="mouse001", ) s = epm.serialize() - decoded = EpochProbeMapDAQSystem.decode(s) + decoded = ndi_epoch_epochprobemap__daqsystem.decode(s) assert decoded.name == epm.name assert decoded.reference == epm.reference assert decoded.type == epm.type @@ -202,9 +202,9 @@ def test_decode_roundtrip(self): assert decoded.subjectstring == epm.subjectstring def test_file_io(self, tmp_path): - from ndi.epoch.epochprobemap_daqsystem import EpochProbeMapDAQSystem + from ndi.epoch.epochprobemap_daqsystem import ndi_epoch_epochprobemap__daqsystem - epm = EpochProbeMapDAQSystem( + epm = ndi_epoch_epochprobemap__daqsystem( name="probe1", reference=1, type="n-trode", @@ -213,34 +213,34 @@ def test_file_io(self, tmp_path): ) filepath = str(tmp_path / "test_epm.txt") epm.savetofile(filepath) - loaded = EpochProbeMapDAQSystem.loadfromfile(filepath) + loaded = ndi_epoch_epochprobemap__daqsystem.loadfromfile(filepath) assert len(loaded) == 1 assert loaded[0].name == "probe1" def test_decode_invalid(self): - from ndi.epoch.epochprobemap_daqsystem import EpochProbeMapDAQSystem + from ndi.epoch.epochprobemap_daqsystem import ndi_epoch_epochprobemap__daqsystem with pytest.raises(ValueError): - EpochProbeMapDAQSystem.decode("only\ttwo\tfields") + ndi_epoch_epochprobemap__daqsystem.decode("only\ttwo\tfields") def test_inherits_epochprobemap(self): - from ndi.epoch.epochprobemap import EpochProbeMap - from ndi.epoch.epochprobemap_daqsystem import EpochProbeMapDAQSystem + from ndi.epoch.epochprobemap import ndi_epoch_epochprobemap + from ndi.epoch.epochprobemap_daqsystem import ndi_epoch_epochprobemap__daqsystem - epm = EpochProbeMapDAQSystem( + epm = ndi_epoch_epochprobemap__daqsystem( name="probe1", reference=1, type="n-trode", devicestring="intan1:ai1-4", ) - assert isinstance(epm, EpochProbeMap) + assert isinstance(epm, ndi_epoch_epochprobemap) assert epm.matches(name="probe1") assert not epm.matches(name="probe2") def test_serialization_struct(self): - from ndi.epoch.epochprobemap_daqsystem import EpochProbeMapDAQSystem + from ndi.epoch.epochprobemap_daqsystem import ndi_epoch_epochprobemap__daqsystem - epm = EpochProbeMapDAQSystem( + epm = ndi_epoch_epochprobemap__daqsystem( name="p1", reference=2, type="t", @@ -253,7 +253,7 @@ def test_serialization_struct(self): # ============================================================================ -# Epoch Functions Tests +# ndi_epoch_epoch Functions Tests # ============================================================================ @@ -340,7 +340,7 @@ def epochtable(self): # ============================================================================ -# MFDAQEpochChannel Tests +# ndi_file_type_mfdaq__epoch__channel Tests # ============================================================================ @@ -348,9 +348,12 @@ class TestMFDAQEpochChannel: """Tests for ndi.file.type.mfdaq_epoch_channel.""" def test_import(self): - from ndi.file.type.mfdaq_epoch_channel import ChannelInfo, MFDAQEpochChannel + from ndi.file.type.mfdaq_epoch_channel import ( + ChannelInfo, + ndi_file_type_mfdaq__epoch__channel, + ) - assert MFDAQEpochChannel is not None + assert ndi_file_type_mfdaq__epoch__channel is not None assert ChannelInfo is not None def test_channel_info_creation(self): @@ -371,83 +374,98 @@ def test_channel_info_dict_roundtrip(self): assert ch2.sample_rate == ch.sample_rate def test_mfdaq_epoch_channel_creation(self): - from ndi.file.type.mfdaq_epoch_channel import ChannelInfo, MFDAQEpochChannel + from ndi.file.type.mfdaq_epoch_channel import ( + ChannelInfo, + ndi_file_type_mfdaq__epoch__channel, + ) channels = [ ChannelInfo(name="ai1", type="analog_in", number=1, sample_rate=30000.0), ChannelInfo(name="ai2", type="analog_in", number=2, sample_rate=30000.0), ChannelInfo(name="di1", type="digital_in", number=1, sample_rate=30000.0), ] - mec = MFDAQEpochChannel(channels) + mec = ndi_file_type_mfdaq__epoch__channel(channels) assert len(mec) == 3 def test_channels_of_type(self): - from ndi.file.type.mfdaq_epoch_channel import ChannelInfo, MFDAQEpochChannel + from ndi.file.type.mfdaq_epoch_channel import ( + ChannelInfo, + ndi_file_type_mfdaq__epoch__channel, + ) channels = [ ChannelInfo(name="ai1", type="analog_in", number=1), ChannelInfo(name="ai2", type="analog_in", number=2), ChannelInfo(name="di1", type="digital_in", number=1), ] - mec = MFDAQEpochChannel(channels) + mec = ndi_file_type_mfdaq__epoch__channel(channels) ai_channels = mec.channels_of_type("analog_in") assert len(ai_channels) == 2 di_channels = mec.channels_of_type("digital_in") assert len(di_channels) == 1 def test_channel_numbers(self): - from ndi.file.type.mfdaq_epoch_channel import ChannelInfo, MFDAQEpochChannel + from ndi.file.type.mfdaq_epoch_channel import ( + ChannelInfo, + ndi_file_type_mfdaq__epoch__channel, + ) channels = [ ChannelInfo(name="ai1", type="analog_in", number=1), ChannelInfo(name="ai3", type="analog_in", number=3), ] - mec = MFDAQEpochChannel(channels) + mec = ndi_file_type_mfdaq__epoch__channel(channels) assert mec.channel_numbers("analog_in") == [1, 3] assert mec.channel_numbers() == [1, 3] def test_file_io(self, tmp_path): - from ndi.file.type.mfdaq_epoch_channel import ChannelInfo, MFDAQEpochChannel + from ndi.file.type.mfdaq_epoch_channel import ( + ChannelInfo, + ndi_file_type_mfdaq__epoch__channel, + ) channels = [ ChannelInfo(name="ai1", type="analog_in", number=1, sample_rate=30000.0), ] - mec = MFDAQEpochChannel(channels) + mec = ndi_file_type_mfdaq__epoch__channel(channels) filepath = str(tmp_path / "channels.json") b, errmsg = mec.writeToFile(filepath) assert b is True assert errmsg == "" - mec2 = MFDAQEpochChannel() + mec2 = ndi_file_type_mfdaq__epoch__channel() mec2.readFromFile(filepath) assert len(mec2) == 1 assert mec2.channel_information[0].name == "ai1" assert mec2.channel_information[0].sample_rate == 30000.0 def test_channelgroupdecoding(self): - from ndi.file.type.mfdaq_epoch_channel import ChannelInfo, MFDAQEpochChannel + from ndi.file.type.mfdaq_epoch_channel import ( + ChannelInfo, + ndi_file_type_mfdaq__epoch__channel, + ) channels = [ ChannelInfo(name="ai1", type="analog_in", number=1, group=1), ChannelInfo(name="ai2", type="analog_in", number=2, group=1), ChannelInfo(name="ai3", type="analog_in", number=3, group=2), ] - groups, ch_in_groups, ch_in_output = MFDAQEpochChannel.channelgroupdecoding( - channels, "analog_in", [1, 3] + groups, ch_in_groups, ch_in_output = ( + ndi_file_type_mfdaq__epoch__channel.channelgroupdecoding(channels, "analog_in", [1, 3]) ) assert groups == [1, 2] assert ch_in_groups == [[1], [3]] assert ch_in_output == [[0], [1]] def test_repr(self): - from ndi.file.type.mfdaq_epoch_channel import MFDAQEpochChannel + from ndi.file.type.mfdaq_epoch_channel import ndi_file_type_mfdaq__epoch__channel - mec = MFDAQEpochChannel() - assert "MFDAQEpochChannel" in repr(mec) + mec = ndi_file_type_mfdaq__epoch__channel() + assert "ndi_file_type_mfdaq__epoch__channel" in repr(mec) # ============================================================================ -# DAQSystemMFDAQ Tests +# ndi_daq_system_mfdaq Tests # ============================================================================ @@ -455,69 +473,69 @@ class TestDAQSystemMFDAQ: """Tests for ndi.daq.system_mfdaq.""" def test_import(self): - from ndi.daq.system_mfdaq import DAQSystemMFDAQ + from ndi.daq.system_mfdaq import ndi_daq_system_mfdaq - assert DAQSystemMFDAQ is not None + assert ndi_daq_system_mfdaq is not None def test_construction(self): - from ndi.daq.system_mfdaq import DAQSystemMFDAQ + from ndi.daq.system_mfdaq import ndi_daq_system_mfdaq - sys = DAQSystemMFDAQ(name="intan1") + sys = ndi_daq_system_mfdaq(name="intan1") assert sys.name == "intan1" def test_inherits_daqsystem(self): - from ndi.daq.system import DAQSystem - from ndi.daq.system_mfdaq import DAQSystemMFDAQ + from ndi.daq.system import ndi_daq_system + from ndi.daq.system_mfdaq import ndi_daq_system_mfdaq - sys = DAQSystemMFDAQ(name="intan1") - assert isinstance(sys, DAQSystem) + sys = ndi_daq_system_mfdaq(name="intan1") + assert isinstance(sys, ndi_daq_system) def test_epochclock_returns_dev_local_time(self): - from ndi.daq.system_mfdaq import DAQSystemMFDAQ + from ndi.daq.system_mfdaq import ndi_daq_system_mfdaq from ndi.time import DEV_LOCAL_TIME - sys = DAQSystemMFDAQ(name="test") + sys = ndi_daq_system_mfdaq(name="test") clocks = sys.epochclock(1) assert len(clocks) == 1 assert clocks[0] == DEV_LOCAL_TIME def test_static_channel_types(self): - from ndi.daq.system_mfdaq import DAQSystemMFDAQ + from ndi.daq.system_mfdaq import ndi_daq_system_mfdaq - types = DAQSystemMFDAQ.mfdaq_channeltypes() + types = ndi_daq_system_mfdaq.mfdaq_channeltypes() assert "analog_in" in types assert "digital_in" in types assert "event" in types def test_mfdaq_prefix(self): - from ndi.daq.system_mfdaq import DAQSystemMFDAQ + from ndi.daq.system_mfdaq import ndi_daq_system_mfdaq - assert DAQSystemMFDAQ.mfdaq_prefix("analog_in") == "ai" - assert DAQSystemMFDAQ.mfdaq_prefix("digital_in") == "di" - assert DAQSystemMFDAQ.mfdaq_prefix("event") == "e" + assert ndi_daq_system_mfdaq.mfdaq_prefix("analog_in") == "ai" + assert ndi_daq_system_mfdaq.mfdaq_prefix("digital_in") == "di" + assert ndi_daq_system_mfdaq.mfdaq_prefix("event") == "e" def test_mfdaq_type(self): - from ndi.daq.system_mfdaq import DAQSystemMFDAQ + from ndi.daq.system_mfdaq import ndi_daq_system_mfdaq - assert DAQSystemMFDAQ.mfdaq_type("ai") == "analog_in" - assert DAQSystemMFDAQ.mfdaq_type("di") == "digital_in" + assert ndi_daq_system_mfdaq.mfdaq_type("ai") == "analog_in" + assert ndi_daq_system_mfdaq.mfdaq_type("di") == "digital_in" def test_no_reader_getchannels(self): - from ndi.daq.system_mfdaq import DAQSystemMFDAQ + from ndi.daq.system_mfdaq import ndi_daq_system_mfdaq - sys = DAQSystemMFDAQ(name="test") + sys = ndi_daq_system_mfdaq(name="test") assert sys.getchannelsepoch(1) == [] def test_no_reader_raises_on_read(self): - from ndi.daq.system_mfdaq import DAQSystemMFDAQ + from ndi.daq.system_mfdaq import ndi_daq_system_mfdaq - sys = DAQSystemMFDAQ(name="test") + sys = ndi_daq_system_mfdaq(name="test") with pytest.raises(RuntimeError): sys.readchannels_epochsamples("ai", [1], 1, 0, 100) # ============================================================================ -# EpochDirNavigator Tests +# ndi_file_navigator_epochdir Tests # ============================================================================ @@ -525,19 +543,19 @@ class TestEpochDirNavigator: """Tests for ndi.file.navigator.epochdir.""" def test_import(self): - from ndi.file.navigator.epochdir import EpochDirNavigator + from ndi.file.navigator.epochdir import ndi_file_navigator_epochdir - assert EpochDirNavigator is not None + assert ndi_file_navigator_epochdir is not None def test_inherits_filenavigator(self): - from ndi.file import FileNavigator - from ndi.file.navigator.epochdir import EpochDirNavigator + from ndi.file import ndi_file_navigator + from ndi.file.navigator.epochdir import ndi_file_navigator_epochdir - nav = EpochDirNavigator() - assert isinstance(nav, FileNavigator) + nav = ndi_file_navigator_epochdir() + assert isinstance(nav, ndi_file_navigator) def test_selectfilegroups_disk(self, tmp_path): - from ndi.file.navigator.epochdir import EpochDirNavigator + from ndi.file.navigator.epochdir import ndi_file_navigator_epochdir # Create test directory structure (tmp_path / "trial_001").mkdir() @@ -547,53 +565,53 @@ def test_selectfilegroups_disk(self, tmp_path): (tmp_path / "trial_003").mkdir() # trial_003 has no matching files - class MockSession: + class ndi_session_mock: def getpath(self): return str(tmp_path) - nav = EpochDirNavigator( - session=MockSession(), + nav = ndi_file_navigator_epochdir( + session=ndi_session_mock(), fileparameters="*.rhd", ) groups = nav.selectfilegroups_disk() assert len(groups) == 2 def test_selectfilegroups_disk_empty(self, tmp_path): - from ndi.file.navigator.epochdir import EpochDirNavigator + from ndi.file.navigator.epochdir import ndi_file_navigator_epochdir - class MockSession: + class ndi_session_mock: def getpath(self): return str(tmp_path) - nav = EpochDirNavigator(session=MockSession(), fileparameters="*.rhd") + nav = ndi_file_navigator_epochdir(session=ndi_session_mock(), fileparameters="*.rhd") groups = nav.selectfilegroups_disk() assert groups == [] def test_selectfilegroups_disk_hidden_dirs(self, tmp_path): - from ndi.file.navigator.epochdir import EpochDirNavigator + from ndi.file.navigator.epochdir import ndi_file_navigator_epochdir (tmp_path / ".hidden").mkdir() (tmp_path / ".hidden" / "data.rhd").write_text("test") (tmp_path / "visible").mkdir() (tmp_path / "visible" / "data.rhd").write_text("test") - class MockSession: + class ndi_session_mock: def getpath(self): return str(tmp_path) - nav = EpochDirNavigator(session=MockSession(), fileparameters="*.rhd") + nav = ndi_file_navigator_epochdir(session=ndi_session_mock(), fileparameters="*.rhd") groups = nav.selectfilegroups_disk() assert len(groups) == 1 # Only visible dir def test_repr(self): - from ndi.file.navigator.epochdir import EpochDirNavigator + from ndi.file.navigator.epochdir import ndi_file_navigator_epochdir - nav = EpochDirNavigator(fileparameters=["*.rhd", "*.dat"]) - assert "EpochDirNavigator" in repr(nav) + nav = ndi_file_navigator_epochdir(fileparameters=["*.rhd", "*.dat"]) + assert "ndi_file_navigator_epochdir" in repr(nav) # ============================================================================ -# ProbeTimeseries Tests +# ndi_probe_timeseries Tests # ============================================================================ @@ -601,41 +619,41 @@ class TestProbeTimeseries: """Tests for ndi.probe.timeseries.""" def test_import(self): - from ndi.probe.timeseries import ProbeTimeseries + from ndi.probe.timeseries import ndi_probe_timeseries - assert ProbeTimeseries is not None + assert ndi_probe_timeseries is not None def test_inherits_probe(self): - from ndi.probe import Probe - from ndi.probe.timeseries import ProbeTimeseries + from ndi.probe import ndi_probe + from ndi.probe.timeseries import ndi_probe_timeseries - pt = ProbeTimeseries(name="test", reference=1, type="n-trode") - assert isinstance(pt, Probe) + pt = ndi_probe_timeseries(name="test", reference=1, type="n-trode") + assert isinstance(pt, ndi_probe) def test_samplerate_default(self): - from ndi.probe.timeseries import ProbeTimeseries + from ndi.probe.timeseries import ndi_probe_timeseries - pt = ProbeTimeseries(name="test", reference=1, type="n-trode") + pt = ndi_probe_timeseries(name="test", reference=1, type="n-trode") assert pt.samplerate(1) == -1.0 def test_readtimeseries_needs_epoch_or_timeref(self): - from ndi.probe.timeseries import ProbeTimeseries + from ndi.probe.timeseries import ndi_probe_timeseries - pt = ProbeTimeseries(name="test", reference=1, type="n-trode") + pt = ndi_probe_timeseries(name="test", reference=1, type="n-trode") with pytest.raises(ValueError): pt.readtimeseries() def test_readtimeseriesepoch_returns_none(self): - from ndi.probe.timeseries import ProbeTimeseries + from ndi.probe.timeseries import ndi_probe_timeseries - pt = ProbeTimeseries(name="test", reference=1, type="n-trode") + pt = ndi_probe_timeseries(name="test", reference=1, type="n-trode") data, t, tr = pt.readtimeseriesepoch(1, 0, 10) assert data is None def test_times2samples(self): - from ndi.probe.timeseries import ProbeTimeseries + from ndi.probe.timeseries import ndi_probe_timeseries - class MockTimeseries(ProbeTimeseries): + class MockTimeseries(ndi_probe_timeseries): def samplerate(self, epoch): return 1000.0 @@ -646,9 +664,9 @@ def samplerate(self, epoch): assert samples[2] == 11 def test_samples2times(self): - from ndi.probe.timeseries import ProbeTimeseries + from ndi.probe.timeseries import ndi_probe_timeseries - class MockTimeseries(ProbeTimeseries): + class MockTimeseries(ndi_probe_timeseries): def samplerate(self, epoch): return 1000.0 @@ -657,14 +675,14 @@ def samplerate(self, epoch): np.testing.assert_allclose(times, [0.0, 0.001, 0.01]) def test_repr(self): - from ndi.probe.timeseries import ProbeTimeseries + from ndi.probe.timeseries import ndi_probe_timeseries - pt = ProbeTimeseries(name="test", reference=1, type="n-trode") - assert "ProbeTimeseries" in repr(pt) + pt = ndi_probe_timeseries(name="test", reference=1, type="n-trode") + assert "ndi_probe_timeseries" in repr(pt) # ============================================================================ -# ProbeTimeseriesMFDAQ Tests +# ndi_probe_timeseries_mfdaq Tests # ============================================================================ @@ -672,45 +690,45 @@ class TestProbeTimeseriesMFDAQ: """Tests for ndi.probe.timeseries_mfdaq.""" def test_import(self): - from ndi.probe.timeseries_mfdaq import ProbeTimeseriesMFDAQ + from ndi.probe.timeseries_mfdaq import ndi_probe_timeseries_mfdaq - assert ProbeTimeseriesMFDAQ is not None + assert ndi_probe_timeseries_mfdaq is not None def test_inherits_probe_timeseries(self): - from ndi.probe.timeseries import ProbeTimeseries - from ndi.probe.timeseries_mfdaq import ProbeTimeseriesMFDAQ + from ndi.probe.timeseries import ndi_probe_timeseries + from ndi.probe.timeseries_mfdaq import ndi_probe_timeseries_mfdaq - pt = ProbeTimeseriesMFDAQ(name="test", reference=1, type="n-trode") - assert isinstance(pt, ProbeTimeseries) + pt = ndi_probe_timeseries_mfdaq(name="test", reference=1, type="n-trode") + assert isinstance(pt, ndi_probe_timeseries) def test_samplerate_no_session(self): - from ndi.probe.timeseries_mfdaq import ProbeTimeseriesMFDAQ + from ndi.probe.timeseries_mfdaq import ndi_probe_timeseries_mfdaq - pt = ProbeTimeseriesMFDAQ(name="test", reference=1, type="n-trode") + pt = ndi_probe_timeseries_mfdaq(name="test", reference=1, type="n-trode") assert pt.samplerate(1) == -1.0 def test_getchanneldevinfo_no_session(self): - from ndi.probe.timeseries_mfdaq import ProbeTimeseriesMFDAQ + from ndi.probe.timeseries_mfdaq import ndi_probe_timeseries_mfdaq - pt = ProbeTimeseriesMFDAQ(name="test", reference=1, type="n-trode") + pt = ndi_probe_timeseries_mfdaq(name="test", reference=1, type="n-trode") assert pt.getchanneldevinfo(1) is None def test_read_epochsamples_no_device(self): - from ndi.probe.timeseries_mfdaq import ProbeTimeseriesMFDAQ + from ndi.probe.timeseries_mfdaq import ndi_probe_timeseries_mfdaq - pt = ProbeTimeseriesMFDAQ(name="test", reference=1, type="n-trode") + pt = ndi_probe_timeseries_mfdaq(name="test", reference=1, type="n-trode") data, t, tr = pt.read_epochsamples(1, 0, 100) assert data is None def test_repr(self): - from ndi.probe.timeseries_mfdaq import ProbeTimeseriesMFDAQ + from ndi.probe.timeseries_mfdaq import ndi_probe_timeseries_mfdaq - pt = ProbeTimeseriesMFDAQ(name="test", reference=1, type="n-trode") - assert "ProbeTimeseriesMFDAQ" in repr(pt) + pt = ndi_probe_timeseries_mfdaq(name="test", reference=1, type="n-trode") + assert "ndi_probe_timeseries_mfdaq" in repr(pt) # ============================================================================ -# Element Functions Tests +# ndi_element Functions Tests # ============================================================================ @@ -787,11 +805,11 @@ def test_missingepochs_with_dicts(self): def test_spikesForProbe_validates_epochs(self, tmp_path): from ndi.element.functions import spikesForProbe - from ndi.session import DirSession + from ndi.session import ndi_session_dir session_path = tmp_path / "test_session" session_path.mkdir() - session = DirSession("test_ref", str(session_path)) + session = ndi_session_dir("test_ref", str(session_path)) class MockProbe: def epochtable(self): @@ -814,19 +832,19 @@ class TestBatchAImports: """Test that all Batch A classes are importable from expected locations.""" def test_daqsystemstring_from_daq(self): - from ndi.daq import DAQSystemString + from ndi.daq import ndi_daq_daqsystemstring - assert DAQSystemString is not None + assert ndi_daq_daqsystemstring is not None def test_daqsystemmfdaq_from_daq(self): - from ndi.daq import DAQSystemMFDAQ + from ndi.daq import ndi_daq_system_mfdaq - assert DAQSystemMFDAQ is not None + assert ndi_daq_system_mfdaq is not None def test_epochprobemap_daqsystem_from_epoch(self): - from ndi.epoch import EpochProbeMapDAQSystem + from ndi.epoch import ndi_epoch_epochprobemap__daqsystem - assert EpochProbeMapDAQSystem is not None + assert ndi_epoch_epochprobemap__daqsystem is not None def test_epochrange_from_epoch(self): from ndi.epoch import epochrange @@ -834,24 +852,24 @@ def test_epochrange_from_epoch(self): assert epochrange is not None def test_epochdir_from_file(self): - from ndi.file import EpochDirNavigator + from ndi.file import ndi_file_navigator_epochdir - assert EpochDirNavigator is not None + assert ndi_file_navigator_epochdir is not None def test_mfdaq_epoch_channel_from_file_type(self): - from ndi.file.type import MFDAQEpochChannel + from ndi.file.type import ndi_file_type_mfdaq__epoch__channel - assert MFDAQEpochChannel is not None + assert ndi_file_type_mfdaq__epoch__channel is not None def test_probe_timeseries(self): - from ndi.probe.timeseries import ProbeTimeseries + from ndi.probe.timeseries import ndi_probe_timeseries - assert ProbeTimeseries is not None + assert ndi_probe_timeseries is not None def test_probe_timeseries_mfdaq(self): - from ndi.probe.timeseries_mfdaq import ProbeTimeseriesMFDAQ + from ndi.probe.timeseries_mfdaq import ndi_probe_timeseries_mfdaq - assert ProbeTimeseriesMFDAQ is not None + assert ndi_probe_timeseries_mfdaq is not None def test_element_functions(self): from ndi.element.functions import missingepochs, oneepoch, spikesForProbe diff --git a/tests/test_batch_b.py b/tests/test_batch_b.py index 4737ec7..c4b1dca 100644 --- a/tests/test_batch_b.py +++ b/tests/test_batch_b.py @@ -1,33 +1,33 @@ """ -Tests for Batch B: DAQ readers, Document methods, MockSession. +Tests for Batch B: DAQ readers, ndi_document methods, ndi_session_mock. -Tests format-specific readers, Document.write() and -remove_dependency_value_n(), and MockSession. +Tests format-specific readers, ndi_document.write() and +remove_dependency_value_n(), and ndi_session_mock. """ import json import os # ============================================================================ -# Document.write() Tests +# ndi_document.write() Tests # ============================================================================ class TestDocumentWrite: - """Tests for Document.write() method.""" + """Tests for ndi_document.write() method.""" def test_write_creates_file(self, tmp_path): - from ndi.document import Document + from ndi.document import ndi_document - doc = Document("base", **{"base.name": "test_write"}) + doc = ndi_document("base", **{"base.name": "test_write"}) filepath = str(tmp_path / "doc.json") doc.write(filepath) assert os.path.exists(filepath) def test_write_valid_json(self, tmp_path): - from ndi.document import Document + from ndi.document import ndi_document - doc = Document("base", **{"base.name": "test_write"}) + doc = ndi_document("base", **{"base.name": "test_write"}) filepath = str(tmp_path / "doc.json") doc.write(filepath) with open(filepath) as f: @@ -35,30 +35,30 @@ def test_write_valid_json(self, tmp_path): assert data["base"]["name"] == "test_write" def test_write_creates_parent_dirs(self, tmp_path): - from ndi.document import Document + from ndi.document import ndi_document - doc = Document("base") + doc = ndi_document("base") filepath = str(tmp_path / "sub" / "dir" / "doc.json") doc.write(filepath) assert os.path.exists(filepath) def test_write_roundtrip(self, tmp_path): - from ndi.document import Document + from ndi.document import ndi_document - doc = Document("base", **{"base.name": "roundtrip"}) + doc = ndi_document("base", **{"base.name": "roundtrip"}) filepath = str(tmp_path / "doc.json") doc.write(filepath) with open(filepath) as f: data = json.load(f) - doc2 = Document(data) + doc2 = ndi_document(data) assert doc2._document_properties["base"]["name"] == "roundtrip" def test_write_with_indent(self, tmp_path): - from ndi.document import Document + from ndi.document import ndi_document - doc = Document("base") + doc = ndi_document("base") filepath = str(tmp_path / "doc.json") doc.write(filepath, indent=4) with open(filepath) as f: @@ -68,17 +68,17 @@ def test_write_with_indent(self, tmp_path): # ============================================================================ -# Document.remove_dependency_value_n() Tests +# ndi_document.remove_dependency_value_n() Tests # ============================================================================ class TestDocumentRemoveDependencyN: - """Tests for Document.remove_dependency_value_n().""" + """Tests for ndi_document.remove_dependency_value_n().""" def test_remove_specific_index(self): - from ndi.document import Document + from ndi.document import ndi_document - doc = Document("base") + doc = ndi_document("base") doc.set_dependency_value("dep_1", "val1", error_if_not_found=False) doc.set_dependency_value("dep_2", "val2", error_if_not_found=False) doc.set_dependency_value("dep_3", "val3", error_if_not_found=False) @@ -92,9 +92,9 @@ def test_remove_specific_index(self): assert "dep_3" in dep_names def test_remove_all_matching(self): - from ndi.document import Document + from ndi.document import ndi_document - doc = Document("base") + doc = ndi_document("base") doc.set_dependency_value("dep_1", "val1", error_if_not_found=False) doc.set_dependency_value("dep_2", "val2", error_if_not_found=False) doc.set_dependency_value("other", "keep", error_if_not_found=False) @@ -107,16 +107,16 @@ def test_remove_all_matching(self): assert doc.dependency_value("other") == "keep" def test_remove_no_dependencies(self): - from ndi.document import Document + from ndi.document import ndi_document - doc = Document("base") + doc = ndi_document("base") # Should not raise doc.remove_dependency_value_n("nonexistent") def test_remove_returns_self(self): - from ndi.document import Document + from ndi.document import ndi_document - doc = Document("base") + doc = ndi_document("base") result = doc.remove_dependency_value_n("dep") assert result is doc @@ -127,113 +127,113 @@ def test_remove_returns_self(self): class TestIntanReader: - """Tests for IntanReader.""" + """Tests for ndi_daq_reader_mfdaq_intan.""" def test_import(self): - from ndi.daq.reader.mfdaq.intan import IntanReader + from ndi.daq.reader.mfdaq.intan import ndi_daq_reader_mfdaq_intan - assert IntanReader is not None + assert ndi_daq_reader_mfdaq_intan is not None def test_construction(self): - from ndi.daq.reader.mfdaq.intan import IntanReader + from ndi.daq.reader.mfdaq.intan import ndi_daq_reader_mfdaq_intan - reader = IntanReader() + reader = ndi_daq_reader_mfdaq_intan() assert reader.NDI_DAQREADER_CLASS == "ndi.daq.reader.mfdaq.intan" def test_inherits_mfdaq_reader(self): - from ndi.daq.mfdaq import MFDAQReader - from ndi.daq.reader.mfdaq.intan import IntanReader + from ndi.daq.mfdaq import ndi_daq_reader_mfdaq + from ndi.daq.reader.mfdaq.intan import ndi_daq_reader_mfdaq_intan - reader = IntanReader() - assert isinstance(reader, MFDAQReader) + reader = ndi_daq_reader_mfdaq_intan() + assert isinstance(reader, ndi_daq_reader_mfdaq) def test_file_extensions(self): - from ndi.daq.reader.mfdaq.intan import IntanReader + from ndi.daq.reader.mfdaq.intan import ndi_daq_reader_mfdaq_intan - assert ".rhd" in IntanReader.FILE_EXTENSIONS - assert ".rhs" in IntanReader.FILE_EXTENSIONS + assert ".rhd" in ndi_daq_reader_mfdaq_intan.FILE_EXTENSIONS + assert ".rhs" in ndi_daq_reader_mfdaq_intan.FILE_EXTENSIONS def test_repr(self): - from ndi.daq.reader.mfdaq.intan import IntanReader + from ndi.daq.reader.mfdaq.intan import ndi_daq_reader_mfdaq_intan - reader = IntanReader() - assert "IntanReader" in repr(reader) + reader = ndi_daq_reader_mfdaq_intan() + assert "ndi_daq_reader_mfdaq_intan" in repr(reader) class TestBlackrockReader: - """Tests for BlackrockReader.""" + """Tests for ndi_daq_reader_mfdaq_blackrock.""" def test_import(self): - from ndi.daq.reader.mfdaq.blackrock import BlackrockReader + from ndi.daq.reader.mfdaq.blackrock import ndi_daq_reader_mfdaq_blackrock - assert BlackrockReader is not None + assert ndi_daq_reader_mfdaq_blackrock is not None def test_construction(self): - from ndi.daq.reader.mfdaq.blackrock import BlackrockReader + from ndi.daq.reader.mfdaq.blackrock import ndi_daq_reader_mfdaq_blackrock - reader = BlackrockReader() + reader = ndi_daq_reader_mfdaq_blackrock() assert reader.NDI_DAQREADER_CLASS == "ndi.daq.reader.mfdaq.blackrock" def test_file_extensions(self): - from ndi.daq.reader.mfdaq.blackrock import BlackrockReader + from ndi.daq.reader.mfdaq.blackrock import ndi_daq_reader_mfdaq_blackrock - assert ".ns5" in BlackrockReader.FILE_EXTENSIONS - assert ".nev" in BlackrockReader.FILE_EXTENSIONS + assert ".ns5" in ndi_daq_reader_mfdaq_blackrock.FILE_EXTENSIONS + assert ".nev" in ndi_daq_reader_mfdaq_blackrock.FILE_EXTENSIONS def test_repr(self): - from ndi.daq.reader.mfdaq.blackrock import BlackrockReader + from ndi.daq.reader.mfdaq.blackrock import ndi_daq_reader_mfdaq_blackrock - assert "BlackrockReader" in repr(BlackrockReader()) + assert "ndi_daq_reader_mfdaq_blackrock" in repr(ndi_daq_reader_mfdaq_blackrock()) class TestCEDSpike2Reader: - """Tests for CEDSpike2Reader.""" + """Tests for ndi_daq_reader_mfdaq_cedspike2.""" def test_import(self): - from ndi.daq.reader.mfdaq.cedspike2 import CEDSpike2Reader + from ndi.daq.reader.mfdaq.cedspike2 import ndi_daq_reader_mfdaq_cedspike2 - assert CEDSpike2Reader is not None + assert ndi_daq_reader_mfdaq_cedspike2 is not None def test_construction(self): - from ndi.daq.reader.mfdaq.cedspike2 import CEDSpike2Reader + from ndi.daq.reader.mfdaq.cedspike2 import ndi_daq_reader_mfdaq_cedspike2 - reader = CEDSpike2Reader() + reader = ndi_daq_reader_mfdaq_cedspike2() assert reader.NDI_DAQREADER_CLASS == "ndi.daq.reader.mfdaq.cedspike2" def test_file_extensions(self): - from ndi.daq.reader.mfdaq.cedspike2 import CEDSpike2Reader + from ndi.daq.reader.mfdaq.cedspike2 import ndi_daq_reader_mfdaq_cedspike2 - assert ".smr" in CEDSpike2Reader.FILE_EXTENSIONS + assert ".smr" in ndi_daq_reader_mfdaq_cedspike2.FILE_EXTENSIONS def test_repr(self): - from ndi.daq.reader.mfdaq.cedspike2 import CEDSpike2Reader + from ndi.daq.reader.mfdaq.cedspike2 import ndi_daq_reader_mfdaq_cedspike2 - assert "CEDSpike2Reader" in repr(CEDSpike2Reader()) + assert "ndi_daq_reader_mfdaq_cedspike2" in repr(ndi_daq_reader_mfdaq_cedspike2()) class TestSpikeGadgetsReader: - """Tests for SpikeGadgetsReader.""" + """Tests for ndi_daq_reader_mfdaq_spikegadgets.""" def test_import(self): - from ndi.daq.reader.mfdaq.spikegadgets import SpikeGadgetsReader + from ndi.daq.reader.mfdaq.spikegadgets import ndi_daq_reader_mfdaq_spikegadgets - assert SpikeGadgetsReader is not None + assert ndi_daq_reader_mfdaq_spikegadgets is not None def test_construction(self): - from ndi.daq.reader.mfdaq.spikegadgets import SpikeGadgetsReader + from ndi.daq.reader.mfdaq.spikegadgets import ndi_daq_reader_mfdaq_spikegadgets - reader = SpikeGadgetsReader() + reader = ndi_daq_reader_mfdaq_spikegadgets() assert reader.NDI_DAQREADER_CLASS == "ndi.daq.reader.mfdaq.spikegadgets" def test_file_extensions(self): - from ndi.daq.reader.mfdaq.spikegadgets import SpikeGadgetsReader + from ndi.daq.reader.mfdaq.spikegadgets import ndi_daq_reader_mfdaq_spikegadgets - assert ".rec" in SpikeGadgetsReader.FILE_EXTENSIONS + assert ".rec" in ndi_daq_reader_mfdaq_spikegadgets.FILE_EXTENSIONS def test_repr(self): - from ndi.daq.reader.mfdaq.spikegadgets import SpikeGadgetsReader + from ndi.daq.reader.mfdaq.spikegadgets import ndi_daq_reader_mfdaq_spikegadgets - assert "SpikeGadgetsReader" in repr(SpikeGadgetsReader()) + assert "ndi_daq_reader_mfdaq_spikegadgets" in repr(ndi_daq_reader_mfdaq_spikegadgets()) class TestReaderPackageImports: @@ -241,108 +241,137 @@ class TestReaderPackageImports: def test_from_reader_mfdaq(self): from ndi.daq.reader.mfdaq import ( - BlackrockReader, - CEDSpike2Reader, - IntanReader, - SpikeGadgetsReader, + ndi_daq_reader_mfdaq_blackrock, + ndi_daq_reader_mfdaq_cedspike2, + ndi_daq_reader_mfdaq_intan, + ndi_daq_reader_mfdaq_spikegadgets, ) - assert all([IntanReader, BlackrockReader, CEDSpike2Reader, SpikeGadgetsReader]) + assert all( + [ + ndi_daq_reader_mfdaq_intan, + ndi_daq_reader_mfdaq_blackrock, + ndi_daq_reader_mfdaq_cedspike2, + ndi_daq_reader_mfdaq_spikegadgets, + ] + ) def test_from_reader(self): - from ndi.daq.reader import BlackrockReader, CEDSpike2Reader, IntanReader, SpikeGadgetsReader + from ndi.daq.reader import ( + ndi_daq_reader_mfdaq_blackrock, + ndi_daq_reader_mfdaq_cedspike2, + ndi_daq_reader_mfdaq_intan, + ndi_daq_reader_mfdaq_spikegadgets, + ) - assert all([IntanReader, BlackrockReader, CEDSpike2Reader, SpikeGadgetsReader]) + assert all( + [ + ndi_daq_reader_mfdaq_intan, + ndi_daq_reader_mfdaq_blackrock, + ndi_daq_reader_mfdaq_cedspike2, + ndi_daq_reader_mfdaq_spikegadgets, + ] + ) def test_all_inherit_from_mfdaq_reader(self): - from ndi.daq.mfdaq import MFDAQReader + from ndi.daq.mfdaq import ndi_daq_reader_mfdaq from ndi.daq.reader.mfdaq import ( - BlackrockReader, - CEDSpike2Reader, - IntanReader, - SpikeGadgetsReader, + ndi_daq_reader_mfdaq_blackrock, + ndi_daq_reader_mfdaq_cedspike2, + ndi_daq_reader_mfdaq_intan, + ndi_daq_reader_mfdaq_spikegadgets, ) - for cls in [IntanReader, BlackrockReader, CEDSpike2Reader, SpikeGadgetsReader]: + for cls in [ + ndi_daq_reader_mfdaq_intan, + ndi_daq_reader_mfdaq_blackrock, + ndi_daq_reader_mfdaq_cedspike2, + ndi_daq_reader_mfdaq_spikegadgets, + ]: reader = cls() - assert isinstance(reader, MFDAQReader) + assert isinstance(reader, ndi_daq_reader_mfdaq) def test_each_has_unique_daqreader_class(self): from ndi.daq.reader.mfdaq import ( - BlackrockReader, - CEDSpike2Reader, - IntanReader, - SpikeGadgetsReader, + ndi_daq_reader_mfdaq_blackrock, + ndi_daq_reader_mfdaq_cedspike2, + ndi_daq_reader_mfdaq_intan, + ndi_daq_reader_mfdaq_spikegadgets, ) classes = set() - for cls in [IntanReader, BlackrockReader, CEDSpike2Reader, SpikeGadgetsReader]: + for cls in [ + ndi_daq_reader_mfdaq_intan, + ndi_daq_reader_mfdaq_blackrock, + ndi_daq_reader_mfdaq_cedspike2, + ndi_daq_reader_mfdaq_spikegadgets, + ]: reader = cls() classes.add(reader._ndi_daqreader_class) assert len(classes) == 4 # ============================================================================ -# MockSession Tests +# ndi_session_mock Tests # ============================================================================ class TestMockSession: - """Tests for MockSession.""" + """Tests for ndi_session_mock.""" def test_import(self): - from ndi.session.mock import MockSession + from ndi.session.mock import ndi_session_mock - assert MockSession is not None + assert ndi_session_mock is not None def test_construction(self): - from ndi.session.mock import MockSession + from ndi.session.mock import ndi_session_mock - session = MockSession("test") + session = ndi_session_mock("test") assert session is not None assert os.path.isdir(session._tmpdir) session.close() def test_inherits_dirsession(self): - from ndi.session import DirSession - from ndi.session.mock import MockSession + from ndi.session import ndi_session_dir + from ndi.session.mock import ndi_session_mock - session = MockSession("test") - assert isinstance(session, DirSession) + session = ndi_session_mock("test") + assert isinstance(session, ndi_session_dir) session.close() def test_context_manager(self): - from ndi.session.mock import MockSession + from ndi.session.mock import ndi_session_mock - with MockSession("test") as session: + with ndi_session_mock("test") as session: tmpdir = session._tmpdir assert os.path.isdir(tmpdir) assert not os.path.exists(tmpdir) def test_database_operations(self): - from ndi.document import Document - from ndi.query import Query - from ndi.session.mock import MockSession + from ndi.document import ndi_document + from ndi.query import ndi_query + from ndi.session.mock import ndi_session_mock - with MockSession("test") as session: - doc = Document("base", **{"base.name": "mock_test"}) + with ndi_session_mock("test") as session: + doc = ndi_document("base", **{"base.name": "mock_test"}) session.database_add(doc) - results = session.database_search(Query("").isa("base")) + results = session.database_search(ndi_query("").isa("base")) assert len(results) >= 1 def test_cleanup_on_close(self): - from ndi.session.mock import MockSession + from ndi.session.mock import ndi_session_mock - session = MockSession("test") + session = ndi_session_mock("test") tmpdir = session._tmpdir assert os.path.isdir(tmpdir) session.close() assert not os.path.exists(tmpdir) def test_no_cleanup_option(self): - from ndi.session.mock import MockSession + from ndi.session.mock import ndi_session_mock - session = MockSession("test", cleanup=False) + session = ndi_session_mock("test", cleanup=False) tmpdir = session._tmpdir session.close() assert os.path.isdir(tmpdir) # Should still exist @@ -352,20 +381,20 @@ def test_no_cleanup_option(self): shutil.rmtree(tmpdir, ignore_errors=True) def test_id_is_method(self): - from ndi.session.mock import MockSession + from ndi.session.mock import ndi_session_mock - with MockSession("test") as session: + with ndi_session_mock("test") as session: sid = session.id() assert isinstance(sid, str) assert len(sid) > 0 def test_repr(self): - from ndi.session.mock import MockSession + from ndi.session.mock import ndi_session_mock - with MockSession("test") as session: - assert "MockSession" in repr(session) + with ndi_session_mock("test") as session: + assert "ndi_session_mock" in repr(session) def test_from_session_module(self): - from ndi.session import MockSession + from ndi.session import ndi_session_mock - assert MockSession is not None + assert ndi_session_mock is not None diff --git a/tests/test_batch_c.py b/tests/test_batch_c.py index 106cf0d..cb262cb 100644 --- a/tests/test_batch_c.py +++ b/tests/test_batch_c.py @@ -1,100 +1,102 @@ """ -Tests for Batch C: App & Calculator subclasses. +Tests for Batch C: ndi_app & ndi_calculator subclasses. -Tests MarkGarbage, SpikeExtractor, SpikeSorter, StimulusDecoder, -TuningResponse, OriDirTuning, and TuningCurveCalc. +Tests ndi_app_markgarbage, ndi_app_spikeextractor, ndi_app_spikesorter, ndi_app_stimulus_decoder, +ndi_app_stimulus_tuning__response, ndi_app_oridirtuning, and ndi_calc_stimulus_tuningcurve. """ from types import SimpleNamespace import pytest -from ndi.app import App -from ndi.app.appdoc import AppDoc +from ndi.app import ndi_app +from ndi.app.appdoc import ndi_app_appdoc # --------------------------------------------------------------------------- # Imports # --------------------------------------------------------------------------- -from ndi.app.markgarbage import MarkGarbage -from ndi.app.oridirtuning import OriDirTuning -from ndi.app.spikeextractor import SpikeExtractor -from ndi.app.spikesorter import SpikeSorter -from ndi.app.stimulus import StimulusDecoder, TuningResponse -from ndi.app.stimulus.decoder import StimulusDecoder as StimulusDecoderDirect -from ndi.app.stimulus.tuning_response import TuningResponse as TuningResponseDirect -from ndi.calc.stimulus import TuningCurveCalc -from ndi.calc.stimulus.tuningcurve import TuningCurveCalc as TuningCurveCalcDirect -from ndi.calculator import Calculator +from ndi.app.markgarbage import ndi_app_markgarbage +from ndi.app.oridirtuning import ndi_app_oridirtuning +from ndi.app.spikeextractor import ndi_app_spikeextractor +from ndi.app.spikesorter import ndi_app_spikesorter +from ndi.app.stimulus import ndi_app_stimulus_decoder, ndi_app_stimulus_tuning__response +from ndi.app.stimulus.decoder import ndi_app_stimulus_decoder as StimulusDecoderDirect +from ndi.app.stimulus.tuning_response import ( + ndi_app_stimulus_tuning__response as TuningResponseDirect, +) +from ndi.calc.stimulus import ndi_calc_stimulus_tuningcurve +from ndi.calc.stimulus.tuningcurve import ndi_calc_stimulus_tuningcurve as TuningCurveCalcDirect +from ndi.calculator import ndi_calculator class TestImports: """Verify all Batch C classes are importable.""" def test_import_markgarbage(self): - assert MarkGarbage is not None + assert ndi_app_markgarbage is not None def test_import_spikeextractor(self): - assert SpikeExtractor is not None + assert ndi_app_spikeextractor is not None def test_import_spikesorter(self): - assert SpikeSorter is not None + assert ndi_app_spikesorter is not None def test_import_stimulus_decoder_from_package(self): - assert StimulusDecoder is StimulusDecoderDirect + assert ndi_app_stimulus_decoder is StimulusDecoderDirect def test_import_tuning_response_from_package(self): - assert TuningResponse is TuningResponseDirect + assert ndi_app_stimulus_tuning__response is TuningResponseDirect def test_import_oridirtuning(self): - assert OriDirTuning is not None + assert ndi_app_oridirtuning is not None def test_import_tuningcurvecalc_from_package(self): - assert TuningCurveCalc is TuningCurveCalcDirect + assert ndi_calc_stimulus_tuningcurve is TuningCurveCalcDirect # =========================================================================== -# MarkGarbage +# ndi_app_markgarbage # =========================================================================== class TestMarkGarbage: - """Tests for the MarkGarbage app.""" + """Tests for the ndi_app_markgarbage app.""" def test_init_no_session(self): - app = MarkGarbage() + app = ndi_app_markgarbage() assert app.session is None assert app.name == "ndi_app_markgarbage" def test_init_with_session(self): session = SimpleNamespace(id=lambda: "sess1") - app = MarkGarbage(session=session) + app = ndi_app_markgarbage(session=session) assert app.session is session def test_inherits_app(self): - assert issubclass(MarkGarbage, App) + assert issubclass(ndi_app_markgarbage, ndi_app) def test_repr(self): - app = MarkGarbage() - assert "MarkGarbage" in repr(app) + app = ndi_app_markgarbage() + assert "ndi_app_markgarbage" in repr(app) assert "False" in repr(app) def test_repr_with_session(self): session = SimpleNamespace(id=lambda: "s") - app = MarkGarbage(session=session) + app = ndi_app_markgarbage(session=session) assert "True" in repr(app) def test_save_no_session_raises(self): - app = MarkGarbage() + app = ndi_app_markgarbage() with pytest.raises(RuntimeError, match="No session"): app.savevalidinterval(None, {"t0": 0, "t1": 1}) def test_clear_no_session(self): - app = MarkGarbage() + app = ndi_app_markgarbage() # Should not raise, just returns app.clearvalidinterval(SimpleNamespace(id="elem1")) def test_load_no_session(self): - app = MarkGarbage() + app = ndi_app_markgarbage() intervals, docs = app.loadvalidinterval(SimpleNamespace(id="elem1")) assert intervals == [] assert docs == [] @@ -103,7 +105,7 @@ def test_markvalidinterval_calls_save(self): """Verify markvalidinterval builds struct and calls savevalidinterval.""" saved = [] - class MockMarkGarbage(MarkGarbage): + class MockMarkGarbage(ndi_app_markgarbage): def savevalidinterval(self, epochset_obj, interval_struct): saved.append(interval_struct) @@ -123,37 +125,37 @@ def savevalidinterval(self, epochset_obj, interval_struct): # =========================================================================== -# SpikeExtractor +# ndi_app_spikeextractor # =========================================================================== class TestSpikeExtractor: - """Tests for the SpikeExtractor app.""" + """Tests for the ndi_app_spikeextractor app.""" def test_init_no_session(self): - app = SpikeExtractor() + app = ndi_app_spikeextractor() assert app.session is None assert app.name == "ndi_app_spikeextractor" def test_inherits_app_and_appdoc(self): - assert issubclass(SpikeExtractor, App) - assert issubclass(SpikeExtractor, AppDoc) + assert issubclass(ndi_app_spikeextractor, ndi_app) + assert issubclass(ndi_app_spikeextractor, ndi_app_appdoc) def test_doc_types(self): - app = SpikeExtractor() + app = ndi_app_spikeextractor() assert "extraction_parameters" in app.doc_types assert "extraction_parameters_modification" in app.doc_types assert "spikewaves" in app.doc_types assert len(app.doc_types) == 3 def test_doc_document_types(self): - app = SpikeExtractor() + app = ndi_app_spikeextractor() assert len(app.doc_document_types) == 3 assert "apps/spikeextractor/spike_extraction_parameters" in app.doc_document_types assert "apps/spikeextractor/spikewaves" in app.doc_document_types def test_default_extraction_parameters(self): - params = SpikeExtractor.default_extraction_parameters() + params = ndi_app_spikeextractor.default_extraction_parameters() assert "filter" in params assert "threshold" in params assert "timing" in params @@ -163,12 +165,12 @@ def test_default_extraction_parameters(self): assert params["timing"]["pre_samples"] == 10 def test_extract_raises(self): - app = SpikeExtractor() + app = ndi_app_spikeextractor() with pytest.raises(NotImplementedError): app.extract(SimpleNamespace()) def test_isvalid_struct_valid(self): - app = SpikeExtractor() + app = ndi_app_spikeextractor() b, errormsg = app.isvalid_appdoc_struct( "extraction_parameters", {"filter": {}, "threshold": {}}, @@ -177,7 +179,7 @@ def test_isvalid_struct_valid(self): assert errormsg == "" def test_isvalid_struct_invalid(self): - app = SpikeExtractor() + app = ndi_app_spikeextractor() b, errormsg = app.isvalid_appdoc_struct( "extraction_parameters", {"filter": {}}, # missing threshold @@ -186,55 +188,55 @@ def test_isvalid_struct_invalid(self): assert "filter" in errormsg or "threshold" in errormsg def test_isvalid_struct_other_type(self): - app = SpikeExtractor() + app = ndi_app_spikeextractor() b, errormsg = app.isvalid_appdoc_struct("spikewaves", {}) assert b is True def test_find_appdoc_no_session(self): - app = SpikeExtractor() + app = ndi_app_spikeextractor() assert app.find_appdoc("extraction_parameters") == [] def test_struct2doc(self): - from ndi.document import Document + from ndi.document import ndi_document - app = SpikeExtractor() + app = ndi_app_spikeextractor() doc = app.struct2doc("extraction_parameters", {"filter": {}, "threshold": {}}) - assert isinstance(doc, Document) + assert isinstance(doc, ndi_document) def test_repr(self): - assert "SpikeExtractor" in repr(SpikeExtractor()) + assert "ndi_app_spikeextractor" in repr(ndi_app_spikeextractor()) # =========================================================================== -# SpikeSorter +# ndi_app_spikesorter # =========================================================================== class TestSpikeSorter: - """Tests for the SpikeSorter app.""" + """Tests for the ndi_app_spikesorter app.""" def test_init_no_session(self): - app = SpikeSorter() + app = ndi_app_spikesorter() assert app.session is None assert app.name == "ndi_app_spikesorter" def test_inherits_app_and_appdoc(self): - assert issubclass(SpikeSorter, App) - assert issubclass(SpikeSorter, AppDoc) + assert issubclass(ndi_app_spikesorter, ndi_app) + assert issubclass(ndi_app_spikesorter, ndi_app_appdoc) def test_doc_types(self): - app = SpikeSorter() + app = ndi_app_spikesorter() assert "sorting_parameters" in app.doc_types assert "spike_clusters" in app.doc_types assert len(app.doc_types) == 2 def test_doc_document_types(self): - app = SpikeSorter() + app = ndi_app_spikesorter() assert "apps/spikesorter/sorting_parameters" in app.doc_document_types assert "apps/spikesorter/spike_clusters" in app.doc_document_types def test_default_sorting_parameters(self): - params = SpikeSorter.default_sorting_parameters() + params = ndi_app_spikesorter.default_sorting_parameters() assert params["graphical_mode"] is False assert params["num_pca_features"] == 4 assert params["interpolation"] == 2 @@ -242,17 +244,17 @@ def test_default_sorting_parameters(self): assert params["max_clusters"] == 5 def test_spike_sort_raises(self): - app = SpikeSorter() + app = ndi_app_spikesorter() with pytest.raises(NotImplementedError): app.spike_sort(SimpleNamespace()) def test_clusters2neurons_raises(self): - app = SpikeSorter() + app = ndi_app_spikesorter() with pytest.raises(NotImplementedError): app.clusters2neurons(SimpleNamespace()) def test_isvalid_struct_valid(self): - app = SpikeSorter() + app = ndi_app_spikesorter() b, errormsg = app.isvalid_appdoc_struct( "sorting_parameters", {"num_pca_features": 4}, @@ -261,7 +263,7 @@ def test_isvalid_struct_valid(self): assert errormsg == "" def test_isvalid_struct_invalid(self): - app = SpikeSorter() + app = ndi_app_spikesorter() b, errormsg = app.isvalid_appdoc_struct( "sorting_parameters", {"interpolation": 2}, # missing num_pca_features @@ -270,38 +272,38 @@ def test_isvalid_struct_invalid(self): assert "num_pca_features" in errormsg def test_find_appdoc_no_session(self): - app = SpikeSorter() + app = ndi_app_spikesorter() assert app.find_appdoc("sorting_parameters") == [] def test_struct2doc(self): - from ndi.document import Document + from ndi.document import ndi_document - app = SpikeSorter() + app = ndi_app_spikesorter() doc = app.struct2doc("sorting_parameters", {"num_pca_features": 4}) - assert isinstance(doc, Document) + assert isinstance(doc, ndi_document) def test_repr(self): - assert "SpikeSorter" in repr(SpikeSorter()) + assert "ndi_app_spikesorter" in repr(ndi_app_spikesorter()) # =========================================================================== -# StimulusDecoder +# ndi_app_stimulus_decoder # =========================================================================== class TestStimulusDecoder: - """Tests for the StimulusDecoder app.""" + """Tests for the ndi_app_stimulus_decoder app.""" def test_init_no_session(self): - app = StimulusDecoder() + app = ndi_app_stimulus_decoder() assert app.session is None assert app.name == "ndi_app_stimulus_decoder" def test_inherits_app(self): - assert issubclass(StimulusDecoder, App) + assert issubclass(ndi_app_stimulus_decoder, ndi_app) def test_parse_no_session_raises(self): - app = StimulusDecoder() + app = ndi_app_stimulus_decoder() with pytest.raises(RuntimeError, match="No session"): app.parse_stimuli(SimpleNamespace()) @@ -311,24 +313,24 @@ def test_parse_returns_empty_tuple(self): database_search=lambda q: [], database_remove=lambda d: None, ) - app = StimulusDecoder(session=session) + app = ndi_app_stimulus_decoder(session=session) newdocs, existingdocs = app.parse_stimuli(SimpleNamespace(id="stim1")) assert newdocs == [] assert existingdocs == [] def test_load_presentation_time_no_session(self): - app = StimulusDecoder() + app = ndi_app_stimulus_decoder() result = app.load_presentation_time(SimpleNamespace()) assert result is None def test_load_presentation_time_with_session(self): session = SimpleNamespace(id=lambda: "s") - app = StimulusDecoder(session=session) + app = ndi_app_stimulus_decoder(session=session) result = app.load_presentation_time(SimpleNamespace()) assert result is None def test_clear_presentations_no_session(self): - app = StimulusDecoder() + app = ndi_app_stimulus_decoder() # Should not raise app._clear_presentations(SimpleNamespace(id="stim1")) @@ -339,47 +341,47 @@ def test_clear_presentations_with_session(self): database_search=lambda q: [SimpleNamespace(id="doc1")], database_remove=lambda d: removed.append(d), ) - app = StimulusDecoder(session=session) + app = ndi_app_stimulus_decoder(session=session) app._clear_presentations(SimpleNamespace(id="stim1")) assert len(removed) == 1 def test_repr(self): - assert "StimulusDecoder" in repr(StimulusDecoder()) + assert "ndi_app_stimulus_decoder" in repr(ndi_app_stimulus_decoder()) # =========================================================================== -# TuningResponse +# ndi_app_stimulus_tuning__response # =========================================================================== class TestTuningResponse: - """Tests for the TuningResponse app.""" + """Tests for the ndi_app_stimulus_tuning__response app.""" def test_init_no_session(self): - app = TuningResponse() + app = ndi_app_stimulus_tuning__response() assert app.session is None assert app.name == "ndi_app_tuning_response" def test_inherits_app(self): - assert issubclass(TuningResponse, App) + assert issubclass(ndi_app_stimulus_tuning__response, ndi_app) def test_stimulus_responses_raises(self): - app = TuningResponse() + app = ndi_app_stimulus_tuning__response() with pytest.raises(NotImplementedError): app.stimulus_responses(SimpleNamespace(), SimpleNamespace()) def test_tuning_curve_raises(self): - app = TuningResponse() + app = ndi_app_stimulus_tuning__response() with pytest.raises(NotImplementedError): app.tuning_curve(SimpleNamespace()) def test_label_control_stimuli(self): - app = TuningResponse() + app = ndi_app_stimulus_tuning__response() result = app.label_control_stimuli(SimpleNamespace()) assert result == [] def test_find_tuningcurve_no_session(self): - app = TuningResponse() + app = ndi_app_stimulus_tuning__response() tc_docs, srs_docs = app.find_tuningcurve_document( SimpleNamespace(id="elem1"), "epoch1", @@ -392,7 +394,7 @@ def test_find_tuningcurve_with_session(self): id=lambda: "s1", database_search=lambda q: [], ) - app = TuningResponse(session=session) + app = ndi_app_stimulus_tuning__response(session=session) tc_docs, srs_docs = app.find_tuningcurve_document( SimpleNamespace(id="elem1"), "epoch1", @@ -401,44 +403,44 @@ def test_find_tuningcurve_with_session(self): assert srs_docs == [] def test_repr(self): - assert "TuningResponse" in repr(TuningResponse()) + assert "ndi_app_stimulus_tuning__response" in repr(ndi_app_stimulus_tuning__response()) # =========================================================================== -# OriDirTuning +# ndi_app_oridirtuning # =========================================================================== class TestOriDirTuning: - """Tests for the OriDirTuning app.""" + """Tests for the ndi_app_oridirtuning app.""" def test_init_no_session(self): - app = OriDirTuning() + app = ndi_app_oridirtuning() assert app.session is None assert app.name == "ndi_app_oridirtuning" def test_inherits_app_and_appdoc(self): - assert issubclass(OriDirTuning, App) - assert issubclass(OriDirTuning, AppDoc) + assert issubclass(ndi_app_oridirtuning, ndi_app) + assert issubclass(ndi_app_oridirtuning, ndi_app_appdoc) def test_doc_types(self): - app = OriDirTuning() + app = ndi_app_oridirtuning() assert "orientation_direction_tuning" in app.doc_types assert "tuning_curve" in app.doc_types assert len(app.doc_types) == 2 def test_doc_document_types(self): - app = OriDirTuning() + app = ndi_app_oridirtuning() assert "apps/oridirtuning/orientation_direction_tuning" in app.doc_document_types assert "apps/oridirtuning/tuning_curve" in app.doc_document_types def test_calculate_tuning_curves_raises(self): - app = OriDirTuning() + app = ndi_app_oridirtuning() with pytest.raises(NotImplementedError): app.calculate_all_tuning_curves(SimpleNamespace()) def test_calculate_oridir_indexes_raises(self): - app = OriDirTuning() + app = ndi_app_oridirtuning() with pytest.raises(NotImplementedError): app.calculate_all_oridir_indexes(SimpleNamespace()) @@ -450,7 +452,7 @@ def test_is_oridir_stimulus_angle(self): ), ), ) - assert OriDirTuning.is_oridir_stimulus_response(doc) is True + assert ndi_app_oridirtuning.is_oridir_stimulus_response(doc) is True def test_is_oridir_stimulus_direction(self): doc = SimpleNamespace( @@ -460,7 +462,7 @@ def test_is_oridir_stimulus_direction(self): ), ), ) - assert OriDirTuning.is_oridir_stimulus_response(doc) is True + assert ndi_app_oridirtuning.is_oridir_stimulus_response(doc) is True def test_is_oridir_stimulus_orientation(self): doc = SimpleNamespace( @@ -470,7 +472,7 @@ def test_is_oridir_stimulus_orientation(self): ), ), ) - assert OriDirTuning.is_oridir_stimulus_response(doc) is True + assert ndi_app_oridirtuning.is_oridir_stimulus_response(doc) is True def test_is_oridir_stimulus_false(self): doc = SimpleNamespace( @@ -480,61 +482,61 @@ def test_is_oridir_stimulus_false(self): ), ), ) - assert OriDirTuning.is_oridir_stimulus_response(doc) is False + assert ndi_app_oridirtuning.is_oridir_stimulus_response(doc) is False def test_is_oridir_stimulus_no_attr(self): doc = SimpleNamespace(document_properties=SimpleNamespace()) - assert OriDirTuning.is_oridir_stimulus_response(doc) is False + assert ndi_app_oridirtuning.is_oridir_stimulus_response(doc) is False def test_struct2doc_maps_type_correctly(self): """struct2doc maps appdoc_type to correct schema path.""" - app = OriDirTuning() + app = ndi_app_oridirtuning() idx = app.doc_types.index("tuning_curve") assert app.doc_document_types[idx] == "apps/oridirtuning/tuning_curve" idx2 = app.doc_types.index("orientation_direction_tuning") assert app.doc_document_types[idx2] == "apps/oridirtuning/orientation_direction_tuning" def test_find_appdoc_no_session(self): - app = OriDirTuning() + app = ndi_app_oridirtuning() assert app.find_appdoc("tuning_curve") == [] def test_isvalid(self): - app = OriDirTuning() + app = ndi_app_oridirtuning() b, errormsg = app.isvalid_appdoc_struct("tuning_curve", {}) assert b is True assert errormsg == "" def test_repr(self): - assert "OriDirTuning" in repr(OriDirTuning()) + assert "ndi_app_oridirtuning" in repr(ndi_app_oridirtuning()) # =========================================================================== -# TuningCurveCalc +# ndi_calc_stimulus_tuningcurve # =========================================================================== class TestTuningCurveCalc: - """Tests for the TuningCurveCalc calculator.""" + """Tests for the ndi_calc_stimulus_tuningcurve calculator.""" def test_init_no_session(self): - calc = TuningCurveCalc() + calc = ndi_calc_stimulus_tuningcurve() assert calc.session is None def test_inherits_calculator(self): - assert issubclass(TuningCurveCalc, Calculator) + assert issubclass(ndi_calc_stimulus_tuningcurve, ndi_calculator) def test_doc_types(self): - calc = TuningCurveCalc() + calc = ndi_calc_stimulus_tuningcurve() assert "tuningcurve_calc" in calc.doc_types def test_doc_document_types(self): - calc = TuningCurveCalc() + calc = ndi_calc_stimulus_tuningcurve() assert "apps/calculators/tuningcurve_calc" in calc.doc_document_types def test_calculate_returns_document(self): - from ndi.document import Document + from ndi.document import ndi_document - calc = TuningCurveCalc() + calc = ndi_calc_stimulus_tuningcurve() params = { "input_parameters": { "independent_label": "angle", @@ -544,10 +546,10 @@ def test_calculate_returns_document(self): } docs = calc.calculate(params) assert len(docs) == 1 - assert isinstance(docs[0], Document) + assert isinstance(docs[0], ndi_document) def test_calculate_with_dependencies(self): - calc = TuningCurveCalc() + calc = ndi_calc_stimulus_tuningcurve() params = { "input_parameters": {"label": "angle"}, "depends_on": [{"name": "stimulus_response_scalar_id", "value": "abc123"}], @@ -560,14 +562,14 @@ def test_calculate_with_dependencies(self): def test_calculate_with_session(self): session = SimpleNamespace(id=lambda: "sess1") - calc = TuningCurveCalc(session=session) + calc = ndi_calc_stimulus_tuningcurve(session=session) params = {"input_parameters": {}, "depends_on": []} docs = calc.calculate(params) assert len(docs) == 1 assert docs[0].session_id == "sess1" def test_default_search_parameters(self): - calc = TuningCurveCalc() + calc = ndi_calc_stimulus_tuningcurve() params = calc.default_search_for_input_parameters() assert "input_parameters" in params assert "depends_on" in params @@ -578,4 +580,4 @@ def test_default_search_parameters(self): assert params["query"][0]["name"] == "document_id" def test_repr(self): - assert "TuningCurveCalc" in repr(TuningCurveCalc()) + assert "ndi_calc_stimulus_tuningcurve" in repr(ndi_calc_stimulus_tuningcurve()) diff --git a/tests/test_batch_d.py b/tests/test_batch_d.py index 0278cbd..eb38620 100644 --- a/tests/test_batch_d.py +++ b/tests/test_batch_d.py @@ -1,8 +1,8 @@ """ -Tests for Batch D: Lab-specific + advanced. +Tests for Batch D: ndi_gui_Lab-specific + advanced. -Tests NewStimStimsReader, NielsenLabStimsReader, -ProbeTimeseriesStimulator, downsample, downsample_timeseries. +Tests ndi_daq_metadatareader_NewStimStims, ndi_daq_metadatareader_NielsenLabStims, +ndi_probe_timeseries_stimulator, downsample, downsample_timeseries. """ import os @@ -15,44 +15,52 @@ # --------------------------------------------------------------------------- # Imports # --------------------------------------------------------------------------- -from ndi.daq.metadatareader import MetadataReader, NewStimStimsReader, NielsenLabStimsReader -from ndi.daq.metadatareader.newstim_stims import NewStimStimsReader as NewStimDirect -from ndi.daq.metadatareader.nielsenlab_stims import NielsenLabStimsReader as NielsenDirect +from ndi.daq.metadatareader import ( + ndi_daq_metadatareader, + ndi_daq_metadatareader_NewStimStims, + ndi_daq_metadatareader_NielsenLabStims, +) +from ndi.daq.metadatareader.newstim_stims import ( + ndi_daq_metadatareader_NewStimStims as NewStimDirect, +) +from ndi.daq.metadatareader.nielsenlab_stims import ( + ndi_daq_metadatareader_NielsenLabStims as NielsenDirect, +) from ndi.element.functions import downsample, downsample_timeseries -from ndi.probe.timeseries_stimulator import ProbeTimeseriesStimulator +from ndi.probe.timeseries_stimulator import ndi_probe_timeseries_stimulator class TestImports: """Verify all Batch D classes are importable.""" def test_import_newstim_from_package(self): - assert NewStimStimsReader is NewStimDirect + assert ndi_daq_metadatareader_NewStimStims is NewStimDirect def test_import_nielsen_from_package(self): - assert NielsenLabStimsReader is NielsenDirect + assert ndi_daq_metadatareader_NielsenLabStims is NielsenDirect def test_import_stimulator(self): - assert ProbeTimeseriesStimulator is not None + assert ndi_probe_timeseries_stimulator is not None def test_import_downsample_functions(self): assert downsample is not None assert downsample_timeseries is not None def test_metadatareader_still_importable_from_daq(self): - from ndi.daq import MetadataReader as MR + from ndi.daq import ndi_daq_metadatareader as MR - assert MR is MetadataReader + assert MR is ndi_daq_metadatareader def test_readers_importable_from_daq(self): - from ndi.daq import NewStimStimsReader as NS - from ndi.daq import NielsenLabStimsReader as NL + from ndi.daq import ndi_daq_metadatareader_NewStimStims as NS + from ndi.daq import ndi_daq_metadatareader_NielsenLabStims as NL - assert NS is NewStimStimsReader - assert NL is NielsenLabStimsReader + assert NS is ndi_daq_metadatareader_NewStimStims + assert NL is ndi_daq_metadatareader_NielsenLabStims # =========================================================================== -# NewStimStimsReader +# ndi_daq_metadatareader_NewStimStims # =========================================================================== @@ -60,46 +68,46 @@ class TestNewStimStimsReader: """Tests for the NewStim metadata reader.""" def test_init(self): - reader = NewStimStimsReader() - assert isinstance(reader, MetadataReader) + reader = ndi_daq_metadatareader_NewStimStims() + assert isinstance(reader, ndi_daq_metadatareader) assert reader.tab_separated_file_parameter == "" def test_init_with_tsv_pattern(self): - reader = NewStimStimsReader(tsv_pattern=r"stim\.tsv") + reader = ndi_daq_metadatareader_NewStimStims(tsv_pattern=r"stim\.tsv") assert reader.tab_separated_file_parameter == r"stim\.tsv" def test_stim_file_pattern(self): - assert "stims" in NewStimStimsReader.STIM_FILE_PATTERN + assert "stims" in ndi_daq_metadatareader_NewStimStims.STIM_FILE_PATTERN def test_find_stim_file_found(self): - reader = NewStimStimsReader() + reader = ndi_daq_metadatareader_NewStimStims() result = reader._find_stim_file(["data.rhd", "events.nev", "stims.mat"]) assert result == "stims.mat" def test_find_stim_file_not_found(self): - reader = NewStimStimsReader() + reader = ndi_daq_metadatareader_NewStimStims() result = reader._find_stim_file(["data.rhd", "events.nev"]) assert result is None def test_find_stim_file_case_insensitive(self): - reader = NewStimStimsReader() + reader = ndi_daq_metadatareader_NewStimStims() result = reader._find_stim_file(["data.rhd", "Stims.MAT"]) assert result == "Stims.MAT" def test_readmetadata_no_stim_file(self): - reader = NewStimStimsReader() + reader = ndi_daq_metadatareader_NewStimStims() result = reader.readmetadata(["data.rhd", "events.nev"]) assert result == [] def test_readmetadata_falls_back_to_tsv(self): """With a TSV pattern set, tries TSV first.""" - reader = NewStimStimsReader(tsv_pattern=r"nonexistent\.tsv") + reader = ndi_daq_metadatareader_NewStimStims(tsv_pattern=r"nonexistent\.tsv") # No matching TSV and no stims.mat → empty result = reader.readmetadata(["data.rhd"]) assert result == [] def test_read_newstim_mat_nonexistent(self): - reader = NewStimStimsReader() + reader = ndi_daq_metadatareader_NewStimStims() result = reader._read_newstim_mat("/nonexistent/stims.mat") # scipy may or may not be installed # If scipy not installed, raises ImportError @@ -107,41 +115,41 @@ def test_read_newstim_mat_nonexistent(self): assert isinstance(result, list) def test_extract_script_parameters_empty(self): - result = NewStimStimsReader._extract_script_parameters(np.array([])) + result = ndi_daq_metadatareader_NewStimStims._extract_script_parameters(np.array([])) assert isinstance(result, list) def test_repr(self): - reader = NewStimStimsReader() - assert "NewStimStimsReader" in repr(reader) + reader = ndi_daq_metadatareader_NewStimStims() + assert "ndi_daq_metadatareader_NewStimStims" in repr(reader) # =========================================================================== -# NielsenLabStimsReader +# ndi_daq_metadatareader_NielsenLabStims # =========================================================================== class TestNielsenLabStimsReader: - """Tests for the Nielsen Lab metadata reader.""" + """Tests for the Nielsen ndi_gui_Lab metadata reader.""" def test_init(self): - reader = NielsenLabStimsReader() - assert isinstance(reader, MetadataReader) + reader = ndi_daq_metadatareader_NielsenLabStims() + assert isinstance(reader, ndi_daq_metadatareader) def test_analyzer_file_pattern(self): - assert "analyzer" in NielsenLabStimsReader.ANALYZER_FILE_PATTERN + assert "analyzer" in ndi_daq_metadatareader_NielsenLabStims.ANALYZER_FILE_PATTERN def test_find_analyzer_file_found(self): - reader = NielsenLabStimsReader() + reader = ndi_daq_metadatareader_NielsenLabStims() result = reader._find_analyzer_file(["data.rhd", "analyzer.mat"]) assert result == "analyzer.mat" def test_find_analyzer_file_not_found(self): - reader = NielsenLabStimsReader() + reader = ndi_daq_metadatareader_NielsenLabStims() result = reader._find_analyzer_file(["data.rhd"]) assert result is None def test_readmetadata_no_analyzer(self): - reader = NielsenLabStimsReader() + reader = ndi_daq_metadatareader_NielsenLabStims() result = reader.readmetadata(["data.rhd"]) assert result == [] @@ -149,34 +157,34 @@ def test_extract_stimulus_parameters_empty(self): # Create a minimal structured array with correct dtype dt = np.dtype([("M", "O"), ("P", "O"), ("loops", "O")]) analyzer = np.zeros((), dtype=dt) - result = NielsenLabStimsReader.extract_stimulus_parameters(analyzer) + result = ndi_daq_metadatareader_NielsenLabStims.extract_stimulus_parameters(analyzer) assert isinstance(result, list) def test_extract_display_order_empty(self): dt = np.dtype([("loops", "O")]) analyzer = np.zeros((), dtype=dt) - result = NielsenLabStimsReader.extract_display_order(analyzer) + result = ndi_daq_metadatareader_NielsenLabStims.extract_display_order(analyzer) assert isinstance(result, list) def test_repr(self): - reader = NielsenLabStimsReader() - assert "NielsenLabStimsReader" in repr(reader) + reader = ndi_daq_metadatareader_NielsenLabStims() + assert "ndi_daq_metadatareader_NielsenLabStims" in repr(reader) # =========================================================================== -# MetadataReader (regression - ensure package refactoring didn't break) +# ndi_daq_metadatareader (regression - ensure package refactoring didn't break) # =========================================================================== class TestMetadataReaderRegression: - """Verify MetadataReader still works after file→package refactoring.""" + """Verify ndi_daq_metadatareader still works after file→package refactoring.""" def test_base_class_works(self): - reader = MetadataReader() + reader = ndi_daq_metadatareader() assert reader.tab_separated_file_parameter == "" def test_base_readmetadata_empty_pattern(self): - reader = MetadataReader() + reader = ndi_daq_metadatareader() result = reader.readmetadata(["file.txt"]) assert result == [] @@ -189,7 +197,7 @@ def test_base_readmetadata_from_tsv(self): filepath = f.name try: - reader = MetadataReader(tsv_pattern=os.path.basename(filepath)) + reader = ndi_daq_metadatareader(tsv_pattern=os.path.basename(filepath)) result = reader.readmetadata([filepath]) assert len(result) == 2 assert result[0]["STIMID"] == 1 @@ -199,24 +207,24 @@ def test_base_readmetadata_from_tsv(self): os.unlink(filepath) def test_equality(self): - r1 = MetadataReader(tsv_pattern="test") - r2 = MetadataReader(tsv_pattern="test") + r1 = ndi_daq_metadatareader(tsv_pattern="test") + r2 = ndi_daq_metadatareader(tsv_pattern="test") assert r1 == r2 def test_inequality(self): - r1 = MetadataReader(tsv_pattern="test") - r2 = MetadataReader(tsv_pattern="other") + r1 = ndi_daq_metadatareader(tsv_pattern="test") + r2 = ndi_daq_metadatareader(tsv_pattern="other") assert r1 != r2 def test_newdocument_class_name(self): """Verify the reader knows its own class for serialization.""" - reader = MetadataReader(tsv_pattern="stim.tsv") - assert reader.__class__.__name__ == "MetadataReader" + reader = ndi_daq_metadatareader(tsv_pattern="stim.tsv") + assert reader.__class__.__name__ == "ndi_daq_metadatareader" assert reader.tab_separated_file_parameter == "stim.tsv" # =========================================================================== -# ProbeTimeseriesStimulator +# ndi_probe_timeseries_stimulator # =========================================================================== @@ -224,7 +232,7 @@ class TestProbeTimeseriesStimulator: """Tests for the stimulus delivery probe.""" def test_init(self): - stim = ProbeTimeseriesStimulator() + stim = ndi_probe_timeseries_stimulator() assert stim._type == "stimulator" def test_init_with_args(self): @@ -232,7 +240,7 @@ def test_init_with_args(self): id=lambda: "s1", database_search=lambda q: [], ) - stim = ProbeTimeseriesStimulator( + stim = ndi_probe_timeseries_stimulator( session=session, name="vis_stim", reference=2, @@ -241,12 +249,12 @@ def test_init_with_args(self): assert stim._reference == 2 def test_inherits_probe_timeseries(self): - from ndi.probe.timeseries import ProbeTimeseries + from ndi.probe.timeseries import ndi_probe_timeseries - assert issubclass(ProbeTimeseriesStimulator, ProbeTimeseries) + assert issubclass(ndi_probe_timeseries_stimulator, ndi_probe_timeseries) def test_readtimeseriesepoch_no_session(self): - stim = ProbeTimeseriesStimulator() + stim = ndi_probe_timeseries_stimulator() data, times, timeref = stim.readtimeseriesepoch(1, 0, 10) assert data is None assert times is None @@ -257,7 +265,7 @@ def test_readtimeseriesepoch_with_session(self): id=lambda: "s1", database_search=lambda q: [], ) - stim = ProbeTimeseriesStimulator(session=session, name="test") + stim = ndi_probe_timeseries_stimulator(session=session, name="test") data, times, timeref = stim.readtimeseriesepoch(1, 0, 10) assert isinstance(data, dict) assert "stimid" in data @@ -270,7 +278,7 @@ def test_readtimeseriesepoch_with_session(self): assert "stimevents" in times def test_parse_marker_data_empty(self): - stim = ProbeTimeseriesStimulator() + stim = ndi_probe_timeseries_stimulator() result = stim.parse_marker_data(np.array([]), np.array([])) assert len(result["stimid"]) == 0 assert len(result["stimon"]) == 0 @@ -278,7 +286,7 @@ def test_parse_marker_data_empty(self): assert result["stimopenclose"].shape == (0, 2) def test_parse_marker_data_onset_offset(self): - stim = ProbeTimeseriesStimulator() + stim = ndi_probe_timeseries_stimulator() # 3 marker channels: on/off, stim_id, setup/clear timestamps = np.array( [ @@ -304,19 +312,19 @@ def test_parse_marker_data_onset_offset(self): assert result["stimopenclose"][0, 1] == 3.0 def test_get_epoch_timeref_no_session(self): - stim = ProbeTimeseriesStimulator() + stim = ndi_probe_timeseries_stimulator() assert stim._get_epoch_timeref(1) is None def test_get_epoch_timeref_with_session(self): - from ndi.time import ClockType + from ndi.time import ndi_time_clocktype session = SimpleNamespace(id=lambda: "s1") - stim = ProbeTimeseriesStimulator(session=session) - assert stim._get_epoch_timeref(1) == ClockType.DEV_LOCAL_TIME + stim = ndi_probe_timeseries_stimulator(session=session) + assert stim._get_epoch_timeref(1) == ndi_time_clocktype.DEV_LOCAL_TIME def test_repr(self): - stim = ProbeTimeseriesStimulator(name="vis") - assert "ProbeTimeseriesStimulator" in repr(stim) + stim = ndi_probe_timeseries_stimulator(name="vis") + assert "ndi_probe_timeseries_stimulator" in repr(stim) assert "vis" in repr(stim) @@ -344,7 +352,7 @@ def test_downsample_negative_freq_raises(self): downsample(session, element_in, -10.0, "ds_out", 1) def test_downsample_returns_element(self): - from ndi.element import Element + from ndi.element import ndi_element session = SimpleNamespace(id=lambda: "s1") element_in = SimpleNamespace( @@ -352,7 +360,7 @@ def test_downsample_returns_element(self): _type="timeseries", ) result = downsample(session, element_in, 100.0, "ds_out", 1) - assert isinstance(result, Element) + assert isinstance(result, ndi_element) # =========================================================================== diff --git a/tests/test_batch_e.py b/tests/test_batch_e.py index 1879c1b..9a04e70 100644 --- a/tests/test_batch_e.py +++ b/tests/test_batch_e.py @@ -1,7 +1,7 @@ """ Tests for Batch E: Minor remaining MATLAB gaps. -Tests findepochnode(), SessionTable, TuningFit. +Tests findepochnode(), ndi_session_sessiontable, ndi_calc_tuning__fit. """ from pathlib import Path @@ -9,13 +9,13 @@ import numpy as np import pytest -from ndi.calc.tuning_fit import TuningFit +from ndi.calc.tuning_fit import ndi_calc_tuning__fit # --------------------------------------------------------------------------- # Imports # --------------------------------------------------------------------------- from ndi.epoch.functions import findepochnode -from ndi.session.sessiontable import SessionTable +from ndi.session.sessiontable import ndi_session_sessiontable class TestImports: @@ -27,14 +27,14 @@ def test_import_findepochnode_from_epoch(self): assert fen is findepochnode def test_import_session_table_from_session(self): - from ndi.session import SessionTable as ST + from ndi.session import ndi_session_sessiontable as ST - assert ST is SessionTable + assert ST is ndi_session_sessiontable def test_import_tuning_fit_from_calc(self): - from ndi.calc import TuningFit as TF + from ndi.calc import ndi_calc_tuning__fit as TF - assert TF is TuningFit + assert TF is ndi_calc_tuning__fit # =========================================================================== @@ -51,7 +51,7 @@ def sample_nodes(self): return [ { "objectname": "probe1", - "objectclass": "Probe", + "objectclass": "ndi_probe", "epoch_id": "e1", "epoch_session_id": "s1", "epoch_clock": "dev_local_time", @@ -59,7 +59,7 @@ def sample_nodes(self): }, { "objectname": "probe2", - "objectclass": "Probe", + "objectclass": "ndi_probe", "epoch_id": "e2", "epoch_session_id": "s1", "epoch_clock": "dev_local_time", @@ -67,7 +67,7 @@ def sample_nodes(self): }, { "objectname": "probe1", - "objectclass": "Element", + "objectclass": "ndi_element", "epoch_id": "e3", "epoch_session_id": "s2", "epoch_clock": "utc", @@ -89,7 +89,7 @@ def test_search_by_objectname(self, sample_nodes): assert result == [0, 2] def test_search_by_objectclass(self, sample_nodes): - result = findepochnode({"objectclass": "Probe"}, sample_nodes) + result = findepochnode({"objectclass": "ndi_probe"}, sample_nodes) assert result == [0, 1] def test_search_by_session_id(self, sample_nodes): @@ -98,7 +98,7 @@ def test_search_by_session_id(self, sample_nodes): def test_search_by_multiple_fields(self, sample_nodes): result = findepochnode( - {"objectname": "probe1", "objectclass": "Probe"}, + {"objectname": "probe1", "objectclass": "ndi_probe"}, sample_nodes, ) assert result == [0] @@ -149,7 +149,7 @@ def test_empty_string_treated_as_wildcard(self, sample_nodes): # =========================================================================== -# SessionTable +# ndi_session_sessiontable # =========================================================================== @@ -164,16 +164,16 @@ def table_dir(self, tmp_path): @pytest.fixture def table(self, table_dir): - """Create a SessionTable instance using a temp file.""" - return SessionTable(table_path=table_dir) + """Create a ndi_session_sessiontable instance using a temp file.""" + return ndi_session_sessiontable(table_path=table_dir) def test_init_default_path(self): - table = SessionTable() + table = ndi_session_sessiontable() expected = Path.home() / ".ndi" / "preferences" / "local_sessiontable.txt" assert table._table_path == expected def test_init_custom_path(self, table_dir): - table = SessionTable(table_path=table_dir) + table = ndi_session_sessiontable(table_path=table_dir) assert table._table_path == table_dir def test_empty_table_on_new(self, table): @@ -299,37 +299,37 @@ def test_add_empty_path_raises(self, table): table.addtableentry("sess1", "") def test_persistence_across_instances(self, table_dir): - t1 = SessionTable(table_path=table_dir) + t1 = ndi_session_sessiontable(table_path=table_dir) t1.addtableentry("sess1", "/data/exp1") - t2 = SessionTable(table_path=table_dir) + t2 = ndi_session_sessiontable(table_path=table_dir) assert t2.getsessionpath("sess1") == "/data/exp1" def test_repr(self, table): - assert "SessionTable" in repr(table) + assert "ndi_session_sessiontable" in repr(table) def test_localtablefilename(self): - path = SessionTable.localtablefilename() + path = ndi_session_sessiontable.localtablefilename() assert "local_sessiontable" in path.name assert path.suffix == ".txt" # =========================================================================== -# TuningFit +# ndi_calc_tuning__fit # =========================================================================== -class ConcreteTuningFit(TuningFit): - """Concrete subclass for testing the abstract TuningFit.""" +class ConcreteTuningFit(ndi_calc_tuning__fit): + """Concrete subclass for testing the abstract ndi_calc_tuning__fit.""" def calculate(self, parameters): - from ndi.document import Document + from ndi.document import ndi_document - doc = Document("base") + doc = ndi_document("base") return [doc] def default_search_for_input_parameters(self): - from ndi.query import Query + from ndi.query import ndi_query return { "input_parameters": { @@ -337,7 +337,7 @@ def default_search_for_input_parameters(self): }, "depends_on": [], "query": [ - {"name": "document_id", "query": Query("").isa("stimulus_response_scalar")}, + {"name": "document_id", "query": ndi_query("").isa("stimulus_response_scalar")}, ], } @@ -353,28 +353,28 @@ def generate_mock_parameters(self, scope, index): class TestTuningFit: - """Tests for the TuningFit abstract base class.""" + """Tests for the ndi_calc_tuning__fit abstract base class.""" def test_is_subclass_of_calculator(self): - from ndi.calculator import Calculator + from ndi.calculator import ndi_calculator - assert issubclass(TuningFit, Calculator) + assert issubclass(ndi_calc_tuning__fit, ndi_calculator) def test_cannot_instantiate_directly(self): with pytest.raises(TypeError): - TuningFit() + ndi_calc_tuning__fit() def test_concrete_subclass_instantiates(self): fit = ConcreteTuningFit() - assert isinstance(fit, TuningFit) + assert isinstance(fit, ndi_calc_tuning__fit) def test_scope_presets(self): - assert "highSNR" in TuningFit.SCOPE_PRESETS - assert "lowSNR" in TuningFit.SCOPE_PRESETS - assert TuningFit.SCOPE_PRESETS["highSNR"]["reps"] == 5 - assert TuningFit.SCOPE_PRESETS["highSNR"]["noise"] == 0.001 - assert TuningFit.SCOPE_PRESETS["lowSNR"]["reps"] == 10 - assert TuningFit.SCOPE_PRESETS["lowSNR"]["noise"] == 1.0 + assert "highSNR" in ndi_calc_tuning__fit.SCOPE_PRESETS + assert "lowSNR" in ndi_calc_tuning__fit.SCOPE_PRESETS + assert ndi_calc_tuning__fit.SCOPE_PRESETS["highSNR"]["reps"] == 5 + assert ndi_calc_tuning__fit.SCOPE_PRESETS["highSNR"]["noise"] == 0.001 + assert ndi_calc_tuning__fit.SCOPE_PRESETS["lowSNR"]["reps"] == 10 + assert ndi_calc_tuning__fit.SCOPE_PRESETS["lowSNR"]["noise"] == 1.0 def test_generate_mock_parameters(self): fit = ConcreteTuningFit() @@ -440,4 +440,4 @@ def test_generate_mock_docs_expected_output(self): def test_repr(self): fit = ConcreteTuningFit() - assert "TuningFit" in repr(fit) + assert "ndi_calc_tuning__fit" in repr(fit) diff --git a/tests/test_cloud_download_live.py b/tests/test_cloud_download_live.py index f543923..7d5815b 100644 --- a/tests/test_cloud_download_live.py +++ b/tests/test_cloud_download_live.py @@ -101,7 +101,7 @@ def run_test(username: str, password: str) -> dict: # ================================================================= # Step 2: Download dataset (docs only, ndic:// rewrite) # ================================================================= - section("Step 2: Download Dataset (docs only, ndic:// URIs)") + section("Step 2: Download ndi_dataset (docs only, ndic:// URIs)") from ndi.cloud.orchestration import downloadDataset @@ -129,9 +129,9 @@ def run_test(username: str, password: str) -> dict: section("Step 3: Verify ndic:// URI Rewriting") from ndi.cloud.filehandler import NDIC_SCHEME - from ndi.query import Query + from ndi.query import ndi_query - all_docs = dataset.database_search(Query("").isa("base")) + all_docs = dataset.database_search(ndi_query("").isa("base")) ndic_count = 0 file_docs = [] for doc in all_docs: @@ -158,11 +158,11 @@ def run_test(username: str, password: str) -> dict: # ================================================================= # Step 4: NDI-python Analysis — Queries # ================================================================= - section("Step 4: NDI-python Analysis — Document Queries") + section("Step 4: NDI-python Analysis — ndi_document Queries") doc_count = len(all_docs) check( - "Document count", + "ndi_document count", doc_count >= EXPECTED_DOCS, f"{doc_count} docs (expected >= {EXPECTED_DOCS})", ) @@ -173,7 +173,7 @@ def run_test(username: str, password: str) -> dict: cname = doc.document_properties.get("document_class", {}).get("class_name", "unknown") type_counts[cname] += 1 - check("Document types", len(type_counts) >= 27, f"{len(type_counts)} types found") + check("ndi_document types", len(type_counts) >= 27, f"{len(type_counts)} types found") # Test isa queries for each major type for doc_type, expected in [ @@ -183,7 +183,7 @@ def run_test(username: str, password: str) -> dict: ("tuningcurve_calc", EXPECTED_TUNING_CURVES), ("element_epoch", EXPECTED_ELEMENT_EPOCHS), ]: - found = dataset.database_search(Query("").isa(doc_type)) + found = dataset.database_search(ndi_query("").isa(doc_type)) check( f" isa('{doc_type}')", len(found) == expected, @@ -197,10 +197,10 @@ def run_test(username: str, password: str) -> dict: # ================================================================= section("Step 5: NDI-python Analysis — Elements & Structure") - elements = dataset.database_search(Query("").isa("element")) + elements = dataset.database_search(ndi_query("").isa("element")) element_types = {d.document_properties.get("element", {}).get("type", "") for d in elements} check( - "Element types", + "ndi_element types", element_types == {"n-trode", "spikes", "stimulator"}, f"{element_types}", ) @@ -216,7 +216,7 @@ def run_test(username: str, password: str) -> dict: check("Spike elements", len(spikes) == 17, "one per neuron") # Check element -> subject dependency chain - subjects = dataset.database_search(Query("").isa("subject")) + subjects = dataset.database_search(ndi_query("").isa("subject")) subject_id = ( subjects[0].document_properties.get("base", {}).get("id", "") if subjects else "" ) @@ -229,7 +229,7 @@ def run_test(username: str, password: str) -> dict: if not subj_deps or subj_deps[0].get("value") != subject_id: deps_ok = False break - check("Element -> Subject deps", deps_ok) + check("ndi_element -> ndi_subject deps", deps_ok) results["analysis_elements"] = deps_ok # ================================================================= @@ -237,8 +237,8 @@ def run_test(username: str, password: str) -> dict: # ================================================================= section("Step 6: NDI-python Analysis — Neurons") - neurons = dataset.database_search(Query("").isa("neuron_extracellular")) - check("Neuron count", len(neurons) == EXPECTED_NEURONS) + neurons = dataset.database_search(ndi_query("").isa("neuron_extracellular")) + check("ndi_neuron count", len(neurons) == EXPECTED_NEURONS) waveform_ok = True for n in neurons: @@ -249,7 +249,7 @@ def run_test(username: str, password: str) -> dict: waveform_ok = False if not ne.get("mean_waveform"): waveform_ok = False - check("Neuron waveform shape (21x16)", waveform_ok) + check("ndi_neuron waveform shape (21x16)", waveform_ok) # Check neuron -> element chain element_ids = {d.document_properties.get("base", {}).get("id", "") for d in elements} @@ -262,7 +262,7 @@ def run_test(username: str, password: str) -> dict: if not elem_dep or elem_dep[0].get("value") not in element_ids: chain_ok = False break - check("Neuron -> Element chain", chain_ok) + check("ndi_neuron -> ndi_element chain", chain_ok) app_ok = all(n.document_properties.get("app", {}).get("name") == "JRCLUST" for n in neurons) check("All neurons sorted by JRCLUST", app_ok) @@ -273,7 +273,7 @@ def run_test(username: str, password: str) -> dict: # ================================================================= section("Step 7: NDI-python Analysis — Tuning Curves") - tcs = dataset.database_search(Query("").isa("tuningcurve_calc")) + tcs = dataset.database_search(ndi_query("").isa("tuningcurve_calc")) check("Tuning curve count", len(tcs) == EXPECTED_TUNING_CURVES) label_counts: Counter[tuple] = Counter() @@ -313,7 +313,7 @@ def run_test(username: str, password: str) -> dict: # ================================================================= # Step 8: Cross-document referential integrity # ================================================================= - section("Step 8: Cross-Document Referential Integrity") + section("Step 8: Cross-ndi_document Referential Integrity") all_ids = {d.document_properties.get("base", {}).get("id", "") for d in all_docs} missing_refs = 0 @@ -368,7 +368,7 @@ def run_test(username: str, password: str) -> dict: from ndi.cloud.filehandler import parse_ndic_uri ds_id, file_uid = parse_ndic_uri(test_uri) - print(f" Dataset ID: {ds_id}") + print(f" ndi_dataset ID: {ds_id}") print(f" File UID: {file_uid}") try: @@ -516,7 +516,7 @@ def main(): sys.exit(1) print("NDI Cloud Live Integration Test") - print(f"Dataset: Carbon fiber microelectrode ({CARBON_FIBER_ID})") + print(f"ndi_dataset: Carbon fiber microelectrode ({CARBON_FIBER_ID})") print(f"User: {username}") results = run_test(username, password) diff --git a/tests/test_cloud_filehandler.py b/tests/test_cloud_filehandler.py index a0e8db0..bc33f02 100644 --- a/tests/test_cloud_filehandler.py +++ b/tests/test_cloud_filehandler.py @@ -312,20 +312,20 @@ def test_env_vars_present(self): class TestTryCloudFetch: - """Tests for Session._try_cloud_fetch with mocked cloud calls.""" + """Tests for ndi_session._try_cloud_fetch with mocked cloud calls.""" def _make_session_with_doc(self, tmp_path, file_info): - """Create a DirSession with a document containing given file_info.""" - from ndi.session.dir import DirSession + """Create a ndi_session_dir with a document containing given file_info.""" + from ndi.session.dir import ndi_session_dir session_dir = tmp_path / "session" session_dir.mkdir() - session = DirSession("test_session", session_dir) + session = ndi_session_dir("test_session", session_dir) # Create and add a document with file_info - from ndi.document import Document + from ndi.document import ndi_document - doc = Document("base") + doc = ndi_document("base") doc = doc.set_session_id(session.id()) # Inject file_info into the document properties @@ -482,9 +482,9 @@ def test_load_dataset_with_cloud_id(self, tmp_path): ) # Find documents with file_info and verify ndic:// URIs - from ndi.query import Query + from ndi.query import ndi_query - all_docs = dataset.database_search(Query("").isa("base")) + all_docs = dataset.database_search(ndi_query("").isa("base")) ndic_count = 0 for doc in all_docs: props = doc.document_properties diff --git a/tests/test_cloud_live.py b/tests/test_cloud_live.py index 6355375..40ddd69 100644 --- a/tests/test_cloud_live.py +++ b/tests/test_cloud_live.py @@ -977,7 +977,7 @@ def test_deferred_delete_and_undelete(self, client, cloud_config, can_write): time.sleep(2) deleted = listDeletedDatasets(client=client) deleted_ids = {d.get("_id", d.get("id", "")) for d in deleted.get("datasets", [])} - assert ds_id in deleted_ids, f"Dataset {ds_id} not found in deleted list" + assert ds_id in deleted_ids, f"ndi_dataset {ds_id} not found in deleted list" # Undelete undelete_result = undeleteDataset(ds_id, client=client) @@ -1192,7 +1192,7 @@ def test_large_has_more_docs(self, client): small_count = countDocuments(SMALL_DATASET, client=client) # On dev, either dataset may have 0 docs — comparison is meaningless if env == "dev" and (large_count == 0 or small_count == 0): - pytest.skip("Dataset(s) have no documents on dev environment") + pytest.skip("ndi_dataset(s) have no documents on dev environment") assert large_count > small_count def test_both_published(self, large_dataset_info, small_dataset_info): diff --git a/tests/test_daq.py b/tests/test_daq.py index df0a7ed..87f48f1 100644 --- a/tests/test_daq.py +++ b/tests/test_daq.py @@ -2,11 +2,11 @@ Tests for ndi.daq module (Phase 5). Tests cover: -- DAQReader abstract base class -- MFDAQReader multi-function DAQ reader -- DAQSystem complete system -- MetadataReader metadata reading -- FileNavigator file navigation +- ndi_daq_reader abstract base class +- ndi_daq_reader_mfdaq multi-function DAQ reader +- ndi_daq_system complete system +- ndi_daq_metadatareader metadata reading +- ndi_file_navigator file navigation """ import os @@ -17,31 +17,31 @@ import numpy as np import pytest -from ndi.daq.metadatareader import MetadataReader +from ndi.daq.metadatareader import ndi_daq_metadatareader from ndi.daq.mfdaq import ( ChannelInfo, ChannelType, - MFDAQReader, + ndi_daq_reader_mfdaq, standardize_channel_type, standardize_channel_types, ) -from ndi.daq.reader_base import DAQReader -from ndi.daq.system import DAQSystem -from ndi.file.navigator import FileNavigator, find_file_groups +from ndi.daq.reader_base import ndi_daq_reader +from ndi.daq.system import ndi_daq_system +from ndi.file.navigator import find_file_groups, ndi_file_navigator from ndi.time import DEV_LOCAL_TIME, NO_TIME # ============================================================================= -# DAQReader Tests +# ndi_daq_reader Tests # ============================================================================= class TestDAQReader: - """Tests for DAQReader base class.""" + """Tests for ndi_daq_reader base class.""" def test_daqreader_creation(self): - """Test creating a DAQReader.""" + """Test creating a ndi_daq_reader.""" - class SimpleReader(DAQReader): + class SimpleReader(ndi_daq_reader): pass reader = SimpleReader() @@ -49,9 +49,9 @@ class SimpleReader(DAQReader): assert len(reader.id) > 0 def test_daqreader_with_identifier(self): - """Test creating DAQReader with custom identifier.""" + """Test creating ndi_daq_reader with custom identifier.""" - class SimpleReader(DAQReader): + class SimpleReader(ndi_daq_reader): pass reader = SimpleReader(identifier="test_reader_123") @@ -60,7 +60,7 @@ class SimpleReader(DAQReader): def test_daqreader_epochclock(self): """Test default epochclock returns NO_TIME.""" - class SimpleReader(DAQReader): + class SimpleReader(ndi_daq_reader): pass reader = SimpleReader() @@ -71,7 +71,7 @@ class SimpleReader(DAQReader): def test_daqreader_t0_t1(self): """Test default t0_t1 returns NaN.""" - class SimpleReader(DAQReader): + class SimpleReader(ndi_daq_reader): pass reader = SimpleReader() @@ -83,7 +83,7 @@ class SimpleReader(DAQReader): def test_daqreader_verifyepochprobemap(self): """Test verifyepochprobemap accepts anything by default.""" - class SimpleReader(DAQReader): + class SimpleReader(ndi_daq_reader): pass reader = SimpleReader() @@ -92,9 +92,9 @@ class SimpleReader(DAQReader): assert msg == "" def test_daqreader_equality(self): - """Test DAQReader equality by ID.""" + """Test ndi_daq_reader equality by ID.""" - class SimpleReader(DAQReader): + class SimpleReader(ndi_daq_reader): pass reader1 = SimpleReader(identifier="abc") @@ -159,11 +159,11 @@ def test_standardize_list(self): # ============================================================================= -# MFDAQReader Tests +# ndi_daq_reader_mfdaq Tests # ============================================================================= -class ConcreteMFDAQReader(MFDAQReader): +class ConcreteMFDAQReader(ndi_daq_reader_mfdaq): """Concrete implementation for testing.""" def __init__(self, channels=None, **kwargs): @@ -192,10 +192,10 @@ def t0_t1(self, epochfiles): class TestMFDAQReader: - """Tests for MFDAQReader class.""" + """Tests for ndi_daq_reader_mfdaq class.""" def test_mfdaq_creation(self): - """Test creating MFDAQReader.""" + """Test creating ndi_daq_reader_mfdaq.""" channels = [ ChannelInfo(name="ai1", type="analog_in", number=1, sample_rate=30000), ChannelInfo(name="ai2", type="analog_in", number=2, sample_rate=30000), @@ -204,7 +204,7 @@ def test_mfdaq_creation(self): assert reader.id is not None def test_mfdaq_epochclock(self): - """Test MFDAQReader returns DEV_LOCAL_TIME.""" + """Test ndi_daq_reader_mfdaq returns DEV_LOCAL_TIME.""" reader = ConcreteMFDAQReader() clocks = reader.epochclock(["test.dat"]) assert len(clocks) == 1 @@ -254,7 +254,7 @@ def test_mfdaq_epochtimes2samples(self): def test_mfdaq_channel_types(self): """Test channel_types static method.""" - types, abbrevs = MFDAQReader.channel_types() + types, abbrevs = ndi_daq_reader_mfdaq.channel_types() assert "analog_in" in types assert "ai" in abbrevs assert len(types) == len(abbrevs) @@ -269,81 +269,81 @@ def test_mfdaq_underlying_datatype(self): # ============================================================================= -# DAQSystem Tests +# ndi_daq_system Tests # ============================================================================= class TestDAQSystem: - """Tests for DAQSystem class.""" + """Tests for ndi_daq_system class.""" def test_daqsystem_creation(self): - """Test creating a DAQSystem.""" - sys = DAQSystem(name="test_daq") + """Test creating a ndi_daq_system.""" + sys = ndi_daq_system(name="test_daq") assert sys.name == "test_daq" assert sys.id is not None def test_daqsystem_with_components(self): - """Test creating DAQSystem with components.""" + """Test creating ndi_daq_system with components.""" reader = ConcreteMFDAQReader() - sys = DAQSystem(name="test_daq", daqreader=reader) + sys = ndi_daq_system(name="test_daq", daqreader=reader) assert sys.daqreader is reader def test_daqsystem_invalid_reader(self): - """Test DAQSystem rejects invalid reader.""" + """Test ndi_daq_system rejects invalid reader.""" with pytest.raises(TypeError): - DAQSystem(name="test", daqreader="not a reader") + ndi_daq_system(name="test", daqreader="not a reader") def test_daqsystem_epochclock(self): - """Test DAQSystem epochclock returns NO_TIME by default.""" - sys = DAQSystem() + """Test ndi_daq_system epochclock returns NO_TIME by default.""" + sys = ndi_daq_system() clocks = sys.epochclock(1) assert len(clocks) == 1 assert clocks[0] == NO_TIME def test_daqsystem_set_metadatareaders(self): """Test setting metadata readers.""" - sys = DAQSystem() - readers = [MetadataReader(), MetadataReader()] + sys = ndi_daq_system() + readers = [ndi_daq_metadatareader(), ndi_daq_metadatareader()] sys.set_daqmetadatareaders(readers) assert len(sys.daqmetadatareaders) == 2 def test_daqsystem_set_invalid_metadatareaders(self): """Test setting invalid metadata readers.""" - sys = DAQSystem() + sys = ndi_daq_system() with pytest.raises(TypeError): sys.set_daqmetadatareaders(["not a reader"]) def test_daqsystem_equality(self): - """Test DAQSystem equality.""" - sys1 = DAQSystem(name="test") - sys2 = DAQSystem(name="test") - sys3 = DAQSystem(name="other") + """Test ndi_daq_system equality.""" + sys1 = ndi_daq_system(name="test") + sys2 = ndi_daq_system(name="test") + sys3 = ndi_daq_system(name="other") assert sys1 == sys2 assert sys1 != sys3 # ============================================================================= -# MetadataReader Tests +# ndi_daq_metadatareader Tests # ============================================================================= class TestMetadataReader: - """Tests for MetadataReader class.""" + """Tests for ndi_daq_metadatareader class.""" def test_metadatareader_creation(self): - """Test creating a MetadataReader.""" - reader = MetadataReader() + """Test creating a ndi_daq_metadatareader.""" + reader = ndi_daq_metadatareader() assert reader.id is not None def test_metadatareader_with_pattern(self): - """Test creating MetadataReader with pattern.""" - reader = MetadataReader(tsv_pattern=r"stim.*\.txt") + """Test creating ndi_daq_metadatareader with pattern.""" + reader = ndi_daq_metadatareader(tsv_pattern=r"stim.*\.txt") assert reader.tab_separated_file_parameter == r"stim.*\.txt" def test_metadatareader_readmetadata_no_pattern(self): """Test readmetadata with no pattern returns empty.""" - reader = MetadataReader() + reader = ndi_daq_metadatareader() result = reader.readmetadata(["file1.dat", "file2.dat"]) assert result == [] @@ -356,7 +356,7 @@ def test_metadatareader_readmetadatafromfile(self): filepath = f.name try: - reader = MetadataReader() + reader = ndi_daq_metadatareader() params = reader.readmetadatafromfile(filepath) assert len(params) == 2 assert params[0]["stimid"] == 1 @@ -376,27 +376,27 @@ def test_metadatareader_readmetadata_with_pattern(self): data_file = Path(tmpdir) / "data.bin" data_file.write_bytes(b"\x00" * 100) - reader = MetadataReader(tsv_pattern=r"stim.*\.txt") + reader = ndi_daq_metadatareader(tsv_pattern=r"stim.*\.txt") params = reader.readmetadata([str(data_file), str(stim_file)]) assert len(params) == 2 assert params[0]["stimid"] == 1 def test_metadatareader_equality(self): - """Test MetadataReader equality.""" - r1 = MetadataReader(tsv_pattern=".*\\.txt") - r2 = MetadataReader(tsv_pattern=".*\\.txt") - r3 = MetadataReader(tsv_pattern=".*\\.csv") + """Test ndi_daq_metadatareader equality.""" + r1 = ndi_daq_metadatareader(tsv_pattern=".*\\.txt") + r2 = ndi_daq_metadatareader(tsv_pattern=".*\\.txt") + r3 = ndi_daq_metadatareader(tsv_pattern=".*\\.csv") assert r1 == r2 assert r1 != r3 # ============================================================================= -# FileNavigator Tests +# ndi_file_navigator Tests # ============================================================================= -class MockSession: +class ndi_session_mock: """Mock session for testing.""" def __init__(self, path): @@ -415,59 +415,59 @@ def database_search(self, query): class TestFileNavigator: - """Tests for FileNavigator class.""" + """Tests for ndi_file_navigator class.""" def test_filenavigator_creation(self): - """Test creating a FileNavigator.""" - nav = FileNavigator() + """Test creating a ndi_file_navigator.""" + nav = ndi_file_navigator() assert nav.id is not None def test_filenavigator_with_session(self): - """Test FileNavigator with session.""" + """Test ndi_file_navigator with session.""" with tempfile.TemporaryDirectory() as tmpdir: - session = MockSession(tmpdir) - nav = FileNavigator(session=session) + session = ndi_session_mock(tmpdir) + nav = ndi_file_navigator(session=session) assert nav.session is session def test_filenavigator_with_fileparameters(self): - """Test FileNavigator with file parameters.""" - nav = FileNavigator(fileparameters="*.dat") + """Test ndi_file_navigator with file parameters.""" + nav = ndi_file_navigator(fileparameters="*.dat") assert nav.fileparameters == {"filematch": ["*.dat"]} def test_filenavigator_with_list_parameters(self): - """Test FileNavigator with list file parameters.""" - nav = FileNavigator(fileparameters=["*.dat", "*.bin"]) + """Test ndi_file_navigator with list file parameters.""" + nav = ndi_file_navigator(fileparameters=["*.dat", "*.bin"]) assert nav.fileparameters == {"filematch": ["*.dat", "*.bin"]} def test_filenavigator_setfileparameters(self): """Test setting file parameters.""" - nav = FileNavigator() + nav = ndi_file_navigator() nav.setfileparameters(["*.rhd", "*.dat"]) assert nav.fileparameters == {"filematch": ["*.rhd", "*.dat"]} def test_filenavigator_filematch_hashstring(self): """Test filematch hash string generation.""" - nav = FileNavigator(fileparameters=["*.dat", "*.bin"]) + nav = ndi_file_navigator(fileparameters=["*.dat", "*.bin"]) hash1 = nav.filematch_hashstring() assert len(hash1) == 32 # MD5 hex digest - nav2 = FileNavigator(fileparameters=["*.dat", "*.bin"]) + nav2 = ndi_file_navigator(fileparameters=["*.dat", "*.bin"]) hash2 = nav2.filematch_hashstring() assert hash1 == hash2 # Same patterns = same hash - nav3 = FileNavigator(fileparameters=["*.rhd"]) + nav3 = ndi_file_navigator(fileparameters=["*.rhd"]) hash3 = nav3.filematch_hashstring() assert hash1 != hash3 # Different patterns = different hash def test_filenavigator_isingested(self): """Test isingested static method.""" - assert FileNavigator.isingested(["epochid://abc123", "file.dat"]) is True - assert FileNavigator.isingested(["file.dat"]) is False - assert FileNavigator.isingested([]) is False + assert ndi_file_navigator.isingested(["epochid://abc123", "file.dat"]) is True + assert ndi_file_navigator.isingested(["file.dat"]) is False + assert ndi_file_navigator.isingested([]) is False def test_filenavigator_ingestedfiles_epochid(self): """Test extracting epoch ID from ingested files.""" - epochid = FileNavigator.ingestedfiles_epochid(["epochid://abc123", "file.dat"]) + epochid = ndi_file_navigator.ingestedfiles_epochid(["epochid://abc123", "file.dat"]) assert epochid == "abc123" def test_filenavigator_selectfilegroups_disk(self): @@ -479,8 +479,8 @@ def test_filenavigator_selectfilegroups_disk(self): (Path(tmpdir) / "epoch2" / "data.rhd").parent.mkdir(parents=True) (Path(tmpdir) / "epoch2" / "data.rhd").touch() - session = MockSession(tmpdir) - nav = FileNavigator(session=session, fileparameters="*.rhd") + session = ndi_session_mock(tmpdir) + nav = ndi_file_navigator(session=session, fileparameters="*.rhd") groups = nav.selectfilegroups_disk() assert len(groups) == 2 @@ -491,8 +491,8 @@ def test_filenavigator_epochid_generation(self): # Create test file (Path(tmpdir) / "data.dat").touch() - session = MockSession(tmpdir) - nav = FileNavigator(session=session, fileparameters="*.dat") + session = ndi_session_mock(tmpdir) + nav = ndi_file_navigator(session=session, fileparameters="*.dat") # First call should generate new ID epochfiles = [str(Path(tmpdir) / "data.dat")] @@ -500,10 +500,10 @@ def test_filenavigator_epochid_generation(self): assert epoch_id.startswith("epoch_") def test_filenavigator_equality(self): - """Test FileNavigator equality.""" - nav1 = FileNavigator(fileparameters="*.dat") - nav2 = FileNavigator(fileparameters="*.dat") - nav3 = FileNavigator(fileparameters="*.bin") + """Test ndi_file_navigator equality.""" + nav1 = ndi_file_navigator(fileparameters="*.dat") + nav2 = ndi_file_navigator(fileparameters="*.dat") + nav3 = ndi_file_navigator(fileparameters="*.bin") assert nav1 == nav2 assert nav1 != nav3 @@ -589,11 +589,11 @@ def test_daqsystem_with_reader_and_navigator(self): # Create test file (Path(tmpdir) / "data.dat").touch() - session = MockSession(tmpdir) + session = ndi_session_mock(tmpdir) reader = ConcreteMFDAQReader() - nav = FileNavigator(session=session, fileparameters="*.dat") + nav = ndi_file_navigator(session=session, fileparameters="*.dat") - sys = DAQSystem( + sys = ndi_daq_system( name="test_system", filenavigator=nav, daqreader=reader, @@ -611,14 +611,14 @@ def test_full_workflow(self): epoch_dir.mkdir() (epoch_dir / "recording.dat").touch() - session = MockSession(tmpdir) + session = ndi_session_mock(tmpdir) channels = [ ChannelInfo(name="ai1", type="analog_in", number=1, sample_rate=30000), ChannelInfo(name="ai2", type="analog_in", number=2, sample_rate=30000), ] reader = ConcreteMFDAQReader(channels=channels) - nav = FileNavigator(session=session, fileparameters="*.dat") + nav = ndi_file_navigator(session=session, fileparameters="*.dat") # Get epoch files groups = nav.selectfilegroups_disk() @@ -634,12 +634,12 @@ def test_full_workflow(self): # ============================================================================= -# Ingested Data Tests +# Ingested ndi_gui_Data Tests # ============================================================================= class TestIngestedDataMethods: - """Tests for ingested data methods in MFDAQReader.""" + """Tests for ingested data methods in ndi_daq_reader_mfdaq.""" def test_samplerate_ingested_no_session(self): """Test samplerate_ingested with mock session.""" @@ -736,16 +736,16 @@ def test_epochtimes2samples_ingested(self): # ============================================================================= -# DAQSystem deleteepoch Tests +# ndi_daq_system deleteepoch Tests # ============================================================================= class TestDAQSystemDeleteEpoch: - """Tests for DAQSystem.deleteepoch method.""" + """Tests for ndi_daq_system.deleteepoch method.""" def test_deleteepoch_no_navigator(self): """Test deleteepoch with no file navigator.""" - sys = DAQSystem(name="test") + sys = ndi_daq_system(name="test") success, msg = sys.deleteepoch(1) assert success is False assert "No file navigator" in msg @@ -753,9 +753,9 @@ def test_deleteepoch_no_navigator(self): def test_deleteepoch_out_of_range(self): """Test deleteepoch with epoch out of range.""" with tempfile.TemporaryDirectory() as tmpdir: - session = MockSession(tmpdir) - nav = FileNavigator(session=session, fileparameters="*.dat") - sys = DAQSystem(name="test", filenavigator=nav) + session = ndi_session_mock(tmpdir) + nav = ndi_file_navigator(session=session, fileparameters="*.dat") + sys = ndi_daq_system(name="test", filenavigator=nav) success, msg = sys.deleteepoch(10) assert success is False @@ -767,13 +767,13 @@ def test_deleteepoch_with_epochs(self): # Create test file (Path(tmpdir) / "data.dat").touch() - session = MockSession(tmpdir) - nav = FileNavigator(session=session, fileparameters="*.dat") + session = ndi_session_mock(tmpdir) + nav = ndi_file_navigator(session=session, fileparameters="*.dat") # Add deleteepoch method to navigator nav.deleteepoch = MagicMock() - sys = DAQSystem(name="test", filenavigator=nav) + sys = ndi_daq_system(name="test", filenavigator=nav) success, msg = sys.deleteepoch(1) assert success is True diff --git a/tests/test_database.py b/tests/test_database.py index 4df9c68..bec9bc0 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -6,7 +6,7 @@ import pytest -from ndi import Database, Document, Ido, Query, open_database +from ndi import ndi_database, ndi_document, ndi_ido, ndi_query, open_database from ndi.common import timestamp @@ -21,8 +21,8 @@ def temp_session(): @pytest.fixture def sample_doc(): """Create a sample document for testing.""" - ido = Ido() - return Document( + ido = ndi_ido() + return ndi_document( { "base": { "id": ido.id, @@ -38,8 +38,8 @@ def sample_doc(): @pytest.fixture def element_doc(): """Create a sample element document for testing.""" - ido = Ido() - return Document( + ido = ndi_ido() + return ndi_document( { "base": { "id": ido.id, @@ -57,56 +57,56 @@ def element_doc(): class TestDatabaseCreation: - """Test Database creation.""" + """Test ndi_database creation.""" def test_create_database(self, temp_session): """Test creating a database (uses DID-python SQLite).""" - db = Database(temp_session) + db = ndi_database(temp_session) assert db.session_path == temp_session assert (temp_session / ".ndi").exists() assert (temp_session / ".ndi" / "did-sqlite.sqlite").exists() def test_create_with_custom_db_name(self, temp_session): """Test creating database with custom name.""" - Database(temp_session, db_name=".mydb") + ndi_database(temp_session, db_name=".mydb") assert (temp_session / ".mydb").exists() def test_open_database_function(self, temp_session): """Test open_database convenience function.""" db = open_database(temp_session) - assert isinstance(db, Database) + assert isinstance(db, ndi_database) assert db.session_path == temp_session def test_database_repr(self, temp_session): """Test database string representation.""" - db = Database(temp_session) - assert "Database" in repr(db) + db = ndi_database(temp_session) + assert "ndi_database" in repr(db) assert str(temp_session) in repr(db) class TestDatabaseAdd: - """Test Database add operations.""" + """Test ndi_database add operations.""" def test_add_document(self, temp_session, sample_doc): """Test adding a document.""" - db = Database(temp_session) + db = ndi_database(temp_session) result = db.add(sample_doc) assert result.id == sample_doc.id def test_add_duplicate_raises(self, temp_session, sample_doc): """Test that adding duplicate raises error.""" - db = Database(temp_session) + db = ndi_database(temp_session) db.add(sample_doc) with pytest.raises(ValueError, match="already exists"): db.add(sample_doc) def test_add_many(self, temp_session): """Test adding multiple documents.""" - db = Database(temp_session) + db = ndi_database(temp_session) docs = [] for i in range(3): - ido = Ido() - doc = Document( + ido = ndi_ido() + doc = ndi_document( { "base": { "id": ido.id, @@ -125,11 +125,11 @@ def test_add_many(self, temp_session): class TestDatabaseRead: - """Test Database read operations.""" + """Test ndi_database read operations.""" def test_read_existing(self, temp_session, sample_doc): """Test reading an existing document.""" - db = Database(temp_session) + db = ndi_database(temp_session) db.add(sample_doc) result = db.read(sample_doc.id) @@ -138,13 +138,13 @@ def test_read_existing(self, temp_session, sample_doc): def test_read_nonexistent(self, temp_session): """Test reading nonexistent document returns None.""" - db = Database(temp_session) + db = ndi_database(temp_session) result = db.read("nonexistent_id") assert result is None def test_find_by_id_alias(self, temp_session, sample_doc): """Test find_by_id is alias for read.""" - db = Database(temp_session) + db = ndi_database(temp_session) db.add(sample_doc) result = db.find_by_id(sample_doc.id) @@ -153,11 +153,11 @@ def test_find_by_id_alias(self, temp_session, sample_doc): class TestDatabaseUpdate: - """Test Database update operations.""" + """Test ndi_database update operations.""" def test_update_existing(self, temp_session, sample_doc): """Test updating an existing document.""" - db = Database(temp_session) + db = ndi_database(temp_session) db.add(sample_doc) # Modify and update @@ -171,17 +171,17 @@ def test_update_existing(self, temp_session, sample_doc): def test_update_nonexistent_raises(self, temp_session, sample_doc): """Test updating nonexistent document raises error.""" - db = Database(temp_session) + db = ndi_database(temp_session) with pytest.raises(ValueError, match="not found"): db.update(sample_doc) class TestDatabaseRemove: - """Test Database remove operations.""" + """Test ndi_database remove operations.""" def test_remove_existing(self, temp_session, sample_doc): """Test removing an existing document.""" - db = Database(temp_session) + db = ndi_database(temp_session) db.add(sample_doc) result = db.remove(sample_doc) @@ -190,7 +190,7 @@ def test_remove_existing(self, temp_session, sample_doc): def test_remove_by_id(self, temp_session, sample_doc): """Test removing by ID string.""" - db = Database(temp_session) + db = ndi_database(temp_session) db.add(sample_doc) result = db.remove(sample_doc.id) @@ -198,24 +198,24 @@ def test_remove_by_id(self, temp_session, sample_doc): def test_remove_nonexistent(self, temp_session): """Test removing nonexistent returns False.""" - db = Database(temp_session) + db = ndi_database(temp_session) result = db.remove("nonexistent_id") assert result is False class TestDatabaseAddOrReplace: - """Test Database add_or_replace operations.""" + """Test ndi_database add_or_replace operations.""" def test_add_or_replace_new(self, temp_session, sample_doc): """Test add_or_replace adds new document.""" - db = Database(temp_session) + db = ndi_database(temp_session) result = db.add_or_replace(sample_doc) assert result.id == sample_doc.id assert db.numdocs() == 1 def test_add_or_replace_existing(self, temp_session, sample_doc): """Test add_or_replace replaces existing document.""" - db = Database(temp_session) + db = ndi_database(temp_session) db.add(sample_doc) sample_doc = sample_doc.setproperties(**{"base.name": "replaced_name"}) @@ -227,16 +227,16 @@ def test_add_or_replace_existing(self, temp_session, sample_doc): class TestDatabaseSearch: - """Test Database search operations.""" + """Test ndi_database search operations.""" def test_search_all(self, temp_session): """Test searching for all documents.""" - db = Database(temp_session) + db = ndi_database(temp_session) # Add some documents for i in range(3): - ido = Ido() - doc = Document( + ido = ndi_ido() + doc = ndi_document( { "base": { "id": ido.id, @@ -254,12 +254,12 @@ def test_search_all(self, temp_session): def test_search_with_query(self, temp_session): """Test searching with a query.""" - db = Database(temp_session) + db = ndi_database(temp_session) # Add documents with different names for name in ["alpha", "beta", "gamma"]: - ido = Ido() - doc = Document( + ido = ndi_ido() + doc = ndi_document( { "base": { "id": ido.id, @@ -273,36 +273,36 @@ def test_search_with_query(self, temp_session): db.add(doc) # Search for specific name - query = Query("base.name") == "beta" + query = ndi_query("base.name") == "beta" results = db.search(query) assert len(results) == 1 assert results[0].document_properties["base"]["name"] == "beta" def test_search_empty_result(self, temp_session, sample_doc): """Test search returns empty list when no matches.""" - db = Database(temp_session) + db = ndi_database(temp_session) db.add(sample_doc) - query = Query("base.name") == "nonexistent" + query = ndi_query("base.name") == "nonexistent" results = db.search(query) assert results == [] class TestDatabaseCounts: - """Test Database counting operations.""" + """Test ndi_database counting operations.""" def test_numdocs_empty(self, temp_session): """Test numdocs on empty database.""" - db = Database(temp_session) + db = ndi_database(temp_session) assert db.numdocs() == 0 def test_numdocs_with_docs(self, temp_session): """Test numdocs with documents.""" - db = Database(temp_session) + db = ndi_database(temp_session) for i in range(5): - ido = Ido() - doc = Document( + ido = ndi_ido() + doc = ndi_document( { "base": { "id": ido.id, @@ -319,12 +319,12 @@ def test_numdocs_with_docs(self, temp_session): def test_alldocids(self, temp_session): """Test getting all document IDs.""" - db = Database(temp_session) + db = ndi_database(temp_session) added_ids = [] for i in range(3): - ido = Ido() - doc = Document( + ido = ndi_ido() + doc = ndi_document( { "base": { "id": ido.id, @@ -345,15 +345,15 @@ def test_alldocids(self, temp_session): class TestDatabaseDependencies: - """Test Database dependency operations.""" + """Test ndi_database dependency operations.""" def test_find_dependencies(self, temp_session): """Test finding dependencies of a document.""" - db = Database(temp_session) + db = ndi_database(temp_session) # Create parent document - parent_ido = Ido() - parent = Document( + parent_ido = ndi_ido() + parent = ndi_document( { "base": { "id": parent_ido.id, @@ -367,8 +367,8 @@ def test_find_dependencies(self, temp_session): db.add(parent) # Create child document with dependency - child_ido = Ido() - child = Document( + child_ido = ndi_ido() + child = ndi_document( { "base": { "id": child_ido.id, @@ -389,37 +389,37 @@ def test_find_dependencies(self, temp_session): class TestDatabasePaths: - """Test Database path properties.""" + """Test ndi_database path properties.""" def test_database_path(self, temp_session): """Test database_path property points to SQLite file.""" - db = Database(temp_session) + db = ndi_database(temp_session) assert db.database_path.exists() assert str(db.database_path).endswith("did-sqlite.sqlite") def test_binary_path(self, temp_session): """Test binary_path property.""" - db = Database(temp_session) + db = ndi_database(temp_session) assert db.binary_path.exists() def test_get_binary_path(self, temp_session, sample_doc): """Test get_binary_path method.""" - db = Database(temp_session) + db = ndi_database(temp_session) path = db.get_binary_path(sample_doc, "data.bin") assert str(path).endswith(f"{sample_doc.id}_data.bin") class TestDatabaseRemoveMany: - """Test Database remove_many operation.""" + """Test ndi_database remove_many operation.""" def test_remove_many_by_query(self, temp_session): """Test removing multiple documents by query.""" - db = Database(temp_session) + db = ndi_database(temp_session) # Add documents with different types for name, doc_type in [("a", "alpha"), ("b", "alpha"), ("c", "beta")]: - ido = Ido() - doc = Document( + ido = ndi_ido() + doc = ndi_document( { "base": { "id": ido.id, @@ -436,7 +436,7 @@ def test_remove_many_by_query(self, temp_session): assert db.numdocs() == 3 # Remove all alpha type - query = Query("meta.type") == "alpha" + query = ndi_query("meta.type") == "alpha" count = db.remove_many(query=query) assert count == 2 assert db.numdocs() == 1 diff --git a/tests/test_database_utils.py b/tests/test_database_utils.py index 0668b03..0b14b1d 100644 --- a/tests/test_database_utils.py +++ b/tests/test_database_utils.py @@ -1,5 +1,5 @@ """ -Tests for Phase 11 Batch 11D: Database Utilities + Ingestion. +Tests for Phase 11 Batch 11D: ndi_database Utilities + Ingestion. Tests dependency traversal, batch retrieval, graph construction, and file ingestion/expulsion system. diff --git a/tests/test_document.py b/tests/test_document.py index b45f7b1..120c982 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -4,12 +4,12 @@ import pytest -from ndi import Document, Ido +from ndi import ndi_document, ndi_ido from ndi.common import timestamp class TestDocumentCreation: - """Test Document creation.""" + """Test ndi_document creation.""" def test_create_document_with_dict(self): """Test creating document from a dictionary.""" @@ -22,7 +22,7 @@ def test_create_document_with_dict(self): }, "document_class": {"class_name": "base", "superclasses": []}, } - doc = Document(props) + doc = ndi_document(props) assert doc.id == "test_id_123" assert doc.document_properties["base"]["name"] == "test_doc" @@ -36,24 +36,24 @@ def test_create_document_without_id_in_dict(self): "base": {"name": "test_doc", "session_id": ""}, "document_class": {"class_name": "base", "superclasses": []}, } - doc = Document(props) + doc = ndi_document(props) # ID should be empty since none was provided in the dict assert doc.id == "" def test_create_document_with_ido_generates_id(self): - """Test that using Ido directly generates a valid ID.""" - ido = Ido() + """Test that using ndi_ido directly generates a valid ID.""" + ido = ndi_ido() props = { "base": {"id": ido.id, "datestamp": timestamp(), "name": "test_doc", "session_id": ""}, "document_class": {"class_name": "base", "superclasses": []}, } - doc = Document(props) - # ID should match the Ido we created + doc = ndi_document(props) + # ID should match the ndi_ido we created assert doc.id == ido.id assert len(doc.id) > 0 def test_create_document_from_another(self): - """Test creating document from another Document.""" + """Test creating document from another ndi_document.""" props = { "base": { "id": "original_id", @@ -63,8 +63,8 @@ def test_create_document_from_another(self): }, "document_class": {"class_name": "base", "superclasses": []}, } - doc1 = Document(props) - doc2 = Document(doc1) + doc1 = ndi_document(props) + doc2 = ndi_document(doc1) # Should be a copy with same properties assert doc2.id == doc1.id @@ -76,7 +76,7 @@ def test_create_document_from_another(self): class TestDocumentProperties: - """Test Document property access.""" + """Test ndi_document property access.""" def test_id_property(self): """Test id property.""" @@ -84,7 +84,7 @@ def test_id_property(self): "base": {"id": "my_id", "datestamp": "", "session_id": ""}, "document_class": {"class_name": "base", "superclasses": []}, } - doc = Document(props) + doc = ndi_document(props) assert doc.id == "my_id" def test_session_id_property(self): @@ -93,7 +93,7 @@ def test_session_id_property(self): "base": {"id": "id", "datestamp": "", "session_id": "sess_123"}, "document_class": {"class_name": "base", "superclasses": []}, } - doc = Document(props) + doc = ndi_document(props) assert doc.session_id == "sess_123" def test_set_session_id(self): @@ -102,7 +102,7 @@ def test_set_session_id(self): "base": {"id": "id", "datestamp": "", "session_id": ""}, "document_class": {"class_name": "base", "superclasses": []}, } - doc = Document(props) + doc = ndi_document(props) doc = doc.set_session_id("new_session") assert doc.session_id == "new_session" @@ -112,7 +112,7 @@ def test_set_session_id_chaining(self): "base": {"id": "id", "datestamp": "", "session_id": ""}, "document_class": {"class_name": "base", "superclasses": []}, } - doc = Document(props) + doc = ndi_document(props) result = doc.set_session_id("sess") assert result is doc @@ -126,7 +126,7 @@ def test_doc_class(self): "base": {"id": "id", "datestamp": "", "session_id": ""}, "document_class": {"class_name": "ndi_element", "superclasses": []}, } - doc = Document(props) + doc = ndi_document(props) assert doc.doc_class() == "ndi_element" def test_doc_isa_true(self): @@ -135,7 +135,7 @@ def test_doc_isa_true(self): "base": {"id": "id", "datestamp": "", "session_id": ""}, "document_class": {"class_name": "ndi_element", "superclasses": []}, } - doc = Document(props) + doc = ndi_document(props) assert doc.doc_isa("ndi_element") def test_doc_isa_false(self): @@ -144,7 +144,7 @@ def test_doc_isa_false(self): "base": {"id": "id", "datestamp": "", "session_id": ""}, "document_class": {"class_name": "ndi_element", "superclasses": []}, } - doc = Document(props) + doc = ndi_document(props) assert not doc.doc_isa("ndi_probe") @@ -157,7 +157,7 @@ def test_dependency_empty(self): "base": {"id": "id", "datestamp": "", "session_id": ""}, "document_class": {"class_name": "base", "superclasses": []}, } - doc = Document(props) + doc = ndi_document(props) names, deps = doc.dependency() assert names == [] assert deps == [] @@ -172,7 +172,7 @@ def test_dependency_with_deps(self): {"name": "element_id", "value": "elem_456"}, ], } - doc = Document(props) + doc = ndi_document(props) names, deps = doc.dependency() assert "session_id" in names assert "element_id" in names @@ -185,7 +185,7 @@ def test_dependency_value(self): "document_class": {"class_name": "base", "superclasses": []}, "depends_on": [{"name": "session_id", "value": "sess_123"}], } - doc = Document(props) + doc = ndi_document(props) assert doc.dependency_value("session_id") == "sess_123" def test_dependency_value_not_found(self): @@ -195,7 +195,7 @@ def test_dependency_value_not_found(self): "document_class": {"class_name": "base", "superclasses": []}, "depends_on": [], } - doc = Document(props) + doc = ndi_document(props) with pytest.raises(KeyError): doc.dependency_value("nonexistent") @@ -206,7 +206,7 @@ def test_dependency_value_not_found_no_error(self): "document_class": {"class_name": "base", "superclasses": []}, "depends_on": [], } - doc = Document(props) + doc = ndi_document(props) result = doc.dependency_value("nonexistent", error_if_not_found=False) assert result is None @@ -217,7 +217,7 @@ def test_set_dependency_value(self): "document_class": {"class_name": "base", "superclasses": []}, "depends_on": [{"name": "session_id", "value": "old_value"}], } - doc = Document(props) + doc = ndi_document(props) doc = doc.set_dependency_value("session_id", "new_value") assert doc.dependency_value("session_id") == "new_value" @@ -228,7 +228,7 @@ def test_set_dependency_value_add_new(self): "document_class": {"class_name": "base", "superclasses": []}, "depends_on": [], } - doc = Document(props) + doc = ndi_document(props) doc = doc.set_dependency_value("new_dep", "new_value", error_if_not_found=False) assert doc.dependency_value("new_dep") == "new_value" @@ -243,7 +243,7 @@ def test_dependency_value_n(self): {"name": "element_3", "value": "elem_c"}, ], } - doc = Document(props) + doc = ndi_document(props) values = doc.dependency_value_n("element") assert values == ["elem_a", "elem_b", "elem_c"] @@ -254,7 +254,7 @@ def test_add_dependency_value_n(self): "document_class": {"class_name": "base", "superclasses": []}, "depends_on": [{"name": "element_1", "value": "elem_a"}], } - doc = Document(props) + doc = ndi_document(props) doc = doc.add_dependency_value_n("element", "elem_b") values = doc.dependency_value_n("element") assert values == ["elem_a", "elem_b"] @@ -270,7 +270,7 @@ def test_has_files_false(self): "document_class": {"class_name": "base", "superclasses": []}, "files": {"file_list": [], "file_info": []}, } - doc = Document(props) + doc = ndi_document(props) assert not doc.has_files() def test_has_files_true(self): @@ -283,7 +283,7 @@ def test_has_files_true(self): "file_info": [{"name": "data.bin", "locations": []}], }, } - doc = Document(props) + doc = ndi_document(props) assert doc.has_files() def test_current_file_list_empty(self): @@ -293,7 +293,7 @@ def test_current_file_list_empty(self): "document_class": {"class_name": "base", "superclasses": []}, "files": {"file_list": ["allowed.bin"], "file_info": []}, } - doc = Document(props) + doc = ndi_document(props) assert doc.current_file_list() == [] def test_add_file(self): @@ -303,7 +303,7 @@ def test_add_file(self): "document_class": {"class_name": "base", "superclasses": []}, "files": {"file_list": ["data.bin"], "file_info": []}, } - doc = Document(props) + doc = ndi_document(props) doc = doc.add_file("data.bin", "/path/to/data.bin") assert "data.bin" in doc.current_file_list() @@ -314,7 +314,7 @@ def test_add_file_not_in_list(self): "document_class": {"class_name": "base", "superclasses": []}, "files": {"file_list": ["allowed.bin"], "file_info": []}, } - doc = Document(props) + doc = ndi_document(props) with pytest.raises(ValueError): doc.add_file("not_allowed.bin", "/path/to/file") @@ -324,7 +324,7 @@ def test_add_file_no_files_field(self): "base": {"id": "id", "datestamp": "", "session_id": ""}, "document_class": {"class_name": "base", "superclasses": []}, } - doc = Document(props) + doc = ndi_document(props) with pytest.raises(ValueError): doc.add_file("data.bin", "/path/to/data.bin") @@ -342,8 +342,8 @@ def test_eq_same_id(self): "base": {"id": "same_id", "datestamp": "", "session_id": ""}, "document_class": {"class_name": "other", "superclasses": []}, } - doc1 = Document(props1) - doc2 = Document(props2) + doc1 = ndi_document(props1) + doc2 = ndi_document(props2) assert doc1 == doc2 def test_eq_different_id(self): @@ -356,8 +356,8 @@ def test_eq_different_id(self): "base": {"id": "id2", "datestamp": "", "session_id": ""}, "document_class": {"class_name": "base", "superclasses": []}, } - doc1 = Document(props1) - doc2 = Document(props2) + doc1 = ndi_document(props1) + doc2 = ndi_document(props2) assert doc1 != doc2 def test_eq_non_document(self): @@ -366,7 +366,7 @@ def test_eq_non_document(self): "base": {"id": "id", "datestamp": "", "session_id": ""}, "document_class": {"class_name": "base", "superclasses": []}, } - doc = Document(props) + doc = ndi_document(props) assert doc != "not a document" assert doc != 123 @@ -380,7 +380,7 @@ def test_to_dict(self): "base": {"id": "id", "datestamp": "", "session_id": ""}, "document_class": {"class_name": "base", "superclasses": []}, } - doc = Document(props) + doc = ndi_document(props) d = doc.to_dict() assert d == props # Modifying returned dict shouldn't affect document @@ -393,7 +393,7 @@ def test_to_json(self): "base": {"id": "id", "datestamp": "", "session_id": ""}, "document_class": {"class_name": "base", "superclasses": []}, } - doc = Document(props) + doc = ndi_document(props) json_str = doc.to_json() # Should be valid JSON parsed = json.loads(json_str) @@ -405,7 +405,7 @@ def test_setproperties(self): "base": {"id": "id", "datestamp": "", "session_id": "", "name": "old"}, "document_class": {"class_name": "base", "superclasses": []}, } - doc = Document(props) + doc = ndi_document(props) doc = doc.setproperties(**{"base.name": "new_name", "base.session_id": "sess"}) assert doc.document_properties["base"]["name"] == "new_name" assert doc.session_id == "sess" @@ -417,26 +417,26 @@ class TestDocumentStaticMethods: def test_find_doc_by_id_found(self): """Test find_doc_by_id finds document.""" docs = [ - Document( + ndi_document( { "base": {"id": "id1", "datestamp": "", "session_id": ""}, "document_class": {"class_name": "base", "superclasses": []}, } ), - Document( + ndi_document( { "base": {"id": "id2", "datestamp": "", "session_id": ""}, "document_class": {"class_name": "base", "superclasses": []}, } ), - Document( + ndi_document( { "base": {"id": "id3", "datestamp": "", "session_id": ""}, "document_class": {"class_name": "base", "superclasses": []}, } ), ] - found, idx = Document.find_doc_by_id(docs, "id2") + found, idx = ndi_document.find_doc_by_id(docs, "id2") assert found is not None assert found.id == "id2" assert idx == 1 @@ -444,21 +444,21 @@ def test_find_doc_by_id_found(self): def test_find_doc_by_id_not_found(self): """Test find_doc_by_id returns None when not found.""" docs = [ - Document( + ndi_document( { "base": {"id": "id1", "datestamp": "", "session_id": ""}, "document_class": {"class_name": "base", "superclasses": []}, } ), ] - found, idx = Document.find_doc_by_id(docs, "nonexistent") + found, idx = ndi_document.find_doc_by_id(docs, "nonexistent") assert found is None assert idx is None def test_find_newest(self): """Test find_newest finds most recent document.""" docs = [ - Document( + ndi_document( { "base": { "id": "id1", @@ -468,7 +468,7 @@ def test_find_newest(self): "document_class": {"class_name": "base", "superclasses": []}, } ), - Document( + ndi_document( { "base": { "id": "id2", @@ -478,7 +478,7 @@ def test_find_newest(self): "document_class": {"class_name": "base", "superclasses": []}, } ), - Document( + ndi_document( { "base": { "id": "id3", @@ -489,14 +489,14 @@ def test_find_newest(self): } ), ] - newest, idx, ts = Document.find_newest(docs) + newest, idx, ts = ndi_document.find_newest(docs) assert newest.id == "id2" # Jan 3 is newest assert idx == 1 def test_find_newest_empty_raises(self): """Test find_newest raises error for empty array.""" with pytest.raises(ValueError): - Document.find_newest([]) + ndi_document.find_newest([]) class TestDocumentRepr: @@ -508,8 +508,8 @@ def test_repr(self): "base": {"id": "my_id", "datestamp": "", "session_id": ""}, "document_class": {"class_name": "ndi_element", "superclasses": []}, } - doc = Document(props) + doc = ndi_document(props) r = repr(doc) - assert "Document" in r + assert "ndi_document" in r assert "ndi_element" in r assert "my_id" in r diff --git a/tests/test_element.py b/tests/test_element.py index 47c8a7b..88b605f 100644 --- a/tests/test_element.py +++ b/tests/test_element.py @@ -2,40 +2,40 @@ Tests for ndi.epoch, ndi.element, and ndi.probe (Phase 6). Tests cover: -- EpochProbeMap dataclass -- EpochSet abstract base class -- Epoch immutable data class -- Element base class -- Probe specialized element +- ndi_epoch_epochprobemap dataclass +- ndi_epoch_epochset abstract base class +- ndi_epoch_epoch immutable data class +- ndi_element base class +- ndi_probe specialized element """ from unittest.mock import MagicMock import pytest -from ndi.documentservice import DocumentService -from ndi.element import Element -from ndi.epoch.epoch import Epoch, is_epoch_or_empty -from ndi.epoch.epochprobemap import EpochProbeMap, build_devicestring, parse_devicestring -from ndi.epoch.epochset import EpochSet -from ndi.probe import Probe -from ndi.time import DEV_LOCAL_TIME, UTC, ClockType +from ndi.documentservice import ndi_documentservice +from ndi.element import ndi_element +from ndi.epoch.epoch import is_epoch_or_empty, ndi_epoch_epoch +from ndi.epoch.epochprobemap import build_devicestring, ndi_epoch_epochprobemap, parse_devicestring +from ndi.epoch.epochset import ndi_epoch_epochset +from ndi.probe import ndi_probe +from ndi.time import DEV_LOCAL_TIME, UTC, ndi_time_clocktype # ============================================================================= -# EpochProbeMap Tests +# ndi_epoch_epochprobemap Tests # ============================================================================= class TestEpochProbeMap: - """Tests for EpochProbeMap dataclass.""" + """Tests for ndi_epoch_epochprobemap dataclass.""" def test_creation(self): - """Test creating an EpochProbeMap.""" - epm = EpochProbeMap( + """Test creating an ndi_epoch_epochprobemap.""" + epm = ndi_epoch_epochprobemap( name="electrode1", reference=1, type="n-trode", - devicestring="intan1:SpikeInterfaceReader:", + devicestring="intan1:ndi_daq_reader_SpikeInterfaceReader:", subjectstring="mouse001", ) assert epm.name == "electrode1" @@ -45,41 +45,47 @@ def test_creation(self): def test_whitespace_in_name_raises(self): """Test that whitespace in name raises error.""" with pytest.raises(ValueError, match="name cannot contain whitespace"): - EpochProbeMap(name="electrode 1", reference=1, type="n-trode") + ndi_epoch_epochprobemap(name="electrode 1", reference=1, type="n-trode") def test_whitespace_in_type_raises(self): """Test that whitespace in type raises error.""" with pytest.raises(ValueError, match="type cannot contain whitespace"): - EpochProbeMap(name="electrode1", reference=1, type="n trode") + ndi_epoch_epochprobemap(name="electrode1", reference=1, type="n trode") def test_negative_reference_raises(self): """Test that negative reference raises error.""" with pytest.raises(ValueError, match="reference must be non-negative"): - EpochProbeMap(name="electrode1", reference=-1, type="n-trode") + ndi_epoch_epochprobemap(name="electrode1", reference=-1, type="n-trode") def test_devicename_property(self): """Test devicename property extraction.""" - epm = EpochProbeMap( - name="e1", reference=1, type="probe", devicestring="intan1:SpikeInterfaceReader:details" + epm = ndi_epoch_epochprobemap( + name="e1", + reference=1, + type="probe", + devicestring="intan1:ndi_daq_reader_SpikeInterfaceReader:details", ) assert epm.devicename == "intan1" def test_deviceclass_property(self): """Test deviceclass property extraction.""" - epm = EpochProbeMap( - name="e1", reference=1, type="probe", devicestring="intan1:SpikeInterfaceReader:details" + epm = ndi_epoch_epochprobemap( + name="e1", + reference=1, + type="probe", + devicestring="intan1:ndi_daq_reader_SpikeInterfaceReader:details", ) - assert epm.deviceclass == "SpikeInterfaceReader" + assert epm.deviceclass == "ndi_daq_reader_SpikeInterfaceReader" def test_matches_all(self): """Test matches with all criteria.""" - epm = EpochProbeMap(name="e1", reference=1, type="probe") + epm = ndi_epoch_epochprobemap(name="e1", reference=1, type="probe") assert epm.matches(name="e1", reference=1, type="probe") is True assert epm.matches(name="e2", reference=1, type="probe") is False def test_matches_partial(self): """Test matches with partial criteria.""" - epm = EpochProbeMap(name="e1", reference=1, type="probe") + epm = ndi_epoch_epochprobemap(name="e1", reference=1, type="probe") assert epm.matches(name="e1") is True assert epm.matches(reference=1) is True assert epm.matches(type="probe") is True @@ -87,7 +93,7 @@ def test_matches_partial(self): def test_to_dict(self): """Test conversion to dictionary.""" - epm = EpochProbeMap( + epm = ndi_epoch_epochprobemap( name="e1", reference=1, type="probe", devicestring="dev:class:", subjectstring="subj" ) d = epm.to_dict() @@ -98,20 +104,20 @@ def test_to_dict(self): def test_from_dict(self): """Test creation from dictionary.""" d = {"name": "e1", "reference": 1, "type": "probe"} - epm = EpochProbeMap.from_dict(d) + epm = ndi_epoch_epochprobemap.from_dict(d) assert epm.name == "e1" assert epm.reference == 1 def test_str(self): """Test string representation.""" - epm = EpochProbeMap(name="e1", reference=1, type="probe") + epm = ndi_epoch_epochprobemap(name="e1", reference=1, type="probe") assert str(epm) == "e1|1|probe" def test_equality(self): """Test equality comparison.""" - epm1 = EpochProbeMap(name="e1", reference=1, type="probe") - epm2 = EpochProbeMap(name="e1", reference=1, type="probe") - epm3 = EpochProbeMap(name="e2", reference=1, type="probe") + epm1 = ndi_epoch_epochprobemap(name="e1", reference=1, type="probe") + epm2 = ndi_epoch_epochprobemap(name="e1", reference=1, type="probe") + epm3 = ndi_epoch_epochprobemap(name="e2", reference=1, type="probe") assert epm1 == epm2 assert epm1 != epm3 @@ -121,9 +127,9 @@ class TestDeviceStringHelpers: def test_parse_devicestring(self): """Test parsing device string.""" - result = parse_devicestring("intan1:SpikeInterfaceReader:extra:info") + result = parse_devicestring("intan1:ndi_daq_reader_SpikeInterfaceReader:extra:info") assert result["name"] == "intan1" - assert result["class"] == "SpikeInterfaceReader" + assert result["class"] == "ndi_daq_reader_SpikeInterfaceReader" assert result["details"] == "extra:info" def test_build_devicestring(self): @@ -134,11 +140,11 @@ def test_build_devicestring(self): # ============================================================================= -# EpochSet Tests +# ndi_epoch_epochset Tests # ============================================================================= -class ConcreteEpochSet(EpochSet): +class ConcreteEpochSet(ndi_epoch_epochset): """Concrete implementation for testing.""" def __init__(self, epochs=None): @@ -156,7 +162,7 @@ def issyncgraphroot(self): class TestEpochSet: - """Tests for EpochSet abstract base class.""" + """Tests for ndi_epoch_epochset abstract base class.""" def test_epochtable_caching(self): """Test epoch table caching.""" @@ -245,16 +251,16 @@ def test_resetepochtable(self): # ============================================================================= -# Epoch Tests +# ndi_epoch_epoch Tests # ============================================================================= class TestEpoch: - """Tests for Epoch immutable data class.""" + """Tests for ndi_epoch_epoch immutable data class.""" def test_creation(self): - """Test creating an Epoch.""" - epoch = Epoch( + """Test creating an ndi_epoch_epoch.""" + epoch = ndi_epoch_epoch( epoch_number=1, epoch_id="ep_abc123", epoch_session_id="sess_xyz", @@ -263,13 +269,13 @@ def test_creation(self): assert epoch.epoch_id == "ep_abc123" def test_immutability(self): - """Test that Epoch is immutable.""" - epoch = Epoch(epoch_number=1, epoch_id="ep1") + """Test that ndi_epoch_epoch is immutable.""" + epoch = ndi_epoch_epoch(epoch_number=1, epoch_id="ep1") with pytest.raises(AttributeError): epoch.epoch_number = 2 def test_from_dict(self): - """Test creating Epoch from dictionary.""" + """Test creating ndi_epoch_epoch from dictionary.""" data = { "epoch_number": 1, "epoch_id": "ep1", @@ -278,14 +284,14 @@ def test_from_dict(self): "epoch_clock": ["dev_local_time"], "t0_t1": [(0.0, 10.0)], } - epoch = Epoch.from_dict(data) + epoch = ndi_epoch_epoch.from_dict(data) assert epoch.epoch_number == 1 assert len(epoch.epochprobemap) == 1 def test_to_dict(self): - """Test converting Epoch to dictionary.""" - epm = EpochProbeMap(name="e1", reference=1, type="probe") - epoch = Epoch( + """Test converting ndi_epoch_epoch to dictionary.""" + epm = ndi_epoch_epochprobemap(name="e1", reference=1, type="probe") + epoch = ndi_epoch_epoch( epoch_number=1, epoch_id="ep1", epochprobemap=(epm,), @@ -298,17 +304,17 @@ def test_to_dict(self): def test_has_clock(self): """Test has_clock method.""" - epoch = Epoch( + epoch = ndi_epoch_epoch( epoch_number=1, epoch_id="ep1", epoch_clock=(DEV_LOCAL_TIME, UTC), ) assert epoch.has_clock(DEV_LOCAL_TIME) is True - assert epoch.has_clock(ClockType.EXP_GLOBAL_TIME) is False + assert epoch.has_clock(ndi_time_clocktype.EXP_GLOBAL_TIME) is False def test_time_range(self): """Test time_range method.""" - epoch = Epoch( + epoch = ndi_epoch_epoch( epoch_number=1, epoch_id="ep1", epoch_clock=(DEV_LOCAL_TIME, UTC), @@ -319,8 +325,8 @@ def test_time_range(self): def test_matches_probe(self): """Test matches_probe method.""" - epm = EpochProbeMap(name="e1", reference=1, type="probe") - epoch = Epoch( + epm = ndi_epoch_epochprobemap(name="e1", reference=1, type="probe") + epoch = ndi_epoch_epoch( epoch_number=1, epoch_id="ep1", epochprobemap=(epm,), @@ -337,8 +343,8 @@ def test_none(self): assert is_epoch_or_empty(None) is True def test_epoch(self): - """Test with Epoch.""" - epoch = Epoch(epoch_number=1, epoch_id="ep1") + """Test with ndi_epoch_epoch.""" + epoch = ndi_epoch_epoch(epoch_number=1, epoch_id="ep1") assert is_epoch_or_empty(epoch) is True def test_empty_list(self): @@ -347,7 +353,7 @@ def test_empty_list(self): def test_list_of_epochs(self): """Test with list of Epochs.""" - epochs = [Epoch(epoch_number=i, epoch_id=f"ep{i}") for i in range(3)] + epochs = [ndi_epoch_epoch(epoch_number=i, epoch_id=f"ep{i}") for i in range(3)] assert is_epoch_or_empty(epochs) is True def test_invalid(self): @@ -356,16 +362,16 @@ def test_invalid(self): # ============================================================================= -# Element Tests +# ndi_element Tests # ============================================================================= class TestElement: - """Tests for Element base class.""" + """Tests for ndi_element base class.""" def test_creation(self): - """Test creating an Element.""" - elem = Element( + """Test creating an ndi_element.""" + elem = ndi_element( name="electrode1", reference=1, type="n-trode", @@ -377,33 +383,33 @@ def test_creation(self): def test_whitespace_in_name_raises(self): """Test that whitespace in name raises error.""" with pytest.raises(ValueError, match="name cannot contain whitespace"): - Element(name="electrode 1", reference=1, type="n-trode") + ndi_element(name="electrode 1", reference=1, type="n-trode") def test_negative_reference_raises(self): """Test that negative reference raises error.""" with pytest.raises(ValueError, match="reference must be non-negative"): - Element(name="electrode1", reference=-1, type="n-trode") + ndi_element(name="electrode1", reference=-1, type="n-trode") def test_elementstring(self): """Test elementstring method.""" - elem = Element(name="electrode1", reference=1, type="n-trode") + elem = ndi_element(name="electrode1", reference=1, type="n-trode") assert elem.elementstring() == "electrode1 | 1" def test_epochsetname(self): """Test epochsetname method.""" - elem = Element(name="electrode1", reference=1, type="n-trode") + elem = ndi_element(name="electrode1", reference=1, type="n-trode") assert "element" in elem.epochsetname() assert "electrode1" in elem.epochsetname() def test_issyncgraphroot_no_underlying(self): """Test issyncgraphroot with no underlying element.""" - elem = Element(name="e1", reference=1, type="probe") + elem = ndi_element(name="e1", reference=1, type="probe") assert elem.issyncgraphroot() is True def test_issyncgraphroot_with_underlying(self): """Test issyncgraphroot with underlying element.""" - underlying = Element(name="base", reference=0, type="base") - elem = Element( + underlying = ndi_element(name="base", reference=0, type="base") + elem = ndi_element( name="derived", reference=1, type="derived", @@ -414,13 +420,13 @@ def test_issyncgraphroot_with_underlying(self): def test_direct_epochtable(self): """Test epoch table from direct underlying element.""" # Create underlying with mock epoch table - underlying = Element(name="base", reference=0, type="base") + underlying = ndi_element(name="base", reference=0, type="base") underlying._epochtable_cache = [ {"epoch_number": 1, "epoch_id": "ep1", "epoch_clock": [], "t0_t1": []} ] underlying._epochtable_hash = "hash1" - elem = Element( + elem = ndi_element( name="derived", reference=1, type="derived", @@ -434,22 +440,22 @@ def test_direct_epochtable(self): def test_equality(self): """Test element equality.""" - elem1 = Element(name="e1", reference=1, type="probe") - elem2 = Element(name="e1", reference=1, type="probe") - elem3 = Element(name="e2", reference=1, type="probe") + elem1 = ndi_element(name="e1", reference=1, type="probe") + elem2 = ndi_element(name="e1", reference=1, type="probe") + elem3 = ndi_element(name="e2", reference=1, type="probe") assert elem1 == elem2 assert elem1 != elem3 def test_has_unique_id(self): """Test that elements have unique IDs.""" - elem1 = Element(name="e1", reference=1, type="probe") - elem2 = Element(name="e1", reference=1, type="probe") + elem1 = ndi_element(name="e1", reference=1, type="probe") + elem2 = ndi_element(name="e1", reference=1, type="probe") assert elem1.id != elem2.id def test_newdocument(self): """Test newdocument method.""" - elem = Element(name="e1", reference=1, type="probe") + elem = ndi_element(name="e1", reference=1, type="probe") try: doc = elem.newdocument() assert doc is not None @@ -457,26 +463,26 @@ def test_newdocument(self): assert hasattr(doc, "document_properties") except FileNotFoundError: # Schema not available in test environment - pytest.skip("Element JSON schema not available") + pytest.skip("ndi_element JSON schema not available") def test_searchquery(self): """Test searchquery method.""" - elem = Element(name="e1", reference=1, type="probe") + elem = ndi_element(name="e1", reference=1, type="probe") q = elem.searchquery() assert q is not None # ============================================================================= -# Probe Tests +# ndi_probe Tests # ============================================================================= class TestProbe: - """Tests for Probe specialized element.""" + """Tests for ndi_probe specialized element.""" def test_creation(self): - """Test creating a Probe.""" - probe = Probe( + """Test creating a ndi_probe.""" + probe = ndi_probe( name="electrode1", reference=1, type="n-trode", @@ -489,60 +495,61 @@ def test_creation(self): def test_probe_is_direct(self): """Test that probes are always direct=True (matching MATLAB).""" - probe = Probe(name="e1", reference=1, type="probe") + probe = ndi_probe(name="e1", reference=1, type="probe") assert probe.direct is True def test_epochsetname(self): """Test epochsetname method.""" - probe = Probe(name="electrode1", reference=1, type="n-trode") + probe = ndi_probe(name="electrode1", reference=1, type="n-trode") assert "probe" in probe.epochsetname() assert "electrode1" in probe.epochsetname() def test_issyncgraphroot(self): """Test issyncgraphroot returns False for probes.""" - probe = Probe(name="e1", reference=1, type="probe") + probe = ndi_probe(name="e1", reference=1, type="probe") assert probe.issyncgraphroot() is False def test_epochprobemapmatch(self): """Test epochprobemapmatch method.""" - probe = Probe(name="e1", reference=1, type="probe") - epm = EpochProbeMap(name="e1", reference=1, type="probe") + probe = ndi_probe(name="e1", reference=1, type="probe") + epm = ndi_epoch_epochprobemap(name="e1", reference=1, type="probe") assert probe.epochprobemapmatch(epm) is True assert ( - probe.epochprobemapmatch(EpochProbeMap(name="e2", reference=1, type="probe")) is False + probe.epochprobemapmatch(ndi_epoch_epochprobemap(name="e2", reference=1, type="probe")) + is False ) def test_epochprobemapmatch_dict(self): """Test epochprobemapmatch with dict.""" - probe = Probe(name="e1", reference=1, type="probe") + probe = ndi_probe(name="e1", reference=1, type="probe") epm_dict = {"name": "e1", "reference": 1, "type": "probe"} assert probe.epochprobemapmatch(epm_dict) is True def test_newdocument(self): """Test newdocument creates probe document.""" - probe = Probe(name="e1", reference=1, type="probe", subject_id="subj1") + probe = ndi_probe(name="e1", reference=1, type="probe", subject_id="subj1") try: doc = probe.newdocument() assert doc is not None except FileNotFoundError: # Schema not available in test environment - pytest.skip("Probe JSON schema not available") + pytest.skip("ndi_probe JSON schema not available") def test_repr(self): """Test string representation.""" - probe = Probe(name="e1", reference=1, type="probe") - assert "Probe" in repr(probe) + probe = ndi_probe(name="e1", reference=1, type="probe") + assert "ndi_probe" in repr(probe) assert "e1" in repr(probe) class TestProbeBuildEpochtable: - """Tests for Probe.buildepochtable with mocked DAQ systems.""" + """Tests for ndi_probe.buildepochtable with mocked DAQ systems.""" def test_buildepochtable_no_session(self): """Test buildepochtable with no session returns empty.""" - probe = Probe(name="e1", reference=1, type="probe") + probe = ndi_probe(name="e1", reference=1, type="probe") et = probe.buildepochtable() assert et == [] @@ -558,7 +565,9 @@ def test_buildepochtable_with_matching_epoch(self): "epoch_id": "dev_ep1", "epoch_session_id": "sess1", "epochprobemap": [ - EpochProbeMap(name="e1", reference=1, type="probe", devicestring="dev1::"), + ndi_epoch_epochprobemap( + name="e1", reference=1, type="probe", devicestring="dev1::" + ), ], "epoch_clock": [DEV_LOCAL_TIME], "t0_t1": [(0.0, 10.0)], @@ -567,7 +576,7 @@ def test_buildepochtable_with_matching_epoch(self): mock_session.getdaqsystems.return_value = [mock_daqsys] - probe = Probe(session=mock_session, name="e1", reference=1, type="probe") + probe = ndi_probe(session=mock_session, name="e1", reference=1, type="probe") et = probe.buildepochtable() assert len(et) == 1 @@ -583,7 +592,7 @@ def test_buildepochtable_no_matching_epoch(self): "epoch_number": 1, "epoch_id": "dev_ep1", "epochprobemap": [ - EpochProbeMap(name="other", reference=99, type="other"), + ndi_epoch_epochprobemap(name="other", reference=99, type="other"), ], "epoch_clock": [], "t0_t1": [], @@ -592,31 +601,31 @@ def test_buildepochtable_no_matching_epoch(self): mock_session.getdaqsystems.return_value = [mock_daqsys] - probe = Probe(session=mock_session, name="e1", reference=1, type="probe") + probe = ndi_probe(session=mock_session, name="e1", reference=1, type="probe") et = probe.buildepochtable() assert len(et) == 0 # ============================================================================= -# DocumentService Tests +# ndi_documentservice Tests # ============================================================================= class TestDocumentService: - """Tests for DocumentService mixin.""" + """Tests for ndi_documentservice mixin.""" def test_element_implements_documentservice(self): - """Test that Element implements DocumentService.""" - elem = Element(name="e1", reference=1, type="probe") - assert isinstance(elem, DocumentService) + """Test that ndi_element implements ndi_documentservice.""" + elem = ndi_element(name="e1", reference=1, type="probe") + assert isinstance(elem, ndi_documentservice) assert hasattr(elem, "newdocument") assert hasattr(elem, "searchquery") def test_probe_implements_documentservice(self): - """Test that Probe implements DocumentService.""" - probe = Probe(name="e1", reference=1, type="probe") - assert isinstance(probe, DocumentService) + """Test that ndi_probe implements ndi_documentservice.""" + probe = ndi_probe(name="e1", reference=1, type="probe") + assert isinstance(probe, ndi_documentservice) if __name__ == "__main__": diff --git a/tests/test_fun.py b/tests/test_fun.py index 1e3f3e0..5549394 100644 --- a/tests/test_fun.py +++ b/tests/test_fun.py @@ -383,7 +383,7 @@ def test_not_found(self): # =========================================================================== -class TestEpochId2Element: +class TestEpochId2ndi_element: """Tests for epoch.epochid2element.""" def test_basic(self): diff --git a/tests/test_ido.py b/tests/test_ido.py index cef505b..b036748 100644 --- a/tests/test_ido.py +++ b/tests/test_ido.py @@ -2,22 +2,22 @@ import re -from ndi import Ido +from ndi import ndi_ido class TestIdo: - """Test cases for the Ido class.""" + """Test cases for the ndi_ido class.""" def test_create_new_id(self): - """Test that creating an Ido generates a unique ID.""" - ido = Ido() + """Test that creating an ndi_ido generates a unique ID.""" + ido = ndi_ido() assert ido.id is not None assert isinstance(ido.id, str) assert len(ido.id) > 0 def test_id_format(self): """Test that ID follows expected format (hex_hex).""" - ido = Ido() + ido = ndi_ido() # ID should be in format: hexstring_hexstring assert "_" in ido.id parts = ido.id.split("_") @@ -27,37 +27,37 @@ def test_id_format(self): assert re.match(r"^[0-9a-f]+$", part), f"Part '{part}' is not valid hex" def test_unique_ids(self): - """Test that multiple Ido instances have unique IDs.""" - ids = [Ido().id for _ in range(100)] + """Test that multiple ndi_ido instances have unique IDs.""" + ids = [ndi_ido().id for _ in range(100)] assert len(ids) == len(set(ids)), "Generated IDs should be unique" def test_create_with_existing_id(self): - """Test creating Ido with an existing ID.""" + """Test creating ndi_ido with an existing ID.""" existing_id = "abc123_def456" - ido = Ido(id=existing_id) + ido = ndi_ido(id=existing_id) assert ido.id == existing_id def test_str_representation(self): """Test string representation returns the ID.""" - ido = Ido() + ido = ndi_ido() assert str(ido) == ido.id def test_repr(self): """Test repr shows class name and ID.""" - ido = Ido() + ido = ndi_ido() repr_str = repr(ido) - assert "Ido(" in repr_str + assert "ndi_ido(" in repr_str assert ido.id in repr_str def test_id_sortable_by_time(self): """Test that IDs are sortable by creation time.""" import time - id1 = Ido().id + id1 = ndi_ido().id time.sleep(0.01) # Small delay - id2 = Ido().id + id2 = ndi_ido().id time.sleep(0.01) - id3 = Ido().id + id3 = ndi_ido().id # When sorted alphabetically, they should maintain creation order sorted_ids = sorted([id3, id1, id2]) diff --git a/tests/test_mock_version.py b/tests/test_mock_version.py index 3eb9f94..6e932cc 100644 --- a/tests/test_mock_version.py +++ b/tests/test_mock_version.py @@ -214,32 +214,32 @@ def test_empty_session(self): # =========================================================================== -# ndi.mock.CalculatorTest tests +# ndi.mock.ndi_mock_ctest tests # =========================================================================== class TestCalculatorTest: - """Tests for CalculatorTest base class.""" + """Tests for ndi_mock_ctest base class.""" def test_default_generate(self): - from ndi.mock import CalculatorTest + from ndi.mock import ndi_mock_ctest - ct = CalculatorTest() + ct = ndi_mock_ctest() result = ct.generate_mock_docs() assert "input_docs" in result assert "expected_output" in result def test_mock_filenames(self): - from ndi.mock import CalculatorTest + from ndi.mock import ndi_mock_ctest - ct = CalculatorTest() + ct = ndi_mock_ctest() assert ct.mock_expected_filename(1) == "mock.1.json" assert ct.mock_comparison_filename(3) == "mock.3.compare.json" def test_write_and_load(self, tmp_path): - from ndi.mock import CalculatorTest + from ndi.mock import ndi_mock_ctest - ct = CalculatorTest() + ct = ndi_mock_ctest() # Override mock_path to use tmp ct.mock_path = lambda: tmp_path @@ -254,16 +254,16 @@ def test_write_and_load(self, tmp_path): assert loaded == {"test": "data"} def test_load_nonexistent(self, tmp_path): - from ndi.mock import CalculatorTest + from ndi.mock import ndi_mock_ctest - ct = CalculatorTest() + ct = ndi_mock_ctest() ct.mock_path = lambda: tmp_path assert ct.load_mock_expected_output(99) is None def test_compare_equal_docs(self): - from ndi.mock import CalculatorTest + from ndi.mock import ndi_mock_ctest - ct = CalculatorTest() + ct = ndi_mock_ctest() d1 = MagicMock() d1.document_properties = {"a": 1, "b": 2} d2 = MagicMock() @@ -272,9 +272,9 @@ def test_compare_equal_docs(self): assert match is True def test_compare_different_docs(self): - from ndi.mock import CalculatorTest + from ndi.mock import ndi_mock_ctest - ct = CalculatorTest() + ct = ndi_mock_ctest() d1 = MagicMock() d1.document_properties = {"a": 1} d2 = MagicMock() @@ -283,9 +283,9 @@ def test_compare_different_docs(self): assert match is False def test_test_no_calculator(self): - from ndi.mock import CalculatorTest + from ndi.mock import ndi_mock_ctest - ct = CalculatorTest() + ct = ndi_mock_ctest() result = ct.test() assert result["passed"] is False diff --git a/tests/test_ontology.py b/tests/test_ontology.py index 1a70d7d..ca66750 100644 --- a/tests/test_ontology.py +++ b/tests/test_ontology.py @@ -441,14 +441,14 @@ def test_cui_lookup(self): provider = NCImProvider() mock_data = { "code": "C0027947", - "name": "Neuron", + "name": "ndi_neuron", "definitions": [{"definition": "A nerve cell"}], "synonyms": [{"name": "Nerve Cell"}, {"name": "Neural Cell"}], } with patch.object(provider, "_http_get_json", return_value=mock_data): result = provider.lookup_term("C0027947") assert result.id == "NCIm:C0027947" - assert result.name == "Neuron" + assert result.name == "ndi_neuron" assert result.definition == "A nerve cell" assert "Nerve Cell" in result.synonyms @@ -461,12 +461,12 @@ def test_name_search(self): } detail_data = { "code": "C0027947", - "name": "Neuron", + "name": "ndi_neuron", "definitions": [], "synonyms": [], } with patch.object(provider, "_http_get_json", side_effect=[search_data, detail_data]): - result = provider.lookup_term("Neuron") + result = provider.lookup_term("ndi_neuron") assert result.id == "NCIm:C0027947" def test_api_error(self): diff --git a/tests/test_phase1_gaps.py b/tests/test_phase1_gaps.py index e0526e5..5ac8e25 100644 --- a/tests/test_phase1_gaps.py +++ b/tests/test_phase1_gaps.py @@ -10,7 +10,7 @@ - table utilities (Batch 6) - copy_session_to_dataset (Batch 7) - Presentation time read/write (Batch 8) -- Subject/Probe doc helpers (Batch 9) +- ndi_subject/ndi_probe doc helpers (Batch 9) - pfilemirror (Batch 10) - readImageStack (Batch 11) - docComparison (Batch 13) @@ -32,9 +32,9 @@ class TestTuningCurveAnalysis: def _make_calc(self): - from ndi.calc.stimulus.tuningcurve import TuningCurveCalc + from ndi.calc.stimulus.tuningcurve import ndi_calc_stimulus_tuningcurve - return TuningCurveCalc(session=None) + return ndi_calc_stimulus_tuningcurve(session=None) def test_best_value_unknown_algorithm(self): calc = self._make_calc() @@ -321,7 +321,7 @@ def test_non_dict_props(self): # ========================================================================= -# Batch 5: Document-to-Table conversions +# Batch 5: ndi_document-to-Table conversions # ========================================================================= @@ -614,7 +614,7 @@ def test_read_subset(self): # ========================================================================= -# Batch 9: Subject/Probe doc helpers +# Batch 9: ndi_subject/ndi_probe doc helpers # ========================================================================= @@ -623,7 +623,7 @@ class TestDocHelpers: def test_make_species_strain_sex(self): """makeSpeciesStrainSex creates real openMINDS NDI Documents.""" pytest.importorskip("openminds", reason="openminds package not installed") - from ndi.document import Document + from ndi.document import ndi_document from ndi.fun.doc import makeSpeciesStrainSex session = MagicMock() @@ -642,7 +642,7 @@ def test_make_species_strain_sex(self): # Species + Strain + BiologicalSex = 3 documents assert len(docs) == 3 for doc in docs: - assert isinstance(doc, Document) + assert isinstance(doc, ndi_document) props = doc.document_properties # All should be openminds_subject docs linked to subject assert props.get("document_class", {}).get("class_name") == "openminds_subject" @@ -652,7 +652,7 @@ def test_make_species_strain_sex(self): def test_make_species_only(self): """makeSpeciesStrainSex with species only creates 1 document.""" pytest.importorskip("openminds", reason="openminds package not installed") - from ndi.document import Document + from ndi.document import ndi_document from ndi.fun.doc import makeSpeciesStrainSex session = MagicMock() @@ -662,13 +662,13 @@ def test_make_species_only(self): docs = makeSpeciesStrainSex(session, subj_doc, species="Rattus") assert len(docs) == 1 - assert isinstance(docs[0], Document) + assert isinstance(docs[0], ndi_document) om = docs[0].document_properties.get("openminds", {}) assert "Species" in om.get("openminds_type", "") def test_probe_locations_for_probes(self): """probeLocations4probes creates real probe_location Documents.""" - from ndi.document import Document + from ndi.document import ndi_document from ndi.fun.doc import probeLocations4probes session = MagicMock() @@ -691,7 +691,7 @@ def test_probe_locations_for_probes(self): ) assert len(docs) == 2 for doc in docs: - assert isinstance(doc, Document) + assert isinstance(doc, ndi_document) props = doc.document_properties assert props.get("document_class", {}).get("class_name") == "probe_location" diff --git a/tests/test_phase2_gaps.py b/tests/test_phase2_gaps.py index a6ab9b2..d8ada91 100644 --- a/tests/test_phase2_gaps.py +++ b/tests/test_phase2_gaps.py @@ -19,7 +19,7 @@ import pytest # ========================================================================= -# Batch 1: Calculator & Doc Utilities +# Batch 1: ndi_calculator & Doc Utilities # ========================================================================= @@ -174,7 +174,7 @@ def test_empty_session(self): # ========================================================================= -# Batch 2: Database Export/Extract +# Batch 2: ndi_database Export/Extract # ========================================================================= @@ -300,7 +300,7 @@ def test_creates_temp_dir_if_none(self): # ========================================================================= -# Batch 3: Probe Type Map +# Batch 3: ndi_probe Type Map # ========================================================================= @@ -437,7 +437,7 @@ class TestOpenmindsObjToNdiDocument: @patch("ndi.openminds_convert.openminds_obj_to_dict") def test_creates_documents(self, mock_to_dict): """openminds_obj_to_ndi_document creates real NDI Documents.""" - from ndi.document import Document + from ndi.document import ndi_document from ndi.openminds_convert import openminds_obj_to_ndi_document mock_to_dict.return_value = [ @@ -453,14 +453,14 @@ def test_creates_documents(self, mock_to_dict): result = openminds_obj_to_ndi_document(MagicMock(), session_id="sess-1") assert len(result) == 1 - assert isinstance(result[0], Document) + assert isinstance(result[0], ndi_document) props = result[0].document_properties assert props.get("document_class", {}).get("class_name") == "openminds" @patch("ndi.openminds_convert.openminds_obj_to_dict") def test_subject_dependency(self, mock_to_dict): """openminds_obj_to_ndi_document creates openminds_subject Documents with dependency.""" - from ndi.document import Document + from ndi.document import ndi_document from ndi.openminds_convert import openminds_obj_to_ndi_document mock_to_dict.return_value = [ @@ -481,7 +481,7 @@ def test_subject_dependency(self, mock_to_dict): ) assert len(result) == 1 - assert isinstance(result[0], Document) + assert isinstance(result[0], ndi_document) props = result[0].document_properties assert props.get("document_class", {}).get("class_name") == "openminds_subject" dep_names = {d["name"]: d["value"] for d in props.get("depends_on", [])} diff --git a/tests/test_phase8.py b/tests/test_phase8.py index 165af43..4368939 100644 --- a/tests/test_phase8.py +++ b/tests/test_phase8.py @@ -7,14 +7,14 @@ import pytest from ndi import ( - Dataset, - DirSession, - Document, - Element, - ElementTimeseries, - Neuron, - Query, - Subject, + ndi_dataset, + ndi_document, + ndi_element, + ndi_element_timeseries, + ndi_neuron, + ndi_query, + ndi_session_dir, + ndi_subject, ) # =========================================================================== @@ -30,173 +30,173 @@ def temp_dir(tmp_path): @pytest.fixture def session(temp_dir): - """Create a DirSession for testing.""" + """Create a ndi_session_dir for testing.""" session_path = temp_dir / "session1" session_path.mkdir(parents=True, exist_ok=True) - return DirSession("TestSession", session_path) + return ndi_session_dir("TestSession", session_path) @pytest.fixture def session2(temp_dir): - """Create a second DirSession for testing.""" + """Create a second ndi_session_dir for testing.""" session_path = temp_dir / "session2" session_path.mkdir(parents=True, exist_ok=True) - return DirSession("TestSession2", session_path) + return ndi_session_dir("TestSession2", session_path) # =========================================================================== -# Subject Tests +# ndi_subject Tests # =========================================================================== class TestSubjectCreation: - """Test Subject construction.""" + """Test ndi_subject construction.""" def test_create_subject(self): """Test basic subject creation.""" - s = Subject("mouse23@vhlab.org", "Laboratory mouse") + s = ndi_subject("mouse23@vhlab.org", "Laboratory mouse") assert s.local_identifier == "mouse23@vhlab.org" assert s.description == "Laboratory mouse" assert s.id # Should have an auto-generated ID def test_create_subject_empty_description(self): """Test creating subject with empty description.""" - s = Subject("rat1@lab.org", "") + s = ndi_subject("rat1@lab.org", "") assert s.local_identifier == "rat1@lab.org" assert s.description == "" def test_create_subject_invalid_no_at(self): """Test that subject without '@' raises ValueError.""" with pytest.raises(ValueError, match="must contain '@'"): - Subject("mouse23", "no at sign") + ndi_subject("mouse23", "no at sign") def test_create_subject_invalid_spaces(self): """Test that subject with spaces raises ValueError.""" with pytest.raises(ValueError, match="cannot contain spaces"): - Subject("mouse 23@lab.org", "has spaces") + ndi_subject("mouse 23@lab.org", "has spaces") def test_create_subject_empty_identifier_ok(self): """Test that empty identifier doesn't raise.""" - s = Subject("", "") + s = ndi_subject("", "") assert s.local_identifier == "" def test_subject_repr(self): """Test string representation.""" - s = Subject("mouse23@vhlab.org", "Lab mouse") + s = ndi_subject("mouse23@vhlab.org", "ndi_gui_Lab mouse") assert "mouse23@vhlab.org" in repr(s) def test_subject_equality(self): """Test subject equality by local_identifier.""" - s1 = Subject("mouse23@vhlab.org", "Mouse A") - s2 = Subject("mouse23@vhlab.org", "Mouse B") - s3 = Subject("rat1@lab.org", "Rat") + s1 = ndi_subject("mouse23@vhlab.org", "Mouse A") + s2 = ndi_subject("mouse23@vhlab.org", "Mouse B") + s3 = ndi_subject("rat1@lab.org", "Rat") assert s1 == s2 assert s1 != s3 class TestSubjectValidation: - """Test Subject validation.""" + """Test ndi_subject validation.""" def test_valid_identifier(self): """Test valid identifier.""" - valid, msg = Subject.is_valid_local_identifier("mouse@lab.org") + valid, msg = ndi_subject.is_valid_local_identifier("mouse@lab.org") assert valid assert msg == "" def test_invalid_no_at(self): """Test invalid identifier without @.""" - valid, msg = Subject.is_valid_local_identifier("mouselaborg") + valid, msg = ndi_subject.is_valid_local_identifier("mouselaborg") assert not valid assert "@" in msg def test_invalid_spaces(self): """Test invalid identifier with spaces.""" - valid, msg = Subject.is_valid_local_identifier("mouse @lab.org") + valid, msg = ndi_subject.is_valid_local_identifier("mouse @lab.org") assert not valid assert "spaces" in msg def test_invalid_empty(self): """Test invalid empty identifier.""" - valid, msg = Subject.is_valid_local_identifier("") + valid, msg = ndi_subject.is_valid_local_identifier("") assert not valid assert "empty" in msg def test_invalid_non_string(self): """Test invalid non-string identifier.""" - valid, msg = Subject.is_valid_local_identifier(123) + valid, msg = ndi_subject.is_valid_local_identifier(123) assert not valid class TestSubjectDocument: - """Test Subject document operations.""" + """Test ndi_subject document operations.""" def test_newdocument(self): """Test creating a subject document.""" - s = Subject("mouse23@vhlab.org", "Lab mouse") + s = ndi_subject("mouse23@vhlab.org", "ndi_gui_Lab mouse") try: doc = s.newdocument() assert doc is not None assert doc.doc_class() == "subject" props = doc.document_properties assert props["subject"]["local_identifier"] == "mouse23@vhlab.org" - assert props["subject"]["description"] == "Lab mouse" + assert props["subject"]["description"] == "ndi_gui_Lab mouse" except FileNotFoundError: - pytest.skip("Subject schema not available") + pytest.skip("ndi_subject schema not available") def test_searchquery(self): """Test creating a search query for subject.""" - s = Subject("mouse23@vhlab.org", "Lab mouse") + s = ndi_subject("mouse23@vhlab.org", "ndi_gui_Lab mouse") q = s.searchquery() assert q is not None def test_load_from_session(self, session): """Test loading subject from session.""" try: - s1 = Subject("mouse23@vhlab.org", "Lab mouse") + s1 = ndi_subject("mouse23@vhlab.org", "ndi_gui_Lab mouse") doc = s1.newdocument() doc.set_session_id(session.id()) session.database_add(doc) # Load from session + document - s2 = Subject(session, doc) + s2 = ndi_subject(session, doc) assert s2.local_identifier == "mouse23@vhlab.org" - assert s2.description == "Lab mouse" + assert s2.description == "ndi_gui_Lab mouse" except FileNotFoundError: - pytest.skip("Subject schema not available") + pytest.skip("ndi_subject schema not available") def test_load_from_session_by_id(self, session): """Test loading subject from session by document ID.""" try: - s1 = Subject("mouse23@vhlab.org", "Lab mouse") + s1 = ndi_subject("mouse23@vhlab.org", "ndi_gui_Lab mouse") doc = s1.newdocument() doc.set_session_id(session.id()) session.database_add(doc) # Load from session + doc_id - s2 = Subject(session, doc.id) + s2 = ndi_subject(session, doc.id) assert s2.local_identifier == "mouse23@vhlab.org" except FileNotFoundError: - pytest.skip("Subject schema not available") + pytest.skip("ndi_subject schema not available") def test_does_subjectstring_match(self, session): """Test does_subjectstring_match_session_document.""" try: - s = Subject("mouse23@vhlab.org", "Lab mouse") + s = ndi_subject("mouse23@vhlab.org", "ndi_gui_Lab mouse") doc = s.newdocument() doc.set_session_id(session.id()) session.database_add(doc) - found, sid = Subject.does_subjectstring_match_session_document( + found, sid = ndi_subject.does_subjectstring_match_session_document( session, "mouse23@vhlab.org" ) assert found assert sid == doc.id except FileNotFoundError: - pytest.skip("Subject schema not available") + pytest.skip("ndi_subject schema not available") def test_does_subjectstring_not_found(self, session): """Test subject string not found.""" - found, sid = Subject.does_subjectstring_match_session_document( + found, sid = ndi_subject.does_subjectstring_match_session_document( session, "nonexistent@lab.org" ) assert not found @@ -205,26 +205,26 @@ def test_does_subjectstring_not_found(self, session): def test_does_subjectstring_make_if_missing(self, session): """Test making subject if missing.""" try: - found, sid = Subject.does_subjectstring_match_session_document( + found, sid = ndi_subject.does_subjectstring_match_session_document( session, "newmouse@lab.org", make_if_missing=True ) assert found assert sid is not None except FileNotFoundError: - pytest.skip("Subject schema not available") + pytest.skip("ndi_subject schema not available") # =========================================================================== -# ElementTimeseries Tests +# ndi_element_timeseries Tests # =========================================================================== class TestElementTimeseries: - """Test ElementTimeseries class.""" + """Test ndi_element_timeseries class.""" def test_create_timeseries_element(self, session): """Test creating a timeseries element.""" - ts = ElementTimeseries( + ts = ndi_element_timeseries( session=session, name="signal1", reference=1, @@ -235,23 +235,23 @@ def test_create_timeseries_element(self, session): assert ts.type == "voltage" def test_is_subclass_of_element(self): - """Test that ElementTimeseries is a subclass of Element.""" - assert issubclass(ElementTimeseries, Element) + """Test that ndi_element_timeseries is a subclass of ndi_element.""" + assert issubclass(ndi_element_timeseries, ndi_element) def test_repr(self, session): """Test string representation.""" - ts = ElementTimeseries( + ts = ndi_element_timeseries( session=session, name="signal1", reference=1, type="voltage", ) - assert "ElementTimeseries" in repr(ts) + assert "ndi_element_timeseries" in repr(ts) assert "signal1" in repr(ts) def test_readtimeseries_empty(self, session): """Test reading from element with no data.""" - ts = ElementTimeseries( + ts = ndi_element_timeseries( session=session, name="signal1", reference=1, @@ -263,7 +263,7 @@ def test_readtimeseries_empty(self, session): def test_samplerate_default(self, session): """Test default sample rate.""" - ts = ElementTimeseries( + ts = ndi_element_timeseries( session=session, name="signal1", reference=1, @@ -273,26 +273,26 @@ def test_samplerate_default(self, session): def test_readtimeseries_no_session(self): """Test that readtimeseries without session raises.""" - ts = ElementTimeseries( + ts = ndi_element_timeseries( name="signal1", reference=1, type="voltage", ) - with pytest.raises(ValueError, match="Session required"): + with pytest.raises(ValueError, match="ndi_session required"): ts.readtimeseries(1) # =========================================================================== -# Neuron Tests +# ndi_neuron Tests # =========================================================================== class TestNeuron: - """Test Neuron class.""" + """Test ndi_neuron class.""" def test_create_neuron(self, session): """Test creating a neuron.""" - n = Neuron( + n = ndi_neuron( session=session, name="neuron1", reference=1, @@ -302,40 +302,40 @@ def test_create_neuron(self, session): assert n.type == "neuron" def test_is_subclass_of_element_timeseries(self): - """Test that Neuron is a subclass of ElementTimeseries.""" - assert issubclass(Neuron, ElementTimeseries) - assert issubclass(Neuron, Element) + """Test that ndi_neuron is a subclass of ndi_element_timeseries.""" + assert issubclass(ndi_neuron, ndi_element_timeseries) + assert issubclass(ndi_neuron, ndi_element) def test_repr(self, session): """Test string representation.""" - n = Neuron(session=session, name="neuron1", reference=1) - assert "Neuron" in repr(n) + n = ndi_neuron(session=session, name="neuron1", reference=1) + assert "ndi_neuron" in repr(n) assert "neuron1" in repr(n) def test_neuron_type_forced(self, session): """Test that neuron type is always 'neuron'.""" - n = Neuron(session=session, name="neuron1", reference=1) + n = ndi_neuron(session=session, name="neuron1", reference=1) assert n.type == "neuron" def test_epochsetname(self, session): """Test neuron epoch set name.""" - n = Neuron(session=session, name="neuron1", reference=1) + n = ndi_neuron(session=session, name="neuron1", reference=1) assert "neuron" in n.epochsetname().lower() def test_issyncgraphroot(self, session): """Test that neurons are not sync graph roots.""" - n = Neuron(session=session, name="neuron1", reference=1) + n = ndi_neuron(session=session, name="neuron1", reference=1) assert n.issyncgraphroot() is False def test_neuron_with_underlying(self, session): """Test neuron with underlying element.""" - probe = Element( + probe = ndi_element( session=session, name="electrode1", reference=1, type="n-trode", ) - n = Neuron( + n = ndi_neuron( session=session, name="neuron1", reference=1, @@ -345,7 +345,7 @@ def test_neuron_with_underlying(self, session): def test_neuron_newdocument(self, session): """Test creating neuron document.""" - n = Neuron(session=session, name="neuron1", reference=1) + n = ndi_neuron(session=session, name="neuron1", reference=1) doc = n.newdocument() assert doc is not None props = doc.document_properties @@ -354,13 +354,13 @@ def test_neuron_newdocument(self, session): def test_neuron_searchquery(self, session): """Test neuron search query.""" - n = Neuron(session=session, name="neuron1", reference=1) + n = ndi_neuron(session=session, name="neuron1", reference=1) q = n.searchquery() assert q is not None def test_neuron_subject_id(self, session): """Test neuron with subject ID.""" - n = Neuron( + n = ndi_neuron( session=session, name="neuron1", reference=1, @@ -370,43 +370,43 @@ def test_neuron_subject_id(self, session): # =========================================================================== -# Dataset Tests +# ndi_dataset Tests # =========================================================================== class TestDatasetCreation: - """Test Dataset creation.""" + """Test ndi_dataset creation.""" def test_create_dataset(self, temp_dir): """Test creating a new dataset.""" - ds = Dataset(temp_dir / "dataset1", "MyDataset") + ds = ndi_dataset(temp_dir / "dataset1", "MyDataset") assert ds.reference == "MyDataset" assert ds.getpath() == temp_dir / "dataset1" def test_create_dataset_auto_reference(self, temp_dir): """Test dataset with auto-generated reference.""" - ds = Dataset(temp_dir / "experiment_2026") + ds = ndi_dataset(temp_dir / "experiment_2026") assert ds.reference == "experiment_2026" def test_dataset_id(self, temp_dir): """Test that dataset has a unique ID.""" - ds = Dataset(temp_dir / "dataset1", "Test") + ds = ndi_dataset(temp_dir / "dataset1", "Test") assert ds.id() assert len(ds.id()) > 0 def test_dataset_repr(self, temp_dir): """Test dataset string representation.""" - ds = Dataset(temp_dir / "dataset1", "Test") - assert "Dataset" in repr(ds) + ds = ndi_dataset(temp_dir / "dataset1", "Test") + assert "ndi_dataset" in repr(ds) assert "Test" in repr(ds) class TestDatasetSessions: - """Test Dataset session management.""" + """Test ndi_dataset session management.""" def test_add_linked_session(self, temp_dir, session): """Test adding a linked session.""" - ds = Dataset(temp_dir / "dataset1", "Test") + ds = ndi_dataset(temp_dir / "dataset1", "Test") ds.add_linked_session(session) refs, session_ids, *_ = ds.session_list() @@ -415,7 +415,7 @@ def test_add_linked_session(self, temp_dir, session): def test_add_multiple_sessions(self, temp_dir, session, session2): """Test adding multiple sessions.""" - ds = Dataset(temp_dir / "dataset1", "Test") + ds = ndi_dataset(temp_dir / "dataset1", "Test") ds.add_linked_session(session) ds.add_linked_session(session2) @@ -424,7 +424,7 @@ def test_add_multiple_sessions(self, temp_dir, session, session2): def test_add_duplicate_session(self, temp_dir, session): """Test that adding same session twice raises ValueError.""" - ds = Dataset(temp_dir / "dataset1", "Test") + ds = ndi_dataset(temp_dir / "dataset1", "Test") ds.add_linked_session(session) with pytest.raises(ValueError, match="already part of"): @@ -436,11 +436,11 @@ def test_add_duplicate_session(self, temp_dir, session): def test_add_ingested_session(self, temp_dir, session): """Test ingesting a session.""" # Add a document to the source session first - doc = Document("base") + doc = ndi_document("base") doc.set_session_id(session.id()) session.database_add(doc) - ds = Dataset(temp_dir / "dataset1", "Test") + ds = ndi_dataset(temp_dir / "dataset1", "Test") ds.add_ingested_session(session) refs, session_ids, *_ = ds.session_list() @@ -448,7 +448,7 @@ def test_add_ingested_session(self, temp_dir, session): def test_unlink_session(self, temp_dir, session): """Test unlinking a session.""" - ds = Dataset(temp_dir / "dataset1", "Test") + ds = ndi_dataset(temp_dir / "dataset1", "Test") ds.add_linked_session(session) refs, session_ids, *_ = ds.session_list() assert len(session_ids) == 1 @@ -459,27 +459,27 @@ def test_unlink_session(self, temp_dir, session): def test_unlink_nonexistent(self, temp_dir): """Test unlinking a nonexistent session raises ValueError.""" - ds = Dataset(temp_dir / "dataset1", "Test") + ds = ndi_dataset(temp_dir / "dataset1", "Test") with pytest.raises(ValueError, match="not found"): ds.unlink_session("nonexistent_id", are_you_sure=True) def test_unlink_requires_confirmation(self, temp_dir, session): """Test that unlinking requires are_you_sure=True.""" - ds = Dataset(temp_dir / "dataset1", "Test") + ds = ndi_dataset(temp_dir / "dataset1", "Test") ds.add_linked_session(session) with pytest.raises(ValueError, match="are_you_sure"): ds.unlink_session(session.id()) def test_session_list_empty(self, temp_dir): """Test session list on empty dataset.""" - ds = Dataset(temp_dir / "dataset1", "Test") + ds = ndi_dataset(temp_dir / "dataset1", "Test") refs, session_ids, *_ = ds.session_list() assert refs == [] assert session_ids == [] def test_session_list_details(self, temp_dir, session): """Test session list returns refs and ids.""" - ds = Dataset(temp_dir / "dataset1", "Test") + ds = ndi_dataset(temp_dir / "dataset1", "Test") ds.add_linked_session(session) refs, session_ids, *_ = ds.session_list() @@ -490,35 +490,35 @@ def test_session_list_details(self, temp_dir, session): class TestDatasetDatabase: - """Test Dataset database operations.""" + """Test ndi_dataset database operations.""" def test_database_add_search(self, temp_dir): """Test adding and searching documents in dataset.""" - ds = Dataset(temp_dir / "dataset1", "Test") - doc = Document("base") + ds = ndi_dataset(temp_dir / "dataset1", "Test") + doc = ndi_document("base") ds.database_add(doc) - results = ds.database_search(Query("base.id") == doc.id) + results = ds.database_search(ndi_query("base.id") == doc.id) assert len(results) == 1 assert results[0].id == doc.id def test_database_rm(self, temp_dir): """Test removing documents from dataset.""" - ds = Dataset(temp_dir / "dataset1", "Test") - doc = Document("base") + ds = ndi_dataset(temp_dir / "dataset1", "Test") + doc = ndi_document("base") ds.database_add(doc) ds.database_rm(doc) - results = ds.database_search(Query("base.id") == doc.id) + results = ds.database_search(ndi_query("base.id") == doc.id) assert len(results) == 0 class TestDatasetIngestion: - """Test Dataset ingestion and deletion.""" + """Test ndi_dataset ingestion and deletion.""" def test_deleteIngestedSession_requires_confirmation(self, temp_dir, session): """Test that deleting ingested session requires confirmation.""" - ds = Dataset(temp_dir / "dataset1", "Test") + ds = ndi_dataset(temp_dir / "dataset1", "Test") ds.add_ingested_session(session) with pytest.raises(ValueError, match="are_you_sure"): @@ -526,7 +526,7 @@ def test_deleteIngestedSession_requires_confirmation(self, temp_dir, session): def test_deleteIngestedSession(self, temp_dir, session): """Test deleting an ingested session.""" - ds = Dataset(temp_dir / "dataset1", "Test") + ds = ndi_dataset(temp_dir / "dataset1", "Test") ds.add_ingested_session(session) refs, session_ids, *_ = ds.session_list() assert len(session_ids) == 1 @@ -537,7 +537,7 @@ def test_deleteIngestedSession(self, temp_dir, session): def test_delete_linked_session_raises(self, temp_dir, session): """Test that deleting a linked session raises error.""" - ds = Dataset(temp_dir / "dataset1", "Test") + ds = ndi_dataset(temp_dir / "dataset1", "Test") ds.add_linked_session(session) with pytest.raises(ValueError, match="linked"): @@ -555,7 +555,7 @@ class TestPhase8Integration: def test_subject_in_session(self, session): """Test creating and storing a subject in a session.""" try: - s = Subject("mouse23@vhlab.org", "C57BL/6 mouse") + s = ndi_subject("mouse23@vhlab.org", "C57BL/6 mouse") doc = s.newdocument() doc.set_session_id(session.id()) session.database_add(doc) @@ -565,17 +565,17 @@ def test_subject_in_session(self, session): results = session.database_search(q) assert len(results) >= 1 except FileNotFoundError: - pytest.skip("Subject schema not available") + pytest.skip("ndi_subject schema not available") def test_neuron_with_subject(self, session): """Test creating a neuron element associated with a subject.""" try: - s = Subject("mouse23@vhlab.org", "Lab mouse") + s = ndi_subject("mouse23@vhlab.org", "ndi_gui_Lab mouse") sdoc = s.newdocument() sdoc.set_session_id(session.id()) session.database_add(sdoc) - n = Neuron( + n = ndi_neuron( session=session, name="neuron1", reference=1, @@ -585,7 +585,7 @@ def test_neuron_with_subject(self, session): session.database_add(ndoc) # Verify neuron document - results = session.database_search(Query("base.id") == ndoc.id) + results = session.database_search(ndi_query("base.id") == ndoc.id) assert len(results) == 1 except FileNotFoundError: pytest.skip("Schema not available") @@ -594,18 +594,18 @@ def test_dataset_with_sessions_and_subjects(self, temp_dir, session, session2): """Test full dataset workflow with sessions and subjects.""" try: # Add subjects to sessions - s1 = Subject("mouse1@lab.org", "Mouse 1") + s1 = ndi_subject("mouse1@lab.org", "Mouse 1") s1doc = s1.newdocument() s1doc.set_session_id(session.id()) session.database_add(s1doc) - s2 = Subject("mouse2@lab.org", "Mouse 2") + s2 = ndi_subject("mouse2@lab.org", "Mouse 2") s2doc = s2.newdocument() s2doc.set_session_id(session2.id()) session2.database_add(s2doc) # Create dataset - ds = Dataset(temp_dir / "experiment", "Full Experiment") + ds = ndi_dataset(temp_dir / "experiment", "Full Experiment") ds.add_linked_session(session) ds.add_linked_session(session2) @@ -616,25 +616,25 @@ def test_dataset_with_sessions_and_subjects(self, temp_dir, session, session2): pytest.skip("Schema not available") def test_element_timeseries_inheritance(self, session): - """Test that ElementTimeseries properly inherits Element behavior.""" - ts = ElementTimeseries( + """Test that ndi_element_timeseries properly inherits ndi_element behavior.""" + ts = ndi_element_timeseries( session=session, name="signal1", reference=1, type="voltage", ) - # Should have Element properties + # Should have ndi_element properties assert ts.name == "signal1" assert ts.elementstring() == "signal1 | 1" - # Should have DocumentService methods + # Should have ndi_documentservice methods doc = ts.newdocument() assert doc is not None assert doc.document_properties["element"]["name"] == "signal1" def test_neuron_inherits_timeseries(self, session): - """Test that Neuron has timeseries capabilities.""" - n = Neuron(session=session, name="neuron1", reference=1) + """Test that ndi_neuron has timeseries capabilities.""" + n = ndi_neuron(session=session, name="neuron1", reference=1) # Should have readtimeseries assert hasattr(n, "readtimeseries") diff --git a/tests/test_phase9.py b/tests/test_phase9.py index 02598ba..2cb5b9f 100644 --- a/tests/test_phase9.py +++ b/tests/test_phase9.py @@ -7,15 +7,15 @@ import pytest from ndi import ( - App, - AppDoc, - Calculator, - DirSession, DocExistsAction, - Document, - Query, + ndi_app, + ndi_app_appdoc, + ndi_calculator, + ndi_document, + ndi_query, + ndi_session_dir, ) -from ndi.calc.example import SimpleCalc +from ndi.calc.example import ndi_calc_example_simple # =========================================================================== # Fixtures @@ -30,70 +30,70 @@ def temp_dir(tmp_path): @pytest.fixture def session(temp_dir): - """Create a DirSession for testing.""" + """Create a ndi_session_dir for testing.""" session_path = temp_dir / "session1" session_path.mkdir(parents=True, exist_ok=True) - return DirSession("TestSession", session_path) + return ndi_session_dir("TestSession", session_path) # =========================================================================== -# App Tests +# ndi_app Tests # =========================================================================== class TestAppCreation: - """Test App construction.""" + """Test ndi_app construction.""" def test_default_construction(self): - app = App() + app = ndi_app() assert app.session is None assert app.name == "generic" def test_construction_with_session(self, session): - app = App(session=session, name="my_analysis") + app = ndi_app(session=session, name="my_analysis") assert app.session is session assert app.name == "my_analysis" def test_repr(self): - app = App(name="test_app") - assert repr(app) == "App('test_app')" + app = ndi_app(name="test_app") + assert repr(app) == "ndi_app('test_app')" class TestAppVarAppName: - """Test App.varappname() sanitization.""" + """Test ndi_app.varappname() sanitization.""" def test_simple_name(self): - app = App(name="my_app") + app = ndi_app(name="my_app") assert app.varappname() == "my_app" def test_name_with_dots(self): - app = App(name="my.app.v2") + app = ndi_app(name="my.app.v2") assert app.varappname() == "my_app_v2" def test_name_with_spaces(self): - app = App(name="my app name") + app = ndi_app(name="my app name") assert app.varappname() == "my_app_name" def test_name_starting_with_digit(self): - app = App(name="3dplot") + app = ndi_app(name="3dplot") assert app.varappname() == "_3dplot" def test_empty_name(self): - app = App(name="") + app = ndi_app(name="") assert app.varappname() == "_app" def test_special_characters(self): - app = App(name="test@app#v1") + app = ndi_app(name="test@app#v1") result = app.varappname() assert result == "test_app_v1" assert result.isidentifier() class TestAppVersionUrl: - """Test App.version_url().""" + """Test ndi_app.version_url().""" def test_returns_tuple(self): - app = App(name="test") + app = ndi_app(name="test") version, url = app.version_url() assert isinstance(version, str) assert isinstance(url, str) @@ -102,44 +102,44 @@ def test_returns_tuple(self): class TestAppNewDocument: - """Test App.newdocument().""" + """Test ndi_app.newdocument().""" def test_creates_app_document(self): - app = App(name="my_analysis") + app = ndi_app(name="my_analysis") doc = app.newdocument() - assert isinstance(doc, Document) + assert isinstance(doc, ndi_document) props = doc.document_properties assert "app" in props assert props["app"]["name"] == "my_analysis" def test_includes_interpreter_info(self): - app = App(name="test") + app = ndi_app(name="test") doc = app.newdocument() props = doc.document_properties assert props["app"]["interpreter"] == "Python" assert "interpreter_version" in props["app"] def test_includes_os_info(self): - app = App(name="test") + app = ndi_app(name="test") doc = app.newdocument() props = doc.document_properties assert "os" in props["app"] assert "os_version" in props["app"] def test_includes_version_and_url(self): - app = App(name="test") + app = ndi_app(name="test") doc = app.newdocument() props = doc.document_properties assert "version" in props["app"] assert "url" in props["app"] def test_sets_session_id_when_session_provided(self, session): - app = App(session=session, name="test") + app = ndi_app(session=session, name="test") doc = app.newdocument() assert doc.session_id == session.id() def test_no_session_id_when_no_session(self): - app = App(name="test") + app = ndi_app(name="test") doc = app.newdocument() # session_id should be empty or unset sid = doc.session_id @@ -147,15 +147,15 @@ def test_no_session_id_when_no_session(self): class TestAppSearchQuery: - """Test App.searchquery().""" + """Test ndi_app.searchquery().""" def test_returns_query(self): - app = App(name="my_analysis") + app = ndi_app(name="my_analysis") q = app.searchquery() assert q is not None def test_query_matches_app_document(self, session): - app = App(session=session, name="my_analysis") + app = ndi_app(session=session, name="my_analysis") doc = app.newdocument() session.database_add(doc) @@ -165,7 +165,7 @@ def test_query_matches_app_document(self, session): # =========================================================================== -# AppDoc Tests +# ndi_app_appdoc Tests # =========================================================================== @@ -187,16 +187,16 @@ def test_all_values(self): class TestAppDocCreation: - """Test AppDoc construction.""" + """Test ndi_app_appdoc construction.""" def test_default_construction(self): - ad = AppDoc() + ad = ndi_app_appdoc() assert ad.doc_types == [] assert ad.doc_document_types == [] assert ad.doc_session is None def test_construction_with_types(self): - ad = AppDoc( + ad = ndi_app_appdoc( doc_types=["my_type"], doc_document_types=["my_doc_type"], ) @@ -205,55 +205,55 @@ def test_construction_with_types(self): class TestAppDocBaseMethods: - """Test AppDoc base class method defaults.""" + """Test ndi_app_appdoc base class method defaults.""" def test_struct2doc_returns_none(self): - ad = AppDoc() + ad = ndi_app_appdoc() result = ad.struct2doc("test_type", {"key": "value"}) assert result is None def test_find_appdoc_returns_empty(self): - ad = AppDoc() + ad = ndi_app_appdoc() result = ad.find_appdoc("test_type") assert result == [] def test_defaultstruct_returns_empty_dict(self): - ad = AppDoc() + ad = ndi_app_appdoc() result = ad.defaultstruct_appdoc("test_type") assert result == {} def test_loaddata_returns_none(self): - ad = AppDoc() + ad = ndi_app_appdoc() result = ad.loaddata_appdoc("test_type") assert result is None def test_isvalid_returns_false(self): - ad = AppDoc() + ad = ndi_app_appdoc() valid, msg = ad.isvalid_appdoc_struct("test_type", {}) assert valid is False assert isinstance(msg, str) def test_isequal_compares_dicts(self): - ad = AppDoc() + ad = ndi_app_appdoc() assert ad.isequal_appdoc_struct("t", {"a": 1}, {"a": 1}) is True assert ad.isequal_appdoc_struct("t", {"a": 1}, {"a": 2}) is False class TestAppDocAddAppdoc: - """Test AppDoc.add_appdoc() with doc_exists_action.""" + """Test ndi_app_appdoc.add_appdoc() with doc_exists_action.""" def test_add_appdoc_no_existing_no_struct2doc(self): """Base class struct2doc returns None, so add returns empty.""" - ad = AppDoc() + ad = ndi_app_appdoc() result = ad.add_appdoc("test_type") assert result == [] def test_add_appdoc_error_when_exists(self): """Test that ERROR action raises when doc exists.""" - class FakeAppDoc(AppDoc): + class FakeAppDoc(ndi_app_appdoc): def find_appdoc(self, appdoc_type, *args, **kwargs): - return [Document("base")] + return [ndi_document("base")] ad = FakeAppDoc() with pytest.raises(RuntimeError, match="already exists"): @@ -261,9 +261,9 @@ def find_appdoc(self, appdoc_type, *args, **kwargs): def test_add_appdoc_no_action_returns_existing(self): """Test that NO_ACTION returns existing docs.""" - existing = [Document("base")] + existing = [ndi_document("base")] - class FakeAppDoc(AppDoc): + class FakeAppDoc(ndi_app_appdoc): def find_appdoc(self, appdoc_type, *args, **kwargs): return existing @@ -273,17 +273,17 @@ def find_appdoc(self, appdoc_type, *args, **kwargs): class TestAppDocClear: - """Test AppDoc.clear_appdoc().""" + """Test ndi_app_appdoc.clear_appdoc().""" def test_clear_no_docs(self): - ad = AppDoc() + ad = ndi_app_appdoc() result = ad.clear_appdoc("test_type") assert result is False def test_clear_with_docs_no_session(self): - class FakeAppDoc(AppDoc): + class FakeAppDoc(ndi_app_appdoc): def find_appdoc(self, appdoc_type, *args, **kwargs): - return [Document("base")] + return [ndi_document("base")] ad = FakeAppDoc() result = ad.clear_appdoc("test_type") @@ -291,44 +291,44 @@ def find_appdoc(self, appdoc_type, *args, **kwargs): # =========================================================================== -# Calculator Tests +# ndi_calculator Tests # =========================================================================== class TestCalculatorCreation: - """Test Calculator construction.""" + """Test ndi_calculator construction.""" def test_default_construction(self): - calc = Calculator() + calc = ndi_calculator() assert calc.session is None assert calc.doc_types == [] def test_construction_with_session(self, session): - calc = Calculator(session=session, document_type="my_calc") + calc = ndi_calculator(session=session, document_type="my_calc") assert calc.session is session assert calc.doc_types == ["my_calc"] assert calc.doc_document_types == ["my_calc"] def test_name_is_class_name(self): - calc = Calculator() - assert calc.name == "Calculator" + calc = ndi_calculator() + assert calc.name == "ndi_calculator" def test_repr(self): - calc = Calculator(document_type="my_calc") - assert "Calculator" in repr(calc) + calc = ndi_calculator(document_type="my_calc") + assert "ndi_calculator" in repr(calc) assert "my_calc" in repr(calc) class TestCalculatorDefaultMethods: - """Test Calculator base class defaults.""" + """Test ndi_calculator base class defaults.""" def test_calculate_returns_empty(self): - calc = Calculator() + calc = ndi_calculator() result = calc.calculate({"input_parameters": {}, "depends_on": []}) assert result == [] def test_default_search_for_input_parameters(self): - calc = Calculator() + calc = ndi_calculator() params = calc.default_search_for_input_parameters() assert "input_parameters" in params assert "depends_on" in params @@ -336,25 +336,25 @@ def test_default_search_for_input_parameters(self): assert params["depends_on"] == [] def test_are_input_parameters_equivalent(self): - calc = Calculator() + calc = ndi_calculator() assert calc.are_input_parameters_equivalent({"a": 1}, {"a": 1}) is True assert calc.are_input_parameters_equivalent({"a": 1}, {"a": 2}) is False def test_is_valid_dependency_input(self): - calc = Calculator() + calc = ndi_calculator() assert calc.is_valid_dependency_input("doc_id", "abc123") is True def test_default_parameters_query(self): - calc = Calculator() + calc = ndi_calculator() result = calc.default_parameters_query({}) assert result == [] class TestCalculatorSearchForInputParameters: - """Test Calculator.search_for_input_parameters().""" + """Test ndi_calculator.search_for_input_parameters().""" def test_no_queries_returns_single_set(self): - calc = Calculator() + calc = ndi_calculator() params = { "input_parameters": {"answer": 5}, "depends_on": [], @@ -364,27 +364,27 @@ def test_no_queries_returns_single_set(self): assert result[0]["input_parameters"] == {"answer": 5} def test_no_session_returns_empty(self): - calc = Calculator() + calc = ndi_calculator() params = { "input_parameters": {"answer": 5}, "depends_on": [], - "query": [{"name": "doc_id", "query": Query("").isa("base")}], + "query": [{"name": "doc_id", "query": ndi_query("").isa("base")}], } result = calc.search_for_input_parameters(params) assert result == [] def test_with_session_and_query(self, session): # Add some documents to the session - doc1 = Document("base", **{"base.name": "test1"}) - doc2 = Document("base", **{"base.name": "test2"}) + doc1 = ndi_document("base", **{"base.name": "test1"}) + doc2 = ndi_document("base", **{"base.name": "test2"}) session.database_add(doc1) session.database_add(doc2) - calc = Calculator(session=session, document_type="test_calc") + calc = ndi_calculator(session=session, document_type="test_calc") params = { "input_parameters": {"answer": 5}, "depends_on": [], - "query": [{"name": "document_id", "query": Query("").isa("base")}], + "query": [{"name": "document_id", "query": ndi_query("").isa("base")}], } result = calc.search_for_input_parameters(params) # Should find one parameter set per base document @@ -394,7 +394,7 @@ def test_with_session_and_query(self, session): assert len(p["depends_on"]) >= 1 def test_fixed_depends_on_preserved(self): - calc = Calculator() + calc = ndi_calculator() params = { "input_parameters": {"x": 1}, "depends_on": [{"name": "fixed_dep", "value": "abc"}], @@ -405,10 +405,10 @@ def test_fixed_depends_on_preserved(self): class TestCalculatorSearchForDocs: - """Test Calculator.search_for_calculator_docs().""" + """Test ndi_calculator.search_for_calculator_docs().""" def test_no_session_returns_empty(self): - calc = Calculator(document_type="my_calc") + calc = ndi_calculator(document_type="my_calc") result = calc.search_for_calculator_docs( { "input_parameters": {}, @@ -418,7 +418,7 @@ def test_no_session_returns_empty(self): assert result == [] def test_no_doc_types_returns_empty(self, session): - calc = Calculator(session=session) + calc = ndi_calculator(session=session) result = calc.search_for_calculator_docs( { "input_parameters": {}, @@ -429,16 +429,16 @@ def test_no_doc_types_returns_empty(self, session): class TestCalculatorRun: - """Test Calculator.run() pipeline.""" + """Test ndi_calculator.run() pipeline.""" def test_run_no_session(self): - calc = Calculator() + calc = ndi_calculator() result = calc.run(DocExistsAction.ERROR) assert result == [] def test_run_with_empty_calculate(self, session): - """Calculator base returns [] from calculate, so run returns [].""" - calc = Calculator(session=session, document_type="test") + """ndi_calculator base returns [] from calculate, so run returns [].""" + calc = ndi_calculator(session=session, document_type="test") # No matching inputs, so calculate never called result = calc.run(DocExistsAction.ERROR) assert isinstance(result, list) @@ -446,9 +446,9 @@ def test_run_with_empty_calculate(self, session): def test_run_error_on_existing(self, session): """Test that ERROR action raises when docs already exist.""" - class TestCalc(Calculator): + class TestCalc(ndi_calculator): def calculate(self, parameters): - doc = Document("base", **{"base.name": "calc_result"}) + doc = ndi_document("base", **{"base.name": "calc_result"}) doc = doc.set_session_id(self._session.id()) return [doc] @@ -466,11 +466,11 @@ def default_search_for_input_parameters(self): def test_run_no_action_does_not_error(self, session): """NO_ACTION should not raise on second run.""" - # Use SimpleCalc which properly stores input_parameters - base_doc = Document("base", **{"base.name": "input"}) + # Use ndi_calc_example_simple which properly stores input_parameters + base_doc = ndi_document("base", **{"base.name": "input"}) session.database_add(base_doc) - sc = SimpleCalc(session=session) + sc = ndi_calc_example_simple(session=session) # First run creates docs docs1 = sc.run(DocExistsAction.REPLACE) assert len(docs1) >= 1 @@ -481,42 +481,42 @@ def test_run_no_action_does_not_error(self, session): # =========================================================================== -# SimpleCalc Tests +# ndi_calc_example_simple Tests # =========================================================================== class TestSimpleCalcCreation: - """Test SimpleCalc construction.""" + """Test ndi_calc_example_simple construction.""" def test_default_construction(self): - sc = SimpleCalc() + sc = ndi_calc_example_simple() assert sc.session is None assert sc.doc_types == ["simple_calc"] assert sc.doc_document_types == ["apps/calculators/simple_calc"] def test_construction_with_session(self, session): - sc = SimpleCalc(session=session) + sc = ndi_calc_example_simple(session=session) assert sc.session is session def test_repr(self): - sc = SimpleCalc() - assert "SimpleCalc" in repr(sc) + sc = ndi_calc_example_simple() + assert "ndi_calc_example_simple" in repr(sc) def test_name_is_class_name(self): - sc = SimpleCalc() - assert sc.name == "SimpleCalc" + sc = ndi_calc_example_simple() + assert sc.name == "ndi_calc_example_simple" class TestSimpleCalcDefaultParameters: - """Test SimpleCalc.default_search_for_input_parameters().""" + """Test ndi_calc_example_simple.default_search_for_input_parameters().""" def test_returns_answer_5(self): - sc = SimpleCalc() + sc = ndi_calc_example_simple() params = sc.default_search_for_input_parameters() assert params["input_parameters"] == {"answer": 5} def test_has_query_for_base(self): - sc = SimpleCalc() + sc = ndi_calc_example_simple() params = sc.default_search_for_input_parameters() assert "query" in params assert len(params["query"]) == 1 @@ -524,20 +524,20 @@ def test_has_query_for_base(self): class TestSimpleCalcCalculate: - """Test SimpleCalc.calculate().""" + """Test ndi_calc_example_simple.calculate().""" def test_calculate_returns_document(self): - sc = SimpleCalc() + sc = ndi_calc_example_simple() params = { "input_parameters": {"answer": 42}, "depends_on": [], } docs = sc.calculate(params) assert len(docs) == 1 - assert isinstance(docs[0], Document) + assert isinstance(docs[0], ndi_document) def test_calculate_stores_answer(self): - sc = SimpleCalc() + sc = ndi_calc_example_simple() params = { "input_parameters": {"answer": 42}, "depends_on": [], @@ -547,7 +547,7 @@ def test_calculate_stores_answer(self): assert props["simple_calc"]["answer"] == 42 def test_calculate_stores_input_parameters(self): - sc = SimpleCalc() + sc = ndi_calc_example_simple() params = { "input_parameters": {"answer": 7}, "depends_on": [], @@ -557,7 +557,7 @@ def test_calculate_stores_input_parameters(self): assert props["simple_calc"]["input_parameters"] == {"answer": 7} def test_calculate_sets_session_id(self, session): - sc = SimpleCalc(session=session) + sc = ndi_calc_example_simple(session=session) params = { "input_parameters": {"answer": 5}, "depends_on": [], @@ -566,7 +566,7 @@ def test_calculate_sets_session_id(self, session): assert docs[0].session_id == session.id() def test_calculate_sets_dependency(self): - sc = SimpleCalc() + sc = ndi_calc_example_simple() params = { "input_parameters": {"answer": 5}, "depends_on": [{"name": "document_id", "value": "abc-123"}], @@ -580,15 +580,15 @@ def test_calculate_sets_dependency(self): class TestSimpleCalcRun: - """Test SimpleCalc end-to-end run.""" + """Test ndi_calc_example_simple end-to-end run.""" def test_run_with_session(self, session): - """Full pipeline: add a base doc, run SimpleCalc, verify output.""" - # Add a base document for SimpleCalc to find - base_doc = Document("base", **{"base.name": "input_data"}) + """Full pipeline: add a base doc, run ndi_calc_example_simple, verify output.""" + # Add a base document for ndi_calc_example_simple to find + base_doc = ndi_document("base", **{"base.name": "input_data"}) session.database_add(base_doc) - sc = SimpleCalc(session=session) + sc = ndi_calc_example_simple(session=session) docs = sc.run(DocExistsAction.REPLACE) assert len(docs) >= 1 @@ -600,20 +600,20 @@ def test_run_with_session(self, session): def test_run_replace_mode(self, session): """Running twice with REPLACE should succeed.""" - base_doc = Document("base", **{"base.name": "input"}) + base_doc = ndi_document("base", **{"base.name": "input"}) session.database_add(base_doc) - sc = SimpleCalc(session=session) + sc = ndi_calc_example_simple(session=session) sc.run(DocExistsAction.REPLACE) docs2 = sc.run(DocExistsAction.REPLACE) assert len(docs2) >= 1 def test_run_no_action_mode(self, session): """Running with NO_ACTION should return existing on second run.""" - base_doc = Document("base", **{"base.name": "input"}) + base_doc = ndi_document("base", **{"base.name": "input"}) session.database_add(base_doc) - sc = SimpleCalc(session=session) + sc = ndi_calc_example_simple(session=session) sc.run(DocExistsAction.REPLACE) # NO_ACTION should not error docs2 = sc.run(DocExistsAction.NO_ACTION) @@ -621,14 +621,14 @@ def test_run_no_action_mode(self, session): def test_run_multiple_inputs(self, session): """Multiple base docs should produce multiple calculator outputs.""" - doc1 = Document("base", **{"base.name": "input1"}) - doc2 = Document("base", **{"base.name": "input2"}) - doc3 = Document("base", **{"base.name": "input3"}) + doc1 = ndi_document("base", **{"base.name": "input1"}) + doc2 = ndi_document("base", **{"base.name": "input2"}) + doc3 = ndi_document("base", **{"base.name": "input3"}) session.database_add(doc1) session.database_add(doc2) session.database_add(doc3) - sc = SimpleCalc(session=session) + sc = ndi_calc_example_simple(session=session) docs = sc.run(DocExistsAction.REPLACE) # Should produce at least 3 results (one per input) assert len(docs) >= 3 @@ -643,14 +643,14 @@ class TestPhase9Imports: """Test that all Phase 9 classes are importable from ndi.""" def test_import_app(self): - from ndi import App + from ndi import ndi_app - assert App is not None + assert ndi_app is not None def test_import_appdoc(self): - from ndi import AppDoc + from ndi import ndi_app_appdoc - assert AppDoc is not None + assert ndi_app_appdoc is not None def test_import_doc_exists_action(self): from ndi import DocExistsAction @@ -658,9 +658,9 @@ def test_import_doc_exists_action(self): assert DocExistsAction is not None def test_import_calculator(self): - from ndi import Calculator + from ndi import ndi_calculator - assert Calculator is not None + assert ndi_calculator is not None def test_import_calc_module(self): from ndi import calc @@ -668,36 +668,36 @@ def test_import_calc_module(self): assert calc is not None def test_import_simple_calc(self): - from ndi.calc.example import SimpleCalc + from ndi.calc.example import ndi_calc_example_simple - assert SimpleCalc is not None + assert ndi_calc_example_simple is not None def test_import_simple_calc_full_path(self): - from ndi.calc.example.simple import SimpleCalc + from ndi.calc.example.simple import ndi_calc_example_simple - assert SimpleCalc is not None + assert ndi_calc_example_simple is not None class TestPhase9Inheritance: """Test class hierarchies are correct.""" def test_app_extends_document_service(self): - from ndi import App, DocumentService + from ndi import ndi_app, ndi_documentservice - assert issubclass(App, DocumentService) + assert issubclass(ndi_app, ndi_documentservice) def test_calculator_extends_app(self): - assert issubclass(Calculator, App) + assert issubclass(ndi_calculator, ndi_app) def test_calculator_extends_appdoc(self): - assert issubclass(Calculator, AppDoc) + assert issubclass(ndi_calculator, ndi_app_appdoc) def test_simple_calc_extends_calculator(self): - assert issubclass(SimpleCalc, Calculator) + assert issubclass(ndi_calc_example_simple, ndi_calculator) def test_calculator_mro(self): - """Calculator MRO should have App before AppDoc.""" - mro = Calculator.__mro__ - app_idx = mro.index(App) - appdoc_idx = mro.index(AppDoc) + """ndi_calculator MRO should have ndi_app before ndi_app_appdoc.""" + mro = ndi_calculator.__mro__ + app_idx = mro.index(ndi_app) + appdoc_idx = mro.index(ndi_app_appdoc) assert app_idx < appdoc_idx diff --git a/tests/test_query.py b/tests/test_query.py index bedf3c9..4631f29 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -4,68 +4,68 @@ import pytest -from ndi import Query +from ndi import ndi_query class TestQueryBasic: - """Test basic Query construction.""" + """Test basic ndi_query construction.""" def test_create_query_with_field(self): """Test creating a query with a field name.""" - q = Query("base.name") + q = ndi_query("base.name") assert q.field == "base.name" def test_create_query_empty_field(self): """Test creating a query with empty field (for isa queries).""" - q = Query("") + q = ndi_query("") assert q.field == "" def test_query_equals(self): """Test equals operator.""" - q = Query("base.name") == "test_doc" + q = ndi_query("base.name") == "test_doc" assert q.field == "base.name" assert q.operator == "==" assert q.value == "test_doc" def test_query_not_equals(self): """Test not equals operator.""" - q = Query("base.name") != "test_doc" + q = ndi_query("base.name") != "test_doc" assert q.operator == "!=" assert q.value == "test_doc" def test_query_greater_than(self): """Test greater than operator.""" - q = Query("ephys.sample_rate") > 30000 + q = ndi_query("ephys.sample_rate") > 30000 assert q.operator == ">" assert q.value == 30000 def test_query_greater_than_or_equal(self): """Test greater than or equal operator.""" - q = Query("ephys.num_channels") >= 4 + q = ndi_query("ephys.num_channels") >= 4 assert q.operator == ">=" assert q.value == 4 def test_query_less_than(self): """Test less than operator.""" - q = Query("ephys.duration") < 100 + q = ndi_query("ephys.duration") < 100 assert q.operator == "<" assert q.value == 100 def test_query_less_than_or_equal(self): """Test less than or equal operator.""" - q = Query("count") <= 10 + q = ndi_query("count") <= 10 assert q.operator == "<=" assert q.value == 10 def test_query_contains(self): """Test contains method.""" - q = Query("base.name").contains("electrode") + q = ndi_query("base.name").contains("electrode") assert q.operator == "contains" assert q.value == "electrode" def test_query_match(self): """Test match (regex) method.""" - q = Query("base.name").match(r"^test_\d+$") + q = ndi_query("base.name").match(r"^test_\d+$") assert q.operator == "match" assert q.value == r"^test_\d+$" @@ -75,25 +75,25 @@ class TestQueryNDISpecific: def test_query_isa(self): """Test isa method for document class checking.""" - q = Query("").isa("ndi.document.element") + q = ndi_query("").isa("ndi.document.element") assert q.operator == "isa" assert q.value == "ndi.document.element" def test_query_depends_on(self): """Test depends_on method.""" - q = Query("").depends_on("element_id", "abc123") + q = ndi_query("").depends_on("element_id", "abc123") assert q.operator == "depends_on" assert q.value == ("element_id", "abc123") def test_query_has_field(self): """Test has_field method.""" - q = Query("optional_field").has_field() + q = ndi_query("optional_field").has_field() assert q.operator == "hasfield" assert q.value def test_query_has_member(self): """Test has_member method.""" - q = Query("tags").has_member("important") + q = ndi_query("tags").has_member("important") assert q.operator == "hasmember" assert q.value == "important" @@ -102,14 +102,14 @@ class TestQueryStaticMethods: """Test static methods.""" def test_query_all(self): - """Test Query.all() returns a query matching all documents.""" - q = Query.all() + """Test ndi_query.all() returns a query matching all documents.""" + q = ndi_query.all() assert q.operator == "isa" assert q.value == "base" def test_query_none(self): - """Test Query.none() returns a query matching no documents.""" - q = Query.none() + """Test ndi_query.none() returns a query matching no documents.""" + q = ndi_query.none() assert q.operator == "isa" # Value should be something that will never match assert "impossible" in q.value.lower() or len(q.value) > 20 @@ -120,55 +120,55 @@ class TestQueryFromSearch: def test_from_search_exact_string(self): """Test from_search with exact_string operation.""" - q = Query.from_search("base.name", "exact_string", "my_doc") + q = ndi_query.from_search("base.name", "exact_string", "my_doc") assert q.field == "base.name" assert q.value == "my_doc" def test_from_search_regexp(self): """Test from_search with regexp operation.""" - q = Query.from_search("base.name", "regexp", r"test_\d+") + q = ndi_query.from_search("base.name", "regexp", r"test_\d+") assert q.operator == "match" assert q.value == r"test_\d+" def test_from_search_isa(self): """Test from_search with isa operation.""" - q = Query.from_search("", "isa", "ndi.document.base") + q = ndi_query.from_search("", "isa", "ndi.document.base") assert q.operator == "isa" assert q.value == "ndi.document.base" def test_from_search_depends_on(self): """Test from_search with depends_on operation.""" - q = Query.from_search("", "depends_on", "session_id", "sess_123") + q = ndi_query.from_search("", "depends_on", "session_id", "sess_123") assert q.operator == "depends_on" assert q.value == ("session_id", "sess_123") def test_from_search_lessthan(self): """Test from_search with lessthan operation.""" - q = Query.from_search("count", "lessthan", 10) + q = ndi_query.from_search("count", "lessthan", 10) assert q.operator == "<" assert q.value == 10 def test_from_search_greaterthaneq(self): """Test from_search with greaterthaneq operation.""" - q = Query.from_search("rate", "greaterthaneq", 1000) + q = ndi_query.from_search("rate", "greaterthaneq", 1000) assert q.operator == ">=" assert q.value == 1000 def test_from_search_hasfield(self): """Test from_search with hasfield operation.""" - q = Query.from_search("optional", "hasfield", "") + q = ndi_query.from_search("optional", "hasfield", "") assert q.operator == "hasfield" def test_from_search_contains_string(self): """Test from_search with contains_string operation.""" - q = Query.from_search("description", "contains_string", "electrode") + q = ndi_query.from_search("description", "contains_string", "electrode") assert q.operator == "contains" assert q.value == "electrode" def test_from_search_invalid_operation(self): """Test from_search with invalid operation raises error.""" with pytest.raises(ValueError): - Query.from_search("field", "invalid_op", "value") + ndi_query.from_search("field", "invalid_op", "value") class TestQueryCombination: @@ -176,8 +176,8 @@ class TestQueryCombination: def test_query_and(self): """Test combining queries with AND (&).""" - q1 = Query("base.name") == "test" - q2 = Query("ephys.channels") > 4 + q1 = ndi_query("base.name") == "test" + q2 = ndi_query("ephys.channels") > 4 combined = q1 & q2 # Combined query should contain both queries assert hasattr(combined, "queries") @@ -185,17 +185,17 @@ def test_query_and(self): def test_query_or(self): """Test combining queries with OR (|).""" - q1 = Query("type") == "ephys" - q2 = Query("type") == "digital" + q1 = ndi_query("type") == "ephys" + q2 = ndi_query("type") == "digital" combined = q1 | q2 assert hasattr(combined, "queries") assert len(list(combined)) == 2 def test_query_complex_combination(self): """Test complex query combination.""" - q1 = Query("base.name") == "test" - q2 = Query("channels") > 4 - q3 = Query("rate") >= 30000 + q1 = ndi_query("base.name") == "test" + q2 = ndi_query("channels") > 4 + q3 = ndi_query("rate") >= 30000 combined = (q1 & q2) | q3 # Should be an OrQuery containing AndQuery and q3 assert hasattr(combined, "queries") @@ -206,7 +206,7 @@ class TestQueryToSearchStructure: def test_to_searchstructure_simple(self): """Test converting simple query to search structure.""" - q = Query("base.name") == "test" + q = ndi_query("base.name") == "test" ss = q.to_searchstructure() assert ss["field"] == "base.name" assert ss["operation"] == "==" @@ -215,7 +215,7 @@ def test_to_searchstructure_simple(self): def test_to_searchstructure_depends_on(self): """Test converting depends_on query to search structure.""" - q = Query("").depends_on("element_id", "abc123") + q = ndi_query("").depends_on("element_id", "abc123") ss = q.to_searchstructure() assert ss["operation"] == "depends_on" assert ss["param1"] == "element_id" @@ -223,76 +223,76 @@ def test_to_searchstructure_depends_on(self): class TestQueryMATLABConstructor: - """Test MATLAB-style Query(field, operation, param1, param2).""" + """Test MATLAB-style ndi_query(field, operation, param1, param2).""" def test_isa_constructor(self): - """Query('', 'isa', 'base') should work like Query.from_search.""" - q = Query("", "isa", "base") + """ndi_query('', 'isa', 'base') should work like ndi_query.from_search.""" + q = ndi_query("", "isa", "base") assert q.operator == "isa" assert q.value == "base" assert q._resolved def test_exact_string_constructor(self): - """Query('base.name', 'exact_string', 'test') should resolve.""" - q = Query("base.name", "exact_string", "test") + """ndi_query('base.name', 'exact_string', 'test') should resolve.""" + q = ndi_query("base.name", "exact_string", "test") assert q.field == "base.name" assert q.operator == "==" assert q.value == "test" def test_regexp_constructor(self): - """Query('base.name', 'regexp', '.*probe.*') should resolve.""" - q = Query("base.name", "regexp", ".*probe.*") + """ndi_query('base.name', 'regexp', '.*probe.*') should resolve.""" + q = ndi_query("base.name", "regexp", ".*probe.*") assert q.operator == "match" assert q.value == ".*probe.*" def test_depends_on_constructor(self): - """Query('', 'depends_on', 'session_id', 'abc') should resolve.""" - q = Query("", "depends_on", "session_id", "abc") + """ndi_query('', 'depends_on', 'session_id', 'abc') should resolve.""" + q = ndi_query("", "depends_on", "session_id", "abc") assert q.operator == "depends_on" assert q.value == ("session_id", "abc") def test_negated_constructor(self): - """Query('', '~isa', 'base') should resolve negated.""" - q = Query("", "~isa", "base") + """ndi_query('', '~isa', 'base') should resolve negated.""" + q = ndi_query("", "~isa", "base") assert q.operator == "~isa" assert q.value == "base" def test_matches_from_search(self): """MATLAB constructor should produce same result as from_search.""" - q1 = Query("", "isa", "element") - q2 = Query.from_search("", "isa", "element") + q1 = ndi_query("", "isa", "element") + q2 = ndi_query.from_search("", "isa", "element") assert q1.field == q2.field assert q1.operator == q2.operator assert q1.value == q2.value def test_no_operation_still_works(self): - """Query('base.name') should still work as before (Pythonic style).""" - q = Query("base.name") + """ndi_query('base.name') should still work as before (Pythonic style).""" + q = ndi_query("base.name") assert q.field == "base.name" assert not q._resolved class TestQueryJSONSerialization: - """Test that Query objects serialize to JSON-compatible dicts.""" + """Test that ndi_query objects serialize to JSON-compatible dicts.""" def test_simple_query_json_serializable(self): """to_search_structure() output should be JSON serializable.""" - q = Query("base.name") == "test" + q = ndi_query("base.name") == "test" ss = q.to_search_structure() result = json.dumps(ss) assert '"exact_string"' in result def test_isa_query_json_serializable(self): """isa query should serialize cleanly.""" - q = Query("", "isa", "base") + q = ndi_query("", "isa", "base") ss = q.to_search_structure() result = json.dumps(ss) assert '"isa"' in result def test_and_query_json_serializable(self): """AND composite query should produce JSON-serializable list.""" - q1 = Query("base.name") == "test" - q2 = Query("").isa("element") + q1 = ndi_query("base.name") == "test" + q2 = ndi_query("").isa("element") combined = q1 & q2 ss = combined.to_search_structure() json.dumps(ss) # should not raise @@ -301,8 +301,8 @@ def test_and_query_json_serializable(self): def test_or_query_json_serializable(self): """OR composite query should produce JSON-serializable dict.""" - q1 = Query("type") == "ephys" - q2 = Query("type") == "digital" + q1 = ndi_query("type") == "ephys" + q2 = ndi_query("type") == "digital" combined = q1 | q2 ss = combined.to_search_structure() json.dumps(ss) # should not raise @@ -310,7 +310,7 @@ def test_or_query_json_serializable(self): def test_depends_on_json_serializable(self): """depends_on query should have param1/param2 in serialized form.""" - q = Query("", "depends_on", "element_id", "abc123") + q = ndi_query("", "depends_on", "element_id", "abc123") ss = q.to_search_structure() json.dumps(ss) # should not raise assert ss["param1"] == "element_id" @@ -318,25 +318,25 @@ def test_depends_on_json_serializable(self): class TestQueryDIDInheritance: - """Test that ndi.Query properly inherits from did.query.Query (Issue #3).""" + """Test that ndi.ndi_query properly inherits from did.query.Query (Issue #3).""" def test_isinstance_did_query(self): - """ndi.Query instances must be instances of did.query.Query.""" + """ndi.ndi_query instances must be instances of did.query.Query.""" import did.query - q = Query("", "isa", "base") + q = ndi_query("", "isa", "base") assert isinstance(q, did.query.Query) def test_isinstance_pythonic_query(self): """Pythonic-constructed queries are also did.query.Query instances.""" import did.query - q = Query("base.name") == "test" + q = ndi_query("base.name") == "test" assert isinstance(q, did.query.Query) def test_search_structure_attribute(self): - """Query should have search_structure attribute from did.Query.""" - q = Query("base.name", "exact_string", "test") + """ndi_query should have search_structure attribute from did.ndi_query.""" + q = ndi_query("base.name", "exact_string", "test") assert hasattr(q, "search_structure") assert isinstance(q.search_structure, list) assert len(q.search_structure) == 1 @@ -344,7 +344,7 @@ def test_search_structure_attribute(self): def test_search_structure_pythonic(self): """Pythonic query should also populate search_structure.""" - q = Query("base.name") == "test" + q = ndi_query("base.name") == "test" assert isinstance(q.search_structure, list) assert len(q.search_structure) == 1 assert q.search_structure[0]["operation"] == "exact_string" @@ -353,16 +353,16 @@ def test_search_structure_pythonic(self): def test_search_structure_and(self): """AND combination should concatenate search_structures.""" - q1 = Query("base.name") == "test" - q2 = Query("").isa("element") + q1 = ndi_query("base.name") == "test" + q2 = ndi_query("").isa("element") combined = q1 & q2 assert isinstance(combined.search_structure, list) assert len(combined.search_structure) == 2 def test_search_structure_or(self): """OR combination should nest search_structures.""" - q1 = Query("type") == "ephys" - q2 = Query("type") == "digital" + q1 = ndi_query("type") == "ephys" + q2 = ndi_query("type") == "digital" combined = q1 | q2 assert isinstance(combined.search_structure, list) assert len(combined.search_structure) == 1 @@ -370,14 +370,14 @@ def test_search_structure_or(self): def test_to_search_structure_inherited(self): """to_search_structure() should return DID-format operations.""" - q = Query("base.name") == "test" + q = ndi_query("base.name") == "test" ss = q.to_search_structure() # DID format uses 'exact_string', not '==' assert ss["operation"] == "exact_string" def test_numeric_uses_exact_number(self): """Numeric == comparisons should use exact_number in DID format.""" - q = Query("count") == 42 + q = ndi_query("count") == 42 ss = q.to_search_structure() assert ss["operation"] == "exact_number" # But Python-style operator property still shows '==' diff --git a/tests/test_session.py b/tests/test_session.py index 999c943..111f2b5 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -6,13 +6,13 @@ import pytest -from ndi.cache import Cache, CacheEntry -from ndi.ido import Ido -from ndi.query import Query -from ndi.session import DirSession, empty_id +from ndi.cache import CacheEntry, ndi_cache +from ndi.ido import ndi_ido +from ndi.query import ndi_query +from ndi.session import empty_id, ndi_session_dir # ============================================================================== -# Cache Tests +# ndi_cache Tests # ============================================================================== @@ -39,29 +39,29 @@ def test_create_entry(self): class TestCache: - """Tests for the Cache class.""" + """Tests for the ndi_cache class.""" def test_create_default_cache(self): """Test creating a cache with defaults.""" - cache = Cache() + cache = ndi_cache() assert cache.max_memory == 10e9 assert cache.replacement_rule == "fifo" assert len(cache) == 0 def test_create_custom_cache(self): """Test creating a cache with custom settings.""" - cache = Cache(max_memory=1e6, replacement_rule="lifo") + cache = ndi_cache(max_memory=1e6, replacement_rule="lifo") assert cache.max_memory == 1e6 assert cache.replacement_rule == "lifo" def test_invalid_replacement_rule(self): """Test that invalid replacement rule raises error.""" with pytest.raises(ValueError, match="Unknown replacement rule"): - Cache(replacement_rule="invalid") + ndi_cache(replacement_rule="invalid") def test_set_replacement_rule(self): """Test changing replacement rule.""" - cache = Cache() + cache = ndi_cache() cache.set_replacement_rule("lifo") assert cache.replacement_rule == "lifo" cache.set_replacement_rule("error") @@ -69,7 +69,7 @@ def test_set_replacement_rule(self): def test_add_and_lookup(self): """Test adding and looking up data.""" - cache = Cache() + cache = ndi_cache() cache.add("key1", "type1", {"value": 42}) entry = cache.lookup("key1", "type1") @@ -80,12 +80,12 @@ def test_add_and_lookup(self): def test_lookup_not_found(self): """Test lookup returns None when not found.""" - cache = Cache() + cache = ndi_cache() assert cache.lookup("nonexistent", "type") is None def test_add_with_priority(self): """Test adding data with priority.""" - cache = Cache() + cache = ndi_cache() cache.add("key1", "type1", "data1", priority=1) cache.add("key2", "type2", "data2", priority=5) @@ -97,7 +97,7 @@ def test_add_with_priority(self): def test_remove_by_key(self): """Test removing by key and type.""" - cache = Cache() + cache = ndi_cache() cache.add("key1", "type1", "data1") cache.add("key2", "type2", "data2") @@ -108,7 +108,7 @@ def test_remove_by_key(self): def test_remove_by_index(self): """Test removing by index.""" - cache = Cache() + cache = ndi_cache() cache.add("key1", "type1", "data1") cache.add("key2", "type2", "data2") @@ -118,7 +118,7 @@ def test_remove_by_index(self): def test_clear(self): """Test clearing all entries.""" - cache = Cache() + cache = ndi_cache() cache.add("key1", "type1", "data1") cache.add("key2", "type2", "data2") @@ -128,14 +128,14 @@ def test_clear(self): def test_bytes(self): """Test bytes calculation.""" - cache = Cache() + cache = ndi_cache() cache.add("key1", "type1", "x" * 100) assert cache.bytes() > 0 def test_eviction_fifo(self): """Test FIFO eviction when memory exceeded.""" - cache = Cache(max_memory=1000, replacement_rule="fifo") + cache = ndi_cache(max_memory=1000, replacement_rule="fifo") # Add data that will approach limit cache.add("key1", "type1", "a" * 100, priority=1) @@ -151,7 +151,7 @@ def test_eviction_fifo(self): def test_eviction_respects_priority(self): """Test that eviction respects priority.""" - cache = Cache(max_memory=500, replacement_rule="fifo") + cache = ndi_cache(max_memory=500, replacement_rule="fifo") cache.add("low_priority", "type", "a" * 100, priority=0) cache.add("high_priority", "type", "b" * 100, priority=10) @@ -166,7 +166,7 @@ def test_eviction_respects_priority(self): def test_memory_error_too_large(self): """Test error when single item exceeds max_memory.""" - cache = Cache(max_memory=100) + cache = ndi_cache(max_memory=100) with pytest.raises(MemoryError, match="exceeds cache max_memory"): cache.add("key", "type", "x" * 1000) @@ -174,7 +174,7 @@ def test_memory_error_too_large(self): def test_memory_error_rule(self): """Test error when replacement_rule is 'error' and full.""" # Use a larger max_memory so individual items fit - cache = Cache(max_memory=500, replacement_rule="error") + cache = ndi_cache(max_memory=500, replacement_rule="error") cache.add("key1", "type1", "x" * 100) cache.add("key2", "type2", "y" * 100) @@ -184,17 +184,17 @@ def test_memory_error_rule(self): def test_repr(self): """Test string representation.""" - cache = Cache() + cache = ndi_cache() cache.add("key", "type", "data") repr_str = repr(cache) - assert "Cache" in repr_str + assert "ndi_cache" in repr_str assert "entries=1" in repr_str def test_numpy_array_size_estimation(self): - """Cache must use ndarray.nbytes, not sys.getsizeof.""" + """ndi_cache must use ndarray.nbytes, not sys.getsizeof.""" import numpy as np - cache = Cache() + cache = ndi_cache() arr = np.zeros((1000, 1000), dtype=np.float64) # 8 MB expected_bytes = arr.nbytes # 8_000_000 @@ -209,7 +209,7 @@ def test_numpy_eviction_by_real_size(self): import numpy as np # 500 KB cache - cache = Cache(max_memory=500_000, replacement_rule="fifo") + cache = ndi_cache(max_memory=500_000, replacement_rule="fifo") small = np.ones(10_000, dtype=np.float64) # 80 KB big = np.ones(50_000, dtype=np.float64) # 400 KB @@ -224,7 +224,7 @@ def test_fifo_eviction_order(self): """FIFO evicts oldest items first.""" import time - cache = Cache(max_memory=500, replacement_rule="fifo") + cache = ndi_cache(max_memory=500, replacement_rule="fifo") cache.add("first", "t", "a" * 100, priority=0) time.sleep(0.01) cache.add("second", "t", "b" * 100, priority=0) @@ -245,7 +245,7 @@ def test_lifo_eviction_order(self): """ import time - cache = Cache(max_memory=500, replacement_rule="lifo") + cache = ndi_cache(max_memory=500, replacement_rule="lifo") cache.add("first", "t", "a" * 100, priority=0) time.sleep(0.01) cache.add("second", "t", "b" * 100, priority=0) @@ -263,7 +263,7 @@ def test_lifo_eviction_order(self): def test_priority_preserved_during_eviction(self): """Higher priority items survive eviction over lower priority ones.""" - cache = Cache(max_memory=500, replacement_rule="fifo") + cache = ndi_cache(max_memory=500, replacement_rule="fifo") cache.add("low", "t", "a" * 100, priority=0) cache.add("high", "t", "b" * 100, priority=10) @@ -274,7 +274,7 @@ def test_priority_preserved_during_eviction(self): # ============================================================================== -# Session Tests +# ndi_session Tests # ============================================================================== @@ -296,12 +296,12 @@ def test_empty_id_length(self): """Test empty_id has correct length.""" eid = empty_id() # Should match the format of regular IDs - regular_id = Ido().id + regular_id = ndi_ido().id assert len(eid) == len(regular_id) class TestDirSession: - """Tests for DirSession class.""" + """Tests for ndi_session_dir class.""" @pytest.fixture def temp_dir(self): @@ -312,7 +312,7 @@ def temp_dir(self): def test_create_session_from_path(self, temp_dir): """Test creating a session from a path.""" - session = DirSession("TestSession", temp_dir) + session = ndi_session_dir("TestSession", temp_dir) assert session.reference == "TestSession" assert session.path == temp_dir @@ -321,7 +321,7 @@ def test_create_session_from_path(self, temp_dir): def test_session_creates_ndi_dir(self, temp_dir): """Test that session creates .ndi directory.""" - DirSession("Test", temp_dir) + ndi_session_dir("Test", temp_dir) ndi_dir = temp_dir / ".ndi" assert ndi_dir.exists() @@ -329,7 +329,7 @@ def test_session_creates_ndi_dir(self, temp_dir): def test_session_writes_reference_files(self, temp_dir): """Test that session writes reference files.""" - session = DirSession("MyReference", temp_dir) + session = ndi_session_dir("MyReference", temp_dir) ref_file = temp_dir / ".ndi" / "reference.txt" unique_ref_file = temp_dir / ".ndi" / "unique_reference.txt" @@ -342,12 +342,12 @@ def test_session_writes_reference_files(self, temp_dir): def test_session_getpath(self, temp_dir): """Test getpath returns the session path.""" - session = DirSession("Test", temp_dir) + session = ndi_session_dir("Test", temp_dir) assert session.getpath() == temp_dir def test_session_creator_args(self, temp_dir): """Test creator_args returns correct arguments.""" - session = DirSession("TestRef", temp_dir) + session = ndi_session_dir("TestRef", temp_dir) args = session.creator_args() assert len(args) == 3 @@ -357,44 +357,44 @@ def test_session_creator_args(self, temp_dir): def test_session_cache(self, temp_dir): """Test session has a cache.""" - session = DirSession("Test", temp_dir) + session = ndi_session_dir("Test", temp_dir) assert session.cache is not None - assert isinstance(session.cache, Cache) + assert isinstance(session.cache, ndi_cache) def test_session_database(self, temp_dir): """Test session has a database.""" - session = DirSession("Test", temp_dir) + session = ndi_session_dir("Test", temp_dir) assert session.database is not None def test_session_equality(self, temp_dir): """Test session equality by ID.""" - session1 = DirSession("Test", temp_dir) + session1 = ndi_session_dir("Test", temp_dir) # Same session assert session1 == session1 # Different ID means different session - session2 = DirSession("Test2", tempfile.mkdtemp()) + session2 = ndi_session_dir("Test2", tempfile.mkdtemp()) assert session1 != session2 shutil.rmtree(session2.path, ignore_errors=True) def test_session_exists_check(self, temp_dir): """Test static exists method.""" # Not a session yet - assert DirSession.exists(temp_dir) is False + assert ndi_session_dir.exists(temp_dir) is False # Create a session - DirSession("Test", temp_dir) + ndi_session_dir("Test", temp_dir) # Now it exists - assert DirSession.exists(temp_dir) is True + assert ndi_session_dir.exists(temp_dir) is True def test_invalid_path_raises(self): """Test that invalid path raises error.""" with pytest.raises(ValueError, match="does not exist"): - DirSession("Test", "/nonexistent/path/12345") + ndi_session_dir("Test", "/nonexistent/path/12345") def test_file_path_raises(self, temp_dir): """Test that file path raises error.""" @@ -402,19 +402,19 @@ def test_file_path_raises(self, temp_dir): file_path.write_text("test") with pytest.raises(ValueError, match="not a directory"): - DirSession("Test", file_path) + ndi_session_dir("Test", file_path) def test_session_repr(self, temp_dir): """Test string representation.""" - session = DirSession("TestRef", temp_dir) + session = ndi_session_dir("TestRef", temp_dir) repr_str = repr(session) - assert "DirSession" in repr_str + assert "ndi_session_dir" in repr_str assert "TestRef" in repr_str def test_newdocument(self, temp_dir): """Test creating a new document with session ID.""" - session = DirSession("Test", temp_dir) + session = ndi_session_dir("Test", temp_dir) try: doc = session.newdocument("base", **{"base.name": "test"}) @@ -424,14 +424,14 @@ def test_newdocument(self, temp_dir): def test_searchquery(self, temp_dir): """Test creating a search query.""" - session = DirSession("Test", temp_dir) + session = ndi_session_dir("Test", temp_dir) q = session.searchquery() assert q is not None def test_validate_documents(self, temp_dir): """Test document validation.""" - session = DirSession("Test", temp_dir) + session = ndi_session_dir("Test", temp_dir) try: doc = session.newdocument() @@ -443,13 +443,13 @@ def test_validate_documents(self, temp_dir): def test_database_add_search(self, temp_dir): """Test adding and searching documents.""" - session = DirSession("Test", temp_dir) + session = ndi_session_dir("Test", temp_dir) try: doc = session.newdocument("base", **{"base.name": "testdoc"}) session.database_add(doc) - results = session.database_search(Query("base.name") == "testdoc") + results = session.database_search(ndi_query("base.name") == "testdoc") assert len(results) >= 1 except FileNotFoundError: pytest.skip("Schema not available") @@ -457,17 +457,17 @@ def test_database_add_search(self, temp_dir): def test_reopen_session(self, temp_dir): """Test reopening an existing session.""" # Create session - session1 = DirSession("MySession", temp_dir) + session1 = ndi_session_dir("MySession", temp_dir) original_id = session1.id() # Reopen by path - session2 = DirSession("MySession", temp_dir, session_id=original_id) + session2 = ndi_session_dir("MySession", temp_dir, session_id=original_id) assert session2.id() == original_id def test_delete_session_data(self, temp_dir): """Test deleting session data structures.""" - session = DirSession("Test", temp_dir) + session = ndi_session_dir("Test", temp_dir) ndi_dir = temp_dir / ".ndi" assert ndi_dir.exists() @@ -477,7 +477,7 @@ def test_delete_session_data(self, temp_dir): def test_delete_session_requires_confirmation(self, temp_dir): """Test that delete requires confirmation.""" - session = DirSession("Test", temp_dir) + session = ndi_session_dir("Test", temp_dir) ndi_dir = temp_dir / ".ndi" result = session.deleteSessionDataStructures(are_you_sure=False) @@ -486,13 +486,13 @@ def test_delete_session_requires_confirmation(self, temp_dir): class TestSessionMethods: - """Tests for Session methods using DirSession.""" + """Tests for ndi_session methods using ndi_session_dir.""" @pytest.fixture def session(self): """Create a test session.""" tmpdir = tempfile.mkdtemp() - sess = DirSession("TestSession", tmpdir) + sess = ndi_session_dir("TestSession", tmpdir) yield sess shutil.rmtree(tmpdir, ignore_errors=True) @@ -535,25 +535,25 @@ def test_database_clear(self, session): # Without confirmation session.database_clear("no") - results = session.database_search(Query("base.name") == "test") + results = session.database_search(ndi_query("base.name") == "test") # Should still exist # With confirmation session.database_clear("yes") - results = session.database_search(Query("base.name") == "test") + results = session.database_search(ndi_query("base.name") == "test") assert len(results) == 0 except FileNotFoundError: pytest.skip("Schema not available") class TestSessionSyncGraph: - """Tests for Session syncgraph methods.""" + """Tests for ndi_session syncgraph methods.""" @pytest.fixture def session(self): """Create a test session.""" tmpdir = tempfile.mkdtemp() - sess = DirSession("TestSession", tmpdir) + sess = ndi_session_dir("TestSession", tmpdir) yield sess shutil.rmtree(tmpdir, ignore_errors=True) @@ -563,9 +563,9 @@ def test_syncgraph_exists(self, session): def test_syncgraph_addrule(self, session): """Test adding a sync rule.""" - from ndi.time.syncrule.filematch import FileMatch + from ndi.time.syncrule.filematch import ndi_time_syncrule_filematch - rule = FileMatch() + rule = ndi_time_syncrule_filematch() try: session.syncgraph_addrule(rule) assert len(session.syncgraph.rules) == 1 @@ -574,9 +574,9 @@ def test_syncgraph_addrule(self, session): def test_syncgraph_rmrule(self, session): """Test removing a sync rule.""" - from ndi.time.syncrule.filematch import FileMatch + from ndi.time.syncrule.filematch import ndi_time_syncrule_filematch - rule = FileMatch() + rule = ndi_time_syncrule_filematch() try: session.syncgraph_addrule(rule) session.syncgraph_rmrule(0) @@ -591,13 +591,13 @@ def test_syncgraph_rmrule(self, session): class TestSessionIntegration: - """Integration tests for Session functionality.""" + """Integration tests for ndi_session functionality.""" @pytest.fixture def session(self): """Create a test session with full setup.""" tmpdir = tempfile.mkdtemp() - sess = DirSession("IntegrationTest", tmpdir) + sess = ndi_session_dir("IntegrationTest", tmpdir) yield sess shutil.rmtree(tmpdir, ignore_errors=True) @@ -613,16 +613,16 @@ def test_full_workflow(self, session): session.database_add(doc2) # Search - results = session.database_search(Query("base.name") == "doc1") + results = session.database_search(ndi_query("base.name") == "doc1") assert len(results) == 1 # Remove session.database_rm(doc1) - results = session.database_search(Query("base.name") == "doc1") + results = session.database_search(ndi_query("base.name") == "doc1") assert len(results) == 0 # doc2 should still exist - results = session.database_search(Query("base.name") == "doc2") + results = session.database_search(ndi_query("base.name") == "doc2") assert len(results) == 1 except FileNotFoundError: pytest.skip("Schema not available") @@ -633,7 +633,7 @@ def test_session_persistence(self): try: # Create session and add data - session1 = DirSession("Persist", tmpdir) + session1 = ndi_session_dir("Persist", tmpdir) session_id = session1.id() try: @@ -643,8 +643,8 @@ def test_session_persistence(self): pytest.skip("Schema not available") # Reopen and check data - session2 = DirSession("Persist", tmpdir, session_id=session_id) - results = session2.database_search(Query("base.name") == "persistent") + session2 = ndi_session_dir("Persist", tmpdir, session_id=session_id) + results = session2.database_search(ndi_query("base.name") == "persistent") assert len(results) >= 1 finally: shutil.rmtree(tmpdir, ignore_errors=True) diff --git a/tests/test_time.py b/tests/test_time.py index 9229dfd..477759d 100644 --- a/tests/test_time.py +++ b/tests/test_time.py @@ -6,17 +6,17 @@ import pytest from ndi.time import ( - ClockType, - EpochNode, - SyncGraph, - TimeMapping, - TimeReference, + ndi_time_clocktype, + ndi_time_epochnode, + ndi_time_syncgraph, + ndi_time_timemapping, + ndi_time_timereference, ) -from ndi.time.syncrule import FileFind, FileMatch +from ndi.time.syncrule import ndi_time_syncrule_filefind, ndi_time_syncrule_filematch class TestClockType: - """Tests for ClockType enum.""" + """Tests for ndi_time_clocktype enum.""" def test_all_clock_types_exist(self): """Test that all 9 clock types are defined.""" @@ -31,94 +31,94 @@ def test_all_clock_types_exist(self): "no_time", "inherited", ] - actual = [ct.value for ct in ClockType] + actual = [ct.value for ct in ndi_time_clocktype] assert sorted(actual) == sorted(expected) def test_from_string(self): - """Test creating ClockType from string.""" - assert ClockType.from_string("utc") == ClockType.UTC - assert ClockType.from_string("UTC") == ClockType.UTC - assert ClockType.from_string("dev_local_time") == ClockType.DEV_LOCAL_TIME + """Test creating ndi_time_clocktype from string.""" + assert ndi_time_clocktype.from_string("utc") == ndi_time_clocktype.UTC + assert ndi_time_clocktype.from_string("UTC") == ndi_time_clocktype.UTC + assert ndi_time_clocktype.from_string("dev_local_time") == ndi_time_clocktype.DEV_LOCAL_TIME def test_from_string_invalid(self): """Test that invalid string raises ValueError.""" with pytest.raises(ValueError): - ClockType.from_string("invalid_clock") + ndi_time_clocktype.from_string("invalid_clock") def test_str(self): """Test string conversion.""" - assert str(ClockType.UTC) == "utc" - assert str(ClockType.DEV_LOCAL_TIME) == "dev_local_time" + assert str(ndi_time_clocktype.UTC) == "utc" + assert str(ndi_time_clocktype.DEV_LOCAL_TIME) == "dev_local_time" def test_needs_epoch(self): """Test needs_epoch method.""" - assert ClockType.DEV_LOCAL_TIME.needs_epoch() - assert not ClockType.UTC.needs_epoch() - assert not ClockType.EXP_GLOBAL_TIME.needs_epoch() + assert ndi_time_clocktype.DEV_LOCAL_TIME.needs_epoch() + assert not ndi_time_clocktype.UTC.needs_epoch() + assert not ndi_time_clocktype.EXP_GLOBAL_TIME.needs_epoch() def test_is_global(self): """Test is_global method.""" - assert ClockType.UTC.is_global() - assert ClockType.APPROX_UTC.is_global() - assert ClockType.EXP_GLOBAL_TIME.is_global() - assert not ClockType.DEV_LOCAL_TIME.is_global() - assert not ClockType.NO_TIME.is_global() + assert ndi_time_clocktype.UTC.is_global() + assert ndi_time_clocktype.APPROX_UTC.is_global() + assert ndi_time_clocktype.EXP_GLOBAL_TIME.is_global() + assert not ndi_time_clocktype.DEV_LOCAL_TIME.is_global() + assert not ndi_time_clocktype.NO_TIME.is_global() def test_assert_global(self): """Test assert_global static method.""" # Should not raise - ClockType.assert_global(ClockType.UTC) - ClockType.assert_global(ClockType.EXP_GLOBAL_TIME) + ndi_time_clocktype.assert_global(ndi_time_clocktype.UTC) + ndi_time_clocktype.assert_global(ndi_time_clocktype.EXP_GLOBAL_TIME) # Should raise with pytest.raises(AssertionError): - ClockType.assert_global(ClockType.DEV_LOCAL_TIME) + ndi_time_clocktype.assert_global(ndi_time_clocktype.DEV_LOCAL_TIME) with pytest.raises(AssertionError): - ClockType.assert_global(ClockType.NO_TIME) + ndi_time_clocktype.assert_global(ndi_time_clocktype.NO_TIME) def test_epochgraph_edge_utc_to_utc(self): """Test epochgraph_edge for utc->utc transition.""" - cost, mapping = ClockType.UTC.epochgraph_edge(ClockType.UTC) + cost, mapping = ndi_time_clocktype.UTC.epochgraph_edge(ndi_time_clocktype.UTC) assert cost == 100.0 assert mapping is not None assert mapping.map(5.0) == 5.0 # Identity mapping def test_epochgraph_edge_utc_to_approx_utc(self): """Test epochgraph_edge for utc->approx_utc transition.""" - cost, mapping = ClockType.UTC.epochgraph_edge(ClockType.APPROX_UTC) + cost, mapping = ndi_time_clocktype.UTC.epochgraph_edge(ndi_time_clocktype.APPROX_UTC) assert cost == 100.0 assert mapping is not None def test_epochgraph_edge_no_time(self): """Test epochgraph_edge with no_time.""" - cost, mapping = ClockType.NO_TIME.epochgraph_edge(ClockType.UTC) + cost, mapping = ndi_time_clocktype.NO_TIME.epochgraph_edge(ndi_time_clocktype.UTC) assert cost == float("inf") assert mapping is None - cost, mapping = ClockType.UTC.epochgraph_edge(ClockType.NO_TIME) + cost, mapping = ndi_time_clocktype.UTC.epochgraph_edge(ndi_time_clocktype.NO_TIME) assert cost == float("inf") assert mapping is None def test_epochgraph_edge_no_transition(self): """Test epochgraph_edge for invalid transition.""" - cost, mapping = ClockType.DEV_LOCAL_TIME.epochgraph_edge(ClockType.UTC) + cost, mapping = ndi_time_clocktype.DEV_LOCAL_TIME.epochgraph_edge(ndi_time_clocktype.UTC) assert cost == float("inf") assert mapping is None class TestTimeMapping: - """Tests for TimeMapping class.""" + """Tests for ndi_time_timemapping class.""" def test_default_mapping(self): """Test default identity mapping.""" - tm = TimeMapping() + tm = ndi_time_timemapping() assert tm.scale == 1.0 assert tm.shift == 0.0 assert tm.map(5.0) == 5.0 def test_linear_mapping(self): """Test linear mapping t_out = 2*t_in + 10.""" - tm = TimeMapping([2.0, 10.0]) + tm = ndi_time_timemapping([2.0, 10.0]) assert tm.scale == 2.0 assert tm.shift == 10.0 assert tm.map(5.0) == 20.0 @@ -126,46 +126,46 @@ def test_linear_mapping(self): def test_identity_classmethod(self): """Test identity classmethod.""" - tm = TimeMapping.identity() + tm = ndi_time_timemapping.identity() assert tm.scale == 1.0 assert tm.shift == 0.0 assert tm.map(100.0) == 100.0 def test_linear_classmethod(self): """Test linear classmethod.""" - tm = TimeMapping.linear(scale=3.0, shift=-5.0) + tm = ndi_time_timemapping.linear(scale=3.0, shift=-5.0) assert tm.map(0.0) == -5.0 assert tm.map(2.0) == 1.0 def test_callable(self): """Test calling mapping directly.""" - tm = TimeMapping([2.0, 1.0]) + tm = ndi_time_timemapping([2.0, 1.0]) assert tm(5.0) == 11.0 def test_array_input(self): """Test with numpy array input.""" - tm = TimeMapping([2.0, 1.0]) + tm = ndi_time_timemapping([2.0, 1.0]) t_in = np.array([1.0, 2.0, 3.0]) t_out = tm.map(t_in) np.testing.assert_array_equal(t_out, np.array([3.0, 5.0, 7.0])) def test_inverse(self): """Test inverse mapping.""" - tm = TimeMapping([2.0, 10.0]) + tm = ndi_time_timemapping([2.0, 10.0]) inv = tm.inverse() assert inv.map(20.0) == pytest.approx(5.0) assert inv.map(10.0) == pytest.approx(0.0) def test_inverse_zero_scale(self): """Test inverse with zero scale raises error.""" - tm = TimeMapping([0.0, 10.0]) + tm = ndi_time_timemapping([0.0, 10.0]) with pytest.raises(ValueError): tm.inverse() def test_compose(self): """Test composing two mappings.""" - tm1 = TimeMapping([2.0, 1.0]) # t1 = 2*t0 + 1 - tm2 = TimeMapping([3.0, 4.0]) # t2 = 3*t1 + 4 + tm1 = ndi_time_timemapping([2.0, 1.0]) # t1 = 2*t0 + 1 + tm2 = ndi_time_timemapping([3.0, 4.0]) # t2 = 3*t1 + 4 composed = tm1.compose(tm2) # t2 = 3*(2*t0 + 1) + 4 = 6*t0 + 7 @@ -175,35 +175,35 @@ def test_compose(self): def test_equality(self): """Test equality comparison.""" - tm1 = TimeMapping([2.0, 1.0]) - tm2 = TimeMapping([2.0, 1.0]) - tm3 = TimeMapping([3.0, 1.0]) + tm1 = ndi_time_timemapping([2.0, 1.0]) + tm2 = ndi_time_timemapping([2.0, 1.0]) + tm3 = ndi_time_timemapping([3.0, 1.0]) assert tm1 == tm2 assert tm1 != tm3 def test_to_dict_from_dict(self): """Test serialization.""" - tm = TimeMapping([2.5, -3.0]) + tm = ndi_time_timemapping([2.5, -3.0]) d = tm.to_dict() - tm2 = TimeMapping.from_dict(d) + tm2 = ndi_time_timemapping.from_dict(d) assert tm == tm2 class TestTimeReference: - """Tests for TimeReference class.""" + """Tests for ndi_time_timereference class.""" @pytest.fixture def mock_referent(self): """Create a mock referent object.""" - class MockSession: + class ndi_session_mock: def id(self): return "session_123" class MockReferent: def __init__(self): - self.session = MockSession() + self.session = ndi_session_mock() self.name = "test_daq" def epochsetname(self): @@ -213,8 +213,10 @@ def epochsetname(self): def test_create_utc_reference(self, mock_referent): """Test creating a UTC time reference.""" - tr = TimeReference(referent=mock_referent, clocktype=ClockType.UTC, time=1234567890.0) - assert tr.clocktype == ClockType.UTC + tr = ndi_time_timereference( + referent=mock_referent, clocktype=ndi_time_clocktype.UTC, time=1234567890.0 + ) + assert tr.clocktype == ndi_time_clocktype.UTC assert tr.time == 1234567890.0 assert tr.epoch is None assert tr.session_id == "session_123" @@ -222,26 +224,31 @@ def test_create_utc_reference(self, mock_referent): def test_create_local_reference_requires_epoch(self, mock_referent): """Test that DEV_LOCAL_TIME requires epoch.""" with pytest.raises(ValueError): - TimeReference(referent=mock_referent, clocktype=ClockType.DEV_LOCAL_TIME, time=0.5) + ndi_time_timereference( + referent=mock_referent, clocktype=ndi_time_clocktype.DEV_LOCAL_TIME, time=0.5 + ) def test_create_local_reference_with_epoch(self, mock_referent): """Test creating a local time reference with epoch.""" - tr = TimeReference( - referent=mock_referent, clocktype=ClockType.DEV_LOCAL_TIME, epoch="epoch_001", time=0.5 + tr = ndi_time_timereference( + referent=mock_referent, + clocktype=ndi_time_clocktype.DEV_LOCAL_TIME, + epoch="epoch_001", + time=0.5, ) - assert tr.clocktype == ClockType.DEV_LOCAL_TIME + assert tr.clocktype == ndi_time_clocktype.DEV_LOCAL_TIME assert tr.epoch == "epoch_001" assert tr.time == 0.5 def test_clocktype_from_string(self, mock_referent): """Test creating with string clocktype.""" - tr = TimeReference(referent=mock_referent, clocktype="utc", time=100.0) - assert tr.clocktype == ClockType.UTC + tr = ndi_time_timereference(referent=mock_referent, clocktype="utc", time=100.0) + assert tr.clocktype == ndi_time_clocktype.UTC def test_to_struct(self, mock_referent): """Test converting to struct.""" - tr = TimeReference( - referent=mock_referent, clocktype=ClockType.UTC, epoch="epoch_001", time=100.0 + tr = ndi_time_timereference( + referent=mock_referent, clocktype=ndi_time_clocktype.UTC, epoch="epoch_001", time=100.0 ) struct = tr.to_struct() assert struct.referent_epochsetname == "test_daq" @@ -251,33 +258,35 @@ def test_to_struct(self, mock_referent): def test_to_dict(self, mock_referent): """Test converting to dict.""" - tr = TimeReference(referent=mock_referent, clocktype=ClockType.UTC, time=100.0) + tr = ndi_time_timereference( + referent=mock_referent, clocktype=ndi_time_clocktype.UTC, time=100.0 + ) d = tr.to_dict() assert d["clocktypestring"] == "utc" assert d["time"] == 100.0 class TestFileMatch: - """Tests for FileMatch sync rule.""" + """Tests for ndi_time_syncrule_filematch sync rule.""" def test_default_parameters(self): """Test default parameters.""" - rule = FileMatch() + rule = ndi_time_syncrule_filematch() assert rule.parameters["number_fullpath_matches"] == 2 def test_custom_parameters(self): """Test custom parameters.""" - rule = FileMatch({"number_fullpath_matches": 3}) + rule = ndi_time_syncrule_filematch({"number_fullpath_matches": 3}) assert rule.parameters["number_fullpath_matches"] == 3 def test_invalid_parameters(self): """Test invalid parameters raise error.""" with pytest.raises(ValueError): - FileMatch({"number_fullpath_matches": "not_a_number"}) + ndi_time_syncrule_filematch({"number_fullpath_matches": "not_a_number"}) def test_apply_matching_files(self): """Test apply with matching files.""" - rule = FileMatch({"number_fullpath_matches": 2}) + rule = ndi_time_syncrule_filematch({"number_fullpath_matches": 2}) node_a = { "objectclass": "ndi.daq.system", @@ -299,7 +308,7 @@ def test_apply_matching_files(self): def test_apply_not_enough_matches(self): """Test apply with not enough matching files.""" - rule = FileMatch({"number_fullpath_matches": 2}) + rule = ndi_time_syncrule_filematch({"number_fullpath_matches": 2}) node_a = { "objectclass": "ndi.daq.system", @@ -316,7 +325,7 @@ def test_apply_not_enough_matches(self): def test_apply_non_daq_system(self): """Test apply with non-DAQ system returns None.""" - rule = FileMatch() + rule = ndi_time_syncrule_filematch() node_a = { "objectclass": "some.other.class", @@ -333,11 +342,11 @@ def test_apply_non_daq_system(self): class TestFileFind: - """Tests for FileFind sync rule.""" + """Tests for ndi_time_syncrule_filefind sync rule.""" def test_default_parameters(self): """Test default parameters.""" - rule = FileFind() + rule = ndi_time_syncrule_filefind() assert rule.parameters["number_fullpath_matches"] == 1 assert rule.parameters["syncfilename"] == "syncfile.txt" assert rule.parameters["daqsystem1"] == "mydaq1" @@ -350,7 +359,7 @@ def test_apply_forward_match(self, tmp_path): syncfile.write_text("0.5 1.0") common_file = str(tmp_path / "shared.dat") - rule = FileFind( + rule = ndi_time_syncrule_filefind( { "number_fullpath_matches": 1, "syncfilename": "syncfile.txt", @@ -376,7 +385,7 @@ def test_apply_forward_match(self, tmp_path): def test_apply_no_match_wrong_daqs(self): """Test apply when DAQ system names don't match parameters.""" - rule = FileFind( + rule = ndi_time_syncrule_filefind( { "number_fullpath_matches": 1, "syncfilename": "syncfile.txt", @@ -401,7 +410,7 @@ def test_apply_no_match_wrong_daqs(self): def test_apply_no_common_files(self): """Test apply when there are no common files.""" - rule = FileFind( + rule = ndi_time_syncrule_filefind( { "number_fullpath_matches": 1, "syncfilename": "syncfile.txt", @@ -426,30 +435,30 @@ def test_apply_no_common_files(self): class TestEpochNode: - """Tests for EpochNode dataclass.""" + """Tests for ndi_time_epochnode dataclass.""" def test_create_epoch_node(self): """Test creating an epoch node.""" - node = EpochNode( + node = ndi_time_epochnode( epoch_id="epoch_001", epoch_session_id="session_123", epochprobemap=None, - epoch_clock=ClockType.UTC, + epoch_clock=ndi_time_clocktype.UTC, t0_t1=(0.0, 100.0), objectname="daq1", objectclass="ndi.daq.system", ) assert node.epoch_id == "epoch_001" - assert node.epoch_clock == ClockType.UTC + assert node.epoch_clock == ndi_time_clocktype.UTC assert node.t0_t1 == (0.0, 100.0) def test_to_dict(self): """Test converting to dict.""" - node = EpochNode( + node = ndi_time_epochnode( epoch_id="epoch_001", epoch_session_id="session_123", epochprobemap=None, - epoch_clock=ClockType.UTC, + epoch_clock=ndi_time_clocktype.UTC, t0_t1=(0.0, 100.0), objectname="daq1", objectclass="ndi.daq.system", @@ -470,25 +479,25 @@ def test_from_dict(self): "objectname": "daq1", "objectclass": "ndi.daq.system", } - node = EpochNode.from_dict(d) + node = ndi_time_epochnode.from_dict(d) assert node.epoch_id == "epoch_001" - assert node.epoch_clock == ClockType.UTC + assert node.epoch_clock == ndi_time_clocktype.UTC assert node.t0_t1 == (0.0, 100.0) class TestSyncGraph: - """Tests for SyncGraph class.""" + """Tests for ndi_time_syncgraph class.""" def test_create_empty_syncgraph(self): """Test creating an empty sync graph.""" - sg = SyncGraph() + sg = ndi_time_syncgraph() assert sg.rules == [] assert sg.session is None def test_add_rule(self): """Test adding a sync rule.""" - sg = SyncGraph() - rule = FileMatch() + sg = ndi_time_syncgraph() + rule = ndi_time_syncrule_filematch() sg.add_rule(rule) assert len(sg.rules) == 1 @@ -496,8 +505,8 @@ def test_add_rule(self): def test_add_duplicate_rule(self): """Test that duplicate rules aren't added.""" - sg = SyncGraph() - rule = FileMatch({"number_fullpath_matches": 2}) + sg = ndi_time_syncgraph() + rule = ndi_time_syncrule_filematch({"number_fullpath_matches": 2}) sg.add_rule(rule) sg.add_rule(rule) # Same rule @@ -505,9 +514,9 @@ def test_add_duplicate_rule(self): def test_remove_rule(self): """Test removing a sync rule.""" - sg = SyncGraph() - rule1 = FileMatch({"number_fullpath_matches": 2}) - rule2 = FileMatch({"number_fullpath_matches": 3}) + sg = ndi_time_syncgraph() + rule1 = ndi_time_syncrule_filematch({"number_fullpath_matches": 2}) + rule2 = ndi_time_syncrule_filematch({"number_fullpath_matches": 3}) sg.add_rule(rule1) sg.add_rule(rule2) @@ -517,6 +526,6 @@ def test_remove_rule(self): def test_has_unique_id(self): """Test that sync graph has unique ID.""" - sg1 = SyncGraph() - sg2 = SyncGraph() + sg1 = ndi_time_syncgraph() + sg2 = ndi_time_syncgraph() assert sg1.id != sg2.id diff --git a/tests/test_validate.py b/tests/test_validate.py index ef47b4c..6ca3949 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -1,5 +1,5 @@ """ -Tests for ndi.validate — Document schema validation. +Tests for ndi.validate — ndi_document schema validation. Tests three-tier validation: 1. This-class property type checking @@ -75,7 +75,7 @@ def element_schema(): def _make_doc(props): - """Create a mock Document with the given document_properties.""" + """Create a mock ndi_document with the given document_properties.""" doc = MagicMock() doc.document_properties = props return doc @@ -524,7 +524,7 @@ def test_no_schema_deps(self): class TestValidate: def test_validate_no_schema(self): - """Document with no matching schema passes (can't validate).""" + """ndi_document with no matching schema passes (can't validate).""" doc = _make_doc({"document_class": {"definition": "", "class_name": ""}}) result = validate(doc) assert result.is_valid is True @@ -691,13 +691,13 @@ def test_validate_missing_superclass_schema(self, element_schema): # =========================================================================== -# Document.validate() integration +# ndi_document.validate() integration # =========================================================================== class TestDocumentValidateIntegration: def test_document_validate_returns_result(self, base_schema): - """Document.validate() should return a ValidationResult.""" + """ndi_document.validate() should return a ValidationResult.""" _schema_cache["base"] = base_schema doc = _make_doc(