Skip to content

Add the DASDAE inventory model layer - #843

Merged
d-chambers merged 32 commits into
devfrom
inventory-spec
Aug 10, 2026
Merged

Add the DASDAE inventory model layer#843
d-chambers merged 32 commits into
devfrom
inventory-spec

Conversation

@d-chambers

@d-chambers d-chambers commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Description

This PR adds the core model layer of the DASDAE inventory — a StationXML-style, fiber-native metadata model for DFOS observing systems — as dascore.core.inventory. It is the first of several planned PRs (patch enrichment, spool integration, and the directory authoring loader will follow separately).

What it provides:

  • Immutable pydantic models for the whole tree: Inventory, Network, Station/Channel/Response, FiberArray, Acquisition, DistanceMap, OpticalPath with its component/geometry/coupling/annotation tracks, and shareable resources (Interrogator, Cable, Enclosure, ExternalResource).
  • Track rules: optical components tile the path span; geometry and coupling are function tracks (partial coverage is legal and resolves as NaN, overlap raises); annotations overlap freely.
  • Half-open UTC validity epochs ([start, end), NaT = ongoing): abutting epochs are legal, overlapping same-code epochs raise at check(), and no more than one optical path per (fiber array, location code) is valid at a time.
  • Channel-to-distance resolution: affine (start_distance + spatial_interval) or a measured DistanceMap (channel or instrument-distance input axis), mutually exclusive.
  • data_source_id + time resolution (Inventory.resolve), code charset enforcement, coordinates stored on canonical (x, y, z) axes with CRS-resolved label aliases.
  • Shareable resources normalize into the flat resources pool with resource_id references: inline objects are hoisted at construction, references are type-checked, and replace() corrections on a resource are single-site.
  • Path operations preserving absolute optical distances (select, split_at, reverse, concatenation), YAML round-trip, and dc.inventory().

Design background: the model was developed through an iterative spec process (with adversarial design reviews) in the DASDAE/inventory repo; that content will migrate into DASCore's docs in a follow-up. The implementation went through two internal review rounds plus two external review rounds; all findings are covered by regression tests in tests/test_core/test_inventory.py (82 tests).

Notable shared-machinery changes: dascore/utils/models.py gains InventoryModel/TimeRangedModel bases, and sensible_model_equals now compares nested structures recursively with positional null-matching (previously nested NaT/NaN values made structurally identical models compare unequal).

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 a comprehensive inventory model for networks, stations, fiber arrays, optical paths, acquisitions, resources, geometry, and coordinate systems.
    • Added inventory validation, resource lookup, time-based resolution, path operations, channel-to-distance mapping, and YAML serialization.
    • Added coordinate interpolation, resource replacement, time-aware metadata handling, and convenient factory creation.
    • Added clear errors for invalid inventory metadata.
  • Tests

    • Added extensive coverage for inventory construction, validation, mapping, resolution, serialization, equality, and edge cases.

Implements the DASDAE inventory specification (dasdae.github.io/inventory)
core model in dascore.core.inventory:

- All spec objects as immutable pydantic models: Inventory, Network,
  Station/Channel/Response, FiberArray, Acquisition, DistanceMap,
  OpticalPath, optical components (discriminated union), Geometry,
  CouplingCondition, OpticalPathAnnotation, resources, CRS
- Track-kind rules: components tile the path span; geometry and coupling
  are function tracks (partial coverage legal, overlap raises);
  annotations overlap freely
- Half-open UTC validity epochs (NaT = ongoing); one optical path per
  (fiber array, location code) at a time; epoch overlap raises at
  validate(), gaps and abutting epochs are legal
- Channel resolution: affine (start_distance + spatial_interval) XOR
  measured DistanceMap (channel or instrument_distance input axis,
  single-point maps take slope from spatial_interval)
- data_source_id + time resolution to (network, array, acquisition,
  path); code charset enforcement (letters/digits/'-', blank location
  allowed)
- Path operations preserving absolute distances: select, split_at,
  reverse (rewrites all tracks), concatenation
- Inventory.replace (corrections), YAML round-trip, dc.inventory()

Tests include a conformance suite driven by a vendored copy of the spec
YAML, so field drift from the specification fails loudly.
- Open-ended epoch overlap uses null-aware comparisons instead of finite
  sentinel dates
- A set end_time must follow start_time (construction-time check)
- Station/Channel accept dynamic coordinate-label fields per the spec;
  get_coordinates(crs) validates them against the CRS and agree-or-raises
  against an explicit coordinates tuple
- replace() requires the replacement to match the replaced type
- Terminal zero-length components survive full selections and splits
- Resource dict keys must agree with resource_ids; duplicate ids raise
- Geometry coordinate points require nonzero dimensionality

Adds regression tests for each finding.
Coordinates are stored only on the canonical axes; labels such as
latitude/longitude/elevation/easting/northing are resolvable aliases
from a controlled vocabulary whose meaning the inventory CRS declares
(CoordinateReferenceSystem.axis_index). Aliases the CRS does not define
raise. Station/Channel return to extra="forbid" — no dynamic coordinate
fields, so typos fail at construction and coordinates have exactly one
stored spelling.

Also retire the vendored spec conformance suite: the design phase is
over and this implementation is now the source of truth for the model.
Resource-valued fields (container, specification, interrogator,
otdr_traces) accept an object or a resource_id string. At Inventory
construction a single sweep moves inline resource objects into the
resources pool (nested resources included) and rewrites the fields to
id references, so the canonical form is flat: one copy per resource,
shared by id. Two inline definitions of one id must be equal and id
references must resolve, or construction raises.

replace() on a resource becomes a single-site pool correction and
requires the resource_id to stay stable so references never dangle.
Adds Inventory.get_resource.
…ation

Correctness:
- new() on inventory models dumps all fields so union discriminators and
  normalizer-installed values survive reconstruction; the normalizer also
  records its fields as set
- sensible_model_equals compares recursively with null-aware semantics,
  so nested NaT epochs no longer break equality, replace() lookup, or
  round-trip comparison
- replace() re-runs validators (normalizing inline resources, rejecting
  dangling references), reaches channels, and documents matching against
  normalized objects
- Keyless dict resources adopt their pool key as resource_id; network
  check() rejects duplicate same-code overlapping arrays/stations; path
  check() rejects mixed-dimensionality geometry
- inventory()/from_yaml dispatch like spool(): Inventory passthrough,
  clear errors for missing files and unhandled types

Conventions:
- Rename tree validation to check(): the instance method shadowed
  pydantic's deprecated BaseModel.validate classmethod and failed ty
- Use spelled-out Literals (DataType, DataCategory, CouplingType via
  get_args) per house style; branch is ty-clean
- Export Inventory from dascore.core; add doctested example

Duplication (per review):
- Eight per-class code validators collapse into CodeStr/LocationCodeStr
  Annotated aliases; ResourceIdStr/NotesStr aliases replace repeated
  field boilerplate
- _IntervalModel base unifies CouplingCondition/OpticalPathAnnotation;
  Geometry.distance_span renamed interval for uniform track access
- One _overlapping_epochs helper replaces three pairwise-loop copies;
  one normalize() mechanism replaces three per-type closures
- Remove dead coupling_at; fold resolve() repetition into exactly_one
…ness

- replace() reaches path track items (components, geometry, coupling,
  annotations); its docstring states the addressable scope precisely
- Normalized string references must resolve to the field's allowed
  resource type, not merely exist in the pool
- Recursive equality requires nulls to match positionally instead of
  treating a null on either side as equal (regression in the previous
  fix affecting shared PatchAttrs machinery)
- Distance-bearing values must be finite (interval fields, component
  lengths, path origins, geometry and distance-map control points)
- Apply ruff format to the changed files
@coderabbitai

coderabbitai Bot commented Aug 8, 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

Adds a DASDAE inventory model with typed metadata, spatial and temporal validation, optical-path operations, resource management, resolution, YAML serialization, factory loading, and public exports.

Changes

Inventory model

Layer / File(s) Summary
Model foundation
dascore/utils/models.py, dascore/exceptions.py
Adds recursive model equality, immutable inventory models, time-range validation, and InvalidInventoryError.
Spatial and acquisition contracts
dascore/core/inventory.py, tests/test_core/test_inventory.py
Adds CRS, resources, optical components, geometry, coupling tracks, distance maps, acquisitions, and validation tests.
Optical paths and hierarchy
dascore/core/inventory.py, tests/test_core/test_inventory.py
Adds optical-path selection, splitting, reversal, concatenation, stations, fiber arrays, networks, and hierarchy tests.
Inventory lifecycle operations
dascore/core/inventory.py, tests/test_core/test_inventory.py
Adds resource normalization, validation, resolution, replacement, YAML I/O, factory dispatch, and lifecycle tests.
Public inventory exports
dascore/core/__init__.py, dascore/__init__.py
Exposes the inventory module, Inventory, and inventory through package namespaces.

Suggested labels: ready_for_review

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the main change: adding the DASDAE inventory model layer.
Description check ✅ Passed The description explains the feature, design, tests, and checklist status; only optional issue and review-tag items remain unchecked.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch inventory-spec

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 Aug 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (db17511) to head (584f36c).
⚠️ Report is 2 commits behind head on dev.

Additional details and impacted files
@@            Coverage Diff             @@
##               dev      #843    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files          170       176     +6     
  Lines        18374     19359   +985     
==========================================
+ Hits         18374     19359   +985     
Flag Coverage Δ
network 48.39% <34.55%> (-0.47%) ⬇️
unittests 100.00% <100.00%> (ø)

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1916f6e0dc

ℹ️ 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".

Comment thread dascore/core/inventory.py
default_factory=CreationInfo,
description="QuakeML-style creation and update metadata.",
)
resources: dict[str, _Resource] = Field(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Store the resource pool in an immutable mapping

Although every inventory model is documented as immutable and resource corrections are routed through replace(), Pydantic's frozen configuration is shallow, so callers can still execute operations such as inventory.resources.clear() or assign a different resource directly. This bypasses normalization and type checks and can leave path components holding dangling or incorrectly typed resource IDs; expose an immutable mapping or a defensive view instead.

Useful? React with 👍 / 👎.

Comment thread dascore/core/inventory.py Outdated
Comment on lines +641 to +642
spatial_interval: float | None = Field(
default=None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject non-finite affine mapping intervals

When an acquisition is constructed with spatial_interval=np.nan or infinity, validation accepts it because this field has no finite-value constraint, and channel_to_distance() only checks for None. Every resolved channel distance then becomes NaN or infinite (and the same value is used as a single-point DistanceMap slope), silently producing unusable coordinates instead of rejecting invalid metadata.

Useful? React with 👍 / 👎.

Comment thread dascore/core/inventory.py
Comment on lines +1365 to +1366
for net in self.networks:
net.check()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate coordinate dimensions against the inventory CRS

For an inventory whose CRS declares three axes, a path containing two-dimensional geometry—or a station/channel with a mismatched coordinate tuple—still passes check(), because nested checks only compare geometry segments with each other. This leaves axis_index("z") reporting index 2 while coordinates_at() returns only two columns, so consumers interpreting coordinates through the inventory-wide CRS can index nonexistent axes or assign incorrect meanings.

Useful? React with 👍 / 👎.

Comment thread dascore/core/inventory.py
Comment on lines +1474 to +1476
if item == old:
out.append(new)
replaced += 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Disambiguate equality-based replacements

When a path contains multiple equal components, replace() replaces every occurrence even if old is the actual object retrieved from just one position. This is common for repeated default connectors or terminators, so correcting one component's metadata can silently rewrite unrelated components elsewhere in the inventory; prefer an identity match when available and reject an ambiguous equality-only match rather than applying it to all matches.

Useful? React with 👍 / 👎.

Comment thread dascore/core/inventory.py
Comment on lines +1082 to +1084
channels: tuple[Channel, ...] = Field(
default=(), description="Channels associated with this station."
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject overlapping duplicate station channels

A station can contain multiple channels with the same (location_code, code) and overlapping validity epochs, and the whole-inventory check() still succeeds because Station has no channel epoch validation. Since those fields form the channel's stream identity, an identifier plus time cannot distinguish such entries; enforce the same overlap rule used for acquisition identities.

Useful? React with 👍 / 👎.

Comment thread dascore/core/inventory.py
Comment on lines +992 to +993
return self.model_copy(
update={

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject concatenation of incompatible path epochs

Adding two paths always retains the left operand's location_code, start_time, and end_time while incorporating all components and tracks from the right operand. If paths from different lineages or validity epochs are combined, the result therefore advertises the left metadata and can resolve right-hand physical metadata at times or locations where it was never valid; require compatible path identity and epoch fields or define the combined metadata explicitly.

Useful? React with 👍 / 👎.

- PyYAML is not a dascore dependency; to_yaml/from_yaml now use
  optional_import so environments without it (wasm, free-thread, ty)
  import cleanly, and yaml-dependent tests importorskip
- The dc.inventory doctest no longer requires yaml
- Cover every remaining new-code branch (codecov requires 100% patch
  coverage); remove one mathematically unreachable degenerate-clip
  check in geometry selection
@coderabbitai coderabbitai Bot added the ready_for_review PR is ready for review label Aug 8, 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

🧹 Nitpick comments (2)
tests/test_core/test_inventory.py (2)

412-415: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the concrete exception type.

OpticalPath.select raises ParameterError. pytest.raises(Exception, ...) also passes if the code raises an unrelated error whose message happens to match, and it hides a future change of exception type.

♻️ Proposed change
+from dascore.exceptions import InvalidInventoryError, ParameterError
     def test_empty_selection_raises(self, path):
         """Empty selection raises."""
-        with pytest.raises(Exception, match="Empty distance selection"):
+        with pytest.raises(ParameterError, match="Empty distance selection"):
             path.select(distance=(300.0, 400.0))
🤖 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_core/test_inventory.py` around lines 412 - 415, Update
test_empty_selection_raises to import and assert the concrete ParameterError
type raised by OpticalPath.select instead of the broad Exception class, while
preserving the existing “Empty distance selection” message match.

477-478: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Name test classes after behavior, not the review that produced them.

TestReviewRegressions, TestInternalReviewRegressions, and TestCodexReviewRegressions group unrelated cases by their origin. A reader cannot tell which rule each class covers, and new tests have no obvious home. Consider folding these cases into the behavior-named classes that already exist, such as TestEpochs, TestResourcePool, and TestPathOperations.

Also applies to: 685-686, 808-809

🤖 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_core/test_inventory.py` around lines 477 - 478, Rename or move the
review-origin test classes TestReviewRegressions, TestInternalReviewRegressions,
and TestCodexReviewRegressions into existing behavior-focused classes such as
TestEpochs, TestResourcePool, and TestPathOperations. Group each test with the
class matching the behavior it verifies, and remove the review-based grouping
names.
🤖 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`:
- Around line 871-873: Add strict=True to the zip call in OpticalPath.select at
dascore/core/inventory.py lines 871-873, and to the zip(val1, val2) call in
_values_equal at dascore/utils/models.py lines 115-118 after its existing length
check; no other changes are needed.
- Around line 798-812: Update coordinates_at to validate that all geometry
segments have the same coordinate dimensionality before allocating or assigning
into out, reusing the existing dimensionality-check logic and
InvalidInventoryError message from OpticalPath.check. Ensure mixed 2-D/3-D paths
raise InvalidInventoryError instead of reaching the broadcast assignment.

---

Nitpick comments:
In `@tests/test_core/test_inventory.py`:
- Around line 412-415: Update test_empty_selection_raises to import and assert
the concrete ParameterError type raised by OpticalPath.select instead of the
broad Exception class, while preserving the existing “Empty distance selection”
message match.
- Around line 477-478: Rename or move the review-origin test classes
TestReviewRegressions, TestInternalReviewRegressions, and
TestCodexReviewRegressions into existing behavior-focused classes such as
TestEpochs, TestResourcePool, and TestPathOperations. Group each test with the
class matching the behavior it verifies, and remove the review-based grouping
names.
🪄 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: b5a44c9d-03c8-4f31-9c8b-29ae1552ba98

📥 Commits

Reviewing files that changed from the base of the PR and between 80116a2 and a9dd551.

📒 Files selected for processing (6)
  • dascore/__init__.py
  • dascore/core/__init__.py
  • dascore/core/inventory.py
  • dascore/exceptions.py
  • dascore/utils/models.py
  • tests/test_core/test_inventory.py

Comment thread dascore/core/inventory.py
Comment thread dascore/core/inventory.py
The serialization type tag is now declared through a _type_tag helper:
hidden from repr, defaulting to the class name, and constrained by its
Literal so it can never be set to anything else. Users never interact
with it; it exists for serialized YAML/JSON, where it drives union
dispatch and the authoring format's type declaration.
Every inventory object now carries the two attachment points for
unmodeled information: notes (free prose) and extra_fields (typed
key-values, e.g. round-tripped vendor or external-format metadata).
This fills the previous gaps (Acquisition and OpticalPath had no notes;
only two classes had extra_fields) and removes the per-class
declarations. Cable drops owner and serial_number (notes-grade custody
trivia, matching the Enclosure trim); Interrogator keeps serial_number
as the physical instrument's identity.

to_yaml now prunes empty strings, mappings, and sequences — lossless,
since all model fields default to empty — so serialized files get
cleaner rather than wider. User values inside extra_fields are kept
verbatim.
Aerial (pole-suspended) fiber joins the coupling vocabulary. Interval
items (coupling conditions and annotations, via their shared base) may
now have optical_length == 0: a point marker documenting a location —
a clamp, a labeled spot — that covers no distance and therefore never
participates in coverage, enrichment, or function-track overlap checks,
so a clamp point inside a trenched span is representable.
Coupling conditions and annotations now cover [start_distance,
end_distance) instead of start plus length. Field data reads off OTDR
and interrogator displays as two absolute positions (transcribing a
length invites arithmetic errors), and start/end matches the model's
one interval idiom everywhere: start_time/end_time epochs and the
path's own start_distance/end_distance span. optical_length remains as
a derived property; a point marker is start == end.
Every path component carries loss_db (one-way transmission loss) and
reflectance_db (return loss) — the two quantities an OTDR trace shows
per event — replacing insertion_loss, the attenuation pair, and the
terminator-only reflectance. Each value pairs with the measurement
record that produced it: the new OpticalMeasurement shareable resource
captures the conditions (method, time, wavelength, pulse width,
direction, trace file) once, and every number from the same run
references the same pooled record; a datasheet claim is an honest
record too (method="datasheet"). Multi-wavelength values are
equal-length tuples paired elementwise with their measurements.

FiberSegment keeps the familiar dB/km reading as a derived property
and drops the engineered-backscatter datasheet fields (specification
links the datasheet). OpticalPath.otdr_traces becomes measurements,
holding OpticalMeasurement references.
units becomes a tuple paired one-per-axis with coordinate_labels (the
old single string claimed elevation in degrees under the default CRS).
A wkt field carries WKT2 definitions for frames an authority code
cannot describe — local engineering grids, derived grids, compound
horizontal+vertical CRSs — which is what GIS tools need to actually
interpret them; registry CRSs leave it empty, since the EPSG code is
already the complete definition.
One uniform prose field on the InventoryModel base, named to match
StationXML's Description element; Network's bespoke description field
is now the inherited one.
Network becomes a TimeRangedModel (FDSN networks carry start/end
dates); Channel gains optional azimuth, dip, and depth so imported
point-sensor channels stay interpretable.
Scheme-prefixed URI strings (doi:, ark:, urn:) mirroring StationXML's
Identifier element on the levels that plausibly get cited.
Comment thread dascore/core/inventory.py Outdated
Comment on lines +134 to +135
is geographic WGS84 3D (EPSG:4979); override it only for exceptional
frames such as mines or laboratories.

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.

We dont need the guidelines; we are all adults here ;)

Comment thread dascore/core/inventory.py Outdated
(``network.fiber_array.location.acquisition``) which, together with time,
resolves against an inventory.

Key model rules (see the DASDAE inventory specification):

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.

we can remove the reference to the spec; remember the goal is an implementation, not a spec.

Comment thread dascore/core/inventory.py
Comment on lines +4 to +28
The inventory extends the StationXML concept with first-class support for
fiber-optic arrays. It describes the physical optical path (fiber, connectors,
splices), the geometry, coupling, and annotation tracks along optical
distance, and the interrogator configurations (acquisitions) that produced
DAS patches. Patches carry a ``data_source_id``
(``network.fiber_array.location.acquisition``) which, together with time,
resolves against an inventory.

Key model rules (see the DASDAE inventory specification):

- Validity intervals are half-open ``[start, end)`` in UTC; an unset (NaT)
end time means ongoing. The outermost endpoint of a coverage domain is
included.
- No more than one ``OpticalPath`` per ``(FiberArray, location_code)`` is
valid at a given time.
- Optical components are the tiling track: they cover the whole path exactly
once. Geometry and coupling are function tracks: coverage may be partial
(uncovered distance is undefined), overlap raises. Annotations overlap
freely.
- Codes use letters, digits, and ``-``; ``.`` is the ``data_source_id``
separator. All codes are non-empty except ``location_code``.
- ``Acquisition`` maps channels onto path distance through either the affine
form (``start_distance`` + ``spatial_interval``) or a measured
``DistanceMap``; the two are mutually exclusive.
"""

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.

Let's make this much more concise. Mostly, such info should live in the appropriate class. You can leave just a simple overview here, not detailed rules. Such rules will be included in a dedicated note in a future PR.

Comment thread dascore/core/inventory.py Outdated
Comment on lines +107 to +110
def _is_strictly_increasing(values) -> bool:
"""Return True if a sequence is strictly increasing."""
arr = np.asarray(values, dtype=float)
return bool(len(arr) < 2 or np.all(np.diff(arr) > 0))

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 smells like a general util that shouldnt live here (there might already be one in utils).

Comment thread dascore/core/inventory.py Outdated
"from the controlled coordinate vocabulary."
),
)
units: str = Field(default="degree", description="Coordinate units when known.")

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 wouldnt apply to elevation. Maybe this should be a tuple (x_units, y_unit, z_unit) instead?

Comment thread dascore/core/inventory.py Outdated
class Enclosure(InventoryModel):
"""Physical housing, pipe, duct, conduit, or carrier resource."""

type: Literal["Enclosure"] = "Enclosure"

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.

Do we need other words? You list some in the docstring but dont support specifying that type here?

Comment thread dascore/core/inventory.py Outdated
least two strictly increasing optical distances, each paired with the
coordinate at that point (interpreted using the inventory CRS). Coverage
is the half-open span of the array; there is no separate length field. A
coil is a segment whose coordinates repeat while distance advances.

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.

"A coil, or other 'clump'" can be represented... "

Comment thread dascore/core/inventory.py Outdated


class Interrogator(InventoryModel):
"""DAS interrogator unit used for data collection."""

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.

Should say DFOS. We want DASCore to work for more than just DAS

Comment thread dascore/core/inventory.py Outdated
material: str = Field(default="", description="Material of the enclosure.")
manufacturer: str = Field(default="", description="Manufacturer name.")
model: str = Field(default="", description="Model name.")
inner_diameter: float | None = Field(

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.

Maybe we just call it diameter, since now we don't have an outer diameter?

Comment thread dascore/utils/models.py
return True


def _values_equal(val1, val2) -> bool:

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.

Doesnt the DASCoreBaseModel already have eq that can be used here?

Annotations become group/value pairs whose value type decides how they
enrich: boolean groups state membership and may overlap, string and
numeric groups are single valued and may not. FiberSegment gains
refractive_index and renames fiber_index/color to fiber_number and
fiber_color. The coordinate vocabulary moves into the type, gains depth,
and leaves only a uniqueness check behind; the module docstring keeps an
overview and lets each object document its own rules; the strict
monotonicity test moves to utils.misc, shared with coords.

Also addresses review findings: non-finite affine parameters, duplicate
channel identities, concatenation across lineages or epochs, ambiguous
replacements, resources addressed by id, coordinate widths against the
CRS, and mixed-dimension geometry in coordinates_at.
Annotation values keep their Python type (a numpy mask element is a flag,
not the number one) and must be finite; an empty value is no longer
pruned on serialization, where it would reload as the boolean default and
change its group's kind. Point markers survive select and split_at, as
they already did for components.

Network epochs are now usable: duplicate codes are judged by overlapping
epochs and resolve honors the requested time. Coordinates must be finite,
a CRS declares one to three axes to match the canonical (x, y, z)
storage, and channel azimuth and dip enforce the ranges they document.
…cuit

Every physical scalar rejects nan and inf, the concatenation precondition
is documented on __add__, and the ambiguous-replacement message names a
field that track items actually have. is_strictly_monotonic answers
ascending input in one pass again and treats non-1D input as unordered
rather than raising.
Reading a document asks whether it is a valid inventory, so from_yaml
checks before returning; in-memory construction stays unchecked so an
inventory can still be assembled a piece at a time.
@d-chambers
d-chambers merged commit f0b6154 into dev Aug 10, 2026
37 of 39 checks passed
@d-chambers
d-chambers deleted the inventory-spec branch August 10, 2026 16:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready_for_review PR is ready for review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant