Skip to content

Expose exact coordinates through scan payloads - #754

Merged
d-chambers merged 9 commits into
devfrom
full-coords-scan-contract
Jul 20, 2026
Merged

Expose exact coordinates through scan payloads#754
d-chambers merged 9 commits into
devfrom
full-coords-scan-contract

Conversation

@d-chambers

@d-chambers d-chambers commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Description

  • add dc.scan_payloads(...) as a sibling to dc.scan(...), returning raw ScanPayload dictionaries with full CoordManager objects and dispatcher-owned source provenance
  • standardize FiberIO.scan(..., snap: bool = True) while keeping dc.scan(...) and dc.scan_to_df(...) output and signatures unchanged
  • make snap=False preserve stored coordinate values through zero-tolerance CoordSegmented.from_array(...) construction in stored-array formats
  • document the scan payload/snap contract and add public API, provenance, forwarding, common conformance, and Terra15 regression coverage

Format audit

Format Coordinate source snap=False behavior
DASDAE serialized range metadata or stored coordinate arrays exact construction for array-backed coordinates
DASHDF5 stored time, channel, trace, and spatial arrays exact construction
Febus BSL/MTX/T1 stored time/distance and auxiliary arrays exact construction
GDR stored DasTimeArray exact construction
H5Simple stored coordinate arrays exact construction
NetCDF CF stored xarray coordinate values exact construction for 1D monotonic coordinates; other arrays remain value-backed
Neubrex RFS stored Unix timestamp array exact construction
OptoDAS header-defined time plus stored channel array exact construction for distance; time is already exact metadata
PRODML stored per-sample time arrays plus header-defined distance exact construction for time
Terra15 stored GPS/Posix time array plus header-defined distance exact construction for time
APSensing, DASVader, Febus A1/G1 CSV, MiniSEED, Neubrex DAS, SEG-Y, Sentek, Silixa H5, Sintela Binary, SR4731, TDMS, XMLBinary start/step/count or equivalent header metadata documented no-op
Pickle, RSF, WAV, and other default-scan formats coordinates from the fully loaded patch best-effort no-op; payload values match the loaded patch

Review notes

  • Exact array construction uses tolerance=0. This keeps every stored value unchanged while still returning CoordRange for truly uniform arrays and CoordSegmented for exact monotonic runs separated by seams.
  • Standardizing read-side snap/snap_dims naming remains a follow-up.
  • A declarative capability flag such as exact_scan_coords remains deferred until planner integration needs it.

Testing

  • pre-commit run --all
  • pytest tests -q — 7,368 passed, 87 skipped, 3 xfailed

Changelog

  • added: dc.scan_payloads(...) returns raw scan payloads with full coordinate managers and source provenance without loading data arrays.
  • changed: FiberIO.scan(...) standardizes snap: bool = True; snap=False requests exact stored coordinate values, and a jittery per-sample array falls back to a plain monotonic coordinate rather than exploding into per-sample segments.
  • changed: some formats gained coordinate units on their default (snap=True) scan and read output — Terra15 exact times, OptoDAS and NetCDF distances.
  • changed: ScanPayload carries optional source_path, source_format, and source_version, populated by dc.scan_payloads(...) rather than by format implementations.

Checklist

I have (if applicable):

  • referenced the GitHub issue this PR closes.
  • documented the new feature with docstrings and/or appropriate doc page.
  • included tests. See testing guidelines.
  • added the "ready_for_review" tag once the PR is ready to be reviewed.

Summary by CodeRabbit

  • New Features

    • Added/exposed scan_payloads(...) to retrieve raw scan payloads with full CoordManager coordinates and optional source provenance (source_path/source_format/source_version).
  • User-Facing Changes

    • Standardized scan(..., snap: bool = True) across supported formats; snap=False requests exact stored per-sample coordinate values when available.
  • Documentation

    • Updated scan-payload and IO docs to describe the payload contract and the snap vs exact-coordinate behavior.
  • Tests

    • Expanded end-to-end coverage for payload completeness, provenance attachment, and snap=False exact-value conformance.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

The scan API adds scan_payloads, provenance fields, and a standardized snap parameter. IO readers propagate exact or snapped coordinate construction, with validation, tests, and documentation covering payload behavior and coordinate contracts.

Scan payload and coordinate flow

Layer / File(s) Summary
Core scan payload API
dascore/io/core.py, dascore/__init__.py, dascore/io/__init__.py
Adds raw payload scanning, provenance injection, centralized iteration, validation, and snap forwarding.
Exact coordinate construction
dascore/io/utils.py, tests/test_io/test_io_core.py
Adds exact coordinate construction with fallback behavior and tests irregular and non-monotonic values.
HDF5 coordinate snap propagation
dascore/io/dasdae/*, dascore/io/dashdf5/*, dascore/io/febus/*, dascore/io/gdr/*, dascore/io/h5simple/*, dascore/io/optodas/*
Propagates snap behavior through scanner and coordinate helpers.
Metadata and time-coordinate snap paths
dascore/io/netcdf/*, dascore/io/neubrex/*, dascore/io/prodml/*, dascore/io/terra15/*
Selects exact or snapped coordinate values during scanning.
Scan behavior validation
tests/test_io/test_common_io.py, tests/test_io/test_io_core.py, tests/test_io/test_netcdf/*, tests/test_io/test_terra15/*, tests/test_io/test_optodas/*
Tests payload preservation, provenance, snap forwarding, exact-coordinate conformity, and format-specific coordinates.
Scan and coordinate contract documentation
docs/changelog.qmd, docs/contributing/*, docs/tutorial/*, docs/notes/*, scripts/_templates/_quarto.yml
Documents scan payloads, provenance, snap behavior, segmented coordinates, summaries, spectral normalization, and documentation guidance.

Possibly related PRs

Suggested labels: documentation, IO

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately names the main change: exposing exact coordinates via scan payloads.
Description check ✅ Passed The description includes the required sections, feature summary, testing, and checklist items, with enough implementation detail.
Docstring Coverage ✅ Passed Docstring coverage is 92.31% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch full-coords-scan-contract

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (9cf09ff) to head (aea075c).
⚠️ Report is 1 commits behind head on dev.

Additional details and impacted files
@@            Coverage Diff             @@
##               dev      #754    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files          162       164     +2     
  Lines        16512     17191   +679     
==========================================
+ Hits         16512     17191   +679     
Flag Coverage Δ
network 48.92% <56.01%> (-0.69%) ⬇️
unittests 99.98% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot added documentation Improvements or additions to documentation IO Work for reading/writing different formats labels Jul 17, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
dascore/io/core.py (1)

748-779: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Honor snap in the default read-backed scan.

Line 775 calls self.read(resource) with its defaults, so formatters relying on this fallback still return snapped coordinates for scan_payloads(..., snap=False) when their reader supports snap or snap_dims.

Proposed fix
 def scan(self, resource, snap: bool = True, **kwargs) -> list[ScanPayload]:
+    read_params = inspect.signature(self.read).parameters
+    read_kwargs = {}
+    if "snap" in read_params:
+        read_kwargs["snap"] = snap
+    elif "snap_dims" in read_params:
+        read_kwargs["snap_dims"] = snap
     try:
-        spool = self.read(resource)
+        spool = self.read(resource, **read_kwargs)

Add a read-only formatter regression test with an explicit exact-read parameter.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dascore/io/core.py` around lines 748 - 779, Update the default scan
implementation in FiberIO.scan to propagate the caller’s snap setting to the
read-backed fallback, including the formatter’s supported exact-coordinate read
parameter such as snap or snap_dims. Preserve the existing NotImplementedError
handling and payload conversion, and add a regression test for a read-only
formatter verifying scan(..., snap=False) uses exact-read behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@dascore/io/core.py`:
- Around line 748-779: Update the default scan implementation in FiberIO.scan to
propagate the caller’s snap setting to the read-backed fallback, including the
formatter’s supported exact-coordinate read parameter such as snap or snap_dims.
Preserve the existing NotImplementedError handling and payload conversion, and
add a regression test for a read-only formatter verifying scan(..., snap=False)
uses exact-read behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b3b43ef8-89de-427b-95e7-ea87a691c787

📥 Commits

Reviewing files that changed from the base of the PR and between a79e8e5 and e971a7f.

📒 Files selected for processing (25)
  • dascore/__init__.py
  • dascore/io/__init__.py
  • dascore/io/core.py
  • dascore/io/dasdae/core.py
  • dascore/io/dasdae/utils.py
  • dascore/io/dashdf5/core.py
  • dascore/io/dashdf5/utils.py
  • dascore/io/febus/core.py
  • dascore/io/febus/g1utils.py
  • dascore/io/febus/t1utils.py
  • dascore/io/gdr/utils_das.py
  • dascore/io/h5simple/utils.py
  • dascore/io/netcdf/core.py
  • dascore/io/neubrex/utils_rfs.py
  • dascore/io/optodas/core.py
  • dascore/io/optodas/utils.py
  • dascore/io/prodml/core.py
  • dascore/io/prodml/utils.py
  • dascore/io/terra15/core.py
  • dascore/io/terra15/utils.py
  • docs/changelog.qmd
  • docs/contributing/new_format.qmd
  • tests/test_io/test_common_io.py
  • tests/test_io/test_io_core.py
  • tests/test_io/test_terra15/test_terra15.py

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown

✅ Documentation built:
👉 Download
Note: You must be logged in to github and a DASDAE member to access the link.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
dascore/io/optodas/utils.py (1)

51-54: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider passing units to the distance coordinate builders.

In the loop above, unit is extracted from the header (line 37) and is correctly passed to get_coord for the "time" dimension (line 47). However, unit is not currently passed when building the "distance" dimension, which could cause the coordinate to lack unit metadata. Consider passing it to both get_coord and get_exact_coord to ensure the metadata is retained.

💡 Proposed optional refactor
-            if snap:
-                coord = get_coord(data=distance)
-            else:
-                coord = get_exact_coord(distance)
+            if snap:
+                coord = get_coord(data=distance, units=unit)
+            else:
+                coord = get_exact_coord(distance, units=unit)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dascore/io/optodas/utils.py` around lines 51 - 54, Update the
distance-dimension branch to pass the extracted unit to both get_coord and
get_exact_coord, matching the time-dimension handling and preserving unit
metadata on the resulting coordinate.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@dascore/io/optodas/utils.py`:
- Around line 51-54: Update the distance-dimension branch to pass the extracted
unit to both get_coord and get_exact_coord, matching the time-dimension handling
and preserving unit metadata on the resulting coordinate.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bfdb57da-d9a3-42db-8b8b-571873afeaf8

📥 Commits

Reviewing files that changed from the base of the PR and between e971a7f and 71e05b2.

📒 Files selected for processing (15)
  • dascore/io/core.py
  • dascore/io/dasdae/utils.py
  • dascore/io/dashdf5/utils.py
  • dascore/io/febus/g1utils.py
  • dascore/io/febus/t1utils.py
  • dascore/io/gdr/utils_das.py
  • dascore/io/h5simple/utils.py
  • dascore/io/netcdf/core.py
  • dascore/io/neubrex/utils_rfs.py
  • dascore/io/optodas/utils.py
  • dascore/io/prodml/utils.py
  • dascore/io/terra15/utils.py
  • dascore/io/utils.py
  • tests/test_io/test_io_core.py
  • tests/test_io/test_netcdf/test_netcdf.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • dascore/io/core.py

@coderabbitai coderabbitai Bot added the ready_for_review PR is ready for review label Jul 18, 2026
@d-chambers
d-chambers force-pushed the full-coords-scan-contract branch from 39c7354 to 06548e6 Compare July 18, 2026 14:47
@coderabbitai coderabbitai Bot removed the ready_for_review PR is ready for review label Jul 18, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@dascore/io/core.py`:
- Around line 149-185: Extend _validate_scan_payload to validate every
ScanPayload field: require valid dimension names, shape, dtype, and attrs
types/values rather than only checking required keys. In strict mode, require
coords to be a full CoordManager and ensure dims exactly equals coords.dims and
shape exactly equals coords.shape; make scan_payloads use this strict validation
before returning payloads.
- Around line 839-846: Initialize read_kwargs from the caller-provided kwargs
before applying the snap or snap_dims translation in the read delegation logic.
Preserve all existing scan filters and reader options while allowing the
translated snap argument to override or augment the corresponding key.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b62aa2a5-eadd-4f17-89ad-74b6a511ea2b

📥 Commits

Reviewing files that changed from the base of the PR and between 39c7354 and 06548e6.

📒 Files selected for processing (39)
  • dascore/__init__.py
  • dascore/io/__init__.py
  • dascore/io/core.py
  • dascore/io/dasdae/core.py
  • dascore/io/dasdae/utils.py
  • dascore/io/dashdf5/core.py
  • dascore/io/dashdf5/utils.py
  • dascore/io/febus/core.py
  • dascore/io/febus/g1utils.py
  • dascore/io/febus/t1utils.py
  • dascore/io/gdr/utils_das.py
  • dascore/io/h5simple/utils.py
  • dascore/io/netcdf/core.py
  • dascore/io/neubrex/utils_rfs.py
  • dascore/io/optodas/core.py
  • dascore/io/optodas/utils.py
  • dascore/io/prodml/core.py
  • dascore/io/prodml/utils.py
  • dascore/io/terra15/core.py
  • dascore/io/terra15/utils.py
  • dascore/io/utils.py
  • docs/changelog.qmd
  • docs/contributing/documentation.qmd
  • docs/contributing/new_format.qmd
  • docs/notes/coordinate_internals.qmd
  • docs/notes/dft_notes.qmd
  • docs/notes/doc_strategy.qmd
  • docs/notes/notes.qmd
  • docs/notes/patch_attrs.qmd
  • docs/tutorial/coords.qmd
  • docs/tutorial/file_io.qmd
  • docs/tutorial/remote_patches.qmd
  • docs/tutorial/spool.qmd
  • scripts/_templates/_quarto.yml
  • tests/test_io/test_common_io.py
  • tests/test_io/test_io_core.py
  • tests/test_io/test_netcdf/test_netcdf.py
  • tests/test_io/test_optodas/test_optodas.py
  • tests/test_io/test_terra15/test_terra15.py
🚧 Files skipped from review as they are similar to previous changes (32)
  • dascore/io/neubrex/utils_rfs.py
  • dascore/io/dashdf5/core.py
  • dascore/io/utils.py
  • tests/test_io/test_optodas/test_optodas.py
  • tests/test_io/test_netcdf/test_netcdf.py
  • dascore/io/optodas/core.py
  • dascore/io/dasdae/core.py
  • dascore/io/prodml/core.py
  • dascore/io/gdr/utils_das.py
  • docs/contributing/documentation.qmd
  • dascore/init.py
  • docs/changelog.qmd
  • dascore/io/terra15/core.py
  • docs/contributing/new_format.qmd
  • docs/tutorial/file_io.qmd
  • docs/notes/patch_attrs.qmd
  • dascore/io/febus/core.py
  • docs/tutorial/coords.qmd
  • tests/test_io/test_terra15/test_terra15.py
  • docs/notes/notes.qmd
  • dascore/io/init.py
  • dascore/io/optodas/utils.py
  • dascore/io/terra15/utils.py
  • tests/test_io/test_common_io.py
  • docs/notes/doc_strategy.qmd
  • dascore/io/dasdae/utils.py
  • dascore/io/prodml/utils.py
  • dascore/io/febus/g1utils.py
  • docs/tutorial/remote_patches.qmd
  • docs/notes/dft_notes.qmd
  • docs/notes/coordinate_internals.qmd
  • tests/test_io/test_io_core.py

Comment thread dascore/io/core.py
Comment thread dascore/io/core.py
@d-chambers

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
dascore/io/core.py (1)

1568-1571: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Only suppress UnknownFiberFormatError here. Catching Exception hides real get_format() failures and can misclassify a directory as non-unit, sending its members through traversal instead of surfacing the error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dascore/io/core.py` around lines 1568 - 1571, In the format-checking logic
around get_format, replace the broad Exception handler with a catch for
UnknownFiberFormatError only, returning False for that expected
unsupported-format case. Allow all other get_format failures, including
directory-related errors, to propagate instead of being classified as non-unit.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@dascore/io/febus/t1utils.py`:
- Line 69: Rename the format-related parameters in _scan_t1 to avoid shadowing
the built-in format(), and update all callers accordingly, including
FebusT1V1.scan() in the file-format keyword path. If _scan_t1 does not use these
parameters, remove them from its signature and call sites instead.

---

Outside diff comments:
In `@dascore/io/core.py`:
- Around line 1568-1571: In the format-checking logic around get_format, replace
the broad Exception handler with a catch for UnknownFiberFormatError only,
returning False for that expected unsupported-format case. Allow all other
get_format failures, including directory-related errors, to propagate instead of
being classified as non-unit.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0db95b5d-7287-4121-b6f2-ee6efc21a2ad

📥 Commits

Reviewing files that changed from the base of the PR and between 39c7354 and 37ab393.

📒 Files selected for processing (39)
  • dascore/__init__.py
  • dascore/io/__init__.py
  • dascore/io/core.py
  • dascore/io/dasdae/core.py
  • dascore/io/dasdae/utils.py
  • dascore/io/dashdf5/core.py
  • dascore/io/dashdf5/utils.py
  • dascore/io/febus/core.py
  • dascore/io/febus/g1utils.py
  • dascore/io/febus/t1utils.py
  • dascore/io/gdr/utils_das.py
  • dascore/io/h5simple/utils.py
  • dascore/io/netcdf/core.py
  • dascore/io/neubrex/utils_rfs.py
  • dascore/io/optodas/core.py
  • dascore/io/optodas/utils.py
  • dascore/io/prodml/core.py
  • dascore/io/prodml/utils.py
  • dascore/io/terra15/core.py
  • dascore/io/terra15/utils.py
  • dascore/io/utils.py
  • docs/changelog.qmd
  • docs/contributing/documentation.qmd
  • docs/contributing/new_format.qmd
  • docs/notes/coordinate_internals.qmd
  • docs/notes/dft_notes.qmd
  • docs/notes/doc_strategy.qmd
  • docs/notes/notes.qmd
  • docs/notes/patch_attrs.qmd
  • docs/tutorial/coords.qmd
  • docs/tutorial/file_io.qmd
  • docs/tutorial/remote_patches.qmd
  • docs/tutorial/spool.qmd
  • scripts/_templates/_quarto.yml
  • tests/test_io/test_common_io.py
  • tests/test_io/test_io_core.py
  • tests/test_io/test_netcdf/test_netcdf.py
  • tests/test_io/test_optodas/test_optodas.py
  • tests/test_io/test_terra15/test_terra15.py
🚧 Files skipped from review as they are similar to previous changes (36)
  • docs/tutorial/spool.qmd
  • dascore/io/dasdae/core.py
  • dascore/init.py
  • dascore/io/utils.py
  • docs/notes/notes.qmd
  • docs/tutorial/file_io.qmd
  • docs/contributing/documentation.qmd
  • docs/tutorial/coords.qmd
  • dascore/io/optodas/core.py
  • docs/notes/patch_attrs.qmd
  • scripts/_templates/_quarto.yml
  • tests/test_io/test_netcdf/test_netcdf.py
  • dascore/io/gdr/utils_das.py
  • docs/contributing/new_format.qmd
  • docs/notes/dft_notes.qmd
  • dascore/io/dashdf5/core.py
  • docs/tutorial/remote_patches.qmd
  • dascore/io/init.py
  • dascore/io/h5simple/utils.py
  • dascore/io/prodml/core.py
  • dascore/io/terra15/core.py
  • dascore/io/netcdf/core.py
  • tests/test_io/test_optodas/test_optodas.py
  • dascore/io/optodas/utils.py
  • dascore/io/prodml/utils.py
  • dascore/io/terra15/utils.py
  • tests/test_io/test_common_io.py
  • dascore/io/dasdae/utils.py
  • dascore/io/dashdf5/utils.py
  • docs/changelog.qmd
  • dascore/io/febus/core.py
  • dascore/io/neubrex/utils_rfs.py
  • tests/test_io/test_terra15/test_terra15.py
  • dascore/io/febus/g1utils.py
  • docs/notes/coordinate_internals.qmd
  • tests/test_io/test_io_core.py

Comment thread dascore/io/febus/t1utils.py Outdated
@d-chambers
d-chambers force-pushed the full-coords-scan-contract branch from 92a189a to 0d8b9cf Compare July 19, 2026 18:23

@d-chambers d-chambers left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice shape overall — splitting _iter_scan_results out so scan and scan_payloads share one traversal is the right factoring, the validation helper gives a real contract boundary, and the format audit table in the description is genuinely useful. The docs work (coordinate_internals, new_format snap section) is a clear improvement.

My main concern is get_exact_coord. CoordSegmented.from_array(..., tolerance=0) degenerates badly on jittery stored timestamps — which is exactly what GPS-stamped DAS time arrays look like — and because terra15._get_raw_time_coord is on the read path, this lands as a performance and representation regression on an existing public API, not just on the new one.

Measured on this branch vs. dev, using the checked-in trimmed terra15 test file (3,620 samples):

dev this branch
dc.read(path, snap_dims=False) 0.154 s 0.812 s
resulting time coord CoordMonotonicArray CoordSegmented, 1001 segments

And synthetic monotonic arrays with ±3 ns jitter on a 1 µs step:

n get_exact_coord get_coord(data=...) segments
1,000 0.125 s 0.0003 s 122
10,000 1.21 s 0.0007 s 1,139
50,000 6.06 s 0.0018 s 5,512
100,000 11.6 s ~11,000

Roughly 120 µs per segment, so a 1M-sample file extrapolates to ~2 minutes. Both problems come from the same place: from_array's run detection (a difference joins a run only when it equals a neighbor) means jitter turns nearly every sample into its own seam, so you get an n/2-segment object that is slower to build, larger in memory, and less useful than the CoordMonotonicArray it replaced.

The clean cases are fine — uniform-with-one-gap gives 2 segments as intended — so the fix is a guard rather than a redesign: bail out to get_coord(data=...) when the detected segment count exceeds some small fraction of len(values). That preserves the real win (exact values plus queryable seams for genuinely piecewise-uniform data) without the pathological path. Ideally detect this before materializing all the segments so the cost is bounded too. Worth a test with a jittery array asserting the coord type and that values round-trip exactly.

Three smaller items inline. Everything else — the validation helper, provenance ownership, forwarding tests, conformance test — reads well to me.

Comment thread dascore/io/utils.py
def get_exact_coord(values, units=None) -> BaseCoord:
"""Return an exact coordinate, including for non-monotonic values."""
try:
return CoordSegmented.from_array(values, tolerance=0, units=units)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where the segment explosion originates (see review body for numbers). With tolerance=0, from_array never raises on a jittery-but-monotonic array — it happily returns a CoordSegmented with ~n/2 segments — so the CoordError fallback below only catches non-monotonic/duplicate/multidim input and never the case that actually needs it.

Suggest an explicit degeneracy guard, e.g.:

def get_exact_coord(values, units=None) -> BaseCoord:
    """Return an exact coordinate, including for non-monotonic values."""
    try:
        out = CoordSegmented.from_array(values, tolerance=0, units=units)
    except CoordError:
        return get_coord(data=values, units=units)
    # Jittery arrays segment into ~n/2 runs; a plain monotonic array is
    # smaller, faster to build, and equally exact.
    if len(getattr(out, "segments", ())) > _MAX_SEGMENT_FRACTION * len(values):
        return get_coord(data=values, units=units)
    return out

get_coord(data=...) is exact for these inputs — its tolerant inference only collapses to CoordRange when the array really is near-uniform, which is the case where segmentation would have produced one segment anyway. A pre-check inside from_array that avoids building all the segments first would be better still, since the cost is ~120 µs/segment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 0dca954. Added a _is_over_segmented pre-check to get_exact_coord: for arrays large enough for the cost to matter, it cheaply predicts the segment count (mirroring from_arrays run detection) and falls back to get_coord(data=...) when segments would exceed one per ten samples. I used a pre-check rather than the post-hoc len(out.segments) guard so the degenerate path never materializes the segments at all (the cost is ~120 µs/segment). Genuinely piecewise-uniform arrays still segment and keep their queryable seams. Regression tests (jittery array stays monotonic + round-trips exactly; piecewise-uniform stays segmented) added in aea075c.

time = _get_time_node(data_node)[:]
return get_coord(data=to_datetime64(time))
values = to_datetime64(time)
return get_exact_coord(values, units="s")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two behavior changes on the read path, not just scan — _get_raw_time_coord is what _read_terra15(..., snap_dims=False) calls:

  1. get_exact_coord here is what makes dc.read(terra15_path, snap_dims=False) 5x slower and return a 1001-segment CoordSegmented instead of a CoordMonotonicArray (numbers in the review body). Fixing get_exact_coord resolves this.
  2. units="s" is newly attached to a datetime64 coord that previously had none. It matches the snap_dims=True branch above so it's probably the intended normalization, but it changes existing dc.read output and isn't in the changelog. Worth a line there either way.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 0dca954. The get_exact_coord guard resolves the read-path regression: dc.read(terra15, snap_dims=False) is back to ~0.12 s and returns a CoordMonotonicArray again instead of a 1001-segment CoordSegmented. The units="s" normalization is intentional (matches the snap_dims=True branch) and is now called out in the changelog.

Comment thread dascore/io/core.py Outdated
try:
get_format(path)
except Exception:
except UnknownFiberFormatError:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This narrowing looks unrelated to the scan-payload work, and it makes directory indexing more fragile. is_directory_format is called from indexer.py for every directory encountered during traversal, and get_format dispatches to arbitrary FiberIO.get_format implementations — an unreadable directory (PermissionError) or a formatter that raises something unexpected on a weird path will now abort the whole index instead of the directory simply not being a scan unit.

The blanket except Exception was admittedly too broad, but I'd rather see the exception set widened (UnknownFiberFormatError, OSError, at minimum) than tightened to one type — or this split into its own PR where the traversal-robustness tradeoff can be argued on its own merits.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 0dca954. Widened to except (UnknownFiberFormatError, OSError) so an unreadable directory (e.g. PermissionError) is simply not treated as a scan unit rather than aborting an index traversal, while genuinely unexpected errors still surface. Kept it in this PR since it sits on the scan path; happy to split it out if you would rather.

Comment thread dascore/io/netcdf/core.py Outdated
values = coord.values
if snap or np.ndim(values) != 1:
return values
return get_exact_coord(values, units=coord.attrs.get("units"))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor asymmetry: the snap=False path attaches coord.attrs.get("units"), but the snap=True path returns bare values with no units, so the two modes produce coords that differ in more than exactness. Optodas got the equivalent fix in this PR (get_coord(data=distance, units=unit) on the snap branch); worth doing the same here so snap only controls exactness.

Separately — that optodas units change is itself a user-visible change to snap=True scan output and isn't mentioned in the changelog.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 0dca954. The snap=True NetCDF branch now returns get_coord(data=values, units=...) too, so snap controls only exactness and both modes carry units. The changelog note now also mentions the snap=True units change for OptoDAS, NetCDF, and Terra15.

time_array = node[time_name]
array_len = len(time_array)
assert array_len > 0, "Missing time array in ProdML file."
if not snap:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth a docs note: ProdML's read has no snap/snap_dims parameter, so dc.scan_payloads(path, snap=False) returns the exact stored per-sample array while dc.read(path) always returns the header-derived PartStartTime/PartEndTime range. The conformance test in test_common_io.py skips the value comparison for formats without an exact read, so nothing catches the divergence.

That's arguably correct — read is the lossy one — but a caller comparing payload coords to loaded patch coords will be surprised. The snap contract section in new_format.qmd would be a good place to say that snap=False describes stored values, not necessarily what read returns.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 0dca954. Added a paragraph to the snap contract section of new_format.qmd stating that snap=False describes the values a format stores, which are not necessarily what read(...) returns (ProdML reconstructs its time coord from PartStartTime/PartEndTime), and noting the common-IO conformance test only compares scan vs read for formats whose read also accepts snap/snap_dims.

get_exact_coord fell back to CoordSegmented.from_array(tolerance=0) for
any monotonic array. Sub-step jitter (e.g. GPS-stamped DAS timestamps)
turns nearly every sample into its own seam, producing an n/2-segment
coordinate that is slower to build (~120us/segment), larger in memory,
and no more exact than a plain monotonic coord. Because Terra15's exact
read path also uses this helper, that regressed dc.read(snap_dims=False).

- Skip segmentation when a large array would exceed one segment per ten
  samples, falling back to an exact monotonic/array coordinate; genuinely
  piecewise-uniform arrays still segment and keep queryable seams.
- Promote 0-d input to length-1 so single-sample files (Febus T1) build a
  valid length-1 coordinate instead of a scalar.
- Attach coordinate units on the NetCDF snap=True scan branch so snap only
  controls exactness, matching OptoDAS.
- Widen is_directory_format to also swallow OSError so an unreadable
  directory is skipped rather than aborting an index traversal.
- Document the jitter fallback and the snap=False (stored values) vs read
  distinction in new_format.qmd; add changelog note.
Add tests for the large non-monotonic fallback in get_exact_coord and
for the NetCDF snap=True (units) and multi-dim scan-coord branches so the
new diff is fully covered.
@d-chambers
d-chambers merged commit 8cc0a07 into dev Jul 20, 2026
27 checks passed
@d-chambers
d-chambers deleted the full-coords-scan-contract branch July 20, 2026 04:49
d-chambers added a commit that referenced this pull request Jul 20, 2026
This test asserts no ResourceWarning fires while it garbage-collects its
own spool. But gc.collect() also reaps collectable garbage left by
earlier tests, and a leaked open SQLite connection emits a
ResourceWarning when collected -- which the recording window then caught
and blamed on this test. The result was a full-suite-order failure
(reproducible on dev independent of this branch; #754 merged with this
same test_code job red) that passed in isolation and within its own
file.

Drain pending garbage with a gc.collect() before opening the recording
window, so only the spool under test is collected inside it. The test's
intent -- this spool's backend connection closes silently on GC -- is
unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation IO Work for reading/writing different formats

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant