You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
LocalFlowOrderer.order's Quantity dispatch (in _interop/interop_unxt.py) reaches algorithm._local_flow_walk directly, rather than delegating to the plain dispatch the way MSTOrderer/SOMOrderer do. That keeps it genuinely unit-aware via quax.quaxify, but it means any postprocessing step added to the plain path has to be separately re-added to the Quantity path, with nothing to catch a forgotten copy.
None reached the walk as a start index — 13 tests failed
Only the third failed loudly.
What's already unified, and what wasn't
velocity_aware and start_idx turned out to already be fixed on main: the former is derived once inside _local_flow_walk itself from the shared config (so both paths get it automatically), and the latter already goes through the shared _resolve_start_idx helper. chord was the one piece still duplicated — _with_chord in the plain dispatch, a second inline chord_along_ordering call in the Quantity dispatch.
The fix (option 1 + 3 from the issue)
Renamed _with_chord to _finalize and gave it a positions parameter instead of always reading result.positions, so both dispatches can call the same function — the plain one directly, the Quantity one on unit-stripped positions before reattaching the unit. A postprocessing step landing only in one of the two call sites is exactly the drift this closes off.
Paired with test_localflow_quantity_agrees_with_plain_field_by_field, which compares every field of the result generically via dataclasses.fields rather than naming a fixed set — so a future field missing from the Quantity path fails this test instead of shipping silently, which is the actual acceptance criterion in #71. Verified it fails when the _finalize call is reverted (confirmed: crashes trying to unit-convert chord=None) and passes with it restored.
…spatches
`LocalFlowOrderer.order`'s Quantity dispatch reaches `algorithm._local_flow_walk`
directly rather than delegating to the plain dispatch, so it is genuinely
unit-aware via `quax.quaxify` -- but it means any postprocessing step added to
the plain path has to be separately re-added here, with nothing to catch a
forgotten copy. Three fields already drifted this way in turn: `velocity_aware`
(GalacticDynamics#56), `chord` (GalacticDynamics#67), and the `init`-derived start index (GalacticDynamics#70) -- the first two
silently, producing plausible-but-wrong results.
`velocity_aware` and `start_idx` are already unified (the former is derived
once inside `_local_flow_walk` itself from the shared `config`; the latter
already goes through the shared `_resolve_start_idx`). `chord` was the one
piece still duplicated: `_with_chord` in the plain path, a second
`chord_along_ordering` call inline in the Quantity path.
Renamed `_with_chord` to `_finalize` and gave it a `positions` parameter
instead of always reading `result.positions`, so both dispatches can call it
-- the plain one directly, the Quantity one on unit-stripped positions before
reattaching the unit. A postprocessing step landing only in one of the two
calls is exactly the drift this closes off.
Paired with a parity test (`test_localflow_quantity_agrees_with_plain_field_by_field`)
that compares every field of the result generically, via `dataclasses.fields`,
rather than naming a fixed set -- so a *future* field missing from the Quantity
path fails this test instead of shipping silently. Verified it fails when the
`_finalize` call is reverted and passes with it.
ClosesGalacticDynamics#71.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
Copilot review overview
🟢 Approval recommended
The refactor unifies previously duplicated logic with a strong regression test to prevent recurrence, and no correctness or API issues were found in the reviewed changes.
Refactors LocalFlowOrderer’s post-processing so both the plain-array and unxt.Quantity dispatches share the same chord-attachment logic, preventing future drift between the two execution paths in phasecurvefit’s ordering pipeline.
Changes:
Replaces the plain-path-only _with_chord helper with a shared _finalize(result, positions) function.
Updates the Quantity dispatch to call _finalize on unit-stripped positions, then reattach units to the resulting chord.
Adds a regression test that compares all result fields between plain and Quantity runs via dataclasses.fields, to catch any future divergence automatically.
File
Description
tests/unit/test_orderers_unxt.py
Adds a generic field-by-field parity test between plain vs Quantity LocalFlow results to prevent silent drift.
src/phasecurvefit/_src/orderers/localflow.py
Introduces _finalize(result, positions) and uses it in the plain dispatch to centralize post-processing.
src/phasecurvefit/_interop/interop_unxt.py
Switches LocalFlow’s Quantity dispatch to reuse _finalize and then unit-reattach the chord.
Copilot review on GalacticDynamics#137: init_plain/init_q are derived from MSTOrderer without
checking they agree, so a future divergence in MST's own Quantity dispatch
would fail this test with a misleading signal about LocalFlowOrderer instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
Copilot review overview
🟡 Changes recommended
The newly added generic field-by-field test is currently brittle for legitimate NaNs and future tuple/list fields, which can cause false failures even when both dispatches agree.
Recursively strip tuple/list fields for comparison
tests/unit/test_orderers_unxt.py:145
_strip_for_comparison claims to handle tuple/list-shaped fields, but it currently returns tuples/lists unchanged. If a future result field is a tuple/list containing arrays, the final assert plain_val == q_val will raise due to ambiguous truth value from array element comparisons. Strip tuple/list contents recursively to keep the test future-proof.
Copilot review on GalacticDynamics#137: chord's own contract is nan for unvisited
observations, but assert_allclose without equal_nan=True would fail on a
matching nan in both paths as if they disagreed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…140)
test_localflow_quantity_agrees_with_plain_field_by_field (#137) manually
iterated dataclasses.fields and dispatched on each value's shape (dict,
array, or scalar) to compare the plain and Quantity results. eqx.tree_equal
already does exactly this over a whole pytree in one call, checking static
fields (gamma_range, velocity_aware) as well as array leaves.
Strips result_q's Quantity-valued fields back to plain arrays first, so both
results share one pytree structure -- only then can tree_equal walk them
together. nan_to_num on both sides first, since tree_equal has no equal_nan
option and chord's own contract is nan for unvisited observations.
Verified this still fails the same way the manual version did when the
_finalize call is removed (a NotFoundLookupError trying to unit-convert
chord=None), and passes with it restored.
* ✅ test: check the nan mask agrees before neutralising it
Copilot review on #140: nan_to_num alone could mask a real divergence -- a
genuine nan on one side and a coincidentally-equal finite value (e.g. 0.0)
on the other would both become 0.0 and read as agreement. Comparing the nan
masks first, separately, catches exactly that case before nan_to_num ever
runs; verified empirically that it returns False for nan-vs-0.0 and True for
nan-vs-nan.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The recurring bug
LocalFlowOrderer.order's Quantity dispatch (in_interop/interop_unxt.py) reachesalgorithm._local_flow_walkdirectly, rather than delegating to the plain dispatch the wayMSTOrderer/SOMOrdererdo. That keeps it genuinely unit-aware viaquax.quaxify, but it means any postprocessing step added to the plain path has to be separately re-added to the Quantity path, with nothing to catch a forgotten copy.Three fields have drifted this way in turn:
velocity_aware(#56)False— a chainedSOMOrderersilently dropped to position-onlychord(#67)None— the encoder silently used its old inline fallbackstart_idxfrominit(#70)Nonereached the walk as a start index — 13 tests failedOnly the third failed loudly.
What's already unified, and what wasn't
velocity_awareandstart_idxturned out to already be fixed onmain: the former is derived once inside_local_flow_walkitself from the sharedconfig(so both paths get it automatically), and the latter already goes through the shared_resolve_start_idxhelper.chordwas the one piece still duplicated —_with_chordin the plain dispatch, a second inlinechord_along_orderingcall in the Quantity dispatch.The fix (option 1 + 3 from the issue)
Renamed
_with_chordto_finalizeand gave it apositionsparameter instead of always readingresult.positions, so both dispatches can call the same function — the plain one directly, the Quantity one on unit-stripped positions before reattaching the unit. A postprocessing step landing only in one of the two call sites is exactly the drift this closes off.Paired with
test_localflow_quantity_agrees_with_plain_field_by_field, which compares every field of the result generically viadataclasses.fieldsrather than naming a fixed set — so a future field missing from the Quantity path fails this test instead of shipping silently, which is the actual acceptance criterion in #71. Verified it fails when the_finalizecall is reverted (confirmed: crashes trying to unit-convertchord=None) and passes with it restored.Full test suite: 1006 passed, 1 skipped.
Closes #71.
🤖 Generated with Claude Code