Skip to content

feat: bind field_relax (Gauss-Seidel relaxation for multi-channel fields) - #21

Merged
balbasty merged 1 commit into
mainfrom
claude/33-wire-field-relax
Aug 2, 2026
Merged

feat: bind field_relax (Gauss-Seidel relaxation for multi-channel fields)#21
balbasty merged 1 commit into
mainfrom
claude/33-wire-field-relax

Conversation

@balbasty

Copy link
Copy Markdown
Contributor

What changed

Binds ff::field_relax as fastfields.dlpack.field_relax, plus a residual test for it.

Why

ff::field_relax already exists all the way through the C++ stack — fastfields-cpu-lib dispatches it (_field_relax + RX_DT), fastfields-cuda-lib mirrors it, and the hub declares it in reg_field.h — but it was never reachable from Python: this layer bound flow_relax only. That left the field family without the in-place solver its flow counterpart has, so the numpy/torch/cupy wrappers had no primitive to build field_relax on.

The binding mirrors flow_relax exactly, with the field family's per-channel penalty vectors (absolute/membrane/bending as length-C sequences) instead of the flow family's scalar Lamé parameters — shears/div describe cross-channel coupling of a displacement field and have no meaning for independent field channels. Same in-place contract: sol is the warm start, mutated by nb_iter sweeps, returning None.

The pinned _fastfields_lib already exports ff::field_relax (its reg_field.{h,cpp} are byte-identical to current fastfields-libmain), so no submodule bump is needed here.

RLS/JRLS relax variants are deliberately out of scope — a separate follow-up, per the "Remaining" list in #33.

How it was verified

Built against a clean fastfields-libmain build (isolated tree, make libcpu && make all CXX=clang++EXIT=0); symbol confirmed:

$ nm -D --defined-only build/libfastfields.so | c++filt | grep 'ff::field_relax('
0000000000006070 T ff::field_relax(DLTensor&, DLTensor const&, DLTensor const&, double const*, double const*, double const*, double const*, signed char, int, int, int)

pip install --no-build-isolation . succeeds and the op is callable:

$ python -m pytest tests -q
37 passed in 1.20s

The new test_field_relax_reduces_residual checks against a sym_matvec + field_matvec-derived reference: the residual (H + L)x - g must shrink monotonically across six batches of 8 sweeps and end below 1e-8, and the call must return None (in place).

Independently cross-checked that the solver reaches the exact solution, not merely a small residual — building (H + L) densely and comparing against numpy.linalg.solve:

max |relax - dense_solve| = 8.33e-17
rel err = 2.33e-16

part of fastfields/fastfields-lib#33

Workstream: claude-jitfields-to-fastfields

🤖 Generated with Claude Code


Generated by Claude Code

…lds)
`ff::field_relax` has existed in the C++ stack (cpu-lib dispatch, cuda-lib
mirror, hub) since the field regulariser was ported, but it was never
reachable from Python: `fastfields.dlpack` bound `flow_relax` only. That
left the field family without the in-place solver its flow counterpart has,
so the numpy/torch/cupy wrappers had no primitive to build `field_relax` on.
Bind it exactly as `flow_relax` is bound, with the field family's per-channel
penalty vectors (`absolute`/`membrane`/`bending` as length-C sequences)
instead of the flow family's scalar Lame parameters -- `shears`/`div` are
flow-specific and do not apply to independent field channels. Same in-place
contract: `sol` is the warm start, mutated by `nb_iter` sweeps, and nothing
is returned.
The pinned `_fastfields_lib` already exports `ff::field_relax`, so no
submodule bump is needed here.
RLS/JRLS relax variants stay out of scope (a separate follow-up, per #33).
part of fastfields/fastfields-lib#33
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016AjQcY78NgbagPSbPJRr6Z
@balbastyClaude

Copy link
Copy Markdown
ContributorAuthor

Review summary (triage, jitfields-to-fastfields): merged as squash. 7/7 CI green. Binds field_relax (Gauss-Seidel relaxation) into the nanobind extension — reviewed earlier in this session alongside the rest of the field_relax series (numpy#22/torch#24/#26 remain held, blocked on a released fastfields-dlpack for their own CI to install against — same root cause as whl#1).

Workstream: claude-jitfields-to-fastfields


Generated by Claude Code

balbasty added a commit that referenced this pull request Aug 10, 2026
`field_relax` was the single binding in ext.cpp still declaring
`int stream`; the other 47 (including its own sibling `field_relax_rls`
and `flow_relax`) already take `intptr_t`. It was added in #21, in
parallel with the widening pass in #22, and never converted.
nanobind's `int` caster range-checks rather than truncating, so any
genuine 64-bit CUDA stream handle -- what both Python callers pass,
`torch.cuda.current_stream(...).cuda_stream` and
`cupy.cuda.get_current_stream().ptr` -- was rejected with TypeError.
`fastfields.torch.field_relax` / `fastfields.cupy.field_relax` therefore
raised on essentially every real GPU call; latent only because there is
no GPU in CI.
Adds a regression test that calls every field_*/flow_* binding with a
1<<40 stream handle, plus a completeness guard so a new binding has to
be added to the table.
Refs fastfields/fastfields-lib#69 (item 1)
Claude-Session: https://claude.ai/code/session_016AjQcY78NgbagPSbPJRr6Z
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@balbasty@claude