Skip to content

Kernels & Green's function

The PIC solver has two orthogonal choices: the particle-mesh shape function (how charge is spread to the grid and field gathered back) and the Green's function (how Poisson is solved).

Particle-mesh shape: CIC vs TSC

CIC TSC
Order 1st (linear B-spline) 2nd (quadratic B-spline)
Cells per particle 8 (one corner each octant) 27 (3×3×3 around centre)
Cost per kick 1× about 1.4× (compiled kernel)
Default yes (legacy) recommended for noise-sensitive cases

When CIC is fine

For lattices with frequent RF cavities (HWR, SSR1, SSR2, LB650, HB650 in PIP-II), CIC is sufficient — the RF restoring force damps grid noise. The 3% PIC-kernel calibration offset documented in HELIX's internal memory notes is separate from grid-noise issues — it's a known calibration bias (see PIP-II validation).

When TSC is needed

For long no-RF transports (BTL — beam transport lines), grid noise + self-force aliasing in CIC accumulate without RF damping and ε_z grows unphysically. Switching to TSC drops the artificial growth rate substantially:

BTL stage CIC ε_z growth TSC ε_z growth TraceWin reference
MEBT exit → BTL end ×14 ×1.6 ×1.7

TSC is the recommended kernel for BTL-style sections.

TSC grid convention

The TSC weights anchor the deposited charge's centroid exactly half a cell low relative to the particle position (verified 2026-07-25). Deposit and gather share the convention, so it cancels in every space-charge kick — no beam-dynamics number is affected. Only raw density maps read directly off the ρ grid are shifted by −dx/2. Any future change must move deposit and gather together.

Performance

TSC deposit and gather run in a compiled kernel, linac_gen._tsc_kernels. It returns the same charge density and fields as the Python implementation, bit for bit, at any thread count. Measured on the PIP-II MEBT with 100 000 particles on a 48³ grid (Apple M3 Max):

CIC TSC, compiled TSC, Python
Space-charge kick 19.0 ms 26.7 ms 117.7 ms
Whole MEBT run 60 s 70 s 153 s

TSC therefore costs about 1.4× CIC per kick, instead of about six times. CIC remains sufficient for lattices with frequent RF (see above).

The kernel is built with HELIX's other C++ extensions. If it is missing, HELIX logs one line and runs the Python kernels, with identical results. LINAC_GEN_TSC_KERNEL=0 forces the Python kernels, and the HDF5 provenance attribute sc_tsc_cpp_kernel records whether the compiled kernel was enabled. (Changed 2026-10: TSC used to run in Python only.)

Green's function: IGF vs point

IGF point
Source Qiang et al. PRSTAB 9, 044204 (2006) sampled 1/(4πε₀ r)
Symmetry symmetric, no near-source bias sampled, has near-source artefacts
Default yes legacy reproduction

The Integrated Green Function (IGF) is the closed-form 8-point cell-difference of the analytic Coulomb antiderivative — it's the exact Green's function for a uniform-density unit cube. The "point" Green's function samples 1/(4πε₀ r) at cell centres with a self-potential regulariser at r=0; it has been shown to introduce grid-resolution bias near the source.

Always use IGF. The "point" option is kept only for legacy regression reproduction.

Build cost

The IGF is built by evaluating the antiderivative once at each vertex of the (2n+1)³ cell-corner lattice and differencing neighbouring vertices. This gives the same bits as evaluating it at the eight corners of every cell, at about one ninth of the cost (48³: 365 ms before, 41 ms after). On the CPU the build runs on HELIX's CPU threads, in pieces of whole grid planes, each value computed by the same operations as in a single call over the grid, so the result is the same for any number of threads. With grid_mode="fixed" it is built once per run. With grid_mode="adaptive" it is rebuilt at every kick whose cell spacing changes, so its cost sets much of the adaptive kick time:

Building the IGF (14-core Mac) 1 thread 14 threads
48³, full build 32 ms 8 ms
48³, symmetric build 5 ms 3 ms
96³, full build 247 ms 35 ms
96³, symmetric build 37 ms 9 ms

On a 545 m linac line with field-map cavities (5 000 particles, adaptive 48³ grid with the full build, 31 487 rebuilds) a whole run took 563 s instead of 1520 s on 14 threads, with the same results bit for bit.

A grid that only moves, with its spacing unchanged, keeps its Green's function. (Changed 2026-10: the IGF used to be evaluated at every cell corner, and until 2026-10-12 it was built on one thread.)

green_build="symmetric" builds one octant and mirrors it: the Green's function is even in each coordinate, so the result equals the full build to rounding (about 5·10⁻¹³ of its peak at 48³) and is exactly even. It costs about a sixth of the full build on one thread (see the table). The named adaptive methods use it; everything else keeps the full build.

Configuration

from linac_gen.core.config import SpaceChargeConfig

# PIP-II-style linac with RF cavities (nx=ny=nz default to 96):
sc_default = SpaceChargeConfig(
    kernel="cic",           # fine with frequent RF
    green_kind="igf",
)

# For BTL or other long no-RF transport:
sc_btl = SpaceChargeConfig(
    kernel="tsc",           # required to avoid ε_z runaway
    green_kind="igf",
)

Cross-references

← PIC solver · Continue to DC mode →