Skip to content

Add Patch.from_parts and stop reconciling attrs twice - #777

Merged
d-chambers merged 1 commit into
masterfrom
patch-from-parts
Jul 25, 2026
Merged

Add Patch.from_parts and stop reconciling attrs twice#777
d-chambers merged 1 commit into
masterfrom
patch-from-parts

Conversation

@d-chambers

Copy link
Copy Markdown
Contributor

Description

Follow-up to #775, which sped up rolling partly by adding a local _new_patch helper that bypassed Patch.update. That helper was a symptom; the redundancy is in update itself. This removes it, and generalizes the fix.

Patch.__init__ reconciles attrs against coords unconditionally, which establishes an invariant every Patch satisfies:

dict(patch.attrs.coords) == patch.coords.to_summary_dict()
patch.attrs.dim_tuple == patch.coords.dims

Patch.update also reconciles, then hands the result to the constructor, which does the same work again. And patch.new(data=arr) — the single most common shape of call in the library — reconciles twice even though nothing that could affect coords or attrs has changed.

The cost here is not pydantic validation, which is worth stating since it is the intuitive suspect. PatchAttrs.model_construct (pydantic's own validation bypass) measures slower than validating, 99 µs against 34.5 µs, because it is pure Python where validation is Rust in pydantic-core. The cost is dascore's own reconciliation: separate_coord_info, model_dump, to_summary_dict, and one CoordSummary per coord.

Changes

Patch.from_parts(data, coords, attrs) — a public classmethod for code which already holds a conforming pair. It validates the data shape and the dimension names, but trusts the coordinate summaries, so it costs a few microseconds rather than 130–250. The docstring documents the contract and the failure mode, including the trap that attrs built with an empty coords do not conform and the dimension check will not catch it.

PatchAttrs._conform_to(coords) — rebuilds attrs from a coord manager using model_copy instead of a dump and re-validate. Verified equal to attrs.update(coords=coords), with identical model_dump(), across unchanged / decimated / dropped-coord / transposed coord managers. 132 µs → 38 µs.

proc.basic.update — returns from_parts directly when attrs, dims and a differing coords are all absent, and otherwise reconciles once and constructs without letting __init__ repeat it.

Call-site conversions — only where there is a real win, meaning sites that pass attrs= (and so hit the more expensive update_from_attrs) or that bypass update entirely: rolling (retiring _new_patch from #775), sobel_filter, notch_filter, differentiate, velocity_to_strain_rate, radians_to_strain, and _apply_aggregator, which backs every aggregation.

Patch.__init__ is unchanged apart from turning its dim-mismatch assert into a raise, so both construction paths behave the same under python -O.

What is deliberately not converted

  • The ~24 sites passing coords= but no attrs (select, decimate, resample, pad, transpose, …) gain nothing from an explicit call, since they would have to build attrs.update(coords=cm) themselves. They speed up for free from _conform_to.
  • The ~25 data=-only sites (abs, taper, detrend, …) likewise need no source change.
  • utils/array.py:197, backing every binary operator — the most tempting target, and currently unsafe. When two patches with differing history are combined, _merge_models pops coords from both, returning attrs with coords == {} while dims still matches, so the dimension check would pass and we would silently emit patches violating the invariant. Needs _merge_models fixed first.
  • Sites which rely on reconciliation to repair stale attrsdropna, convert_units, rename_coords, dft/idft/stft/istft, integrate, spectrogram, velocity_to_strain_rate_edgeless, concatenate_patches, stack_patches. They still get the double→single reconcile win with no source change. Worth a separate issue; several are latent bugs the moment anyone bypasses reconciliation.
  • IO readers and other direct-constructor sites — they parse untrusted files, where validation earns its keep and 139 µs is noise against disk I/O.

Verification

Output is unchanged. A sweep of 648 combinations — 8 patch kinds (including int64, float32, NaN, units, non-dimensional coords, custom attrs) × 81 operations — compared data bytes, shape, dtype, coords repr, serialized attrs, dims and the read-only flag against master. All 648 identical, and the 72 that raise produce the same exception and message. The sweep covers every converted site, every excluded site, the full rolling matrix from #775, and binary ops between patches with differing attrs.

A new TestAttrsCoordsInvariant asserts the invariant across 20 operations. It is what makes from_parts legal, so if a future change breaks it, the test names the reason. Writing it caught that p + p.update_attrs(tag="x") raises IncompatiblePatchError outright — only history and dims differences are tolerated between operands — so the test uses the case that actually exercises the coord-dropping path.

Also repairs test_conflicting_attrs_coords_raises, which was decorated @pytest.fixture and so had never been collected, and asserted a message that does not exist in the codebase.

Timings

Default example patch, mean per call:

call master this PR
patch.new(data=arr) 515 µs 6 µs
patch.new(data, coords=cm) 503 µs 76 µs
patch.new(data, coords, attrs) 689 µs 448 µs
patch.abs() 957 µs 407 µs
patch.select(time=...) 824 µs 359 µs
patch.mean("time") 1854 µs 1351 µs
patch + 1 1800 µs 986 µs
patch.differentiate("time") 5.85 ms 4.64 ms
patch.rolling(time=5).mean() 11.24 ms 9.44 ms
dc.Patch(...) direct — control 252 µs 251 µs

The control is deliberately unmoved; the strict constructor was left alone. Ops on the large example patch gain proportionally less because they are compute-dominated.

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.

Patch.__init__ reconciles attrs against coords unconditionally, which
establishes an invariant every Patch satisfies: the coord summaries on
attrs match the coord manager. Patch.update reconciles as well, then
hands the result to the constructor, which does the same work again.

Add a public Patch.from_parts for callers which already hold a
conforming pair. It validates the data shape and the dimension names but
trusts the coord summaries, so it costs a few microseconds instead of
the 130-250 the reconcile does. Patch.update now uses it: once directly,
when nothing that can affect coords or attrs changed, and once at the
end, where the constructor would otherwise repeat the reconcile just
performed. PatchAttrs._conform_to rebuilds attrs from a coord manager
with model_copy rather than a dump and re-validate.

Convert the call sites which pass attrs, and so pay the more expensive
update_from_attrs, or which bypass update entirely: rolling (retiring
the _new_patch helper added in #775), sobel_filter, notch_filter,
differentiate, velocity_to_strain_rate, radians_to_strain, and the
aggregator backing mean/max/min/std/sum/median/first/last.

Patch.__init__ is unchanged apart from turning its dim-mismatch assert
into a raise, so the two paths behave the same under -O. IO readers and
the sites which rely on reconciliation to repair stale attrs keep the
strict path.

Output is unchanged: data, coords, attrs, dims, the read-only flag and
raised errors all match master across 648 patch/operation combinations.

  patch.new(data=arr)          515 us ->   6 us
  patch.new(data, coords=cm)   503 us ->  76 us
  patch.abs()                  957 us -> 407 us
  patch.select(...)            824 us -> 359 us
  patch + 1                   1800 us -> 986 us
  patch.rolling(time=5).mean() 11.2 ms -> 9.4 ms
  dc.Patch(...) direct         252 us -> 251 us (control)
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@d-chambers d-chambers added the ready_for_review PR is ready for review label Jul 25, 2026
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@d-chambers, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 33 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e0d0cde2-2f48-4f6f-a235-cb0b11aed65d

📥 Commits

Reviewing files that changed from the base of the PR and between 357968b and 4dc456e.

📒 Files selected for processing (11)
  • benchmarks/test_patch_benchmarks.py
  • dascore/core/attrs.py
  • dascore/core/patch.py
  • dascore/proc/basic.py
  • dascore/proc/filter.py
  • dascore/proc/rolling.py
  • dascore/transform/differentiate.py
  • dascore/transform/strain.py
  • dascore/utils/array.py
  • tests/test_core/test_patch.py
  • tests/test_proc/test_rolling.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch patch-from-parts

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

❤️ Share

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

@codecov

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.93%. Comparing base (357968b) to head (4dc456e).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #777   +/-   ##
=======================================
  Coverage   99.93%   99.93%           
=======================================
  Files         145      145           
  Lines       12864    12881   +17     
=======================================
+ Hits        12856    12873   +17     
  Misses          8        8           
Flag Coverage Δ
unittests 99.93% <100.00%> (+<0.01%) ⬆️

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

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

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

@d-chambers
d-chambers merged commit 052cc29 into master Jul 25, 2026
26 checks passed
@d-chambers
d-chambers deleted the patch-from-parts branch July 25, 2026 10:01
@d-chambers d-chambers mentioned this pull request Aug 4, 2026
4 tasks
@d-chambers d-chambers removed the ready_for_review PR is ready for review label Aug 11, 2026
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