Add the inventory patch surface: acquisition_key, reader vocabulary, and enrich - #855
Conversation
Patch attrs had no story about who owns which field. Identity was spread over network/station/instrument_id/acquisition_id, every reader spelled the observing system's facts its own way, some shipped a companion attr naming a value's units, and a few put the file path in the patch. None of that can meet an inventory: two spellings of gauge length are two attrs, and nothing downstream can reconcile them. Sort every attr into one of four kinds and let the kind decide the name and the owner. Identity becomes the single data_source_id field holding network.fiber_array.location.acquisition; component queries are wildcard matches on it. station is not reused because DAS MiniSEED already spends it on a per-sampling-point identity. Observing-system facts take the inventory's own names, listed in INVENTORY_ATTRS and tested against the models they mirror, with nested facts dotted (interrogator.serial_number). Those attrs carry fixed units, so readers convert at the parse boundary through convert_attr_units and the *_units companions are gone. Storage provenance keeps the source_ prefix everywhere, including the contents and index columns, and leaves patch attrs entirely: it says where the bytes live, and a patch merged from three files has no single answer. A conformance test holds every shipped reader to the vocabulary, so a format added next year cannot quietly reinvent channel_spacing. Extra attrs are still allowed; the test is a review checkpoint, not a runtime rule. Breaking: the removed attrs fields, the reader respellings, the *_units removal, and the source_* column rename are all in the changelog.
An inventory that a patch cannot be joined to is a document, not metadata. enrich is that join: the patch's data_source_id and time resolve one acquisition and optical path, the acquisition's channel map places each channel on the path axis, and the path's tracks project onto the patch. The rules follow from who owns what. Blanket attrs=True copies the observing-system facts under the inventory's own names and leaves the data trio alone, since processing maintains those; naming one restores the as-acquired value. Disagreements go through conflicts, reusing chunking's flag and vocabulary, so a header contradicting the resolved acquisition can raise instead of being silently replaced -- usually the sign that the data_source_id resolved to the wrong place. A patch straddling an epoch boundary raises rather than picking one side, because acquisition metadata is scalar per patch. Which patch coordinate the channel map applies to is the map's to declare: channel numbers for the affine form and a channel-axis distance map, interrogator meters for an instrument_distance map. Guessing would be wrong by a factor of the channel spacing, so a patch without the declared coordinate raises and says which map form fits it. Long argument reprs are now truncated in patch history; without that every enriched patch carried a full inventory dump in its history.
Calling enrich on every extracted patch by hand is the kind of step that gets forgotten halfway through an analysis, and the spool already knows which patches it is about to hand out. attach_inventory holds the inventory and enriches on extraction, so metadata arrives with the data. The inventory joins the spool's equality state: two spools over the same rows yield different patches when one is attached, so they are not the same spool. This is deliberately only the extraction hook. Resolving the index against the inventory, subdividing it at epoch boundaries, and selecting on inventory tracks are phase 3 and are called out as absent in the method's own documentation.
Four reviews (one Codex, three adversarial) against the three phase-2 commits. Every finding below was reproduced before it was fixed. Data that was silently wrong: - Febus A1 states its pulse as a length in meters, which the deleted units companion had recorded correctly; converting it as nanoseconds made it wrong by eight orders of magnitude. It keeps terra15's pulse_length name for the same quantity. - Silixa's Carina variant reads the same nanosecond header key through a second code path, which the conversion missed, so 110 ns became 110 seconds. - Neubrex spells the units key in the singular, so its conversion never ran and any file stating cm would have passed through as meters. - A file which declares units that cannot be used has a value of unknown scale. Keeping the number passed "5 s" off as 5 meters; the value is now dropped with a warning. - TDMS filtered its parsed header through PatchAttrs.model_fields, which no dotted name can be in, so it dropped the interrogator serial it had just read. Failures which only surfaced at load: - An attr named path, file_format, or file_version stopped being reserved when the flat columns were renamed, so it was splatted into dc.read beside the row's own value and every trimmed read of that spool raised TypeError. - A hive path stamping an incomplete data_source_id indexed cleanly and then failed at every patch access, far from the directory that caused it. Refuse it at indexing instead. Enrichment: - Projected coordinates now carry the units the CRS and the model state. - Re-enriching is a refresh, coordinates included: an identical coordinate is not a collision, but a contradicting one still is. - NaN is how a reader spells an unknown number, so it is filled rather than treated as a value which disagrees. Previously conflicts="drop" deleted the very value enrichment exists to supply. - sample_rate and spatial_interval are maintained by processing, so blanket enrichment no longer restores stale ones; naming one still does. A patch decimated in time kept its as-acquired sample rate. - Coverage runs end where they end: a track interval elsewhere on the path no longer changes which channels an earlier interval covers. - A geometry-less path, an unset or misspelled track field, and a multi-valued component field now report through on_missing or raise a named error instead of returning silent NaN or a numpy broadcast error. - A union of spools carries an attached inventory instead of dropping it, and two different attachments raise.
An acquisition had two ways to place its channels on the path and a validator to keep authors from using both. The affine form (start_distance + spatial_interval) mapped channel numbers, which no DASCore reader produces: of the 38 example files, 36 carry meters and none carry channels, most with a spacing that is not one and an origin that is not zero. So the form that looked simple was the one nobody could use, and the rule policing the pair existed only because there were two. A single control point says everything the affine form said -- an origin, with spatial_interval as its slope on the channel axis and one meter of path per interrogator meter on the other -- and it extrapolates, where a two-point map goes undefined past its ends. So the affine form and its exclusivity rule are gone. With one mechanism left, "exactly one input axis" was also the wrong rule for the map itself. It now states at least one, and may state both, so one set of control points can be written in whichever coordinates were measured and serve patches whose axes differ. The patch decides which axis is read. A patch carrying more than one of them must agree with the map about all of them: two coordinates which place the same channel in different places mean the patch contradicts the map, and picking one would answer a question the data disputes. An acquisition written against the removed field now says what replaced it instead of raising pydantic's "extra inputs are not permitted".
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe PR adds inventory-based patch enrichment and spool attachment. It consolidates patch identity into ChangesInventory and metadata integration
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Reviews of the two-axis relaxation. A map stating both axes describes one interrogator, which samples at a fixed spacing, so axes implying a spacing that varies along the fiber now raise: they describe no instrument, and reading such a map on one axis contradicts reading it on the other. The axis argument was an unvalidated getattr, so axis="distance" -- the name of the patch coordinate the instrument axis is read from, and so the likeliest wrong value -- silently interpolated distance onto distance and returned the input unchanged. An axis the map cannot be read on (a channel axis with no spacing) no longer vetoes the axes that can, and the failure that results names every reason rather than the first. When two coordinates have different lengths the message says so instead of reporting two equal numbers as a disagreement, and the agreement tolerance is absolute: a relative one widens to a whole channel tens of kilometers down the fiber, which is exactly where a disagreement matters.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2eb7f151ea
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "inventory to the combined spool instead." | ||
| ) | ||
| raise InvalidSpoolError(msg) | ||
| return attached[0] |
There was a problem hiding this comment.
Do not apply one operand's inventory to the entire union
When only one operand has an attached inventory, returning that attachment causes the combined spool's _maybe_enrich path to apply it to patches from both operands. If the unattached operand contains another data source, accessing those patches will either fail resolution or silently enrich them with unrelated metadata; the union must either retain attachment state per operand or reject mixed attached/unattached nonempty spools.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Kept deliberately, and the hazard is now handled elsewhere. A union carries an inventory attached to either operand, which is the rule we want: attaching the same inventory to the other operand must not turn a working union into an error. The failure you describe is real but is not union-specific -- dc.spool([resolvable, unresolvable]).enrich(inv) fails identically -- so it was fixed at the root: Spool.enrich now takes on_unresolved (warn by default, plus raise/ignore), and a patch the inventory does not describe comes out unenriched rather than raising. Deciding membership stays with the not-yet-implemented prune_to_inventory (#857).
Note: the identity attr was renamed data_source_id -> acquisition_key after this review, so line numbers and some names have moved.
| new._inventory = inventory | ||
| new._enrich_kwargs = dict(kwargs) if enrich else None |
There was a problem hiding this comment.
Preserve inventory attachments across single-file updates
When an inventory is attached to a root created by Spool.from_file, calling update() is permitted but its single-file branch reconstructs the result with self.from_file(...), restoring _inventory and _enrich_kwargs to their class defaults. The returned spool therefore stops enriching patches after a rescan, unlike directory and in-memory roots; the attachment state needs to be copied to the refreshed file spool.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed. update()'s single-file branch rebuilt through the from_file classmethod, which constructs a spool from the file alone; the refreshed spool now inherits _inventory, _enrich_kwargs, and _on_unresolved. The directory-backed branch copy-constructs, which is what hid it. Covered by TestSplitReviewFindings::test_update_keeps_the_inventory.
Note: the identity attr was renamed data_source_id -> acquisition_key after this review, so line numbers and some names have moved.
| for name, _, distances in resolved[1:]: | ||
| if not _distances_agree(first[2], distances): |
There was a problem hiding this comment.
Reject map coordinates attached to different dimensions
When a patch carries both map-supported coordinates on different one-dimensional dimensions, this loop discards each later dimension and compares only the mapped value arrays. If the dimensions happen to have equal lengths and values, enrichment accepts the ambiguous patch and attaches every projected geometry or annotation coordinate to the first dimension, potentially labeling the wrong axis; all resolved map coordinates should be required to share the same dimension.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed. All resolved axes must now share one dimension, raising and naming both dimensions otherwise. That also made the length check below it unreachable -- two coordinates on the same dimension always have the same length -- so the dead branch was removed rather than left as unreachable code.
Note: the identity attr was renamed data_source_id -> acquisition_key after this review, so line numbers and some names have moved.
| axis = self.axes[0] if axis is None else axis | ||
| out = getattr(self, axis, None) | ||
| if out is None: | ||
| msg = f"This DistanceMap is not written in {axis!r}; it has {self.axes}." |
There was a problem hiding this comment.
Validate requested axes against DistanceMap.axes
When a caller passes an existing non-input attribute such as axis="distance", getattr returns the map's target-distance control points, so this check accepts them as an input axis. map_to_distance can then silently use the output distances as its source coordinate and return an incorrect identity-like calibration; reject every explicit axis that is not present in self.axes before reading the attribute.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Already handled, in two layers. source_values first rejects any name that is not a DistanceMap input axis (so axis="distance" raises rather than returning the target control points), then rejects an input axis this particular map is not written in, naming self.axes. Verified: axis="distance" and axis="nope" give "is not a DistanceMap input axis", and axis="channel" on an instrument_distance-only map gives "This DistanceMap is not written in 'channel'; it has ('instrument_distance',)".
Note: the identity attr was renamed data_source_id -> acquisition_key after this review, so line numbers and some names have moved.
| if isinstance(value, tuple | list): | ||
| msg = ( | ||
| f"Cannot project the multi-valued {value!r} onto channels; " | ||
| "a coordinate holds one value per channel." | ||
| ) | ||
| raise PatchError(msg) |
There was a problem hiding this comment.
Reject mapping-valued track fields before projection
When a qualified request targets a populated mapping field such as coupling.extra_fields, the value bypasses this multi-value guard because only tuples and lists are recognized. _annotation_kind then classifies the dict as numeric and the final float conversion raises a raw TypeError, even though extra_fields is a legitimate field on every inventory track model; mapping and other non-scalar values should be rejected with the same PatchError as the already-handled multi-valued fields.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed. The guard caught only tuples and lists, so a mapping reached the numeric branch and raised a bare TypeError out of float() that on_missing could not intercept. It now rejects any sized non-string value, which covers extra_fields -- a real field on every track model, so this was a reachable request rather than a contrived one.
Note: the identity attr was renamed data_source_id -> acquisition_key after this review, so line numbers and some names have moved.
There was a problem hiding this comment.
Actionable comments posted: 14
🧹 Nitpick comments (4)
dascore/core/attrs.py (1)
88-99: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider rejecting the removed identity keys explicitly.
extra="allow"meansPatchAttrs(network="XX", station="S1")still constructs. The values become inert extra attrs, and grouping, merging, and naming ignore them silently.Acquisition._reject_start_distanceindascore/core/inventory.pygives the analogous removal an explicit migration message. Adding the same treatment here would surface stale reader and user code at construction time.♻️ Proposed migration guard
+_REMOVED_IDENTITY_FIELDS = ("network", "station", "instrument_id", "acquisition_id") + + class PatchAttrs(DascoreBaseModel):`@model_validator`(mode="before") `@classmethod` def reject_coordinate_attributes(cls, data: Any) -> Any: """Reject nested coord payloads and ignore structural dims input.""" if not isinstance(data, Mapping): return data data = dict(data) if "coords" in data and not isinstance(data["coords"], str): msg = "PatchAttrs no longer accepts coordinate metadata. Received: coords." raise ValueError(msg) + if removed := sorted(set(data) & set(_REMOVED_IDENTITY_FIELDS)): + msg = ( + f"PatchAttrs no longer accepts {removed}; these are now spelled " + "as one data_source_id: network.fiber_array.location.acquisition." + ) + raise ValueError(msg) data.pop("dims", None) return data🤖 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/core/attrs.py` around lines 88 - 99, Update PatchAttrs.reject_coordinate_attributes to explicitly reject the removed identity keys network and station, raising a clear migration error when either is present before extra attributes are accepted. Preserve the existing coords validation and dims removal behavior, and follow Acquisition._reject_start_distance’s migration-message pattern.dascore/proc/inventory.py (1)
470-480: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDrop the redundant
optical_distancesparameter.The only call site passes
distancesfor both parameters, so the two names describe one array. The split signature suggests they can differ, which invites a future caller to pass mismatched arrays into the"distance"branch.♻️ Proposed refactor
-def _get_coord_values(inventory, path, name, distances, optical_distances): +def _get_coord_values(inventory, path, name, distances): """Return the values of one requested coordinate, or None if undefined.""" if name == "distance": # Optical path distance is in meters, as every path length is. - return get_coord(data=optical_distances, units="m") + return get_coord(data=distances, units="m")- values = _get_coord_values(inventory, path, name, distances, distances) + values = _get_coord_values(inventory, path, name, distances)Also applies to: 524-524
🤖 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/proc/inventory.py` around lines 470 - 480, Remove the redundant optical_distances parameter from _get_coord_values and use distances directly in the "distance" branch. Update the function’s call site to pass only distances, preserving all other coordinate-resolution behavior.dascore/io/prodml/utils.py (1)
347-347: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winConsider accepting
acquisition_idas a deprecated input alias.The writer now reads only
experiment_id. A patch that still carriesacquisition_idgets a freshuuid4()as itsAcquisitionId, so the recorded campaign identity is lost without any message. Issue#854recommended keepingacquisition_idas a deprecated alias with a warning.🤖 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/prodml/utils.py` at line 347, Update the acquisition ID handling around the `"AcquisitionId"` attribute so it first uses `experiment_id`, falls back to the deprecated `acquisition_id` alias when needed, and only generates a new UUID when neither is provided. Emit a deprecation warning whenever `acquisition_id` is used, while preserving the existing string conversion.tests/test_proc/test_proc_inventory.py (1)
646-657: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDrop the
andguard; callenrichdirectly.
with_far.networksis always a non-empty tuple here, sowith_far.networks and patch.enrich(...)always evaluates to the enrich result. The guard adds no coverage and hides the intent of the assertion.♻️ Proposed simplification
near = patch.enrich(inventory, attrs=False, coords=("coupling.medium",)) - both = with_far.networks and patch.enrich( - with_far, attrs=False, coords=("coupling.medium",) - ) + both = patch.enrich(with_far, attrs=False, coords=("coupling.medium",))🤖 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 `@tests/test_proc/test_proc_inventory.py` around lines 646 - 657, Remove the redundant with_far.networks and guard in test_endpoint_belongs_to_its_own_run and call patch.enrich(with_far, attrs=False, coords=("coupling.medium",)) directly when assigning both, preserving the existing assertions.
🤖 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/core/spool.py`:
- Around line 80-102: Update _combine_inventories so a union with an inventory
attached to only one operand is rejected rather than returning that inventory
and enrichment arguments. Preserve the existing rejection for two differing
attachments, and only return inventory data when both operands carry the same
attachment.
- Around line 681-684: Update the spool-copy logic around __class__(self) so
refreshed spools preserve the source spool’s _inventory and _enrich_kwargs
attachment state, including when single-file update() rebuilds via from_file.
Copy both fields to the returned spool before returning it, while retaining the
existing enrich-based kwargs behavior where applicable.
In `@dascore/io/index/schema.py`:
- Around line 286-288: Remove the duplicated "source_path" and "source_format"
members from the set near "source_version" in the schema definition, leaving
only the new "source_version" entry while preserving the existing members.
In `@dascore/proc/inventory.py`:
- Around line 171-174: Update the data-state attribute lookup loop in
_get_system_attrs to call getattr with a None default, matching the existing
_get_system_attrs readers and allowing missing Acquisition fields to continue
through the on_missing path instead of raising AttributeError.
- Line 377: Update the zip call constructing spans to pass strict=True,
preserving the existing filtering and tuple construction while ensuring
intervals and values length mismatches raise instead of truncating.
- Around line 442-452: Update _get_geometry_coord to resolve the CRS axis index
defensively, treating an unknown coordinate label as a missing value instead of
allowing crs.axis_index(label) to raise InvalidInventoryError. Return None for
labels outside the CRS so the existing values-is-None handling can apply the
caller’s on_missing behavior.
In `@docs/recipes/real_time_proc.qmd`:
- Around line 112-113: Update the output-path construction around source_path
and output_path to extract only the source file’s basename, then append a
patch-specific suffix using patch_num so multiple patches cannot collide. Join
this sanitized patch filename with output_data_dir, ensuring absolute or nested
source paths never escape the output directory.
In `@tests/test_core/test_spool.py`:
- Around line 468-473: Require both selection examples to verify non-empty
results: in tests/test_core/test_spool.py lines 468-473, update
test_select_data_source_id to assert len(out) > 0 before iterating; in
docs/notes/spool_selection.qmd lines 31-32, add assert len(selected) > 0 before
the existing all(...) assertion.
In `@tests/test_io/test_common_io.py`:
- Around line 707-713: Update test_no_storage_provenance_attrs to include
source_path, source_format, and source_version in the prohibited PatchAttrs
field set, while preserving the existing rejection of path, file_format, and
file_version.
In `@tests/test_io/test_febus/test_febusbsl.py`:
- Around line 81-84: Update test_read_attrs_omit_storage_provenance to include
source_path, source_format, and source_version in the forbidden attribute-name
set, while preserving the existing assertions for path, file_format, and
file_version.
In `@tests/test_io/test_febus/test_febusg1.py`:
- Around line 178-182: Update test_read_attrs_omit_storage_provenance in
tests/test_io/test_febus/test_febusg1.py (lines 178-182) to assert absence of
source_path, source_format, and source_version alongside the legacy names;
update the corresponding SOR scan reader test in
tests/test_io/test_sr4731/test_sr4731.py (lines 183-185) with the same source_*
absence assertion.
In `@tests/test_io/test_xml_binary/test_xml_binary.py`:
- Around line 295-305: Update test_read_omits_provenance and
test_scan_omits_provenance to also reject source_path, source_format, and
source_version in the attribute-name assertions, preserving the existing checks
for the legacy provenance fields.
In `@tests/test_utils/test_patch_utils.py`:
- Line 872: Replace the "/tmp/real_file.h5" test value in the source_path
assignment with a neutral absolute non-memory path, preserving the existing test
behavior and the "memory://registry/patch" value.
---
Nitpick comments:
In `@dascore/core/attrs.py`:
- Around line 88-99: Update PatchAttrs.reject_coordinate_attributes to
explicitly reject the removed identity keys network and station, raising a clear
migration error when either is present before extra attributes are accepted.
Preserve the existing coords validation and dims removal behavior, and follow
Acquisition._reject_start_distance’s migration-message pattern.
In `@dascore/io/prodml/utils.py`:
- Line 347: Update the acquisition ID handling around the `"AcquisitionId"`
attribute so it first uses `experiment_id`, falls back to the deprecated
`acquisition_id` alias when needed, and only generates a new UUID when neither
is provided. Emit a deprecation warning whenever `acquisition_id` is used, while
preserving the existing string conversion.
In `@dascore/proc/inventory.py`:
- Around line 470-480: Remove the redundant optical_distances parameter from
_get_coord_values and use distances directly in the "distance" branch. Update
the function’s call site to pass only distances, preserving all other
coordinate-resolution behavior.
In `@tests/test_proc/test_proc_inventory.py`:
- Around line 646-657: Remove the redundant with_far.networks and guard in
test_endpoint_belongs_to_its_own_run and call patch.enrich(with_far,
attrs=False, coords=("coupling.medium",)) directly when assigning both,
preserving the existing assertions.
🪄 Autofix
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 Plus
Run ID: 219f551e-782a-44ba-b61a-cd59412ec4ae
📒 Files selected for processing (90)
benchmarks/test_spool_benchmarks.pydascore/config.pydascore/constants.pydascore/core/attrs.pydascore/core/inventory.pydascore/core/patch.pydascore/core/spool.pydascore/core/summary.pydascore/examples.pydascore/io/ap_sensing/utils.pydascore/io/dasdae/core.pydascore/io/dashdf5/core.pydascore/io/febus/a1utils.pydascore/io/febus/core.pydascore/io/febus/g1utils.pydascore/io/febus/t1utils.pydascore/io/gdr/core.pydascore/io/gdr/utils_das.pydascore/io/h5simple/core.pydascore/io/h5simple/utils.pydascore/io/index/backend.pydascore/io/index/catalog.pydascore/io/index/ingest.pydascore/io/index/planned.pydascore/io/index/schema.pydascore/io/neubrex/core.pydascore/io/neubrex/utils_das.pydascore/io/odh4/core.pydascore/io/optodas/core.pydascore/io/optodas/utils.pydascore/io/prodml/core.pydascore/io/prodml/utils.pydascore/io/segy/core.pydascore/io/segy/utils.pydascore/io/silixah5/core.pydascore/io/silixah5/utils.pydascore/io/sintela/core.pydascore/io/sintela/protobuf_utils.pydascore/io/sr4731/core.pydascore/io/sr4731/utils.pydascore/io/tdms/utils.pydascore/io/terra15/utils.pydascore/io/utils.pydascore/io/xml_binary/core.pydascore/io/xml_binary/utils.pydascore/proc/__init__.pydascore/proc/inventory.pydascore/utils/chunk_plan.pydascore/utils/misc.pydascore/utils/patch.pydascore/utils/paths.pydocs/changelog.qmddocs/contributing/new_format.qmddocs/notes/spool_chunking.qmddocs/notes/spool_index.qmddocs/notes/spool_selection.qmddocs/recipes/real_time_proc.qmddocs/tutorial/file_io.qmddocs/tutorial/patch.qmddocs/tutorial/spool.qmdtests/test_core/test_attrs.pytests/test_core/test_directory_spool.pytests/test_core/test_file_spool.pytests/test_core/test_inventory.pytests/test_core/test_patch_chunk.pytests/test_core/test_spool.pytests/test_io/test_common_io.pytests/test_io/test_dasdae/test_dasdae.pytests/test_io/test_febus/test_febusbsl.pytests/test_io/test_febus/test_febusg1.pytests/test_io/test_index/test_catalog.pytests/test_io/test_index/test_db_dirspool.pytests/test_io/test_index/test_heterogeneity_stress.pytests/test_io/test_index/test_hive_attrs.pytests/test_io/test_index/test_index_contract.pytests/test_io/test_index/test_index_edge_cases.pytests/test_io/test_index/test_plan.pytests/test_io/test_index/test_planned.pytests/test_io/test_index/test_union.pytests/test_io/test_indexer.pytests/test_io/test_io_core.pytests/test_io/test_pickle/test_pickle.pytests/test_io/test_prodml/test_prodml_write.pytests/test_io/test_sr4731/test_sr4731.pytests/test_io/test_xml_binary/test_xml_binary.pytests/test_proc/test_proc_inventory.pytests/test_utils/test_attrs_utils.pytests/test_utils/test_chunk.pytests/test_utils/test_config.pytests/test_utils/test_patch_utils.py
💤 Files with no reviewable changes (6)
- dascore/io/gdr/core.py
- dascore/io/odh4/core.py
- dascore/io/sintela/core.py
- dascore/io/neubrex/core.py
- dascore/io/silixah5/core.py
- dascore/io/sr4731/core.py
A review pass that ran real workflows against dev and against this branch, plus the first CI run. Upgrading: - Default patch names now contain dots, because data_source_id does, and get_patch_names stripped everything after the first dot when reading a name back off a path. Writing patches under their own names and re-indexing collapsed every patch of one source onto one truncated, colliding name. It strips the extension only. - An archive partitioned by network=/station= hive keys stopped being partitioned when those left the default groupby_attrs, so chunking merged patches from different places -- silently discarding one of them under conflict="drop". They stay in the default: a name missing from a spool is ignored, grouping too finely only leaves patches unmerged, and grouping too coarsely loses data. - A patch written before the units were fixed still carries the companion attr naming its own, so the ProdML writer honors it instead of stamping seconds on a value in nanoseconds. - The index columns kept their names while their vocabulary and units changed, which no version check could catch. The index version is bumped, so a stale index is rebuilt rather than mixing the two. - A spool carrying an inventory did not equal its own pickle: deep_equality_check walks a model's fields, where an unset NaT time never equals itself. Inventories compare with == in __eq__ instead. CI: - The lint hook wants the future import wherever annotations appear. - The docs link validator resolves callables, not module constants. - NetCDF hands back the attrs stored in the file, like DASDAE, so the file rather than the reader chooses the names; the vocabulary test exempts both and lists MiniSEED's own reader-chosen names.
Two review passes looking for helpers that do not earn their keep and for rules stated more than once. The half-open coverage rule with the run end included was written twice, in coordinates_at for geometry and in _fill_from_intervals for the tracks. It is one rule, so it is one function, interval_masks, which the model owns and enrichment uses. The "paired, finite, strictly increasing" control-point check was written three times across DistanceMap and Geometry; it is now one check with the object's name passed in. enrich carried its own _values_equal which returned False for any array attr, because bool(array == array) raises and it caught that as a disagreement. The models' own comparison already handles arrays, nulls, mappings and sequences, so it is promoted to public and used instead. The dotted-name owner lookup is one helper rather than two spellings, and the on_missing values derive from the signature's own Literal instead of being written twice. convert_attr_units swaps a parameter no reader used for one two readers need: a from_units default for headers whose units live in the key name, which is what silixa's private converter was doing. Gone entirely: a segy wrapper whose body no longer read the header it named, an sr4731 extras chain whose last caller this branch removed, a prodml wrapper restating its own loop, a febus zero-argument function returning a constant, and a bool() cast around one numpy call.
Splatting a plain dict widens the Literal-typed data_type and data_category to str; from_dict validates the same mapping without claiming to. The prodml measure is checked for absence before float(), which cannot take None.
|
✅ Documentation built: |
The reader emits the interrogator's facts under their dotted names; the test still asked for the vendor spellings, which only CI could see because the protobuf reader needs an optional dependency.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/core/inventory.py`:
- Line 1070: Update the zip call in the geometry/mask iteration to use
strict=True, enforcing that self.geometry and masks contain exactly one
corresponding entry per interval as guaranteed by interval_masks.
🪄 Autofix
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 Plus
Run ID: 87bc1b7f-cf63-46bf-9c00-db42a1e228fe
📒 Files selected for processing (24)
dascore/config.pydascore/constants.pydascore/core/inventory.pydascore/core/spool.pydascore/io/febus/t1utils.pydascore/io/index/schema.pydascore/io/prodml/utils.pydascore/io/segy/core.pydascore/io/segy/utils.pydascore/io/silixah5/utils.pydascore/io/sr4731/utils.pydascore/io/utils.pydascore/proc/inventory.pydascore/utils/models.pydascore/utils/patch.pydocs/changelog.qmddocs/contributing/new_format.qmdtests/test_core/test_inventory.pytests/test_io/test_common_io.pytests/test_io/test_io_core.pytests/test_io/test_sintela/test_protobuf.pytests/test_proc/test_proc_inventory.pytests/test_utils/test_config.pytests/test_utils/test_patch_utils.py
💤 Files with no reviewable changes (1)
- dascore/io/segy/utils.py
🚧 Files skipped from review as they are similar to previous changes (13)
- docs/changelog.qmd
- dascore/io/index/schema.py
- dascore/io/utils.py
- dascore/io/febus/t1utils.py
- tests/test_utils/test_patch_utils.py
- docs/contributing/new_format.qmd
- tests/test_io/test_io_core.py
- dascore/constants.py
- dascore/utils/patch.py
- dascore/io/prodml/utils.py
- dascore/core/spool.py
- dascore/io/silixah5/utils.py
- tests/test_core/test_inventory.py
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## dev #855 +/- ##
==========================================
Coverage 100.00% 100.00%
==========================================
Files 176 177 +1
Lines 19501 20017 +516
==========================================
+ Hits 19501 20017 +516
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Febus' own reference library declares PulseWidth with unit "m" (febus_optics_lib 1.4.2, plugins/plugins_das_febus.py), which settles it: the value is a length, not a time, and keeps terra15's pulse_length name. The legacy group attrs say they are legacy, in the comment and in the config field's own description. And the reason sample_rate and spatial_interval sit out of blanket enrichment is stated as what it is: nothing should be redundant between a patch's coordinates and its attrs, and those two are what the time and distance coordinates already say.
Attaching an inventory and using it were one operation, so a spool could not carry an inventory without paying to enrich every patch it yielded. Enrichment is real per-patch work — roughly a 70% overhead on extracting a patch from disk — and a spool being selected on rather than read should not pay it. attach_inventory now only carries the inventory. Spool.enrich is what uses it, holding its arguments and enriching each patch as it comes out, and Spool.remove_inventory drops both. Attaching clears enrichment set up from a previous inventory: swapping the source underneath a configured enrichment would silently rewrite every patch's metadata. A union carries the inventory and the enrichment independently, so attaching the same inventory to the other operand can no longer turn a working union into an error. Enrich arguments are bound against Patch.enrich's signature when they are given, so a misspelled one fails there rather than on some patch pulled much later, and two spools which enrich identically compare equal however the arguments were spelled. Patch.enrich no longer writes a nested update_coords history entry, which pasted a rendered repr of every added coordinate into the history of every enriched patch and cost 24% of its runtime. Selecting on inventory-defined fields still is not supported; the query error now says so instead of denying the field exists (#857).
There was a problem hiding this comment.
🧹 Nitpick comments (2)
tests/test_proc/test_proc_inventory.py (1)
951-954: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueSilence the pickle rule on this round-trip assertion.
Ruff reports S301 on line 954. The payload comes from
pickle.dumpsin the same statement, so the data is trusted. If S301 is enforced in CI, add a targeted suppression to keep the run clean.🔧 Proposed change
- assert pickle.loads(pickle.dumps(spool)) == spool + assert pickle.loads(pickle.dumps(spool)) == spool # noqa: S301🤖 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 `@tests/test_proc/test_proc_inventory.py` around lines 951 - 954, Suppress Ruff S301 only on the pickle round-trip assertion in test_attached_spool_survives_pickle, using the repository’s targeted inline-noqa convention so the trusted pickle.loads(pickle.dumps(spool)) call no longer fails linting without disabling the rule elsewhere.Source: Linters/SAST tools
dascore/core/spool.py (1)
574-575: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAnnotate
_inventoryfor consistency.
_enrich_kwargscarries a type annotation._inventorydoes not. Add one so both attachment fields declare their type.♻️ Proposed change
- _inventory = None + _inventory: Inventory | None = None _enrich_kwargs: dict | None = None🤖 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/core/spool.py` around lines 574 - 575, Annotate the class-level `_inventory` field alongside `_enrich_kwargs` with its appropriate type, preserving its existing `None` initialization and matching the type declaration style used for the attachment fields.
🤖 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/core/spool.py`:
- Around line 574-575: Annotate the class-level `_inventory` field alongside
`_enrich_kwargs` with its appropriate type, preserving its existing `None`
initialization and matching the type declaration style used for the attachment
fields.
In `@tests/test_proc/test_proc_inventory.py`:
- Around line 951-954: Suppress Ruff S301 only on the pickle round-trip
assertion in test_attached_spool_survives_pickle, using the repository’s
targeted inline-noqa convention so the trusted pickle.loads(pickle.dumps(spool))
call no longer fails linting without disabling the rule elsewhere.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: cf647bd9-aefc-49c4-8b83-69610b92391d
📒 Files selected for processing (4)
dascore/core/spool.pydascore/proc/inventory.pydocs/changelog.qmdtests/test_proc/test_proc_inventory.py
🚧 Files skipped from review as they are similar to previous changes (2)
- docs/changelog.qmd
- dascore/proc/inventory.py
Enriching a spool raised as soon as it reached a patch the inventory had no entry for, so an inventory covering part of an archive could not be used on it at all. It now leaves such a patch as it found it, under an on_unresolved policy of warn, ignore, or raise. Enrich decides metadata; deciding membership stays prune_to_inventory's job, which is what keeps enrichment lazy: nothing resolves until a patch is pulled, so len(spool) and get_contents() keep their meaning. A patch straddling two epochs is described twice rather than not at all and still raises, since the answer there is to subdivide it; UnresolvedPatchError tells the two conditions apart. Also addresses the PR review comments: requires the map's axes to share one dimension rather than picking the first (which made the length check below it unreachable), drops two duplicate reserved column names, adds strict= to two zips, and checks the renamed storage provenance names in the reader tests through a new STORAGE_PROVENANCE_ATTRS constant. The real-time recipe no longer joins an absolute source_path onto the output directory, which resolved to the input file and overwrote it.
_resolve_context converted every InvalidInventoryError from resolve into UnresolvedPatchError, so on_unresolved could wave through an id which is not four dotted parts. A patch's own id is validated when its attrs are built, so only the explicit argument can be malformed, and that is the caller getting it wrong rather than the inventory being silent about a patch it does not describe.
The guard caught tuples and lists, so a mapping reached the numeric branch and raised a bare TypeError out of float() which on_missing could not intercept. Every track model carries an extra_fields mapping, so asking for one was a real request rather than a contrived one.
The field resolves a patch against an inventory: it is a structured lookup path, network.fiber_array.location.acquisition, validated to exactly four dotted codes. Naming it with an _id suffix promised the opposite of what it holds, since _id elsewhere is free to be an opaque token and usually a UUID. acquisition_key says what it is without claiming to be an acquisition (it resolves with time to a sequence of them) or to be one code (Acquisition.code holds only the last part). It also reduces vocabulary: the entity was already called an acquisition everywhere else in the model, the spec, ProdML, and DAS-RCN. This frees acquisition_id for what ProdML means by AcquisitionId, which every example file confirms is a UUID for the acquisition run, so the reader stops renaming it. Since the value identifies a run rather than an inventory entry, it can cross-check a resolution rather than drive one. OptoDAS keeps its own header name too: experiment rather than experiment_id. Readers now have a stated rule to stay as close to the file's own spelling as the vocabulary allows. The index version was already bumped this release and nothing has been released, so the column rename needs no second bump. Carried along, because they touch the same lines: the on_unresolved warning no longer names a patch, so it stays one warning instead of one per distinct key on exactly the partly-covered archive the default exists to serve; and a test pins update_coords to declaring no requirements, which is what makes enrich's raw_function bypass legal.
The rename skipped benchmarks/, where a hive-path test kept using the old name and passed anyway: the old spelling is now an unvalidated extra, so the benchmark had stopped exercising the field it was meant to cover. The collapsed on_unresolved warning silenced every later spool in a session, since a constant message registers once per module. It now dedupes per spool, which is the granularity that means anything. Removes an invented history from the contributor docs: neither of the renames it held up as a cautionary tale ever shipped. Corrects the changelog's claim that index columns kept their names, in the release which renamed two sets of them, and stops it filing two vendor extras inside the canonical vocabulary list. The rest is mechanical-rename fallout: six "a acquisition_key", eight stale "data source id" phrases including the validator's own docstring, two stale test class names, and the spec still explaining a data_ prefix on a name which no longer has one.
Dev's shared FiberIO scan/read scaffolding (#859) touched most of the readers this branch had already rewritten for the attr vocabulary, so several files needed both changes rather than either. Resolutions worth noting: io/utils.py keeps both sides' helpers, since each added different functions to the same region; prodml/core.py and dashdf5/core.py take dev's deletion of the unused ProdMLPatchAttrs, the real classes living in prodml/utils.py where this branch's changes already applied; febus/t1utils.py uses dev's make_scan_payload with this branch's _T1_ATTRS, which never carried the provenance dev's version strips; sr4731/utils.py gains dev's extras plumbing on top of this branch's interrogator.* naming; and h5simple keeps dev's structure minus the two lines putting file_format and file_version back into patch attrs, which this branch removed and which the vocabulary conformance test caught immediately.
on_missing="nan" fills NaN for floats but None for strings, so the name described half the cases it covered. "null" is true across dtypes. Also stops Spool.enrich's kwargs documentation listing the argument names it forwards. The list could drift from Patch.enrich, which documents them and whose signature the names are actually read from.
Two of these made a patch unsaveable rather than merely odd.
A track or annotation covering only part of a patch filled the uncovered
channels of a string coordinate with None, leaving an object array of str
and None which could not be written, chunked, or sorted. The shipped
example inventory reproduces it through plain enrich, since blanket
coords copy every annotation group. A coordinate carries one dtype, and a
string array has no null, so absence is now the empty string; the
boolean and numeric kinds already had markers.
closed_fiber_loop is the first bool DASCore routinely puts in patch
attrs, and netCDF has no boolean attribute type, so every enriched patch
failed to write as netcdf_cf. Bools are stored as the integer flags CF
uses for exactly this.
The merge also resurrected sr4731's extras plumbing, which this branch
had deleted along with the _get_extras that fed it storage provenance.
Nothing calls it, but out.update(extras or {}) ran last and would have
overwritten the canonical interrogator.* names.
Inventory.resolve validated acquisition keys itself rather than through
the shared validator, so a trailing space read as "resolves to 0
acquisitions" instead of naming the malformed key -- the distinction
97e0b02 drew for enrich, missing from the public entry point.
Also: h5simple excludes storage provenance and the format discriminator
when copying root attrs, so scan and read agree and neither leaks a
file-declared file_version; Spool.enrich stops referring to
prune_to_inventory in the present tense; and max_lens drops the three
provenance entries left behind by the source_* rename.
AP Sensing reported the wrong device. The reader took its serial from the DAQ group, which describes the digitizer card inside the interrogator, so patches carried a uint32 card serial as interrogator.serial_number while the file's own Interrogator/SerialNumber sat unmapped. On dev the key was misspelled and never reached a canonical field, so renaming it is what promoted the wrong value; Interrogator/Model is now read too. Re-enriching a selection had stopped being a refresh. A string coordinate's width is fixed by the longest value present, so a patch sliced down to the short values keeps the wider dtype, and _coords_equal compared dtypes exactly. It now compares the kind, which still tells a float coordinate from a string one. A track field every interval leaves at its empty default now reads as missing rather than yielding a coordinate blank throughout, and a CRS label this inventory does not define does the same instead of raising an error on_missing cannot intercept. Both now behave like every other name the inventory has no answer for. Also: Inventory.resolve rejects the empty key rather than failing to unpack it; the shared validator enforces the same length PatchAttrs does, so a key too long to store cannot read as merely unknown; an annotation value may not be the empty string, which a string coordinate spends on uncovered channels; the netCDF bool coercion covers arrays and sequences and no longer touches the dataset attrs, which are always empty. Two findings are recorded rather than fixed. Spool.select reads a bare numeric range as canonical SI while Patch.select reads it in the coordinate's own units, so enriched geometry in degrees selects differently through a spool than through a patch (#863) -- it predates this work and changing it would change indexed-query semantics for every non-SI coordinate. And DASDAE caches written before this release keep their unconverted values and *_units companions, which nothing migrates.
Released DASDAE caches store *_units companions (gauge_length, pulse_width, pulse_rate) or spell the unit into the key (pulse_width_ns). Reading now spends that unit statement at the parse boundary like every reader does: values convert to the fixed units, a length-dimensioned pulse becomes pulse_length, and coordinate-named keys are left to coord reconstruction. Febus A1 and Silixa H5 caches never wrote their companions (the released writer excluded unset model defaults), so they keep the rewrite warning. The netcdf bool-attr tests now pass file_format explicitly like the rest of their module; the conda CI env writes NETCDF3 via the scipy backend, which bare-path format detection cannot sniff.
Without netCDF4 or h5netcdf, xarray's to_netcdf falls back to scipy and writes NETCDF3 classic, which this module's HDF5-based reader and format detection cannot open — the write succeeded and every later read failed (#866). Writing now raises MissingOptionalDependencyError with an install hint instead, after input validation so shape errors still come first. The bool-attr round-trip tests gate on _require_xarray_netcdf_engine() like the rest of the module; NETCDF3 read support is tracked in #867.
The test fakes optional_import and the whole xarray surface, but the new engine guard probes installed packages directly, so in an environment without netCDF4/h5netcdf it fired inside a test whose point is that nothing real is touched.
Dev's protobuf scan work (#865) landed after this branch renamed the vendor identification attrs, so its trailing-META test asserted the pre-rename spellings; the reader itself already emits the dotted names.
Description
Phase 2 of the DASDAE inventory work, on top of the model layer merged in #843: the patch surface. It gives a patch a single identity the inventory can resolve, makes the readers speak the inventory's own vocabulary, and adds the operation that joins the two.
Patch attrs are reorganized around one identity field.
data_source_idholds the whole inventory identity (network.fiber_array.location.acquisition), replacingnetwork,station,instrument_id, andacquisition_id. Component queries are wildcard matches on the composite (spool.select(data_source_id="XX.*")).stationis not reused because DAS MiniSEED already spends it on a per-sampling-point identity.Readers speak one vocabulary, shared with the inventory.
constants.INVENTORY_ATTRSlists the observing-system facts a reader may emit; each is a field of the inventory's acquisition, or of its interrogator when dotted (interrogator.serial_number). Those attrs carry fixed units, so readers convert at the parse boundary through the newio.utils.convert_attr_unitsand the*_unitscompanion attrs are gone. A conformance test holds every shipped reader to the vocabulary; vendor-specific attrs are untouched and listed explicitly.Storage provenance is spelled
source_*everywhere — theget_contents()and index columnspath/file_format/file_versionbecomesource_path/source_format/source_version, matchingPatchSummary— and no reader puts them in patch attrs: they say where the bytes live, and a patch merged from three files has no single answer.Patch.enrichandSpool.attach_inventorycopy inventory metadata onto a patch: the patch'sdata_source_idand time resolve one acquisition and optical path, the acquisition's channel map places each channel on the path, and the path's tracks project onto it.attrsandcoordseach takeTrue, a tuple of names, orFalse;conflictssettles disagreements using chunking's vocabulary;on_missinggoverns names the inventory does not define.One model rule changed while implementing it: an acquisition now places its channels one way, the
distance_map. The affinestart_distanceform mapped channel numbers, which no reader produces (36 of the 38 example files carry meters, none carry channels), and a single control point does the same job while extrapolating. The map may now state its points in both input coordinates, and the patch's own coordinate decides which is read.All breaking changes are described in
docs/changelog.qmd.Reviewed before opening: one Codex pass and three adversarial review passes on the attrs/vocabulary work, plus one Codex and three adversarial passes on the channel-resolution change. Those found (and this PR fixes) a Febus pulse width converted as nanoseconds when the header states meters, a second Silixa code path that skipped its conversion, an attr name that broke every trimmed read of a spool, a hive path that indexed an unusable
data_source_idand failed only at load, and a dual-axis map that could silently be read on the wrong coordinate.Changelog
Patch.enrichcopies DASDAE inventory metadata onto a patch, resolved byacquisition_keyand time, adding the acquisition's geometry axes and annotation groups as coordinates in the CRS's own units (x/yin degrees under the default EPSG:4979), withattrs,coords,conflicts, andon_missingcontrols.Spool.attach_inventory,Spool.enrich, andSpool.remove_inventory. Attaching costs nothing per patch;Spool.enrichenriches each patch as it is extracted (~70% overhead on extraction), and a patch the inventory cannot resolve comes out unchanged underon_unresolved("warn","ignore","raise") while one straddling two epochs raisesUnresolvedPatchError.acquisition_key(network.fiber_array.location.acquisition);network,station,instrument_id, andacquisition_idare removed fromPatchAttrs, andget_patch_nameskeys off it — though the defaultgroupby_attrsstill listsnetworkandstationso archives partitioned by them keep their patches apart.dascore.constants.INVENTORY_ATTRS) in fixed units —instrument_idbecomesinterrogator.serial_number, the*_unitscompanion attrs are gone, and a length-dimensioned pulse ispulse_length; ProdML'sacquisition_idand OptoDAS'sexperimentkeep their own spellings, andxml_binary'spulse_width_nsbecomespulse_widthin seconds.source_path,source_format, andsource_versioninget_contents()and the index; no reader puts it in patch attrs.*_unitscompanions into the fixed unit and drops them, so the cached patch matches the same patch read fresh from source — except Febus A1 and Silixa H5 caches, whose unit never reached the file and which must be rewritten.distance_maponly — the affinestart_distanceform is gone, and a map may be written in both input coordinates at once. An annotation group may not take a reserved name, and interval coverage includes the end of each coverage run.netcdf_cfrequires an HDF5-capable engine (h5netcdfornetCDF4) and raises without one, instead of silently writing a NETCDF3 file DASCore's own reader cannot open (#866).Checklist
I have (if applicable):
Closes #854.
Summary by CodeRabbit
New Features
data_source_idmetadata, standardized interrogator attributes, and canonical unit conversion across supported formats.Improvements
source_path,source_format, andsource_version.Documentation