GUINEA-PIG++ · src/guineapigCPP.{h,cc} · src/gridCPP.cc · src/fieldCPP.cc

How GUINEA-PIG slices a collision: n_z vs n_t

Every number and formula below is read off the source in this repository. Line references are given inline. The toy collision at the bottom re-implements GUINEA-PIG's actual crossing schedule, deposit, Green's-function field solve and push in JavaScript so you can watch the discretization converge.

The short answer

Neither all-to-all nor slice-i-with-slice-i. It is a sliding crossing. Slices are indexed from the head of each bunch (index 0) to the tail (index n_z−1). At crossing step k the pairs that overlap are exactly those with

i1 + i2 = k   (k = 0 … 2·n_z − 2)

so over the full crossing every slice of beam 1 does meet every slice of beam 2 — each pair exactly once, at its own step, never simultaneously. Total make_step() calls per bunch crossing = n_z² · n_t.

Your follow-up — do the beams move longitudinally during the n_t iterations? Physical time does advance (each sub-step drifts every active particle by step_, and n_t·step_ = Δz/2 — exactly the lab distance that slides the crossing on by one k), but the slice bookkeeping is frozen: the same pair list, the same min_z luminosity z-bin, for all n_t sub-steps. Beam particles never carry a z at all — their longitudinal position is their slice index (advancePosition() touches only xpos_/ypos_, abstractParticle.h:122). Slices not in the current pair list are frozen solid — they do not drift.

The one identity that ties them together. deltaz = 2·cut_z/n_z (gridCPP.cc:138) and step_ = deltaz/(2·n_t) (gridCPP.cc:146), so the integration step is

step_ = cut_z / (n_z · n_t)

Integration accuracy is governed by the product n_z·n_t; CPU cost is governed by n_z²·n_t. At a fixed product, cost ∝ n_z — so buying a shorter push step with n_t is n_z× cheaper than buying it with n_z. What only n_z can buy is longitudinal geometry: z-resolution of the pinch, and the Δz bin width of every z-differential output.

Correction to what I said earlier: I claimed n_t "does not multiply the FFT-solve count". It does — iteration_on_overlaping_slices loops for(i0=0;i0<get_timestep();i0++) around make_time_step_on_slices (guineapigCPP.cc:819-822), so field solves scale as n_z²·n_t: quadratic in n_z, linear in n_t. Both multiply; only the power differs.

Parameters

transverse grid cells, x
transverse grid cells, y
longitudinal slices per bunch
sub-steps per crossing step
macroparticles per bunch
0 % = rigid beams (L/L₀ must → 1)
derived directly from the source formulas

What your settings actually mean

Where each number comes from
QuantityExpressionSource
Δz (slice thickness)2·cut_z / n_zgridCPP.cc:138
Δx, Δy (cell size)2·cut_x / n_x, 2·cut_y / n_ygridCPP.cc:136-137
step_ (push length)Δz / (2·n_t) = cut_z/(n_z·n_t)gridCPP.cc:146
crossing steps2·n_z − 1guineapigCPP.h:464-471
slice pairs at step kmin(k, 2n_z−2−k) + 1guineapigCPP.h:513-517
make_step() callsn_z² · n_tguineapigCPP.cc:819 + .h:513
complex FFTs2 per make_step, size 2n_x × 2n_yfieldCPP.h:40-43, fieldCPP.cc:95,104
particle pushes2 · n_z · n_t · n_mgridCPP.h:1172-1178
macroparticles / slice≈ n_m / n_z (Gaussian-weighted)particleBeamCPP.h:298
total drift per particlen_z · n_t · step_ = cut_z— (identity)
guineapigCPP.h:460–524

Who meets whom, and when

Left: the two bunches sliding through each other, one k per frame. Slices currently paired are joined by a link. Right: the full n_z × n_z pair table — every cell is computed exactly once, on the anti-diagonal i₁+i₂=k. Hover either panel.

beam 1 → (head = slice 0) ← beam 2 (head = slice 0) frozen (not in a pair this step)
Beam 1 slice i₁ starts at z = −(i₁+½)Δz, beam 2 slice i₂ at z = +(i₂−½)Δz; both advance Δz/2 per crossing step. Their meeting point z = ½(i₂−i₁−1)Δz is exactly GUINEA-PIG's min_z (guineapigCPP.cc:677, consumed at meshCPP.h:34).
early klate kcomputing now
Rows = beam 1 slice, columns = beam 2 slice. Shade = crossing step k = i₁+i₂. All n_z² cells fill in; each is visited n_t times back-to-back.
The loop nest, verbatim
// guineapigCPP.h:460  — the crossing schedule
for (k=0; k<n_slice; k++)                 iteration_on_overlaping_slices(0, k, sor);
for (k=n_slice; k<2*n_slice-1; k++)       iteration_on_overlaping_slices(k-n_slice+1, n_slice-1, sor);

// guineapigCPP.cc:819 — n_t sub-steps, pairing FROZEN inside this loop
for (i0=0; i0<grid_.get_timestep(); i0++) make_time_step_on_slices(firstSliceOfBeam1, lastSliceOfBeam2, sor);

// guineapigCPP.h:513 — the pairs of one step: indices run in OPPOSITE directions
slice_beam_2 = lastSliceOfBeam2;
for (slice_beam_1 = firstSliceOfBeam1; slice_beam_1 <= lastSliceOfBeam2; slice_beam_1++) {
    make_step(slice_beam_1, slice_beam_2, sor);      // deposit -> FFT solve -> interp -> kick+drift
    slice_beam_2--;                                  // => i1 + i2 = k is invariant
}
the interplay

The (n_z, n_t) plane

Fill colour is cost (n_z²·n_t field solves). The contour lines are lines of constant push length step_ = cut_z/(n_z·n_t) — i.e. lines of roughly equal integration accuracy. Because the contours have slope −1 in log–log while cost climbs twice as fast along n_z, the cheapest point on any accuracy contour is always its bottom-right corner: small n_z, large n_t. You move up and left only to buy longitudinal resolution.

Hover for cost, push length and Δz at any (n_z, n_t). ✚ marks your current setting.

What each knob alone controls

n_zn_t
push length step_∝ 1/n_z∝ 1/n_t
field solves∝ n_z²∝ n_t
particle pushes∝ n_z∝ n_t
Δz output bin∝ 1/n_z
distinct lumi z-bins2n_z−1
macro / slice (noise)∝ 1/n_z ↑noise
hourglass / pinch geometryresolves it
2-D FFT size

The last row is the point about n_x/n_y: the transform is always 2n_x × 2n_y (fieldCPP.h:40-41) regardless of n_z or n_t — they set how many transforms, never how big.

The n_z trap: raising n_z improves geometry and the push step, but simultaneously divides your macroparticles among more slices. Charge-deposit shot noise per slice grows like √(n_z/n_m), so n_z and n_m have to be raised together. Watch it happen in the run below.

a faithful miniature: same schedule, same deposit, same Green's function

Run the collision

This runs GUINEA-PIG's algorithm at reduced fidelity in your browser: CIC charge assignment (gridCPP.cc:763), the cell-integrated 2-D Green's kernel f_potential_2 (fieldCPP.cc:275), both beams packed into one complex FFT of size 2n_x×2n_y (fieldCPP.cc:47-104), the backSlices pre-drift (particleBeamCPP.h:232), and the luminosity estimator Σρ₁ρ₂ /(Δx·Δy·n_t) (gridCPP.cc:916). Beamstrahlung, pairs, coherent production and energy spread are omitted.

Cumulative luminosity through the crossing

L(k) / L₀ rigid-beam reference = 1
Rises past 1 as the pinch focuses the beams. With the force slider at 0 % this must land on 1.000 — any residual is pure n_x,n_y grid bias plus n_m shot noise.

Vertical beam size, slice by slice

beam 1, final beam 2, final initial
Head slices (index 0) pass through an undisturbed opposing bunch; tail slices are focused by the field of an already-pinched partner. Resolving this gradient is what n_z is for — it is invisible at n_z = 1.

Where the luminosity happens along z

dL/dz, binned at Δz/2 = cut_z/n_z
The 2n_z−1 discrete values of min_z = ½(i₂−i₁−1) (guineapigCPP.cc:677) are the only longitudinal positions GUINEA-PIG can report, each smeared uniformly over one Δz (meshCPP.h:34). Drop n_z to 2 and this histogram — and every z-differential background distribution built on it — collapses to three spikes. n_t changes nothing here.
Table view of the run
the money plot

Convergence sweep: is it n_z, n_t, or the product?

Runs the toy collision over a grid of (n_z, n_t) at your current n_x, n_y, n_m. Left: L/L₀ against the product n_z·n_t — if the push step were the only thing that mattered, all four curves would lie on top of each other. Where they don't is the part of the answer that only n_z can supply. Right: the same points against what they cost you.

Accuracy vs the product n_z·n_t

x is the thing that sets step_. Vertical spread at fixed x = the genuinely longitudinal part of the physics.

Accuracy vs cost (field solves = n_z²·n_t)

The efficient frontier hugs the low-n_z curves until longitudinal structure starts to matter, then jumps.
Table view of the sweep

Source map

Everything asserted above, with the file and line to check it against.

ClaimLocationCode
crossing schedule, 2n_z−1 stepsguineapigCPP.h:460beam_interaction()
pairs satisfy i₁+i₂=kguineapigCPP.h:513slice_beam_2--
n_t sub-steps, pairing frozenguineapigCPP.cc:819for(i0<get_timestep())
push lengthgridCPP.cc:146step_=deltaz/(2.0*timestep_)
slice thicknessgridCPP.cc:138deltaz=2.0*cut_z/n_cell_z_
particles have no z motionabstractParticle.h:122advancePosition(): xpos_,ypos_ only
only the paired slices are pushedgridCPP.h:1172moveAllParticles(i_slice1,i_slice2)
per-slice pre-drift uses n_tparticleBeamCPP.h:232backSlices()
collision z-bin from index differenceguineapigCPP.cc:677min_z=0.5*(i2-i1-1)
z smeared over one ΔzmeshCPP.h:34z=(min_z+rndm())*delta_z_
lumi normalised by n_tgridCPP.cc:916sum*1e18/(dx*dy*timestep_)
pair weights normalised by n_tgridCPP.cc:515/sqrt(dx*dy*timestep_)
FFT size independent of n_z, n_tfieldCPP.h:40nn[0]=2*n_y; nn[1]=2*n_x
2 FFTs per make_step, beams packedfieldCPP.cc:56,95,104temp[j]=rho1; temp[j+1]=rho2
parameter namesgridCPP.cc:265-279n_x n_y n_z n_t n_m.1 n_m.2

Unrelated snag found while reading: src/particleBeamCPP.cc is ISO-8859 encoded, not UTF-8. Plain grep treats it as binary and silently reports no matches — use grep -a or you will conclude functions like backstep don't exist.