Skip to content

Commit 95d4adb

Browse files
committed
work on sigma_store=canonical and bootstrap_rec3D
1 parent 8993247 commit 95d4adb

18 files changed

Lines changed: 427 additions & 88 deletions

doc/implementation_notes/pcg_priors.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3473,6 +3473,32 @@ the acceptable-looking outputs do not validate the prior.
34733473
nparts=... nthr=... rec_backend=pcg` (UI now exposes rec_backend,
34743474
maxits_pcg, rtol).
34753475

3476+
11. **Canonical sigma2 store review, streptavidin 2/10 failures (2026-09-07).**
3477+
- Proven non-equivalences with the legacy STAR store: (a) freshness,
3478+
canonical commits residuals(N) right after matcher pass N and its
3479+
stage-boundary and final reconstructions consume them, legacy's STAR N
3480+
holds residuals(N-1); (b) the final reconstruction: legacy refreshes
3481+
sigmas at native sampling whenever the registration box differs from
3482+
the native box, canonical reused any structurally consumable state and
3483+
skipped the refresh. Precision, averaging and grouping are the same.
3484+
- Failure surfaces only canonical had: hard abort on any active record
3485+
with a non-finite or non-positive shell; generation/digest/checksum/
3486+
coverage aborts on merge; silent re-seed from image power on any
3487+
validation failure. Design hazard: the state was registered as an
3488+
absolute path that survives every project copy, so a copied execution
3489+
project consumed and mutated the originating run's sigma2_state.bin;
3490+
candidate and range names carried no transaction identity.
3491+
- Fixes: registration by name resolved against the project file's
3492+
directory (absolute only for explicit cross-directory states);
3493+
abinitio3D drops an inherited registration at start; candidate and
3494+
range names carry the generation they commit
3495+
(`<stem>.g<N>.next`, `<stem>.g<N>.part<NN>.range`) and a worker checks
3496+
the candidate's generation; invalid records are skipped with a warning
3497+
in reduction and commit validation; the canonical final reconstruction
3498+
applies the legacy registration-box rule. Test
3499+
`simple_test_exec prg=sigma2_state` extended (invalid-record skip,
3500+
scoped names). Uncompiled.
3501+
34763502
## 11. The NU machinery as the prior infrastructure
34773503

34783504
The nonuniform-regularization machinery

doc/policies/abinitio3D_policy.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,18 @@ program takes any project with 3D orientations, it is also the standalone
144144
test for this stage (`simple_exec prg=bootstrap_rec3D projfile=... pgrp=...
145145
mskdiam=... nparts=... nthr=... rec_backend=...`), so failures in the final
146146
reconstruction can be reproduced in minutes rather than after a full run.
147+
Its bootstrap map is a gridding assembly that carries the last stage's
148+
`filt_mode`, `nu_refine` and `automsk` (the residual sigmas depend on the
149+
regularization of the reference they are scored against, so that reference is
150+
regularized like the last stage's matching references); the shipped map is
151+
classical and runs on the workflow's backend with the PCG cold-solve budget
152+
applied inside `bootstrap_rec3D` (2026-09-07).
153+
Whether the final reconstruction refreshes its sigmas at native sampling is
154+
decided by one rule for both sigma stores: a registration box different from
155+
the native box (2026-09-07; the canonical store used to reuse its committed
156+
state regardless). With `sigma_store=canonical`, abinitio3D drops any state
157+
registration inherited with its input project before the first stage, so
158+
every run seeds and owns its sigmas in its own directory.
147159

148160
## 4. Low-Pass and Cropping
149161

doc/policies/refine3D_policy.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,26 @@ an empty directory, and every final reconstruction at a new sampling. Since
189189
sequence: seed, bootstrap map, residual pass, consolidation, final map;
190190
abinitio3D's `calc_final_rec` and refine3D_auto call it and carry no copy
191191
of the sequence. It runs standalone on any project with 3D orientations
192-
and is the test entry point for the final-reconstruction stage.
192+
and is the test entry point for the final-reconstruction stage. The
193+
bootstrap map only serves as the residual reference, so it is always a
194+
gridding assembly with ML regularization (one particle pass, no
195+
postprocessing) that keeps the workflow's `filt_mode`, `nu_refine` and
196+
`automsk`: the residual sigmas depend on the regularization of the
197+
reference they are scored against, so that reference is regularized
198+
exactly as the refinement's matching references were; the shipped map keeps the
199+
caller's backend and, on PCG, starts from nothing at the native box and
200+
therefore gets the cold-solve budget of at least `FINAL_PCG_MAXITS_FLOOR`
201+
(5) iterations whoever the caller is (2026-09-07). The base plus ML solve
202+
pair of that final PCG reconstruction is inherent to ML regularization:
203+
the prior is built from the base pair's independent-half FSC, which is
204+
also the reported FSC and the unfiltered pair postprocessing uses.
205+
- The registration-box rule is the same for both stores (2026-09-07): a
206+
final reconstruction whose registration (crop) box differs from the native
207+
box refreshes the sigmas at native sampling (image-power seed, bootstrap
208+
map, residual pass) before the shipped map. Before this the canonical store
209+
reused its committed state whenever it was structurally consumable, which
210+
is always true at the native box, and so skipped the refresh legacy
211+
performs; the two stores could not produce the same final map.
193212

194213
## 6. Reference Preparation
195214

doc/policies/sigma_calculation_policy.md

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,17 +148,38 @@ normalization by `2*pftsz`.
148148

149149
Each canonical iteration is transactional:
150150

151-
1. the master creates `sigma2_state.next`, copying the committed generation
152-
when unchanged rows must survive;
153-
2. each worker writes one exclusive temporary range for its assigned global
154-
particle rows;
151+
1. the master creates the candidate `<state stem>.g<N>.next`, where N is the
152+
generation this update commits (committed generation + 1), copying the
153+
committed generation when unchanged rows must survive;
154+
2. each worker writes one exclusive range file
155+
`<state stem>.g<N>.part<NN>.range` for its assigned global particle rows,
156+
after checking that the candidate it found carries generation N;
155157
3. the master verifies generation/layout identity and exact non-overlapping
156158
scheduled coverage, merges the ranges, and reduces active records into the
157159
grouped model;
158160
4. commit-time scientific and integrity validation runs;
159161
5. the candidate is synced and atomically published over the committed file,
160162
followed by directory sync.
161163

164+
Candidate and range names carry the generation they belong to (2026-09-07),
165+
so a file left by another transaction, a crashed run or a second run in the
166+
same directory, can never be merged into this one; the mismatch fails loudly.
167+
168+
Execution-local state (2026-09-07): the project registers its state by name
169+
only when the state lives in the project file's own directory, and the name
170+
is resolved against that directory on lookup. A project copied into a new
171+
execution directory (`mkdir=yes`) therefore no longer points back into the
172+
originating run's `sigma2_state.bin`; the copy seeds its own state. An
173+
absolute registration is kept only for an explicit cross-directory state
174+
(the streaming pool, project merges). abinitio3D drops any registration
175+
inherited with its input project at start, so a fresh workflow always seeds.
176+
177+
Invalid records (2026-09-07): an active particle record with a non-finite or
178+
non-positive shell is skipped by the reduction and by commit-time validation,
179+
with a warning giving the count, instead of aborting the run. The legacy
180+
store averages whatever its part files hold; the canonical store now degrades
181+
the same way rather than failing where legacy would not.
182+
162183
A failed or incomplete candidate never replaces the previous committed state.
163184
The committed format is independent of partition count and number padding.
164185

doc/refactoring_notes/canonical_sigma2_state_refactoring.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,10 @@ the complete generation.
155155

156156
## 5. Distributed Write and Recovery
157157

158-
The committed `sigma2_state.bin` is immutable. Every update targets
159-
`sigma2_state.next`; only the master may create, size, validate, publish, or
158+
The committed `sigma2_state.bin` is immutable. Every update targets the
159+
generation-scoped candidate `sigma2_state.g<N>.next` (N = the generation the
160+
update commits; worker ranges are `sigma2_state.g<N>.part<NN>.range`, both
161+
since 2026-09-07); only the master may create, size, validate, publish, or
160162
discard it.
161163

162164
Protocol:

production/tests/simple_test_sigma2_state.f90

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,19 @@ program simple_test_sigma2_state
55
use simple_syslib, only: del_file, file_exists
66
use simple_sigma2_state_file, only: sigma2_state_header, sigma2_state_init_header, &
77
&sigma2_state_create_candidate, sigma2_state_write_local_range, sigma2_state_read_header, &
8-
&sigma2_state_validate_file, SIGMA2_GROUP_GLOBAL, SIGMA2_GROUP_STACK, SIGMA2_PROV_PSPEC, &
9-
&SIGMA2_PROV_RESIDUAL, SIGMA2_STATE_COMMITTED
8+
&sigma2_state_read_groups, sigma2_state_validate_file, SIGMA2_GROUP_GLOBAL, SIGMA2_GROUP_STACK, &
9+
&SIGMA2_PROV_PSPEC, SIGMA2_PROV_RESIDUAL, SIGMA2_STATE_COMMITTED
1010
use simple_sigma2_state, only: sigma2_state_layout_digest, sigma2_state_merge_local_ranges, &
1111
&sigma2_state_reduce_groups, sigma2_state_validate_identity, sigma2_state_validate_science, &
1212
&sigma2_state_candidate_path, sigma2_state_commit, sigma2_state_prepare_update, &
13-
&sigma2_state_range_path
13+
&sigma2_state_range_path, sigma2_state_next_generation
1414
implicit none
1515

1616
call exercise_policy('global', SIGMA2_GROUP_GLOBAL, 1, 4)
1717
call exercise_policy('group', SIGMA2_GROUP_STACK, 2, 8)
1818
call exercise_recovery_guards()
1919
call exercise_update_preparation()
20+
call exercise_invalid_record_skip()
2021
write(*,'(A)') 'SIMPLE_TEST_SIGMA2_STATE NORMAL STOP'
2122

2223
contains
@@ -171,13 +172,15 @@ subroutine exercise_update_preparation()
171172
logical :: active(4)
172173
integer :: eo(4), groups(4), status
173174
integer(int64), parameter :: DIGEST = 991_int64
175+
integer(int64) :: next_gen
174176
character(len=128) :: message
175177
character(len=*), parameter :: COMMITTED = 'sigma2_state.bin'
176-
candidate_path = sigma2_state_candidate_path(COMMITTED)
177-
range_path = sigma2_state_range_path(COMMITTED, 2, 3)
178+
candidate_path = sigma2_state_candidate_path(COMMITTED, 1_int64)
179+
range_path = sigma2_state_range_path(COMMITTED, 1_int64, 2, 3)
178180
call del_file(COMMITTED)
179181
call del_file(candidate_path)
180182
call del_file(range_path)
183+
call del_file(sigma2_state_candidate_path(COMMITTED, 2_int64))
181184
spectra(:,1) = [1.0,2.0,3.0]
182185
spectra(:,2) = [2.0,3.0,4.0]
183186
spectra(:,3) = [3.0,4.0,5.0]
@@ -197,19 +200,80 @@ subroutine exercise_update_preparation()
197200
call require_ok(status, message)
198201
call sigma2_state_commit(candidate_path%to_char(), COMMITTED, active, eo, groups, status, message)
199202
call require_ok(status, message)
203+
call require(index(candidate_path%to_char(), 'sigma2_state.g1.next') > 0, &
204+
&'candidate path is scoped to the generation it commits')
205+
call require(index(range_path%to_char(), 'sigma2_state.g1.part002.range') > 0, &
206+
&'canonical range path is scoped to the generation and includes the padded partition')
207+
! the next transaction is named for the generation it will commit
208+
call sigma2_state_next_generation(COMMITTED, next_gen, status, message)
209+
call require_ok(status, message)
210+
call require(next_gen == 2_int64, 'next generation follows the committed one')
211+
candidate_path = sigma2_state_candidate_path(COMMITTED, next_gen)
212+
call require(index(candidate_path%to_char(), 'sigma2_state.g2.next') > 0, &
213+
&'next candidate path is scoped to the next generation')
200214
call sigma2_state_prepare_update(COMMITTED, candidate_path%to_char(), status, message)
201215
call require_ok(status, message)
202216
call sigma2_state_read_header(candidate_path%to_char(), header, status, message)
203217
call require_ok(status, message)
204218
call require(header%generation == 2_int64, 'prepared update advances the generation')
205219
call require(header%provenance == SIGMA2_PROV_RESIDUAL, 'prepared update records residual provenance')
206-
call require(index(range_path%to_char(), 'sigma2_state_range_part002.bin') > 0, &
207-
&'canonical range path includes the padded partition')
208220
call del_file(COMMITTED)
209221
call del_file(candidate_path)
210222
call del_file(range_path)
211223
end subroutine exercise_update_preparation
212224

225+
!> an active record with a non-positive shell is skipped by the
226+
!! reduction (with a warning) instead of aborting; the grouped model
227+
!! is the mean of the valid records and commit-time validation agrees
228+
subroutine exercise_invalid_record_skip()
229+
type(sigma2_state_header) :: header
230+
type(string) :: candidate_path, range_path
231+
real(real32), allocatable :: stored(:,:,:)
232+
real(real32) :: spectra(3,4), expected_even(3), expected_odd(3)
233+
logical :: active(4)
234+
integer :: eo(4), groups(4), status
235+
integer(int64), parameter :: DIGEST = 1231_int64
236+
character(len=128) :: message
237+
character(len=*), parameter :: COMMITTED = 'skip_sigma2_state.bin'
238+
candidate_path = sigma2_state_candidate_path(COMMITTED, 1_int64)
239+
range_path = sigma2_state_range_path(COMMITTED, 1_int64, 1, 1)
240+
call del_file(COMMITTED)
241+
call del_file(candidate_path)
242+
call del_file(range_path)
243+
spectra(:,1) = [1.0,2.0,3.0]
244+
spectra(:,2) = [2.0,3.0,4.0]
245+
spectra(:,3) = [3.0,0.0,5.0] ! invalid: a non-positive shell
246+
spectra(:,4) = [4.0,5.0,6.0]
247+
active = .true.; eo = [0,1,0,1]; groups = 1
248+
expected_even = spectra(:,1)
249+
expected_odd = 0.5*(spectra(:,2)+spectra(:,4))
250+
call sigma2_state_init_header(header, 1, 3, 4, 8, 1.5, 1, SIGMA2_GROUP_GLOBAL, &
251+
&1_int64, DIGEST, SIGMA2_PROV_PSPEC)
252+
call sigma2_state_create_candidate(candidate_path%to_char(), header, status, message)
253+
call require_ok(status, message)
254+
call sigma2_state_write_local_range(range_path%to_char(), 1_int64, DIGEST, 1, spectra, &
255+
&1, 3, status, message)
256+
call require_ok(status, message)
257+
call sigma2_state_merge_local_ranges(candidate_path%to_char(), [range_path], &
258+
&[.true.,.true.,.true.,.true.], status, message)
259+
call require_ok(status, message)
260+
call sigma2_state_reduce_groups(candidate_path%to_char(), active, eo, groups, status, message)
261+
call require(status == 0, 'invalid record is skipped, not fatal')
262+
call sigma2_state_read_groups(candidate_path%to_char(), stored, status, message)
263+
call require_ok(status, message)
264+
call require(all(abs(stored(:,1,1)-expected_even) <= 1.e-6*expected_even), &
265+
&'even group mean excludes the invalid record')
266+
call require(all(abs(stored(:,2,1)-expected_odd) <= 1.e-6*expected_odd), &
267+
&'odd group mean is the mean of the valid records')
268+
call sigma2_state_validate_science(candidate_path%to_char(), active, eo, groups, status, message)
269+
call require(status == 0, 'commit-time validation applies the same skip rule')
270+
call sigma2_state_commit(candidate_path%to_char(), COMMITTED, active, eo, groups, status, message)
271+
call require_ok(status, message)
272+
call del_file(COMMITTED)
273+
call del_file(candidate_path)
274+
call del_file(range_path)
275+
end subroutine exercise_invalid_record_skip
276+
213277
subroutine assert_committed_generation(path, expected)
214278
character(len=*), intent(in) :: path
215279
integer(int64), intent(in) :: expected

src/fileio/simple_projfile_utils.f90

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ subroutine concatenate_canonical_states
328328
layout_digest = sigma2_state_project_layout_digest(merged_proj, merged_proj%os_ptcl2D)
329329
if( layout_digest == 0_int64 ) THROW_HARD('cannot derive merged canonical sigma2 layout identity')
330330
target_path = dir//'sigma2_state.bin'
331-
candidate_path = sigma2_state_candidate_path(target_path%to_char())
331+
candidate_path = sigma2_state_candidate_path(target_path%to_char(), generation)
332332
call sigma2_state_init_header(target_header, int(target_header%kfrom), int(target_header%kto), &
333333
&nallptcls, int(target_header%box), real(target_header%smpd), ngroups, &
334334
&target_header%grouping, generation, layout_digest, SIGMA2_PROV_RESIDUAL)
@@ -697,7 +697,7 @@ subroutine merge_selected_canonical_field( target_particles )
697697
if( layout_digest == 0_int64 ) THROW_HARD('cannot derive merged canonical sigma2 layout identity')
698698
target_dir = get_fpath(projfile_out)
699699
target_path = target_dir//'sigma2_state.bin'
700-
candidate_path = sigma2_state_candidate_path(target_path%to_char())
700+
candidate_path = sigma2_state_candidate_path(target_path%to_char(), generation)
701701
call sigma2_state_init_header(target_header, int(target_header%kfrom), int(target_header%kto), &
702702
&nrows, int(target_header%box), real(target_header%smpd), ngroups, &
703703
&target_header%grouping, generation, layout_digest, SIGMA2_PROV_RESIDUAL)

src/main/commanders/simple/simple_commanders_abinitio.f90

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,16 @@ subroutine exec_abinitio3D( self, cline )
642642
call nice_comm%cycle()
643643
! read project
644644
call spproj%read(params%projfile)
645+
! A fresh abinitio3D never continues another run's sigma2 estimate: a
646+
! canonical registration inherited with the project is dropped so the
647+
! first euclid stage seeds in this run's own directory (2026-09-07)
648+
if( spproj%projinfo%get_noris() == 1 )then
649+
if( spproj%projinfo%isthere(1, 'sigma2_state') )then
650+
call spproj%projinfo%delete_entry('sigma2_state')
651+
call spproj%write_segment_inside('projinfo', params%projfile)
652+
write(logfhandle,'(A)') '>>> ABINITIO3D: dropped an inherited canonical sigma2 registration; sigmas are seeded here'
653+
endif
654+
endif
645655
! provide initialization of 3D alignment using class averages?
646656
start_stage = 1
647657
l_ini3D = .false.

0 commit comments

Comments
 (0)