Skip to main content

stablefmmpy

A stable Python implementation of the wideband Fast Multipole Method (FMM) for the 2D Helmholtz kernel and related 2D kernels (generalised Cauchy, logarithmic), based on:

[HK] "A Stable Matrix Version of the Wideband FMM for the 2D Helmholtz Kernel"
Michelle, Ou, Xia — preprint 2024

[M2D] "A Stable Matrix Version of the 2D Fast Multipole Method"
Ou, Michelle, Xia — SIAM J. Matrix Anal. Appl. 46(1), 2025

Why this library?

The classical FMM is numerically unstable in the low-frequency regime (k·δ ≪ 1) because Hankel function values grow factorially with expansion order r. This library implements the balanced matrix version from [HK]/[M2D]: scaling factors λ_{x,p} keep ‖U‖_max ≤ 1, giving backward error that grows only logarithmically with N — instead of exponentially.

Method ‖B‖_max at r=180 Relative error
Naive FMM Inf NaN
Balanced FMM (this library) ≈ 0.629 ≈ 1e-14

Installation

pip install stablefmmpy

Requires Python 3.12+, numpy, scipy, matplotlib (all standard scientific Python).

Quick Start

import numpy as np
from stablefmmpy import PointSet, FMMSolver, HelmholtzKernel

rng = np.random.default_rng(42)
X = PointSet.random_uniform(n=100, center=0+0j, radius=0.01, rng=rng)
Y = PointSet.random_uniform(n=100, center=0.1+0j, radius=0.01, rng=rng)
q = rng.standard_normal(100) + 1j * rng.standard_normal(100)

# Leaf-only solver (default): O(N·r)
solver = FMMSolver(k=10.0, r=20, tau=0.6, N0=32, balanced=True)
phi = solver.solve(X, Y, q)

# True multi-level solver (M2M→M2L@all levels→L2L→P2P): same accuracy
solver_ml = FMMSolver(k=10.0, r=20, tau=0.6, N0=32, balanced=True, multilevel=True)
phi_ml = solver_ml.solve(X, Y, q)

# Exact reference: O(N^2)
kern = HelmholtzKernel(10.0)
phi_exact = kern.matvec(X, Y, q)
print(f"Leaf-only error:   {kern.relative_error(phi, phi_exact):.2e}")    # ~1e-14
print(f"Multi-level error: {kern.relative_error(phi_ml, phi_exact):.2e}") # ~1e-14

Multi-Level FMM

FMMSolver(multilevel=True) implements [M2D]/[HK] Algorithm 4.1 — the classical four-pass FMM traversal:

Pass Direction Operation
M2M Postorder (bottom-up) v_parent += R @ v_child
M2L All levels t_node += B @ v_partner for each interaction-list partner
L2L Preorder (top-down) u_child = R.T @ u_parent + t_child
Eval + P2P Leaves phi += U @ u_leaf + K_exact @ q_near

The inter-level restriction matrix R is built by LeafMatrices.build_R_lf ([HK §3 Theorem 3.1]):

R[p, l] = (λ_parent[|p|] / λ_child[|l|]) * J_{|p-l|}(k|d|) * exp(i(p-l)·arg(d))

where d = child.center − parent.center. M2M applies R directly; L2L applies R.T (plain transpose — not conjugate transpose).

QuadTree.build(multilevel=True) populates classical FMM interaction lists at every level (pair A,B added at the coarsest level where they first become well-separated) plus a reconciliation pass that routes any uncovered leaf pairs to direct P2P.

Running Benchmarks

from stablefmmpy import BenchmarkSuite

bs = BenchmarkSuite()

# Reproduce Table 6.1 from [HK] (Helmholtz, k=100)
rows = bs.run_helmholtz_table61()
for row in rows:
    print(f"r={row['r']:3d}  stable_B={row['stable_B']:.4f}  naive_B={row['naive_B']}")

# Reproduce Table 6.1 from [M2D] (Cauchy kernel)
rows2 = bs.run_multipole2d_table61()
for row in rows2:
    print(f"r={row['r']:3d}  balanced_err={row['balanced_err']:.2e}  naive_err={row['naive_err']:.2e}")

Verifying Stability

from stablefmmpy import PointSet, StabilityAnalyzer

rng = np.random.default_rng(0)
X = PointSet.random_uniform(40, center=0+0j, radius=0.005, rng=rng)
Y = PointSet.random_uniform(40, center=0.05+0j, radius=0.005, rng=rng)

sa = StabilityAnalyzer(k=50.0, r=15)
bounds = sa.verify_norm_bounds(X, Y)
print(bounds)
# -> {'U_max_balanced': 0.9..., 'theorem_satisfied': True, ...}

Module Layout

src/stablefmmpy/
├── __init__.py      public API (9 classes)
├── core.py          PointSet, ScalingFactors, HelmholtzKernel
├── matrices.py      LeafMatrices — U, B, V, R builders (LF and HF regimes)
├── tree.py          FMMNode, QuadTree — adaptive quad-tree, leaf-only and
│                    multilevel interaction lists, M2M/L2L traversal support
├── solver.py        FMMSolver — leaf-only (_solve_wideband) and true
│                    multi-level (_solve_multilevel) wideband FMM
└── analysis.py      BenchmarkSuite, StabilityAnalyzer

tests/
├── test_core.py
├── test_matrices.py
├── test_solver.py
├── test_solver_multilevel.py   ← multi-level FMM correctness + tree structure
└── test_stability.py

examples/               see examples/README.md
├── 01_benchmarks.ipynb
├── 02_fmm_solver.ipynb
├── 03_stability_analysis.ipynb
├── 04_backward_error.ipynb
└── 05_multilevel.ipynb

Running Tests

pytest tests/ -v

All 47 tests pass (38 original + 9 multilevel-specific).

Mathematical Background

Problem: Given point sets X = {xᵢ} and Y = {yⱼ} in ℂ and a charge vector q, evaluate φᵢ = Σⱼ H₀(k|xᵢ − yⱼ|) qⱼ. Brute force is O(MN); FMM achieves O(M+N).

Low-rank factorisation: For well-separated sets (separation ratio τ), the kernel admits K ≈ U B Vᵀ. The balanced version scales U by λ_{x,p} = max{1, p! · (2/(kδ))^p} so that ‖U‖_max ≤ 1 [HK Theorem 2.7].

Two-regime architecture:

  • Low-frequency (k·δ ≤ r/e): Bessel/Hankel recurrences with balancing
  • High-frequency (k·δ > r/e): equispaced DFT basis (inherently stable)

Backward error bound ([M2D] Theorem 5.1): for a tree of depth l, ‖K − K_approx‖ / ‖K‖ ≤ l · C(r) · ε_mac, where C(r) → 0 exponentially. Since l = O(log N), the backward error grows logarithmically with N.

Public API

from stablefmmpy import (
    PointSet,          # point set with bounding disk (center, radius)
    ScalingFactors,    # λ_{x,p} balancing factors
    HelmholtzKernel,   # direct O(MN) evaluator + Cauchy/log factories
    LeafMatrices,      # U, B, V, R matrix builders (LF + HF regimes)
    FMMNode,           # single quad-tree node
    QuadTree,          # adaptive quad-tree (leaf-only or multilevel lists)
    FMMSolver,         # wideband FMM solver (leaf-only or multilevel=True)
    StabilityAnalyzer, # norm-bound verification and rank sweeps
    BenchmarkSuite,    # reproduces Tables 6.1–6.3 from [HK] and [M2D]
)

Citation

@article{michelle2024helmholtz,
  title={A Stable Matrix Version of the Wideband {FMM} for the 2D {Helmholtz} Kernel},
  author={Michelle, Ou, Xia},
  year={2024},
  note={preprint}
}

@article{ou2025multipole2d,
  title={A Stable Matrix Version of the 2D Fast Multipole Method},
  author={Ou, Michelle, Xia},
  journal={SIAM Journal on Matrix Analysis and Applications},
  volume={46},
  number={1},
  year={2025}
}

Metadata

Release files for stablefmmpy 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for stablefmmpy 0.1.0
File Size Uploaded Provenance
stablefmmpy-0.1.0.tar.gz 29.6 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for stablefmmpy 0.1.0
File Interpreter ABI Platform
stablefmmpy-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 54.6 kB

Release files / stablefmmpy-0.1.0.tar.gz

Download URL stablefmmpy-0.1.0.tar.gz
Size 29.6 kB
Tags Source
SHA-256 checksum
How to use checksums
df9e3587f8d81f2a2de9fbf285642953d2099555c747f80b86c751d51b6fb0b6
BLAKE2b-256 checksum
How to use checksums
2051fe90289e17068f8acb28b3d683c3f8b25c929ced58fd25b51a46cff6bc99
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.14

Release files / stablefmmpy-0.1.0-py3-none-any.whl

Download URL stablefmmpy-0.1.0-py3-none-any.whl
Size 25.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4c7114ad4cba6694a20cab94c4c81a39d57d10d5797b1172362eb22091d36431
BLAKE2b-256 checksum
How to use checksums
d154c84554b327c58c96d2cb484a8d97809ae653398ac173249f26850f5fee10
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.14

Security for stablefmmpy 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page