Skip to content

Add JopRBF: scattered-node compact-support RBF model parameterization - #48

Open
jkwashbourne-oss wants to merge 6 commits into
masterfrom
wask/rbf
Open

Add JopRBF: scattered-node compact-support RBF model parameterization#48
jkwashbourne-oss wants to merge 6 commits into
masterfrom
wask/rbf

Conversation

@jkwashbourne-oss

@jkwashbourne-oss jkwashbourne-oss commented Jul 9, 2026

Copy link
Copy Markdown
Member

Add JopRBF: scattered-node compact-support RBF model parameterization

Summary

Adds a new linear operator, JopRBF, that maps a small set of scattered control-node coefficients to a regular fine model grid using normalized compact-support radial basis functions (Wendland C2). It is a reduced (coarse) model parameterization for Jets inverse problems (FWI dimension reduction, regularization, preconditioning), with an exact adjoint and a matrix-free implementation that scales to very large grids (for example 1000^3). No new dependencies are added to JetPack.

The operator

$$ d(x) = \frac{\sum_j \varphi(r_j(x)), c_j}{\sum_j \varphi(r_j(x))}, \qquad r_j(x) = \sqrt{\sum_k \left(\frac{x_k - \xi_{j,k}}{\delta_{k,j}}\right)^2}, \qquad \varphi(r) = (1-r)^4 (4r+1)\ \text{ on } 0 \le r < 1. $$

  • Compact support, so it scales. Each fine point sees only the O(1) nodes within δ. Forward and adjoint are matrix-free via a node bucket index (no dense N_fine x M matrix): the forward threads over fine points and gathers nearby nodes, the adjoint threads over nodes and walks each node's fine-grid box. Both enumerate the identical (fine point, node) pairs, so the adjoint is exact.
  • C2 smooth, no hot zones. φ, φ', φ'' vanish at r = 1 (no bilinear-style creases).
  • Reproduces constants (Shepard normalization is a partition of unity) and does not overshoot (φ ≥ 0, convex combination of nearby coefficients).
  • Support radius may be a scalar, a per-axis vector (anisotropic grids), or a per-node δ_{k,j} matrix (multiresolution).
  • Build once, reuse. The bucket index and normalization field 1/Σφ depend only on node positions and δ; there is no linear solve.

Tests (test/jop_RBF.jl)

Wendland C2 kernel (value and first/second derivatives vanish at the support edge, non-negative); dot-product (adjoint) tests in 1D/2D/3D; partition of unity; compact support (influence vanishes beyond δ); no overshoot; anisotropic per-axis and per-node δ; Float32. Registered in src/JetPack.jl and test/runtests.jl. The full suite passes locally (Pkg.test()).

Docs and demo (docs/JopRBF/)

  • JopRBF-demo.md: explains the operator, the water-bottom "freeze" workflow, and why normalized compact-support RBF is preferred over global RBF / triangulation-cubic / barycentric-linear.
  • JopRBF_waterbottom_demo.jl: a 101x201 water-bottom example. The sediment below a sloping water bottom is meshed with Gmsh using a points-per-wavelength size field (spacing = λ(z)/ppw(z)), with the water bottom embedded so mesh nodes land on it; the water column is frozen via P = S_below ∘ A. The figure compares the parameterization across inversion frequencies (lower frequency => longer wavelength => coarser mesh).
  • JopRBF_demo.jl: kernel and C0-vs-C2 smoothness figures.
  • Plotting/meshing dependencies (PyPlot, Gmsh) live in a local docs/JopRBF/Project.toml, not in JetPack. Run with julia --project=docs/JopRBF docs/JopRBF/JopRBF_waterbottom_demo.jl.
image

Notes

  • Non-goals: this PR does not change any existing operator; it only adds JopRBF and its docs/tests.

- JopRBF operator: normalized compact-support Wendland-C2 RBF from a scattered
  node cloud to a regular fine grid; scalar/per-axis/per-node support radii;
  matrix-free forward+adjoint via a node bucket index (scales to large grids).
- Unit tests: kernel C2, dot-product (adjoint) 1D/2D/3D, partition of unity,
  compact support, no overshoot, anisotropic and per-node delta, Float32.
- docs/JopRBF: water-bottom freeze-workflow demo (Gmsh-meshed nodes, per-frequency
  comparison) + kernel/smoothness demo, explanatory JopRBF-demo.md, and a local
  project (Project.toml) so PyPlot/Gmsh stay out of JetPack.
@jkwashbourne-oss
jkwashbourne-oss requested a review from nmbader July 9, 2026 22:06
@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.89922% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.04%. Comparing base (51d413c) to head (6bf762b).
⚠️ Report is 10 commits behind head on master.

Files with missing lines Patch % Lines
src/jop_RBF.jl 96.89% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master      #48      +/-   ##
==========================================
+ Coverage   95.94%   96.04%   +0.10%     
==========================================
  Files          40       41       +1     
  Lines        1086     1215     +129     
==========================================
+ Hits         1042     1167     +125     
- Misses         44       48       +4     

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

…operator at construction

Adds precondition::Bool=true + optional precondition_weight to JopRBF. At construction it computes the exact per-node column norm (reusing the adjoint support-box enumeration) and folds colscale_j = clamp(median/norm, 0.1, 10) into the forward and adjoint, so the operator columns are ~equal norm: a unit coefficient maps to an order-unity model perturbation (a Jacobi preconditioner for a reduced-parameterization inverse problem; removes the node support-size bias), adjoint still exact. NOTE: precondition=true trades away the raw kernel exact partition-of-unity and constant-reproduction (the all-ones coefficient no longer maps to a constant); pass precondition=false for the pure kernel. Tests: partition-of-unity/constant/no-overshoot tests use precondition=false; added a precondition column-norm-equalization test; dot-product/compact-support tests validate the preconditioned adjoint at the default.
…cloud

The water-bottom demo now places RBF centers with a pure base-Julia depth-tapered structured cloud (rbf_tapered_nodes.jl: brick-offset rows, spacing = v(z)/(freq*ppw(z)), ppw tapering with depth) instead of a Gmsh mesh. Removes Gmsh from docs/JopRBF/Project.toml and Manifest, rewrites JopRBF_waterbottom_demo.jl, and removes all Gmsh mentions from JopRBF-demo.md. Figure regenerated (6/3/1.5 Hz to 474/128/43 nodes).
…sided support

tapered_rbf_nodes now lays nodes in rows PARALLEL to the water bottom (no horizontal rows cross-cutting a dipping WB), with a capped ghost row above the WB (nghost/ghost_cap), a flat collar below the model bottom (nghost_bot), and the lateral edges x=1/x=nx pinned in every row, so all four boundaries get two-sided support and the fit does not overshoot at any edge.

Water-bottom demo: build the true model at the SAME integer WB as the below-WB mask (was fractional), which removes the spurious WB overshoot band (water velocity was leaking into the top sediment cell). Regenerated figure and refreshed the node-count/RMS numbers (6 Hz 608/0.0008, 3 Hz 186/0.0013, 1.5 Hz 65/0.0032, precondition=false so coverage reads ~1.0).
Comment thread src/jop_RBF.jl
within some node's support; fine points not covered by any node (`Σ_j φ = 0`) map
to zero.

`precondition` (default `true`): scale each node coefficient by `1/‖A e_j‖` (the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

precondition is missing on the call docstring on line 2

Comment thread src/jop_RBF.jl
Gauss-Newton Gram `AᵀA` have ~unit diagonal. It trades away the raw kernel's exact
partition-of-unity / constant-reproduction (the all-ones coefficient no longer maps
to a constant); set `precondition = false` for the pure normalized RBF.
`precondition_weight` (optional, a range-sized array `w`): balance `‖diag(w) A e_j‖`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same for precondition_weight

Comment thread src/jop_RBF.jl
precondition_weight === nothing || size(precondition_weight) == n ||
error("precondition_weight must match the range size $(n), got $(size(precondition_weight))")

δ = _delta_matrix(delta, D, M) # (D, M) per-node per-axis radii

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any safeguard on delta? if it is too small, wouldn't that cause some output grid points to be "orphaned".

@nmbader

nmbader commented Jul 14, 2026

Copy link
Copy Markdown
Member

Very nice addition to JetPack!
You got the same type of missed lines by Codecov :-P.

It will be interesting to compare the RBF with Bspline with the same number of degrees of freedom and same node locations. RBF certainly offers much more flexibility, but that comes at a cost; apart from being a heavier operator, my guess is that the RBF is not as well conditioned as the Bspline operator and leaves clear footprints of the node's locations. But of course, it is more powerful in enforcing sharp irregular boundaries and variable smoothing by regions.

A = JopRBF(JetSpace(Float64, M), JetSpace(Float64, nz, nx), nodes; delta = deltas, precondition = false)
P = JopDiagonal(Float64.(below)) ∘ A # freeze the water column
pou = A * ones(domain(A))
c = convert(Matrix, P) \ vec(vtrue .* below) # fit coefficients to the true sediment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this inversion \ could be costly in practice depending on which method Julia used to actually solve \. It would be nice to use a guaranteed matrix-free method such as lsqr or lsmr from IterativeSolvers or others, and run a reasonable number of iterations to see how fast and how well the RBF can be reconstructed. That also gives information about how well conditioned the RBF operator is.

@samtkaplan

samtkaplan commented Jul 14, 2026

Copy link
Copy Markdown
Member

I think the added docs would need to be tied into docs/src/index.html somehow. Up until now, we have only used doc strings. Perhaps the demo can go in the Examples repository instead?

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.

4 participants