Skip to content

Commit efb4a67

Browse files
committed
Add accelerated moving-domain and surface PDE workflows
Bring the public Python examples to MATLAB PDE parity with Poisson, variable and nonlinear Poisson, fixed and multispecies diffusion, two- and three-dimensional moving-domain ADR, stationary and moving-surface ADR, mean-curvature flow, marker rearrangement, and the IBAMR RBC trajectory problem. Implement tangent-plane PHS+Legendre RBF-FD, cached-factor defect updates, adaptive hyperviscosity, spherical and toroidal geometry models, conservative quadrature projection, semi-Lagrangian history backfill, and dimension-generic moving-domain transport. Move repeated surface assembly, interpolation, stencil matvec, SBF, and control-point kernels into cached Numba implementations; parallelize local-system construction and use batched SciPy LAPACK plus compiled sparse GMRES and ILU. Polish the README and generated figures, add the verified RBC data downloader, package all new modules, and extend numerical and acceleration regressions across the new workflows.
1 parent ca6739e commit efb4a67

34 files changed

Lines changed: 3269 additions & 121 deletions

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ artifacts/
88
dist/
99
build/
1010
*.egg-info/
11+
data/ibamr_rbc_3d/

‎README.md‎

Lines changed: 119 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,28 @@
88
**Meshfree geometry, RBF-FD, partition-of-unity methods, and PDE solvers for
99
Python.**
1010

11-
`kernelpack-python` provides Python implementations of meshfree geometry,
12-
scattered-node discretizations, and PDE solvers on embedded fixed domains. It
13-
combines reusable geometry and node-generation tools with standard and
14-
overlapped PHS+poly RBF-FD, weighted least squares, localized
15-
partition-of-unity approximations, and sparse elliptic and diffusion solvers.
16-
17-
The package is the Python member of the KernelPack family. The companion
18-
[`kernelpack-matlab`](https://github.com/VarShankar/kernelpack-matlab)
19-
repository includes the moving-surface implementation accompanying the
20-
preprint [*A high-order, meshless, Lagrangian--Eulerian RBF-FD method for
21-
advection--diffusion--reaction on moving manifolds*](https://arxiv.org/abs/2608.19384)
22-
by Matthew Lowery, Grady B. Wright, and Varun Shankar. This Python release is
23-
focused on the fixed-domain numerical core and does not claim that
24-
moving-surface solver.
11+
`kernelpack-python` provides NumPy/SciPy implementations of meshfree geometry,
12+
scattered-node discretizations, and PDE solvers on fixed domains, moving
13+
domains, and evolving surfaces. It combines reusable geometry and node
14+
generation with standard and overlapped PHS+poly RBF-FD, weighted least
15+
squares, localized partition-of-unity approximations, and sparse iterative
16+
solvers.
2517

26-
![Poisson solution and nodal error on an embedded domain](docs/readme_assets/poisson_solution.png)
18+
The moving-domain and moving-surface implementations follow the same published
19+
methods and numerical conventions as
20+
[`kernelpack-matlab`](https://github.com/VarShankar/kernelpack-matlab) and
21+
[`kernelpack-jax`](https://github.com/VarShankar/kernelpack-jax), while keeping
22+
the execution path in NumPy and SciPy.
23+
24+
![Geometry and a boundary-refined meshfree node cloud](docs/readme_assets/geometry_domain.png)
2725

28-
The figure shows an end-to-end Poisson solve on a geometry-defined scattered
29-
node set: the geometric model supplies the boundary and normals, the node
30-
generator fills the domain, and PHS+poly RBF-FD supplies the differential and
31-
boundary operators.
26+
The figure shows the geometry pipeline used by the PDE solvers: an embedded
27+
boundary and its normals define a level set, and the node generator fills the
28+
interior while retaining boundary and ghost-node structure.
3229

3330
[Install](#installation) | [First solve](#first-solve) |
31+
[Workflows](#fixed-domain-workflows) | [Moving domains](#moving-domain-pdes) |
32+
[Moving surfaces](#moving-surface-pdes) |
3433
[Examples](#examples) | [Tests](#verification) |
3534
[Papers](#research-foundations) | [Citation](#citation)
3635

@@ -43,10 +42,15 @@ want to:
4342
- generate scattered nodes and differential operators on embedded domains;
4443
- compare standard and overlapped local assembly;
4544
- solve elliptic and diffusion problems without constructing a volume mesh;
45+
- solve semi-Lagrangian ADR problems with moving embedded boundaries;
46+
- solve conservative ADR problems on stationary or evolving closed surfaces;
47+
- evolve point-cloud surfaces by mean curvature;
4648
- build localized PU or divergence-free RBF approximations; or
4749
- extend a tested, inspectable numerical research codebase.
4850

49-
It is research software, not a general-purpose finite-element package.
51+
It is research software, not a general-purpose finite-element package. The
52+
implementation favors transparent SciPy sparse algebra and targeted Numba
53+
kernels; the JAX repository is the accelerator-oriented member of the family.
5054

5155
## At a glance
5256

@@ -56,19 +60,28 @@ It is research software, not a general-purpose finite-element package.
5660
| Node generation | Seeded fixed- and variable-radius Poisson sampling in boxes, clipping by embedded geometry, boundary and ghost nodes, boundary-zone outer refinement, and dual node sets |
5761
| Local approximation | Centered and scaled Legendre polynomial bases, standard and overlapped PHS+poly RBF-FD, weighted-least-squares stencils, and local divergence-free PHS interpolation |
5862
| Fixed-domain solvers | Poisson, variable-coefficient and nonlinear variable-coefficient Poisson, BDF1--BDF3 diffusion, localized PU diffusion, and homogeneous or heterogeneous multispecies diffusion |
59-
| Numerical infrastructure | SciPy KD trees and sparse matrices, cached local polynomial templates, and targeted Numba kernels for repeated geometry, polynomial, and stencil calculations |
63+
| Moving-domain ADR | RK3 boundary motion, geometric reconstruction, carve/refill node updates, local PHS+Legendre departure-point interpolation, BDF1--BDF3 transport, and implicit diffusion--reaction solves in two and three dimensions |
64+
| Moving-surface ADR | Tangent-plane PHS+Legendre RBF-FD, cached-factor defect updates, adaptive surface hyperviscosity, spherical or toroidal geometry models, Lagrangian BDF1--BDF3 stepping, mass projection, and semi-Lagrangian history backfill |
65+
| Geometric surface evolution | Explicit or matrix-free semi-implicit tangent-plane RBF-FD mean-curvature-flow steps |
66+
| Numerical infrastructure | SciPy KD trees, batched LAPACK and sparse iterative solvers, plus cached Numba-parallel kernels for tangent-plane systems, stencil matvecs, SBF geometry, polynomial evaluation, and control-point selection |
6067

6168
The main namespaces are `kernelpack.geometry`, `kernelpack.nodes`,
62-
`kernelpack.domain`, `kernelpack.poly`, `kernelpack.rbffd`,
69+
`kernelpack.domain`, `kernelpack.manifold`, `kernelpack.poly`, `kernelpack.rbffd`,
6370
`kernelpack.divfree`, and `kernelpack.solvers`.
6471

72+
Repeated numerical kernels run outside the Python interpreter: Numba builds
73+
tangent-plane local systems in parallel and applies stencil operators inside
74+
GMRES, while SciPy supplies compiled KD-tree searches, batched LU solves, ILU,
75+
and sparse Krylov iterations. Python remains responsible for user callbacks,
76+
time-level orchestration, and topology-changing node-set updates.
77+
6578
## Requirements
6679

6780
- Python 3.11 or newer
6881
- NumPy 2.0 or newer
6982
- SciPy 1.14 or newer
7083
- Numba 0.61 or newer
71-
- Matplotlib 3.9 or newer for examples and figures
84+
- Matplotlib 3.9 or newer is optional and used by examples and figure scripts
7285

7386
## Installation
7487

@@ -153,9 +166,9 @@ plt.title("Poisson solution")
153166
plt.show()
154167
```
155168

156-
![Geometry-clipped interior, boundary, and ghost nodes](docs/readme_assets/geometry_domain.png)
169+
![Poisson solution and nodal error on an embedded domain](docs/readme_assets/poisson_solution.png)
157170

158-
## Solver workflows
171+
## Fixed-domain workflows
159172

160173
All solvers use a `DomainDescriptor`, so geometry, node generation, and local
161174
operator construction remain separate from the PDE definition. A target
@@ -172,16 +185,94 @@ reuses time-independent operators and preconditioners where possible. The PU
172185
variants localize approximation and support single- or multispecies diffusion,
173186
including distinct diffusivities by species.
174187

188+
The same geometry and node descriptors feed every solver. This makes it
189+
possible to compare standard RBF-FD, overlapped RBF-FD, weighted least squares,
190+
and localized PU assembly without rewriting the domain construction or
191+
boundary callbacks.
192+
175193
![Diffusion solution, error, and time history](docs/readme_assets/diffusion_solution.png)
176194

195+
## Moving-domain PDEs
196+
197+
`MovingDomainADRSolver` advances
198+
199+
\[
200+
\frac{D c}{D t}=\nu\Delta c+\lambda c+f
201+
\]
202+
203+
on domains with moving embedded boundaries. RK3 advances boundary markers;
204+
the geometric model reconstructs the boundary; the background cloud is carved
205+
and refilled; and local PHS+Legendre interpolation supplies semi-Lagrangian
206+
BDF1--BDF3 history values. Diffusion and reaction are solved implicitly with
207+
overlapped RBF-FD, GMRES, and ILU. The same implementation supports the 2D
208+
moving-hole and 3D moving-cavity examples.
209+
210+
```bash
211+
python examples/moving_domain_adr_example.py
212+
python examples/moving_domain_adr_3d_example.py
213+
```
214+
215+
## Moving-surface PDEs
216+
217+
The surface solver discretizes
218+
219+
\[
220+
\partial^\bullet c+c\,\nabla_\Gamma\!\cdot\mathbf{w}
221+
=\nu\Delta_\Gamma c+R+f
222+
\]
223+
224+
with target-centered tangent-plane PHS+Legendre RBF-FD. The implementation
225+
provides direct assembly, cached-factor defect updates, adaptive
226+
hyperviscosity, spherical and toroidal geometry models with analytic normals
227+
and quadrature, matrix-free implicit diffusion, BDF1--BDF3 material histories,
228+
mass projection, RK3 marker motion, local or SBF transfer, and semi-Lagrangian
229+
history backfill. The same operators also advance
230+
\(\mathbf{X}_t=\Delta_\Gamma\mathbf{X}\) by explicit or semi-implicit
231+
mean-curvature-flow steps.
232+
233+
```bash
234+
python examples/stationary_surface_adr_example.py
235+
python examples/moving_surface_adr_example.py
236+
python examples/mean_curvature_flow_sphere_example.py
237+
python examples/mean_curvature_flow_ellipsoid_example.py
238+
python examples/surface_rearrangement_example.py
239+
```
240+
241+
The three-dimensional red-blood-cell example replays the public IBAMR
242+
trajectory used by the moving-surface paper. The downloader verifies the
243+
release archive before installing the trajectory locally; the solver then
244+
reconstructs SBF geometry and normals, advances a source-free diffusing tracer
245+
with the same tangent-plane ADR machinery, enforces the quadrature mass law,
246+
and writes the final point cloud and concentration to `artifacts/`.
247+
248+
```bash
249+
python scripts/download_rbc_capstone_data.py
250+
python examples/moving_surface_rbc_capstone.py
251+
```
252+
253+
The default command processes the complete 401-frame trajectory. Use
254+
`--steps 2` only for a short installation check.
255+
177256
## Examples
178257

179-
Complete convergence drivers live in [`examples`](examples):
258+
Complete workflows live in [`examples`](examples):
180259

181260
| Goal | Example |
182261
| --- | --- |
262+
| Solve Poisson's equation | [`poisson_solver_example.py`](examples/poisson_solver_example.py) |
183263
| Verify a two-dimensional pure-Neumann Poisson solve | [`poisson_convergence_2d_neumann.py`](examples/poisson_convergence_2d_neumann.py) |
184264
| Verify a three-dimensional pure-Neumann Poisson solve | [`poisson_convergence_3d_neumann.py`](examples/poisson_convergence_3d_neumann.py) |
265+
| Solve variable and nonlinear variable-coefficient Poisson problems | [`variable_poisson_solver_example.py`](examples/variable_poisson_solver_example.py) |
266+
| Compare FD and localized-PU diffusion | [`diffusion_solver_example.py`](examples/diffusion_solver_example.py) |
267+
| Compare standard, heterogeneous, and PU multispecies diffusion | [`multispecies_diffusion_example.py`](examples/multispecies_diffusion_example.py) |
268+
| Solve moving-domain ADR in two dimensions | [`moving_domain_adr_example.py`](examples/moving_domain_adr_example.py) |
269+
| Exercise moving-domain ADR in three dimensions | [`moving_domain_adr_3d_example.py`](examples/moving_domain_adr_3d_example.py) |
270+
| Verify stationary-surface ADR | [`stationary_surface_adr_example.py`](examples/stationary_surface_adr_example.py) |
271+
| Verify moving-surface ADR on a breathing sphere | [`moving_surface_adr_example.py`](examples/moving_surface_adr_example.py) |
272+
| Evolve a sphere by mean curvature | [`mean_curvature_flow_sphere_example.py`](examples/mean_curvature_flow_sphere_example.py) |
273+
| Smooth an ellipsoid by mean curvature | [`mean_curvature_flow_ellipsoid_example.py`](examples/mean_curvature_flow_ellipsoid_example.py) |
274+
| Remap markers and semi-Lagrangian-backfill BDF history | [`surface_rearrangement_example.py`](examples/surface_rearrangement_example.py) |
275+
| Transport and diffuse a tracer on an IBAMR red-blood-cell trajectory | [`moving_surface_rbc_capstone.py`](examples/moving_surface_rbc_capstone.py) |
185276

186277
Run a study from the repository root, for example:
187278

@@ -220,11 +311,8 @@ your work.
220311
| Overlapped RBF-FD assembly (`kernelpack.rbffd.FDODiffOp`) | V. Shankar, [*The overlapped radial basis function-finite difference (RBF-FD) method: A generalization of RBF-FD*](https://doi.org/10.1016/j.jcp.2017.04.037), Journal of Computational Physics 342 (2017), 211--228 |
221312
| PHS geometric models and Poisson node generation (`kernelpack.geometry`, `kernelpack.nodes`) | V. Shankar, R. M. Kirby, and A. L. Fogelson, [*Robust node generation for mesh-free discretizations on irregular domains and surfaces*](https://doi.org/10.1137/17M114090X), SIAM Journal on Scientific Computing 40 (2018), A2584--A2608 |
222313
| Lower odd-degree PHS selection used by the solver stencil defaults | V. Shankar and A. L. Fogelson, [*Hyperviscosity-based stabilization for radial basis function-finite difference (RBF-FD) discretizations of advection-diffusion equations*](https://doi.org/10.1016/j.jcp.2018.06.036), Journal of Computational Physics 372 (2018), 616--639 |
223-
224-
For moving-surface ADR, surface hyperviscosity, and the associated research
225-
drivers, see the public
226-
[`kernelpack-matlab`](https://github.com/VarShankar/kernelpack-matlab)
227-
release and its research-foundations table.
314+
| Moving-domain ADR | V. Shankar, G. B. Wright, and A. L. Fogelson, [*An efficient high-order meshless method for advection-diffusion equations on time-varying irregular domains*](https://doi.org/10.1016/j.jcp.2021.110633), Journal of Computational Physics 445 (2021), 110633 |
315+
| Moving-surface ADR | M. Lowery, G. B. Wright, and V. Shankar, [*A high-order, meshless, Lagrangian--Eulerian RBF-FD method for advection--diffusion--reaction on moving manifolds*](https://doi.org/10.48550/arXiv.2608.19384), arXiv:2608.19384 (2026) |
228316

229317
## Citation
230318

257 KB
Loading
78.7 KB
Loading
322 KB
Loading

‎examples/_common.py‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Shared domain and manufactured fields for the public PDE examples."""
2+
3+
import numpy as np
4+
5+
from kernelpack.domain import DomainDescriptor
6+
7+
8+
def build_square_domain(grid_size: int = 11) -> DomainDescriptor:
9+
coordinates = np.linspace(-1.0, 1.0, grid_size)
10+
xx, yy = np.meshgrid(coordinates, coordinates, indexing="ij")
11+
points = np.column_stack((xx.ravel(), yy.ravel()))
12+
boundary_mask = np.any(np.isclose(np.abs(points), 1.0), axis=1)
13+
boundary = points[boundary_mask]
14+
interior = points[~boundary_mask]
15+
normals = np.where(np.isclose(np.abs(boundary), 1.0), np.sign(boundary), 0.0)
16+
normals /= np.linalg.norm(normals, axis=1, keepdims=True)
17+
ghosts = boundary + 0.25 * normals
18+
19+
domain = DomainDescriptor()
20+
domain.set_nodes(interior, boundary, ghosts)
21+
domain.set_normals(normals)
22+
domain.set_sep_rad(float(coordinates[1] - coordinates[0]))
23+
domain.build_structs()
24+
return domain
25+
26+
27+
def exact_solution(time: float, points: np.ndarray) -> np.ndarray:
28+
return np.exp(-time) * (points[:, 0] ** 2 + points[:, 1] ** 2)
29+
30+
31+
def zero_neumann(points: np.ndarray) -> np.ndarray:
32+
return np.zeros(points.shape[0])
33+
34+
35+
def unit_dirichlet(points: np.ndarray) -> np.ndarray:
36+
return np.ones(points.shape[0])
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Advance the same manufactured diffusion problem with FD and PU solvers."""
2+
3+
import numpy as np
4+
5+
from kernelpack import solvers
6+
7+
from _common import build_square_domain, exact_solution, unit_dirichlet, zero_neumann
8+
9+
10+
def run_solver(solver, domain) -> np.ndarray:
11+
dt = 0.02
12+
diffusivity = 0.25
13+
points = domain.get_int_bdry_nodes()
14+
forcing = lambda nu, time, x: (
15+
-np.exp(-time) * (x[:, 0] ** 2 + x[:, 1] ** 2)
16+
- 4.0 * nu * np.exp(-time)
17+
)
18+
boundary = lambda alpha, beta, normals, time, xb: exact_solution(time, xb)
19+
solver.init(domain, 3, dt, diffusivity)
20+
solver.set_initial_state(exact_solution(0.0, points))
21+
solver.bdf1_step(dt, forcing, zero_neumann, unit_dirichlet, boundary)
22+
solver.bdf2_step(2.0 * dt, forcing, zero_neumann, unit_dirichlet, boundary)
23+
return solver.bdf3_step(3.0 * dt, forcing, zero_neumann, unit_dirichlet, boundary)
24+
25+
26+
def main() -> None:
27+
domain = build_square_domain()
28+
points = domain.get_int_bdry_nodes()
29+
fd_solution = run_solver(
30+
solvers.DiffusionSolver(
31+
lap_assembler="fd",
32+
bc_assembler="fd",
33+
lap_stencil="wls",
34+
bc_stencil="wls",
35+
),
36+
domain,
37+
)
38+
pu_solution = run_solver(solvers.PUDiffusionSolver(), domain)
39+
truth = exact_solution(0.06, points)
40+
print(f"FD relative l2 error: {np.linalg.norm(fd_solution - truth) / np.linalg.norm(truth):.6e}")
41+
print(f"PU relative l2 error: {np.linalg.norm(pu_solution - truth) / np.linalg.norm(truth):.6e}")
42+
43+
44+
if __name__ == "__main__":
45+
main()
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Smooth an ellipsoid with semi-implicit mean-curvature flow."""
2+
3+
import numpy as np
4+
5+
from kernelpack import geometry, manifold, solvers
6+
7+
8+
def main() -> None:
9+
points = geometry.fibonacci_sphere(256) * np.array([1.35, 0.85, 0.65])
10+
xi = 2
11+
dt = 2.0e-4
12+
step_count = 5
13+
ell = manifold.surface_polynomial_degree(xi)
14+
neighbors = manifold.build_surface_stencil_graph(
15+
points, manifold.surface_stencil_size(ell)
16+
)
17+
normals = manifold.estimate_pca_normals(points, stencil_size=24)
18+
initial_radius = np.linalg.norm(points - np.mean(points, axis=0), axis=1)
19+
for _ in range(step_count):
20+
operators, _ = manifold.assemble_tangent_plane_operators(
21+
points, normals, neighbors, xi=xi
22+
)
23+
points, info = solvers.mean_curvature_flow_step(
24+
points, normals, operators, dt
25+
)
26+
normals = manifold.estimate_pca_normals(
27+
points, previous_normals=normals, stencil_size=24
28+
)
29+
final_radius = np.linalg.norm(points - np.mean(points, axis=0), axis=1)
30+
print(f"initial radius range: {np.ptp(initial_radius):.6e}")
31+
print(f"final radius range: {np.ptp(final_radius):.6e}")
32+
print(f"linear relative residual: {info.relative_residual:.6e}")
33+
34+
35+
if __name__ == "__main__":
36+
main()
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Evolve a sphere under ``X_t = Delta_Gamma X``."""
2+
3+
import numpy as np
4+
5+
from kernelpack import geometry, manifold, solvers
6+
7+
8+
def main() -> None:
9+
node_count = 256
10+
xi = 2
11+
radius_zero = 1.4
12+
dt = 2.0e-4
13+
step_count = 10
14+
directions = geometry.fibonacci_sphere(node_count)
15+
points = radius_zero * directions
16+
ell = manifold.surface_polynomial_degree(xi)
17+
neighbors = manifold.build_surface_stencil_graph(
18+
points, manifold.surface_stencil_size(ell)
19+
)
20+
21+
for _ in range(step_count):
22+
normals = points / np.linalg.norm(points, axis=1, keepdims=True)
23+
operators, _ = manifold.assemble_tangent_plane_operators(
24+
points, normals, neighbors, xi=xi
25+
)
26+
points, info = solvers.mean_curvature_flow_step(
27+
points, normals, operators, dt
28+
)
29+
30+
final_time = step_count * dt
31+
exact_radius = np.sqrt(radius_zero**2 - 4.0 * final_time)
32+
radius = np.linalg.norm(points, axis=1)
33+
relative_error = np.linalg.norm(radius - exact_radius) / np.linalg.norm(
34+
np.full_like(radius, exact_radius)
35+
)
36+
print(f"relative radius error: {relative_error:.6e}")
37+
print(f"linear relative residual: {info.relative_residual:.6e}")
38+
39+
40+
if __name__ == "__main__":
41+
main()

0 commit comments

Comments
 (0)