Uh oh!
There was an error while loading. Please reload this page.
test(reg): cover the diag_bending/diag_all corner cross-term bug - #53
test(reg): cover the diag_bending/diag_all corner cross-term bug#53balbasty wants to merge 1 commit into
Conversation
Adds three boundary-symmetry regression tests for the corner cross-term sign error fixed upstream in fastfields-kernels#49 (issue fastfields-kernels#48): diag_bending's (and flow diag_all's) boundary correction expanded the corner weight as (fx0*fy0 + fx1*fy0 + fx1*fy0 + fx1*fy1) -- fx1*fy0 twice, fx0*fy1 dropped. These are TEST COVERAGE ONLY; the fix itself lives in the kernels repo. Nothing in this repo changes behaviour. The tests need no independent reference implementation. On a square domain with the same boundary condition on every axis (DST2, bending active), the operator and hence its diagonal must be symmetric under axis swap, so diag(0,j,c) == diag(j,0,c); for diag_all with shears == div the Lame terms are symmetric under simultaneous axis and channel swap, so diag(0,j,c) == diag(j,0,1-c). The buggy corner sum is asymmetric under fx<->fy relabelling wherever the two axes' one-sided boundary signs differ, which is exactly what the existing interior-only diag tests could never see. Verified by the full revert/restore cycle against this same tree: kernels @ 15295c2 (pre-fix) -> test_reg_field 4 failures, test_reg_flow 16 failures kernels @ 9b6517f (post-fix) -> both PASS, whole suite 0 failures Relates to fastfields-kernels#48, fastfields-kernels#49 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015xcQBMEdA5eQtTsx2wB3AC
Adds the {field,flow}_{diag,kernel}_{add_,sub_} in-place entry points, which
were lost in the port from jitfields. There, every regulariser entry point is
templated on `char op` (Op<'='> = set, '+' = iadd, '-' = isub, see
jitfields/csrc/lib/regularisers/{field,flow}/utils.h) and writes *through*
`out`, so the add/sub forms are in-place only at the C level -- there is no
separate "return a fresh tensor" C entry point.
The `char op` templating survived the port in kernels/ and cpu-impl; only the
surface was missing. _field_diag / _field_kernel / _flow_diag / _flow_kernel
hardcoded '=' and are now op-templated, with ADD_/SUB_ dispatch macros
mirroring the existing ADD_MV_DT / SUB_MV_DT.
Also renames the four symbols added by task #53,
{field,flow}_matvec_{add,sub} -> ..._{add_,sub_}: they are in-place only, and
ff:: already marks accumulate-into-out with a trailing underscore
(sym_addmatvec_, sym_submatvec_, and the field_precond / field_precond_ pair).
Without it the name would also collide with the *out-of-place*
fastfields.{numpy,torch,cupy}.field_matvec_add.
Tests: run_2d_diag_kernel_addsub in tests/test_reg_{field,flow}.cpp, in the
same oracle style as the existing run_2d_matvec_addsub -- the '=' path computes
the reference, then add_/sub_ must reproduce base +/- ref against a *nonzero*
pre-existing buffer, which is what catches an op that silently overwrites.
Covers absolute/membrane/bending, the flow Lame matrix-stencil shape, three
boundary conditions, and float+double.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016AjQcY78NgbagPSbPJRr6Z…m jitfields) (#50) * reg: restore in-place diag/kernel accumulate + rename matvec accumulate Adds the {field,flow}_{diag,kernel}_{add_,sub_} in-place entry points, which were lost in the port from jitfields. There, every regulariser entry point is templated on `char op` (Op<'='> = set, '+' = iadd, '-' = isub, see jitfields/csrc/lib/regularisers/{field,flow}/utils.h) and writes *through* `out`, so the add/sub forms are in-place only at the C level -- there is no separate "return a fresh tensor" C entry point. The `char op` templating survived the port in kernels/ and cpu-impl; only the surface was missing. _field_diag / _field_kernel / _flow_diag / _flow_kernel hardcoded '=' and are now op-templated, with ADD_/SUB_ dispatch macros mirroring the existing ADD_MV_DT / SUB_MV_DT. Also renames the four symbols added by task #53, {field,flow}_matvec_{add,sub} -> ..._{add_,sub_}: they are in-place only, and ff:: already marks accumulate-into-out with a trailing underscore (sym_addmatvec_, sym_submatvec_, and the field_precond / field_precond_ pair). Without it the name would also collide with the *out-of-place* fastfields.{numpy,torch,cupy}.field_matvec_add. Tests: run_2d_diag_kernel_addsub in tests/test_reg_{field,flow}.cpp, in the same oracle style as the existing run_2d_matvec_addsub -- the '=' path computes the reference, then add_/sub_ must reproduce base +/- ref against a *nonzero* pre-existing buffer, which is what catches an op that silently overwrites. Covers absolute/membrane/bending, the flow Lame matrix-stencil shape, three boundary conditions, and float+double. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016AjQcY78NgbagPSbPJRr6Z * reg: rename accumulate entry points to verb-first (addmatvec_/adddiag_/...) Repo-owner decision: the C++ and Python surfaces should be consistent with each other and with this codebase's own existing convention, even where that means diverging from jitfields' own naming (jitfields itself keeps field_matvec_add/field_matvec_add_, noun-first). Renames the 12 public ff:: entry points restored in the previous commit on this branch: {field,flow}_matvec_add_ -> {field,flow}_addmatvec_ {field,flow}_matvec_sub_ -> {field,flow}_submatvec_ {field,flow}_diag_add_ -> {field,flow}_adddiag_ {field,flow}_diag_sub_ -> {field,flow}_subdiag_ {field,flow}_kernel_add_ -> {field,flow}_addkernel_ {field,flow}_kernel_sub_ -> {field,flow}_subkernel_ This extends the verb-first + trailing-underscore convention already used for posdef (sym_addmatvec_ / sym_submatvec_) to reg_field/reg_flow, rather than inventing a new one. Internal dispatch macro names (ADD_MV_DT, ADD_DG_DT, ADD_KN_DT, ...) are untouched -- only the public ff:: symbols move. field_matvec / field_diag / field_kernel (the plain, non-accumulate ops) and field_relax / field_forward / field_precond{,_} / field_matvec_rls / field_diag_rls / field_relax_rls (and flow equivalents) are out of scope and unchanged. Verified with a full rebuild + test run, not just a mechanical find/replace: make test, all 12 binaries, 0 failures (identical check counts to the pre-rename commit -- 7426/11415/186 for reg_field/reg_flow/reg_op). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016AjQcY78NgbagPSbPJRr6Z --------- Co-authored-by: Claude <noreply@anthropic.com>
…loses#65 (#73) * fix: correct field_rls_is_jrls RLS/JRLS predicate (was backwards) field_rls_is_jrls() had the two cases exactly backwards relative to the original jitfields/nitorch semantics: RLS = per-channel weight (wc == nc), JRLS = one shared weight broadcast across channels (wc == 1). The predicate labelled wc==1 as RLS and wc==nc as JRLS, routing a genuine per-channel weight map to the broadcast-only _jrls kernels, which silently discarded channels 1..nc-1 in favor of channel 0's weight. Bump the impl pin to fastfields-cpu-impl's revert of #48/#53 (which themselves undid the wrong-direction consequences of this mislabel: #53 threaded a per-channel stride into _jrls thinking wc==nc needed it, and #48 forced wsc=0 in _rls thinking wc==1 needed broadcasting -- both backwards). With the predicate corrected and the impl/kernels layers back to their pre-#48/#53 state, wc==nc now correctly reaches the genuine per-channel _rls kernels and wc==1 reaches the broadcast _jrls kernels. Add run_2d_rls_jrls_per_channel: a wc==nc weight map with genuinely different values per channel, checked against an independent oracle (decomposition into C single-channel wc==1 problems, a different, already-heavily-tested dispatch branch) -- not a self-consistency check. None of the existing tests catch this because they either use an all-ones weight (invisible to a broadcast bug) or only check self-adjointness (symmetric regardless of which value is broadcast). Closes#65. * style: run clang-format on the RLS/JRLS predicate fix and new test Mechanical formatting only (alignment, pointer-space style, line wrapping) -- no semantic change. Re-verified with a full make test run: 12/12 suites pass, test_reg_field 13360 checks, 0 failures. --------- Co-authored-by: Claude <noreply@anthropic.com>
balbasty
commented
Aug 18, 2026
Closing this — not because the test was wrong. It wasn't: the oracle is sound, and I reused the idea directly. Why it's being closedThis PR comes from the The oracle was sound — credit where it's dueThe key insight here is a good one and needs no reference implementation: on a square domain with the same boundary condition on every axis, the operator's diagonal must be symmetric under an axis swap, so Where the coverage actually landedOn
It demonstrably bitesSame tests, same flags; only the kernels headers differ:
Check counts identical in both directions, so only pass/fail differs, and all 2024 failures are in the new checks — no pre-existing test changes value. No follow-up issue neededI checked The one thing This PR's branch has not been modified. Generated by Claude Code |
Agent: claude-fastfields-to-teeny
Relates to fastfields/fastfields-kernels#48 and fastfields/fastfields-kernels#49.
What the bug was
diag_bending(field) anddiag_bending/diag_all(flow) expanded the boundary-corrected corner weight asfx1*fy0counted twice,fx0*fy1dropped. The correct expansion of(fx0+fx1)*(fy0+fy1)isfx0*fy0 + fx1*fy0 + fx0*fy1 + fx1*fy1, which is whatmatvec_bending/matvec_allalready computed. Fixed in fastfields/fastfields-kernels#49 (12 terms across 6 functions).What the tests do
Three functions, no independent reference implementation needed — they exploit a symmetry the buggy expression breaks:
test_diag_boundary_symmetry_2dtests/test_reg_field.cppfield_diag: on a square domain with the same BC on every axis,diag(0,j) == diag(j,0)test_diag_boundary_symmetry_2d_bendingtests/test_reg_flow.cppflow_diagwithshears = div = 0: channels are independent, sodiag(0,j,c) == diag(j,0,c)test_diag_boundary_symmetry_2d_alltests/test_reg_flow.cppflow_diagvia the combineddiag_allpath withshears == div: the Lamé terms are symmetric under simultaneous axis and channel swap, sodiag(0,j,c) == diag(j,0,1-c)All three use a 6×6 domain under DST2 (sign-flipping) with bending active. The buggy corner sum is asymmetric under
fx <-> fyrelabelling wherever the two axes' one-sided boundary signs differ — it cancels exactly at fully-symmetric corners, which is precisely why the existing interior-only diag tests never caught it.DST2 is compiled statically under the
make testdefaultBOUNDFLAGS(-DFF_STATIC_BOUND_DCT2=1 -DFF_STATIC_BOUND_DST2=1) and is one of the two static conditions in thecuda-defaultCI leg, so these tests exercise a real shipped instantiation in every matrix leg.Revert/restore cycle — the whole point of this PR
Run on this exact branch, with only the
kernelssubmodule swapped between the two runs (implheld atb6eae48,main's pin):test_reg_fieldtest_reg_flow15295c2— pre-fix (main's current pin)9b6517f— post-fix (head of kernels#49)All 20 pre-fix failures are the new checks, and none of them are marginal:
A uniform ~8.0 discrepancy at the affected boundary voxels — far outside the
1e-5tolerance, so this is a decisive detector rather than a tolerance-sensitive one. No other suite changes: the remaining ten binaries pass identically in both runs.Merge ordering
CI checks out submodules recursively at their pinned commits, and
main's chain iscpu-lib -> impl b6eae48 -> kernels 15295c2— the pre-fix kernels. So onmaintoday these tests fail by construction. Merge sequence:mainfastfields-cpu-implbumps itskernelspinfastfields-cpu-libbumps itsimplpinmain) and goes greenMerging this before step 3 turns the CI gate red for everyone. I deliberately did not bump any submodule pin here — pin bumps are their own change, and bundling one into a test-only PR would hide a dependency update inside a test diff.
Generated by Claude Code