Skip to content

0.2.8 - #79

Merged
atrabattoni merged 83 commits into
mainfrom
release/0.2.8
Jul 31, 2026
Merged

0.2.8#79
atrabattoni merged 83 commits into
mainfrom
release/0.2.8

Conversation

@atrabattoni

Copy link
Copy Markdown
Contributor

New Features

  • Regular coordinates. A coordinate can now declare a nominal sampling_interval (with a tolerance bounding the allowed jitter). Query it with isregular() / get_sampling_interval(); promote an irregular coordinate with to_regular(). File engines, from_block, and the fft/stft outputs produce regular coordinates out of the box (@atrabattoni).
  • Chunked and unchunked processing now yield identical coordinates: operations that derive a new rate record their rounding error in tolerance, and simplify/concat spend the declared tolerance by default, fusing chunk seams away (@atrabattoni).
  • simplify gained reduce and regularize keywords, and the gaps/overlaps API now works on every axis coordinate, including dense ones (@atrabattoni).

Deprecations

  • The sampling interval is now declared metadata rather than a computed end-to-end average (which was silently wrong on jittery or gappy axes). Data saved by earlier versions carries no declared rate: querying it — e.g. through any signal-processing routine — still works for now, but the rate is inferred and a FutureWarning explains how to make the coordinate regular (da[dim] = da[dim].to_regular(tolerance=...)). A future release will raise instead (@atrabattoni).

Bug Fixes

  • Fix Sequential.reset() silently doing nothing: it only reset Partial atoms, so stateful atoms such as IIRFilter or ResamplePoly kept their state and a reused sequence returned wrong data (@atrabattoni).
  • Fix stft ignoring the "first"/"last" dimension aliases — including its own default dim — which raised a size-conflict error instead of transforming the named axis (@atrabattoni).

Refactoring

  • Reworked the coordinate class hierarchy: Coordinate is now a proper ABC and the new AxisCoordinate ABC holds the axis-mapping contract shared by dense, interpolated, and sampled coordinates. Use isinstance(coord, AxisCoordinate) instead of the removed is* predicates (@atrabattoni).
  • Cleaned up internal-leaning APIs: removed DefaultCoordinate, to_dict/from_dict, get_div_points, decimate, and from_array; made underscore-private concat, get_indexer, get_value, format_index, slice_index(er), isvalid, and get_query; NumPy 2.0 copy keyword compliance (@atrabattoni).
  • concat_coords now simplifies its result by default, like concat; values are unchanged, only redundant tie points are dropped (@atrabattoni).
  • Added xdas.testing.dummy, a configurable fixture generator replacing xdas.synthetics.dummy (@atrabattoni).
  • Comply with ruff 0.16, whose default rule set is considerably broader (B, C4, SIM, RUF, PERF, TRY, BLE, S, DTZ, FLY, PL…). Mutable argument defaults (the dim={...} mappings of fft, rfft, ifft, irfft, stft, to_stream) became None sentinels documenting the same defaults; class-level registries and engine specs are annotated ClassVar; deliberate patterns (engine-fallback blind excepts, the long-lived TDMS handle, the grouped __all__) carry targeted noqa. TRY004 is disabled project-wide, since xdas raises ValueError for all argument validation, including type checks (@atrabattoni).
  • The abstract VirtualArray stubs (__getitem__, __array__, shape, dtype, to_dataset) now raise NotImplementedError instead of silently returning None (@atrabattoni).

Increase sleep delays from 1ms to 10ms in test_asn.py to give ZMQ
enough time to process subscriptions and deliver XPUB_WELCOME_MSG.
…_coords in trigger instead of local implementation.
Rename all public coordinate API methods to private (_isvalid, _get_value,
_get_indexer, _slice, _concat, _to_dataset, _collect_from_dataset,
_is_monotonic_increasing) throughout source and tests. Implement abstract
methods in DefaultCoordinate, DenseCoordinate, and ScalarCoordinate.
Restore removed methods (decimate, from_array, from_block) and update
call sites in core, io, and picking.

Adapt tests to match the revised semantics: SampledCoordinate.end now
returns the inclusive last value (base-class _get_value); slice_indexer
overrides are removed from Default and Dense so step raises
NotImplementedError and stop is exclusive-open; decimate tests replaced
by equivalent coord[::q] indexing.
atrabattoni and others added 23 commits June 22, 2026 08:13
Replace the median-based nominal sampling interval with the exact minimax
spacing: the value minimising the worst per-segment accumulated drift, which
is precisely what _is_valid_sampling_interval bounds. Fold the logic into
to_regular (dropping _nominal_sampling_interval), add a tolerance="auto"
mode that picks the smallest valid tolerance, and skip recomputation when the
coordinate is already regular.
Detect split candidates against a running reference step that follows
sustained sampling-rate changes, so a continuous axis whose rate changes is
no longer reported as a discontinuity while genuine gaps still are. Also make
parse_scalar_delta raise on a None value when no default is available.
… rule

Add _continuous_segments to encode the CF rule (den==1 = discontinuity, see
section 8.3) in one place, used by both validity checking and spacing
inference. Expand the class docstring with the continuous-area/discontinuity
model and regularity semantics, add short docstrings to the sampling-interval
helpers, and drop the misleading tolerance=None default on
_is_valid_sampling_interval.
Replace the O(n^2) pairwise height matrix in to_regular's spacing
inference with the upper-envelope (convex-hull trick) of the 2n lines
+/-(den*si - num); its lowest vertex is the binding pair. The inner
hull loop is numba-compiled, keeping prep/lexsort vectorized.
Add a docstring explaining how the tolerance bound implicitly preserves
CF 8.3 structure, and extend the test suite with cases for real and
soft discontinuities, multiple runs with isolated tie points, kink
preservation, and datetime tie values.
`__len__` uses the documented `tie_indices[0] == 0` invariant, `_slice`
drops a Python-loop in favour of a vectorised `_get_value` call, the
Chebyshev-center pair is returned as `(pos_idx, neg_idx)` rather than
the opaque `(i, j)`, and `_upper_envelope_min_pair` carries an explicit
invariant comment on why its scan is bounded.
Douglas-Peucker can fuse a soft discontinuity into a continuous ramp;
the merged segment then carries the absorbed jump on top of the
original tie-value jitter. Storing `self.tolerance + tolerance` covers
that worst case and degrades to `self.tolerance` for a lossless
simplify (`tolerance=0`).
Previously a regular coord short-circuited to `self.copy()` whenever no
`sampling_interval` was forced, silently dropping any user-supplied
`tolerance` (including `tolerance="auto"`). Default each unspecified
argument to the stored value instead, so an explicit override on either
axis is always respected.
`_concat` is a low-level primitive: it preserves the regular contract
only when both sides advertise the exact same `sampling_interval`,
otherwise the merged coord is irregular. The joining tie pair is a CF
discontinuity, so each side's segments validate independently and
`max(tolerance)` bounds the union; raising on a mismatch was punishing
a perfectly representable result.

User-facing reconciliation moves up to `concat_coords`: after the usual
`simplify` step, an irregular merge gets one chance to recover a single
shared rate via `to_regular(tolerance=...)`, falling through unchanged
when no spacing fits.
The "auto" branch picked the smallest tolerance that kept the inferred
spacing valid, which silently accepted arbitrarily large drift on
pathological inputs - the very opposite of what a tolerance argument
is supposed to enforce. Callers that genuinely want to absorb the
worst-case drift can now pass an explicit numeric tolerance instead.
Split AxisCoordinate.simplify into two opt-in stages: reduce (drop
redundant tie points, default on) and regularize (acquire a nominal
sampling_interval, default off). InterpCoordinate gains the promotion
logic; Sampled/Dense treat regularize as a no-op.

Thread reduce/regularize through concat_coords (regularize on, so the
rate-recovery path keeps working) and the public concat (regularize off,
preserving round-trip equality). Make to_regular/infer_regular private.
Both the time and frequency output coordinates now use
coord_cls.from_block() where coord_cls mirrors the input dimension's
coordinate type, rather than falling back to a generic InterpCoordinate
dict. This ensures sampling_interval is properly preserved for
SampledCoordinate inputs.
Provides a minimal DataArray for use in tests and doctests. Defaults to
100 × 10 (100 Hz, 10 m spacing → 1 s × 100 m). Accepts a step argument
(scalar or per-dimension tuple); float steps on the datetime dimension are
auto-converted to timedelta64[ns].
Replace the standalone get_sampling_interval(da, dim) calls in signal.py
with da.coords[dim].get_sampling_interval(), propagate sampling_interval
through UpSample and resample_poly coordinate reconstruction, and update
tests to use the new coordinate API.
Promote to_regular to the public AxisCoordinate interface: InterpCoordinate
enforces or infers a spacing, SampledCoordinate validates and copies, and
DenseCoordinate converts to a regular InterpCoordinate. isregular() moves to
the Coordinate base (False for scalars) so the predicate exists on the whole
hierarchy.

Regularity now means "carries a declared sampling_interval". Accordingly
DenseCoordinate.get_sampling_interval returns None instead of the end-to-end
average, which was vacuously "regular" for any dense axis and fed meaningless
rates to signal processing on jittery data.

The module-level get_sampling_interval becomes the single choke point for
signal routines (signal.py stops open-coding the check). Data written by
earlier versions carries no declared rate, so rather than breaking every
existing archive it falls back to inferring one and emits a FutureWarning
stating the inferred value, the tolerance it requires and the migration path.
It raises only when no spacing can be inferred at all.

Also fix from_block for sizes below two, which built invalid tie indices.
Treat tolerance as a property of the coordinate rather than a per-call
parameter: simplify(tolerance=None) now spends the coordinate's own declared
jitter instead of a zero-like default, and a regular coordinate keeps its
tolerance through a reduce pass unless the fused values no longer validate,
in which case it widens by the spent budget only.

Operations that derive a new rate declare the error they introduce: UpSample
records the truncation residue of delta // factor on top of the inherited
jitter. Chunk seams then land within tolerance of the nominal grid, so
chunked and unchunked pipelines produce equal coordinates again.

Align concat_coords defaults with concat (tolerance=None, regularize=False);
regular inputs stay regular through concatenation, so promotion is only
needed for irregular ones and remains opt-in. Bag compatibility checks use
the coordinate-level primitive so an irregular chunk yields a
CompatibilityError rather than a TypeError.
Scanners know the acquisition rate, so build coordinates that declare it:
prodml and terra15 derive it from the file's own timestamps, the ASN ZMQ
subscriber from its header, and from_stream via from_block at nanosecond
resolution so a to_stream round trip preserves the coordinate. Per-file
tolerance stays zero; cross-file jitter is reconciled at concat time.

The FFT functions likewise emit regular frequency and signal axes, without
which an fft/ifft round trip would leave the result unusable by any further
signal processing.
Add a "Regular coordinates" section to the interpolated-coordinates guide,
list to_regular and the module-level get_sampling_interval in the API
reference, give xdas.testing its own page, and drop the stale
synthetics.dummy entries left by the move to xdas.testing.

Rewrite the 0.2.8 release notes as a net diff from 0.2.7 rather than a log of
the development history: the sampling-interval change and its transition
shim are stated once under Deprecations, and API surface churn that no
ordinary user code touches sits under Refactoring.

docs/plan_regular_coordinates.md records the design decisions behind the
change and can be dropped before merging.
Replace hand-rolled DataArray construction and wavelet_wavefronts payloads
with xd.testing.dummy wherever the test only cares about shapes, coordinate
positions and round-tripping rather than the signal itself. Coordinate,
trigger, picking and StreamWriter tests keep their explicit fixtures since
the data values or datetime literals are load-bearing there.
UpSample read coord.tolerance directly, which is None on every coordinate
written before 0.2.8, so UpSample and ResamplePoly raised TypeError on all
existing data — GPS-synced or not — where 0.2.7 had worked. An irregular
input carries neither a rate to inherit nor a jitter bound to derive one
from, so the result now stays irregular rather than claiming a precision the
source never declared.

simplify widened a fused coordinate's tolerance by the accuracy budget
without re-checking it. Douglas-Peucker bounds how far values move, not how
much drift fusing a discontinuity exposes, so that sum is no bound at all:
on jittery multi-file archives it fell short by tens of nanoseconds and the
constructor raised, making open_mfdataarray fail for a band of tolerances
while succeeding above and below it. Widen to the least value that describes
the surviving tie points instead, and stay irregular rather than raise if
even that fails.

Sequential.reset only reset Partial atoms, leaving every stateful atom
holding its filter state, so a reused sequence silently returned wrong data.
The constructor wraps non-atoms into Partial, so the inherited Atom.reset
already covers every element and the override is gone.

The ASN engine now builds one regular block per ROI from dx * roiDec and
concatenates them, so distance declares a spacing like every other engine.
Taking the step from the metadata rather than re-deriving it from each ROI's
bounds keeps it bit-identical across ROIs, which is what lets concatenation
preserve the axis; regularizing recovers it when the steps differ only by
float rounding, while genuinely different decimations still stay irregular.
resample and resample_poly likewise carry the declared jitter across the
rate change rather than resetting it to zero.
Ruff 0.16 enables a much broader default rule set (B, C4, SIM, RUF, PERF,
TRY, BLE, S, DTZ, FLY, PL...), which surfaced 161 errors. This makes the
codebase pass again.

Mostly mechanical: collection literals instead of dict()/list()/tuple()
calls, dict.get, itertools.pairwise, next(iter(...)), `not x == y` ->
`x != y`, f-strings, dropped stale noqa directives.

Genuine defects caught along the way:
- a missing `assert` in test_sampled, so the assertion never ran;
- a pointless `== 0` on the call inside a pytest.raises block;
- a dead `dx` statement in the apsensing engine;
- the VirtualArray abstract stubs were bare `NotImplemented` expressions,
  so they silently returned None; they now raise NotImplementedError.

Mutable argument defaults (the dim={...} mappings of fft, rfft, ifft,
irfft, stft and to_stream) became None sentinels; the defaults are
unchanged and stay documented. Class-level registries and engine specs
are annotated ClassVar. Deliberate patterns keep targeted noqa with a
reason: engine-fallback blind excepts, the long-lived TDMS handle and its
naive-UTC epoch, and the grouped __all__.

TRY004 is disabled in pyproject: it wants TypeError for type checks, but
xdas raises ValueError for all argument validation and its public API and
tests assert that. RUF012 is ignored under tests/, where class attributes
are fixture tables.

Making stft's default dim reachable exposed that stft never honoured the
"first"/"last" dimension aliases at all: it compared coordinate names
against the unresolved alias, so `stft(da)` raised a size conflict. Fixed
with the same resolution fft.py already uses, plus a regression test.
@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (a562cbb) to head (d573e1c).

Additional details and impacted files
@@            Coverage Diff             @@
##              main       #79    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files           43        44     +1     
  Lines         4656      4772   +116     
  Branches       727       748    +21     
==========================================
+ Hits          4656      4772   +116     

☔ 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.

Set the version to 0.2.8rc0 so the branch builds pre-release artifacts.
test_version required every dot-separated part to be a digit, which
rejects any PEP 440 pre-release marker; match the version pattern
instead.
The version was duplicated in pyproject.toml and xdas/__init__.py, and
docs/conf.py carried a third copy that had already drifted to 0.2.7.
Declare it dynamic and let setuptools read xdas.__version__, which is
now the only place to edit; conf.py derives its release from it too.
@atrabattoni
atrabattoni merged commit bf3c584 into main Jul 31, 2026
8 checks passed
@atrabattoni
atrabattoni deleted the release/0.2.8 branch July 31, 2026 06:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant