reconstruct3D PCG backend policy¶
Contract of the code that runs today: the opt-in CTF- and sigma-weighted
preconditioned-conjugate-gradient (PCG) reconstruction path. The PCG backend
carries no regularization of its own beyond the ordinary FSC/SSNR P_tau
replay; nonuniform filtering is the same assembly-owned competition as on the
gridding backend (simple_nu_state_filter, policy 2026-09-06). The record of
the removed prior experiments -- the binary-envelope solvent prior (2026-08-27)
and the direct NU-evidence replay precision Q_NU with its auto-lambda and
auto-target controllers (2026-09-06) -- lives in
doc/implementation_notes/pcg_priors_history.md. A present-tense four-page
description of the backend is doc/implementation_notes/pcg_backend_overview.md
and the dated list of decisions with their evidence is
doc/implementation_notes/pcg_decision_log.md; new decisions are recorded
there, and this policy is updated when a decision changes a contract.
The production reconstruct3D command accepts the selector
rec_backend=gridding|pcg, with gridding unchanged as the default. Its pcg
branch runs independent kernel accumulation and solves for the even and odd
halfsets, writes the standard halfmaps and FSC, and forms the merged map by
averaging those two dense halfmaps. It supports both shared-memory execution and
distributed worker accumulation followed by a master solve. The former
reconstruct3D_pcg command has been retired; there is one production workflow
and one backend selector.
The production backend currently supports full-box fixed-pose
reconstruction from raw or denoised particle sources, multiple populated
states, CTF-aware cc or sigma-weighted euclid, point-group replication,
standard halfmap/FSC/merged-map names, project output registration, and the
usual final postprocessing handoff. FSC summaries include both the 0.5 and
0.143 resolutions and the cFAR diagnostic, using the same spherical or
density-envelope mask as the corresponding FSC. Each selected particle is read
once into its state/half accumulation and is never reread during PCG iterations.
In distributed execution, each worker atomically publishes one versioned raw
artifact per (state,half,part). It contains the unfolded complex B, real
D, particle count, geometry, and provenance. The master validates and adds
these artifacts in ascending part-number order. Only after the complete
state/half reduction does it fold the RHS, calculate rho floors, finalize
Khat, or solve. Empty partitions publish a valid header-only artifact so the
association order and completeness check do not depend on particle balance.
Box cropping is supported under the constant-field-of-view contract
(box*smpd == box_crop*smpd_crop, enforced at entry). The shared-memory path
deliberately rejects projrec=yes, fractional/trailing reconstruction, and
conical_fsc=yes regularization; those cases must not silently fall back to
gridding or matrix-free PCG. The distributed master integrates the
fractional/trailing algebra — raw (B,D) chains blended as
(u/f) current + (1-u) previous at full mass, priors applied only after the
blend (test=pcg_frac_update is the equivalence gate). The persisted chain is
continuous across constant-FOV crop growth: consecutive padded lattices share
their frequency step, so the smaller previous grid is an index-aligned central
subset and add_raw_accum_weighted embeds it by zero-extension — the PCG
analogue of the gridding trailing chain's autoscale ramp. The chain
continuation identity (pcg_chain_provenance) therefore excludes the crop
fields; a field-of-view change, a shrinking box, or any identity change
discards the chain pair and re-seeds through the trailing bootstrap. The one
recorded approximation is the old lattice's wrap rim: KB windows that wrapped
around the old period leave aliased mass in the outermost old shells, at or
beyond the producing stage's matching band and decaying as (1-u)^k — the
same approximation the gridding ramp accepts.
1. Production scope and fixed inputs¶
For each populated state and halfset, the backend solves a CTF- and
noise-weighted fixed-pose least-squares problem with PCG. Orientations, in-plane
shifts, state assignments, half assignments, CTF parameters, phase shifts and
ctfflag are read from the project and are never optimized by reconstruction.
The Euclidean objective uses per-particle sigma2; correlation is unweighted.
mskdiam supplies the spherical fallback support and pgrp is applied by
coordinate replication. automsk=yes and automsk=nu replace it with density
support; the NU-evidence envelope is never a solve support.
Both execution modes run one route: workers publish raw accumulators and the
master reduces, finalizes and solves them. Shared-memory execution runs it in
one process, as the only worker and then the master (2026-09-27; the separate
in-process solve route and its refusal of fractional and trailing
reconstruction are retired). Standard project volumes, halfmaps, FSC, cFAR, resolution fields and
postprocessing handoffs are written by the ordinary reconstruct3D workflow.
Per-half diagnostics record stop reason, requested/completed iteration count,
convergence flag, initial/final true relative residual, final relative update,
per-iteration residuals and timings.
Particle images are streamed through a bounded batch. The backend calls
begin_accum, repeats accumulate_batch, closes with end_accum, and solves
with solve_accum; it never materializes all observed particle planes at once.
The two particle-dependent accumulated quantities are the weighted RHS and the
|T_i|^2 Gram/sampling-density precursor. The RHS, preconditioner, and kernel
are derived from those quantities, so the PCG iteration itself performs no
particle-image I/O. Peak image/plane memory is therefore constant in particle
count.
Streaming accumulation fuses those two updates. For each particle it evaluates
the CTF, rotated coordinate, and KB interpolation window once, then updates
both accumulators in the same coloured OpenMP traversal. The monolithic RHS
and density routines remain the monolithic test oracle; the streaming-versus-
monolithic gate covers both single-batch and multi-batch fused accumulation.
It gates B and the D-derived kernel directly at 1e-6 relative error. The
20-step volume comparison has a separate 5e-4 bound because the guarded
reciprocal preconditioner and Krylov recurrence amplify single-precision
accumulation roundoff; passing the volume gate cannot excuse failure of either
direct accumulator gate.
B is complex and D is real. Finalization computes deterministic per-thread
rho shell statistics, then produces the reciprocal preconditioner and packed
Khat together in one OpenMP pass limited to the reachable Fourier sphere.
2. Where the code lives¶
| File | Contents |
|---|---|
src/main/volume/simple_reconstructor_pcg.f90 |
reconstructor_pcg operator/solver type |
src/main/strategies/parallelization/simple_rec3D_pcg_strategy.f90 |
PCG worker accumulation and master reduction and solve, for both execution modes |
src/defs/simple_refine3D_fnames.f90 |
raw (state,half,part) artifact names |
src/main/commanders/simple/simple_commanders_rec.f90 |
reconstruct3D commander and backend default |
src/main/ui/simple/simple_ui_refine3D.f90 |
reconstruct3D UI and backend selector |
src/main/strategies/parallelization/simple_rec3D_strategy.f90 |
backend dispatch |
src/main/params/simple_parameters*.f90 |
pcgop, rtol parameters |
src/fileio/simple_sigma2_files.f90 |
shared, builder-free sigma2 discovery/loading |
src/main/commanders/test/simple_commanders_test_highlevel.f90 |
operator/solver tests |
3. Statistical model¶
For volume x and 3-D transform F: G_i extracts an oriented Cartesian
Fourier plane by Kaiser-Bessel (KB) interpolation, S_i is the shift phase,
C_i the complex CTF (including the phase-flip convention), N_i the diagonal
noise covariance from the particle's sigma2. With
K_i = N_i^{-1/2} C_i S_i G_i F, the solver targets
H x = b, H = sum_i K_i^dagger K_i + Lambda, b = sum_i K_i^dagger N_i^{-1/2} y_i
Lambda = lambda*I is a fixed positive prior, constant for one solve, with
the absolute coefficient 1e-3 (PCG_LAMBDA). The relative-lambda CLI
(pcg_lambda_rel) was removed as unused; the internal set_lambda_relative
mechanism and the deterministic linear fixed-band data scale s_data(D)
remain for tests, diagnostics, and future prior anchoring
(pcg_priors_history.md). A solve may receive a frozen real-space support. It is fixed
before CG starts; no state changes or nonlinear clipping occur during an
iteration, so the operator remains linear.
The normal operator carries only the real weight |T_i|^2 = |C_i|^2/sigma2_i.
The shift phase is unit-modulus and cancels between the forward and adjoint
passes of K_i^dagger K_i. The full complex T_i = C_i S_i / sqrt(sigma2_i) is
needed only for b, whose adjoint uses conjg(T_i) — required for shifted
particles and phase shifts even when the CTF itself is real.
Output amplitude convention and scale continuity¶
Solved maps are written at the data-quotient convention — the plain
weighted least-squares solution, with no box-size scaling — identical to the
gridding backend after the legacy division/multiplication pair was retired
(doc/implementation_notes/drop_legacy_box_division.md). Deapodization is
applied inside the solver; PCG maps must never receive the gridding
correction or a second sampling-density correction. Scale continuity is a
contract: the euclid/sigma2 equilibrium survives any stable reference
amplitude but not an abrupt scale change between consecutive refinement
iterations, so any backend handoff, prior, or convention change must
preserve the amplitude scale seen by matching (the historical
gridding-to-PCG handoff crash was exactly such a jump).
ML two-map contract and starts¶
Refinement solves are two phases from one particle accumulation. The base
solve (H_data + lambda I) produces the _unfil half pair; FSC/cFAR and
resolution metadata come from that pair. It starts from zero, every
iteration: there are NO cross-iteration warm starts (policy 2026-09-10, PfCRT
regression gridding_vs_pcg). Warm-starting from the previous iteration's
half maps carried an unconverged transient of a slightly different system
(new FSC prior, new poses) into the fixed 2-iteration budget, which cannot
pull it back; the replay's relative residual then grew iteration over
iteration to 10^3-10^4 in the failed runs and never fell below 1 in any run,
the shipped ML halves were CG transients, and the base pair, two warm
iterations from a stale start, carried no fine-shell evidence into the NU
competition at the take-off stage (0% of the mask at 7.96 A where gridding
had 6.4%). The fixed small budget from a state-free start is what the
simulated-data calibration validated: two iterations beat gridding, and
beyond five the residual moves but nothing interpretable in the map does.
Every solve line reports INIT= (the relative residual of the start, 1.0
from zero) next to RESID=. A solve from a nonzero start (the replay) that
loses positive-definiteness returns stop_reason=indefinite from the solver
and is restarted once from zero (solve_with_cold_restart) before the
failure is fatal; a solve from zero that loses positive-definiteness fails
immediately. Distributed recovery is compute-only inside the even/odd
sections; restart reporting and fatal handling occur afterward at the serial
finalization boundary. Solver callers that do not request an outcome retain
the historical immediate hard failure. The
regularized pair deterministically replays kernel finalization from the
persisted raw (B,D) with the FSC/SSNR shell-diagonal P_tau installed, and
is the closed-form optimum of the diagonal model on that operator
(shrink_by_ml_prior, 2026-09-14): the CURRENT same-half base solution's
Fourier coefficients on the padded lattice scaled voxelwise by
(rho+floor)/(rho+floor+P_tau), i.e. by the ratio of the regularized and
base preconditioners. The closed form is shipped as is (maxits_ml=0,
the default; ITS=0, STOP=closed_form, RESID/MRES its residuals
against the replay system). maxits_ml>0 runs that many coupled PCG
iterations of the regularized system FROM the closed form (rtol=0, no
start rejection, an indefinite stop falls back to the closed form) and adds
a CF MRES a -> b line with the FSC between the start and the solved map
inside the band; the sidecar carries closed_form_rel_resid_* and
closed_form_vs_solved_*. Tested 2026-09-16 on bgal at box 256, where the
closed form's preconditioned residual reads 0.7-1.3 against ~0.1 in the
cropped stages: two iterations brought it to 0.3-0.7, changed the map
inside the band by FSC >= 0.965 and left every iteration's FSC0.5/0.143,
handoff and final map identical to three decimals, so the diagonal model
is adequate for the regularized member's jobs and the default is 0. The
former replay solve
(two CG iterations from a shell-isotropic FSC-shrunk base start, retired
2026-09-14) was prior-dominated over most of Fourier space, its start was
rejected on every anisotropically sampled dataset, and its L2 residual above
1 was mostly noise the prior refuses to fit; pcg_decision_log.md. With NU
filtering active the base (_unfil) pair seeds the NU candidate bank and the
regularized pair joins the competition as the auxiliary member, exactly as on
gridding. (The nu_input=gridding|ml alternatives of 2026-09-08 were retired
on 2026-09-09; records in doc/implementation_notes/pcg_priors_history.md.)
Neither precision nor lambda is ever accumulated into raw B or D.
Every shipped state volume carries a solve-support provenance sidecar
(<vol>_pcg_support.txt, solve_support=density|sphere and
solve_kind=base|regularized|mixed). The trailing bootstrap follows the same
recipe as the gridding bootstrap, applied to the PCG solver's own maps: the
FSC prior comes from the lag-one previous shipped pair, and the NU candidate
bank is seeded from the CURRENT base pair, volume blended with the previous
pair at the applied update weight together with the regularized pair (the
gridding trail_restored_halves_if_needed blend). The lag-one pair is never
an NU input (fix 2026-09-06). Same recipe and the same kinds of inputs on both
backends; the maps differ because the estimators differ. The bootstrap reads the support field for the lag-one FSC pair so the envelope and
phase-randomization FSC preprocessing is skipped exactly when that pair was
density-constrained in the estimator; a pair without a sidecar is treated as unconstrained, and a
bootstrap blend is constrained only if both contributions were. The kind
field is provenance only (its former consumer, the base warm-start selector,
went with the warm starts). The NU evidence built from a density-constrained
pair designates its null on the density envelope's dilation ring
(doc/policies/3D/automasking_policy.md).
PCG runs the one static NU competition (2026-09-18: the static ladder [20,15,12,10,8,6,5,4] A capped at fsc/1.5 of the base pair, with the ML-regularized pair as one more member beside the finest retained rung, competing with it at zero prior cost (ml_reg=yes) -- the ed36eb4c abinitio3D machinery, the only NU mechanism since 2026-09-18). Integer
Potts coordinates, unit candidate masses, the four fixed evidence bands.
The matching handoff is the finest selected label with at least 1% of the
signal voxels at that label or finer (nonuniform_filtering_policy.md
sections 8 and 12).
Solve support is an automsk feature (policy 2026-09-06). With automsk=no,
the default in abinitio3D, every PCG solve, base and regularized replay, runs
on the spherical mskdiam support and no density envelope is built. With
automsk=yes the conservative density envelope is the support of BOTH the
base solve and the regularized replay (policy 2026-09-09; the former
envfsc=no split with a spherical base is retired), and envfsc=yes is
implied: the FSC pair is envelope-constrained inside the estimator, so no
post-hoc mask and no phase-randomized correction are applied to it, and the
>>> FSC MODE line in the log and the resolution text says so. This is a
deliberate, reported choice, not a claim that a constrained estimate is free
of masked-FSC bias.
With automsk=nu the same contract holds: the density envelope is the
support of both solves, and the NU-evidence envelope is never installed as a
solve support (review 2026-09-17). The evidence null of a constrained base pair
lies on the density envelope's dilation ring, outside an evidence envelope, so
an evidence-supported solve would empty its own null, invalidate the next
envelope and fall back to a density envelope derived from a map that is zero
outside the evidence support: an oscillating support with no way back for
density the evidence excluded (the micelle in the PfCRT collapse, 2026-09-02).
Under nu the evidence envelope multiplies the matching references (assembly,
matcher fallback) and masks the gridding FSC post hoc. A first solve without a
density source bootstraps on the sphere.
Before any reconstruction-derived density source exists, the base necessarily
bootstraps on the sphere and its completed pair supplies the conservative
replay support. No PCG map is multiplied by either mask after reconstruction.
envfsc=yes with automsk=no affects only the phase-randomized FSC
evaluation, never a solve. An explicit pcg_mskfile remains the development
override and constrains every solve regardless of automsk; it is reported as
the state support, so the FSC mode, the support-provenance sidecar and the NU
evidence null regime all see a constrained pair.
The original-sampling final reconstructions launched by abinitio3D and
refine3D_auto are cold solves. They use a PCG iteration budget of at least
five; a larger user-supplied maxits_pcg remains in force. An explicit
positive rtol may still stop a converged solve earlier. Ordinary refinement
iterations keep the default budget of two iterations from a state-free start
(the calibrated regime).
Automatic final-map sharpening estimates its Guinier B-factor from the
unregularized half-pair average (_even_unfil/_odd_unfil, no mask of its
own since 2026-09-21) between HPLIM_GUINIER = 10 A (2026-09-26, RELION's
autob_lowres default; was 20 A) and the FSC=0.143 cutoff. The shipped
regularized map is B-sharpened and Butterworth-filtered at the cutoff with
no second FSC weighting, since its ML prior already shrank each shell by
about its FSC (support-provenance solve_kind=regularized; refine3D policy,
final sharpening). The reconstructed map, _lp, _pproc, and mirrored
products are not multiplied by any mask; the prohibition on post-hoc PCG
masking remains absolute. Iteration-time postprocessing retains its
existing behavior.
Beyond-band diagnostic¶
report_beyond_band_excess (module-level in the strategy, both execution
paths) compares the RMS of shells beyond the matching band with the band-edge
shell and logs >>> PCG BEYOND-BAND EXCESS at ratio >= 10. It is the
regression signal for solver defects that park energy above the matched band,
where a later stage transition would expose them to euclid matching. The
structural mitigation is the closed-form regularized pair (the voxelwise
P_tau optimum of the base solution, 2026-09-14), not spectral smoothing
(see the removed-experiment record in pcg_priors_history.md).
Backend regression gate¶
test=rec3D_backends reconstructs one fixed particle set with both backends
and hard-fails on gated shell-amplitude, FSC, and radial-flatness criteria
(band capped at lp); its ground-truth mode adds map-to-truth FSC and radial
LS-profile flatness. Mutations restoring the legacy box factor or omitting
deapodization must fail it; it is the standing acceptance harness for
convention and deapodization changes.
4. Numerical invariants¶
Load-bearing conventions. Changing any of them requires re-passing §9.
Full-symmetric-disk planes. forward_plane/adjoint_plane_add always use
an unpacked, both-sign-h disk, never the packed half. This makes them an exact
adjoint pair for any orientation, at 2x plane work. The packed half-plane's
Nyquist-bin bookkeeping is an orientation-dependent silent energy-loss trap that
a single-orientation adjoint test does not catch; the full disk removes that
surface permanently. The KB window is used at its natural odd width
(2*iwinsz+1); an even width would not be centred on nint(loc) and would
break the mirror consistency the fold depends on.
Oversampling. The unknown lives on the native box, but every Fourier
operation runs on an OSMPL_PAD_FAC-times padded lattice, centre-padded in and
centre-cropped out, pad_vol/crop_vol exact adjoints — the same arrangement
gridding uses. Without it KB interpolation is only percent-accurate, the
roll-off envelope swings ~30x across the box, and the Gram kernel cannot match
the operator. Since the padded lattice is already 2*box, it is the grid the
linear (non-wrapping) Gram convolution needs, so kernel and operator share one
lattice.
Two KB envelopes, both exact discrete transforms.
| Envelope | Origin | Handling |
|---|---|---|
| Gather | reading the volume through the KB window | build_env, divided out by deapod_mul |
| Deposition | laying |T|^2 onto the kernel grid |
divided out in the kernel build |
Both use the separable cosine transform of the operator's normalized discrete
KB stencil. This is algebraically identical to scattering a unit spike and
running the 3-D FFT, without allocating another padded volume. It is not
kbinterpol%instr: that continuous transform disagrees with the three discrete
renormalized weights by ~2x at the box edge. The matrix-free operator applies
the gather envelope twice, so it is A = A_env^{-1}(E T E)A_env^{-1};
deapod_mul brackets it to recover the true T. Not optional for real
particles: synthetic data from the operator carries the same envelope and
cancels it (an inverse crime), real particles do not, so an uncorrected solve
returns E^{-1} x_true.
Soft spherical support. With mskdiam set, the solve is constrained by
writing x = P u and minimising ||A P u - y||^2, giving (P H P) u = P b —
P on both sides of the operator and once on the RHS. P H P stays symmetric
positive semidefinite and its null space is never entered from x = 0. Besides
removing edge artefacts where deapodization amplifies hardest, it shrinks the
problem: at mskdiam 180 in a 256 box the sphere is ~18% of the volume. Edge
profile is production's cosedge_r2_3d.
Scatter reproducibility. Every scatter into a Fourier accumulator is
parallelized by h-strided colouring, whose separation guarantee holds on
unwrapped coordinates only. The accumulator is periodic, so windows that reach
the wrap boundary can collide after folding even in the same colour. All scatter
sites therefore split: the parallel colour sweep skips any window win_wraps
flags, and a serial pass handles the rim (pre-filtered by sq_rim, a radius
below which wrapping is provably impossible). Serial, not atomic — an atomic
would fix the race but leave summation order thread-dependent, and bitwise
run-to-run reproducibility is what makes a residual trace usable as a regression
signal. The rim is a thin outer shell, so the serial cost is small.
5. Preconditioner and kernelized operator¶
Preconditioner. M(k) = rho(k) + floor(shell), with
rho = sum_i G_i^dagger |T_i|^2 G_i the sampling density. The floor is a fixed
fraction of the shell-mean rho, not an absolute constant: rho spans many
orders of magnitude and is genuinely zero between rotated planes, so an absolute
floor amplifies the least-constrained modes by six to nine orders of magnitude
and PCG fills the map with noise. Modes beyond padf*Rnat are unconstrained and
zeroed, making M singular but positive semidefinite, which keeps the Krylov
space out of the null space. The envelope belongs in M too: the operator being
solved is E^{-1} T E^{-1}, so M^{-1} brackets its Fourier divide with E,
not E^{-1}.
Kernelized (Toeplitz/Gram) operator. pcgop=kernel, the default. Replaces
the per-iteration particle loop with one padded FFT, a pointwise multiply by a
precomputed real Khat, and an inverse FFT — per-iteration cost independent of
particle count (~7x faster per iteration). Khat uses the standard NUFFT Gram
construction: scatter |T_i|^2 onto the 2x oversampled grid at doubled
coordinates. The oversampling is the mechanism, not a safety margin; it resolves
the sub-pixel frequency offsets that distinguish the operator from gridding. (A
literal impulse-response kernel does not work: KB weights sum to 1, so it
reduces exactly to gridding and PCG converges in one step to the gridding
answer.) Overall scale is the analytic constant padsc^2;
measure_kernel_scale is a test-only check that it is right.
The kernelized operator is shift-invariant and the true operator is not, so it stays an approximation (~3.4% interior error). The matrix-free path is the exact numerical reference for unit tests and small diagnostic fixtures, not a feasible real-workflow backend and not a production fallback. Its per-iteration particle loop becomes prohibitive at experimental particle counts and under symmetry replication.
The approximation is not reliably positive definite under late over-iteration:
on the deterministic fixture the residual bottoms out and then curvature fails
after the useful map has already saturated. Production therefore defaults to
two iterations and rejects maxits>8. Low-level tests may use more iterations
when diagnosing the approximation boundary.
pcgop=kernel is therefore the only candidate for production workflows. It
must be validated against matrix-free on deterministic, small enough fixtures
where both can run. Agreement of a residual trace alone is insufficient: a
uniform operator-scale error cancels from the CG recurrence while changing map
amplitude. Kernel validation must compare the operator action and scale
directly, then compare fixed-iteration solutions built from the same RHS. Real
workflow acceptance additionally requires independently reconstructed halfmaps
and FSC; a similar operator is not enough.
6. Symmetry¶
Point-group symmetry is applied by coordinate replication inside the
operator: each plane pixel is gathered and scattered at all M orientations
R_i . S_g. Replication is applied to H (accumulate_absT2,
apply_normal_matrixfree) and to b (scatter_plane, reached from
apply_adjoint_all and accumulate_rhs_density), so the system solved is consistently
symmetrized.
Composition order is production's. matmul(R_i, S_g) in the row-vector
convention loc = matmul([h,k,0], rot) is exactly what sym%apply produces and
what reconstructor%insert_fplane scatters at. test=pcg_recon stage 9 asserts
this by composing the reference path through sym%apply rather than repeating
the operator's own expression.
symmats(:,:,1) is exactly the identity for every supported group, so pgrp=c1
(nsym=1) is bit-identical to the pre-symmetry path.
Two consequences to keep in view:
- The
gloop sits outside the h-strided colour sweep. Inside it,Mreplicas would writeMfootprints per colour and separation would have to hold for every pair(g, g'), which it does not. - Cost scales linearly in
nsym. Fine for c2/d2; icosahedral (60) multiplies the whole accumulation, including the serial rim pass, by 60. The lattice-exact permutation path — available only for groups whose operators are signed permutations (c2,c4,d2,d4,t,o) — is the way out and is not implemented.
Symmetry-as-constraint and data replication are the same estimator, so there is no SNR difference between them; symmetry's gain is fewer effective unknowns.
7. PCG solver¶
Standard left-preconditioned CG of H x = b (P H P under a support
constraint). Zero initialisation. All real-volume dot products use a
deterministic double-precision reduction. The solve fails explicitly on
non-finite or non-positive dot(p,Hp), a non-positive-definite preconditioner,
or a zero RHS, rather than continuing past a broken assumption.
The headline and stopping test are the true relative residual
||r||_2 / ||b||_2, not the preconditioned M-norm — which is not monotone under
a singular preconditioner and reads like a diverging solver while the solve is
converging. The M-norm is still logged as the diagnostic for the
preconditioner: a large gap between the two says M models H poorly. The
recurrence residual is periodically audited against a recomputed b - Hx; they
agree to six significant figures, which licenses reporting the cheap recurrence
norm.
A second criterion, dx/x <= PCG_XTOL, exits on diminishing returns: on noisy
real data ||r||/||b|| plateaus above rtol while the map keeps settling, so
dx/x is what actually terminates a real solve. It is suppressed by
rtol <= 0, the caller's way of demanding exactly maxits iterations —
stage 7 depends on that, since comparing two solves stopped by a data-dependent
criterion asserts nothing.
Successful completion returns a typed pcg_solver_outcome identifying rtol,
xtol, intentional fixed_iterations, or exhausted maxits, with the
requested/completed iteration count and initial/final residual/update values.
Broken curvature, a non-finite state, a non-positive preconditioner, and a zero
RHS are hard errors. Conversion of those failures into recoverable workflow
outcomes remains future production work.
The final fixed iteration does not apply the preconditioner because no next search direction will consume it. The terminal residual and update diagnostics remain unchanged; only the unused M-norm is omitted.
8. Sigma2 handling¶
Gated on objfun, matching reconstruct3D: objfun=cc runs unweighted
(sigma2 = 1), objfun=euclid requires sigma2 files and weights the fit by
them. A missing sigma2 file under objfun=euclid is a hard error, never a silent
unweighted fallback — that would quietly change which objective is minimised.
Sigma2 is per-particle-per-shell, read via euclid_sigma2 from the canonical state
file (as flex_analysis does), then upsampled to the operator's shell range.
Discovery/carry-over/loading lives once in src/fileio/simple_sigma2_files.f90,
called by both flex_analysis and the reconstruct3D PCG strategy. That module is
deliberately builder-free: builder depends on euclid_sigma2, so the sigma2
side must not depend back on builder; callers pass pftc, esig and
orientations explicitly.
9. Tests¶
test=pcg_recon in simple_commanders_test_highlevel.f90 — one gate, nine
fail-fast stages, in memory with no project I/O. Each stage gates the ones after
it; a visually plausible map is not evidence that the CTF/sigma adjoint is
correct.
| # | Stage |
|---|---|
| 1 | adjoint dot-product identity, T_i = 1 |
| 2 | same with nonzero shift, astigmatic CTF and sigma2 — isolates build_transfer |
| 3 | normal-operator symmetry and positive-definiteness |
| 4 | heterogeneous phantom recovery |
| 5 | kernelized-vs-matrix-free operator, scale/energy, and fixed-iteration solution baseline |
| 6 | kernel shift-invariance, CTF-dependence, and the preconditioner |
| 7 | streaming batches and serialized fixed-order raw reduction reproduce monolithic accumulation |
| 8 | deapodization against envelope-free data — the one stage without an inverse crime |
| 9 | symmetry replication equals a c1 build of the symmetry-expanded particle set |
Stages 1-7 generate observations with forward_plane, so the gather envelope
cancels; they gate operator algebra only. Envelope correctness for real
particles is gated solely by stage 8.
Matrix-free is the oracle for kernel development. Any change to kernel construction, scale, masks, symmetry, resolution limiting, weights, preconditioning, or box conversion must add or extend a deterministic kernel-versus-matrix-free gate. Production-sized runs exercise kernel only; they do not establish correctness by comparing kernel output with another kernel output.
Not covered, and worth remembering before trusting a change: the backend's own particle I/O loop, and replicated symmetry through the matrix-free operator (stage 9 compares kernel to kernel).
10. Current backend exclusions¶
Not implemented, and hard-errored or absent rather than silently approximated:
- no orientation search or pose optimization inside reconstruction;
- no online pose update inside reconstruction; fractional/trailing reconstruction is supported through persisted raw accumulator chains, in shared-memory and distributed execution alike;
- no post-hoc masking of PCG maps; NU filtering is the assembly-owned
competition shared with gridding and writes derived
_nu_filtproducts without touching the primary maps; - no projection-direction compression, conical-FSC regularization, or GPU/offload path;
- no reuse of SPIDER BP-CG real-space code — the design is Fourier central-section and the architectures do not transfer. There is no licensing barrier: SIMPLE is GPL-3.0 and SPIDER GPL-2.0-or-later.
11. Reused vs. reimplemented¶
Established data preparation and conventions are reused; the adjoint pair is implemented and tested here rather than repurposed from production gridding.
- CTF/sigma physics evaluated directly via
ctf/ctfparams(simple_ctf.f90), the same physicsimage%apply_ctfuses. The per-pixel evaluation is the flatft_map_ctf_kernelform with per-particle constants hoisted, inlined rather than called: the library routine's memoized(h,k)maps span theh >= 0half only, whilelims2is a full both-sign-hdisk.image%gen_fplane4recis not reused — it requires a module-global memoised cache (against this path's no-module-global rule) and a packed, padded-box plane convention that clashes with the full-disk native representation. - Particle I/O uses production's batched pattern
(
prepimgbatch/discrete_read_imgbatch) withnorm_noise+taper_edges_particle+fftper particle before plane extraction — the same steps production fuses intonorm_noise_taper_edge_pad_fft. Batches stream into the accumulators and are discarded, so peak memory is constant innptcls. - The adjoint is written fresh, not derived from
reconstructor%compress_exporinsert_plane_oversamp: those are production gridding/storage conversions, not established linear adjoints.
12. Execution-path identity and performance rules¶
Backend comparison protocol (review 2026-09-09)¶
A rec_backend=gridding versus rec_backend=pcg comparison in abinitio3D
is meaningful because the two paths share everything but the estimator:
- stages 1-2 are gridding on both (
PCG_REC_START_STAGE=3); the stage ladder, the FSC=0.5 promotion, the uncapped NU-stage handoff, early stopping, the sigma2 handling, the matching references and the final bootstrap_rec3D sequence (gridding bootstrap map, residual sigma pass) are backend-agnostic; only the final map's solver differs; - the worker-side particle preparation is the same (noise normalization
against the same mask, edge taper, FFT), the same sigma2 weights and CTF
parameters enter both accumulations, and the KB stencil and its
deapodization envelope are shared (
kb_stencil_centered_crop_inv_envelope_1d); - the ML regularizer is the same formula on both backends:
1/tau2 = <rho>_shell / (tau * fsc/(1-fsc)), FSC clamped to [0.001, 0.999], no prior belowhp, driven by the current iteration's unfiltered pair (add_invtausq2rhoandbuild_ml_prior_from_density); - the NU competition, its ladder bound and the
handoff run on the unfiltered
pair with the regularized pair as the finest member through the one
nonuniform_filter_stateon both backends; both ship deapodized halves and merged maps carrying the same soft spherical support atmsk_crop(the PCG solve support; the gridding restoration applies the identicalmask3D_softafter deapodization, 2026-09-09) and compute the FSC on those halves throughevaluate_halfmap_pair, which applies no mask of its own; - the master phase gets the same thread budget on local execution
(
rec3D_master_nthr: nparts x nthr capped at 32, the PCG rule, now also applied to the gridding volassemble instead ofNTHR_SHMEM_MAX), and both log oneRECONSTRUCTION MASTER PHASE (<backend>): <s>line per iteration; the worker side is in the bench files (partial reconstruction), memory in the peak-RSS fields.
Differences that are the estimator itself and belong in the comparison:
PCG solves (H + lambda) x = b with two CG iterations per half (base, from
zero) and takes the regularized pair in closed form from it (2026-09-14), a
fresh estimate every iteration like the gridding density quotient
(cross-iteration warm starts retired 2026-09-10).
The former measurement asymmetry (gridding FSC on the apodized halves for legacy parity, PCG on the solved halves) was removed on 2026-09-09: the gridding path now computes its FSC on the deapodized, support-masked halves it ships. The one-mask contract that came with it:
- every reconstruction product (halves,
_unfilhalves, merged map) carries the soft spherical support atmsk_cropexactly once, installed by the estimator (PCG) or by the restoration after deapodization (gridding), and recorded in the support-provenance sidecar<vol>_pcg_support.txt(solve_kind=griddingfor gridding products,gridding_regularizedwhenml_reg=yesput the FSC-derived prior into the restoration; since 2026-09-26 postprocess reads the kind to skip its FSC weighting on ML-regularized maps); evaluate_halfmap_pairmasks nothing of its own (the envfsc envelope + phase-randomization correction is applied only to an unconstrained pair, and the mode actually used is logged as>>> FSC MODE);postprocessapplies no post-hoc mask to a volume carrying the sidecar (previously PCG-only by backend name; an imported map without the sidecar still gets the classical spherical/envelope mask);- the PCG support is a hard solve domain plus one soft window (review
2026-09-09, finding 4.2):
set_maskbuilds the window withmask3D_soft, the solve runs on the domainwindow > 0(P^2 = P, exact projections in operator, RHS and preconditioner) and the shipped map iswindow * u. That is the same "estimate times one soft window" the gridding restoration ships, so the two backends' band treatment is identical and the only estimator difference is that the PCG estimate is zero outside the domain. The softP H Pformulation was not equivalent: where0 < P < 1the solved variable compensates forP, so the band was a solver-state dependent mixture (PCG_HARD_SOLVE_SUPPORTin the solver restores it for experiments). Output-space maps entering the solver (nonzero starts, and the base map the closed-form regularized pair is derived from) are never re-masked: the entry converts them back tou = x / windowwherewindow >= PCG_SUPPORT_DIV_MIN(zero below) instead of projecting again; the former entry projection squared the edge on every restarted iteration and compounded over a stage. Regressions:test=pcg_reconstage 14 (band profile of the constrained solve against the windowed unconstrained one; band stability under repeated starts); - the matcher still applies
mask3D_soft(msk_crop)to its reprojection reference after Fourier filtering (both backends,mask_matching_reference). That is reference preparation, not an estimate: it restores compact support after the filter's ringing and covers user-supplied start volumes. In the 12 px cosine band the reference therefore carries P^2; it is the one remaining second application and is deliberate.
Comparison contract (review 2026-09-09)¶
Two inputs that could change the result independently of the backend are now shared, and the measurement records are complete enough to separate wall time, compute cost and memory:
- one observation preparation. Cropping and edge tapering do not commute.
Gridding normalized at the native box, Fourier-cropped, then tapered at the
crop box; PCG tapered at the native box and cropped afterwards. Both now go
through
prep_rec_observation(simple_matcher_ptcl_io): normalize at the native box, Fourier-crop, taper at the cropped box; gridding pads and transforms in its fused routine, PCG takes the native plane of the tapered observation. Without a crop the observation is tapered first and normalized second on both backends.test=pcg_reconstage 13 gates the two routes in real space at 1e-5 relative; - the volume and its support sidecar are one artifact. Stage-boundary
renames, final copies, symmetric-map copies and the refine3D start-volume
rename move or copy the sidecar with the map (
copy/rename_support_provenance); noise starts and imported maps remove a stale sidecar (remove_support_provenance); the PCG masters publish the map first and the sidecar second. Before this a PCG half lost its provenance at every stage boundary and the next stage's first base solve was an unintended cold start; a copied final gridding map could be masked a second time by a standalone postprocess; - cost records. Every partition writes
REFINE3D_BENCH_ITERnnn_PARTppp.txt(partition 1 also the legacy file) with nparts, worker threads, box, box_crop, backend,maxits_pcg,rtol, peak and phase RSS, the phase timings and their thread-seconds; the master logs oneRECONSTRUCTION MASTER PHASE (<backend>): s on n threads = thread-s; master peak RSSline per iteration. Load imbalance is the spread of the per-part partial-reconstruction times; core-hours are the sum of worker thread-seconds plus master thread-seconds.
Baseline profile for the first quality/cost study: automsk=no, envfsc=no,
conical_fsc=no, no pcg_mskfile; identical starting project, maps, seed,
even/odd assignment, sampling and sigma state, stage schedule and resolution
limits, nparts, threads, queue and hardware between the paired runs; each
backend in its own clone of the starting project; maxits_pcg, rtol and the
operator mode fixed and recorded. Gates before interpreting results:
prepared-observation parity (stage 13), the support regressions (stage 14),
no reconstruction losing provenance across a stage boundary, and wall time,
thread-seconds and peak RSS recomputable from the emitted records. Judge
quality on paired replicas (at least 10 seeds), never on a single run.
Shared-memory and distributed execution are two parallelizations of one algorithm. Output conventions, warm starts, and diagnostics are implemented once at module level and invoked identically from both entry points; nothing may depend on which path produced a map.
Durable performance rules (from the retired production-readiness note):
- one logical particle read per accumulation phase (direct source reads for
standalone
reconstruct3D; the validated downscaled cache for cache-enabled refinement); particle residency bounded byMAXIMGBATCHSZ; no particle plane cache — kernel iterations are data-free after(B,D); - the preconditioner uses the padded Toeplitz lattice; do not trade the pad/crop geometry for a native-grid FFT optimization;
- process one state/half at a time; do not overlap the largest accumulation
and solve scratch allocations; keep
Dreal; keep the fused reciprocal/Khatpacking over the reachable Fourier sphere and the exact separable discrete KB-envelope construction; - kernel failure never retries with matrix-free (too slow, hides defects, impossible after distributed reduction); fail structurally instead;
- symmetry cost scales with group order (coordinate replication); validate C1/C2/D2 and treat high-order groups as a measured-budget contract; the lattice-exact permutation path for signed-permutation groups is the eventual optimization and is not implemented;
- use the per-phase timing diagnostics, not total wall time, to choose the next optimization.