4545)
4646from murfey .util .processing_params import default_spa_parameters , motion_corrected_mrc
4747from murfey .util .tomo import midpoint
48+ from gemmi import cif
49+ from pipeliner .star_keys import GENERAL_BLOCK , JOB_COUNTER
4850
4951logger = logging .getLogger ("murfey.server.feedback" )
5052
5153
54+ def _current_pipeline_job_counter (visit_name : str ) -> int :
55+ """Return the next jobNNN Pipeliner will allocate for visit_name.
56+
57+ Reads the JOB_COUNTER value from default_pipeline.star so that
58+ SPA feedback decisions are anchored to Pipeliner's actual state instead
59+ of an independent integer counter that drifts.
60+
61+ Falls back to 7 (previous default) if the file is
62+ missing — this preserves the previous behaviour for non Doppio runs
63+ """
64+ pipeline_file = Path (visit_name ) / "default_pipeline.star"
65+ if not pipeline_file .is_file ():
66+ return 7
67+ try :
68+ dp = cif .read_file (str (pipeline_file ))
69+ block = dp .find_block (GENERAL_BLOCK )
70+ if block is None :
71+ return 7
72+ return int (block .find_value (JOB_COUNTER ))
73+ except Exception :
74+ logger .warning (
75+ "Failed to read JOB_COUNTER from %s — falling back to legacy job number" ,
76+ pipeline_file ,
77+ exc_info = True ,
78+ )
79+ return 7
80+
81+
82+ def _visit_name_for_session (session_id : int , _db ) -> str :
83+ """Return the visit (project directory) for a Murfey session id."""
84+ session_row = _db .exec (select (db .Session ).where (db .Session .id == session_id )).one ()
85+ return session_row .visit
86+
87+
5288try :
5389 _url = url (get_security_config ())
5490 engine = create_engine (_url )
@@ -373,9 +409,8 @@ def _release_2d_hold(message: dict, _db):
373409 "recipes" : ["em-spa-class2d" ],
374410 }
375411 if first_class2d .complete :
376- feedback_params .next_job += (
377- 4 if default_spa_parameters .do_icebreaker_jobs else 3
378- )
412+ visit_name = _visit_name_for_session (message ["session_id" ], _db )
413+ feedback_params .next_job = _current_pipeline_job_counter (visit_name )
379414 feedback_params .rerun_class2d = False
380415 _db .add (feedback_params )
381416 if first_class2d .complete :
@@ -585,7 +620,9 @@ def _register_incomplete_2d_batch(message: dict, _db):
585620 _db .commit ()
586621 _db .close ()
587622 return
588- feedback_params .next_job = 10 if default_spa_parameters .do_icebreaker_jobs else 7
623+ # Get next_job from the actual Pipeliner counter
624+ visit_name = _visit_name_for_session (message ["session_id" ], _db )
625+ feedback_params .next_job = _current_pipeline_job_counter (visit_name )
589626 feedback_params .hold_class2d = True
590627 relion_options = dict (relion_params )
591628 other_options = dict (feedback_params )
@@ -735,15 +772,8 @@ def _register_complete_2d_batch(message: dict, _db):
735772 murfey_ids , class2d_message ["particles_file" ], _app_id (pj_id , _db ), _db
736773 )
737774 elif not feedback_params .class_selection_score :
738- # For the first batch, start a container and set the database to wait
739- job_number_after_first_batch = (
740- 10 if default_spa_parameters .do_icebreaker_jobs else 7
741- )
742- if (
743- feedback_params .next_job is not None
744- and feedback_params .next_job < job_number_after_first_batch
745- ):
746- feedback_params .next_job = job_number_after_first_batch
775+ visit_name = _visit_name_for_session (message ["session_id" ], _db )
776+ feedback_params .next_job = _current_pipeline_job_counter (visit_name )
747777 if not feedback_params .star_combination_job :
748778 feedback_params .star_combination_job = feedback_params .next_job + (
749779 3 if default_spa_parameters .do_icebreaker_jobs else 2
@@ -815,14 +845,14 @@ def _register_complete_2d_batch(message: dict, _db):
815845 "processing_recipe" , zocalo_message , new_connection = True
816846 )
817847 feedback_params .hold_class2d = True
818- feedback_params .next_job += (
819- 4 if default_spa_parameters .do_icebreaker_jobs else 3
820- )
848+ # next_job is re-anchored from Pipeliner on the next entry to this
849+ # function — no manual increment needed.
821850 _db .add (feedback_params )
822851 _db .commit ()
823852 _db .close ()
824853 else :
825- # Send all other messages on to a container
854+ visit_name = _visit_name_for_session (message ["session_id" ], _db )
855+ feedback_params .next_job = _current_pipeline_job_counter (visit_name )
826856 if _db .exec (
827857 select (func .count (db .Class2DParameters .particles_file ))
828858 .where (db .Class2DParameters .pj_id == pj_id )
@@ -889,9 +919,6 @@ def _register_complete_2d_batch(message: dict, _db):
889919 murfey .server ._transport_object .send (
890920 "processing_recipe" , zocalo_message , new_connection = True
891921 )
892- feedback_params .next_job += (
893- 3 if default_spa_parameters .do_icebreaker_jobs else 2
894- )
895922 _db .add (feedback_params )
896923 _db .commit ()
897924 _db .close ()
@@ -936,10 +963,9 @@ def _flush_class2d(
936963 .where (db .Class2DParameters .pj_id == pj_id )
937964 .where (db .Class2DParameters .complete )
938965 ).all ()
939- if not feedback_params .next_job :
940- feedback_params .next_job = (
941- 10 if default_spa_parameters .do_icebreaker_jobs else 7
942- )
966+ # Check pipeliner counter
967+ visit_name = _visit_name_for_session (session_id , _db )
968+ feedback_params .next_job = _current_pipeline_job_counter (visit_name )
943969 if not feedback_params .star_combination_job :
944970 feedback_params .star_combination_job = feedback_params .next_job + (
945971 3 if default_spa_parameters .do_icebreaker_jobs else 2
@@ -1196,6 +1222,10 @@ def _register_3d_batch(message: dict, _db):
11961222 .visit
11971223 )
11981224
1225+ # Check Pipeliner's job counter
1226+ feedback_params .next_job = _current_pipeline_job_counter (visit_name )
1227+ other_options ["next_job" ] = feedback_params .next_job
1228+
11991229 provided_initial_model = _find_initial_model (visit_name , machine_config )
12001230 if provided_initial_model and not feedback_params .initial_model :
12011231 rescaled_initial_model_path = (
@@ -1218,7 +1248,6 @@ def _register_3d_batch(message: dict, _db):
12181248 class3d_dir = (
12191249 f"{ class3d_message ['class3d_dir' ]} { (feedback_params .next_job + 1 ):03} "
12201250 )
1221- feedback_params .next_job += 1
12221251 _db .add (feedback_params )
12231252 _db .commit ()
12241253
@@ -1253,7 +1282,6 @@ def _register_3d_batch(message: dict, _db):
12531282 _db .close ()
12541283 elif not feedback_params .initial_model :
12551284 # For the first batch, start a container and set the database to wait
1256- next_job = feedback_params .next_job
12571285 class3d_dir = (
12581286 f"{ class3d_message ['class3d_dir' ]} { (feedback_params .next_job + 1 ):03} "
12591287 )
@@ -1273,8 +1301,6 @@ def _register_3d_batch(message: dict, _db):
12731301 )
12741302
12751303 feedback_params .hold_class3d = True
1276- next_job += 2
1277- feedback_params .next_job = next_job
12781304 zocalo_message : dict = {
12791305 "parameters" : {
12801306 "particles_file" : class3d_message ["particles_file" ],
@@ -1534,6 +1560,11 @@ def _register_refinement(message: dict, _db):
15341560 db .ClassificationFeedbackParameters .pj_id == pj_id_params
15351561 )
15361562 ).one ()
1563+
1564+ # Re-anchor next_job to Pipeliner's actual counter so the predicted
1565+ # Refine3D / MaskCreate / PostProcess slots line up with reality.
1566+ visit_name = _visit_name_for_session (message ["session_id" ], _db )
1567+ feedback_params .next_job = _current_pipeline_job_counter (visit_name )
15371568 other_options = dict (feedback_params )
15381569
15391570 if feedback_params .hold_refine :
@@ -1564,7 +1595,6 @@ def _register_refinement(message: dict, _db):
15641595 .where (db .RefineParameters .tag == "symmetry" )
15651596 ).one ()
15661597 except SQLAlchemyError :
1567- next_job = feedback_params .next_job
15681598 refine_dir = f"{ message ['refine_dir' ]} { (feedback_params .next_job + 2 ):03} "
15691599 refined_grp_uuid = _murfey_id (message ["program_id" ], _db )[0 ]
15701600 refined_class_uuid = _murfey_id (message ["program_id" ], _db )[0 ]
@@ -1605,14 +1635,6 @@ def _register_refinement(message: dict, _db):
16051635 _db = _db ,
16061636 )
16071637
1608- if relion_options ["symmetry" ] == "C1" :
1609- # Extra Refine, Mask, PostProcess beyond for determined symmetry
1610- next_job += 8
1611- else :
1612- # Select and Extract particles, then Refine, Mask, PostProcess
1613- next_job += 5
1614- feedback_params .next_job = next_job
1615-
16161638 zocalo_message : dict = {
16171639 "parameters" : {
16181640 "refine_job_dir" : refine_params .refine_dir ,
0 commit comments