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.
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
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
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.
| Quantity | Expression | Source |
|---|---|---|
| Δz (slice thickness) | 2·cut_z / n_z | gridCPP.cc:138 |
| Δx, Δy (cell size) | 2·cut_x / n_x, 2·cut_y / n_y | gridCPP.cc:136-137 |
| step_ (push length) | Δz / (2·n_t) = cut_z/(n_z·n_t) | gridCPP.cc:146 |
| crossing steps | 2·n_z − 1 | guineapigCPP.h:464-471 |
| slice pairs at step k | min(k, 2n_z−2−k) + 1 | guineapigCPP.h:513-517 |
| make_step() calls | n_z² · n_t | guineapigCPP.cc:819 + .h:513 |
| complex FFTs | 2 per make_step, size 2n_x × 2n_y | fieldCPP.h:40-43, fieldCPP.cc:95,104 |
| particle pushes | 2 · n_z · n_t · n_m | gridCPP.h:1172-1178 |
| macroparticles / slice | ≈ n_m / n_z (Gaussian-weighted) | particleBeamCPP.h:298 |
| total drift per particle | n_z · n_t · step_ = cut_z | — (identity) |
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.
min_z (guineapigCPP.cc:677, consumed at meshCPP.h:34).
// 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
}
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.
| n_z | n_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-bins | 2n_z−1 | — |
| macro / slice (noise) | ∝ 1/n_z ↑noise | — |
| hourglass / pinch geometry | resolves 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.
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.
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.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.
Everything asserted above, with the file and line to check it against.
| Claim | Location | Code |
|---|---|---|
| crossing schedule, 2n_z−1 steps | guineapigCPP.h:460 | beam_interaction() |
| pairs satisfy i₁+i₂=k | guineapigCPP.h:513 | slice_beam_2-- |
| n_t sub-steps, pairing frozen | guineapigCPP.cc:819 | for(i0<get_timestep()) |
| push length | gridCPP.cc:146 | step_=deltaz/(2.0*timestep_) |
| slice thickness | gridCPP.cc:138 | deltaz=2.0*cut_z/n_cell_z_ |
| particles have no z motion | abstractParticle.h:122 | advancePosition(): xpos_,ypos_ only |
| only the paired slices are pushed | gridCPP.h:1172 | moveAllParticles(i_slice1,i_slice2) |
| per-slice pre-drift uses n_t | particleBeamCPP.h:232 | backSlices() |
| collision z-bin from index difference | guineapigCPP.cc:677 | min_z=0.5*(i2-i1-1) |
| z smeared over one Δz | meshCPP.h:34 | z=(min_z+rndm())*delta_z_ |
| lumi normalised by n_t | gridCPP.cc:916 | sum*1e18/(dx*dy*timestep_) |
| pair weights normalised by n_t | gridCPP.cc:515 | /sqrt(dx*dy*timestep_) |
| FFT size independent of n_z, n_t | fieldCPP.h:40 | nn[0]=2*n_y; nn[1]=2*n_x |
| 2 FFTs per make_step, beams packed | fieldCPP.cc:56,95,104 | temp[j]=rho1; temp[j+1]=rho2 |
| parameter names | gridCPP.cc:265-279 | n_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.