@@ -2775,6 +2775,8 @@ def _collect_task_batch(self, n: int) -> list[tuple[ExplorationTask, str, str, i
27752775 task = self .queue .pop ()
27762776 if task is None :
27772777 break # truly exhausted
2778+ max_skip = max (max_skip , len (self .queue ) + n )
2779+
27782780 gamma_sign , atom_i , atom_j = self ._parse_afir_task_key (task )
27792781 if self .explored_log .has (task .node_id , atom_i , atom_j , gamma_sign ):
27802782 skip_count += 1
@@ -2923,6 +2925,7 @@ def _run_batch_parallel(
29232925 try :
29242926 tag , payload = result_q .get_nowait ()
29252927 if tag == "err" :
2928+ logger .error ("_run_batch_parallel: AutoTS failed (crash-recovery) for run %s:\n %s" , run_dir , payload )
29262929 self ._process_single_result (task , run_dir , [], "FAILED" , iteration , history_log )
29272930 else :
29282931 self ._process_single_result (task , run_dir , payload , "DONE" , iteration , history_log )
@@ -3419,11 +3422,9 @@ def _run_autots(self, task: ExplorationTask, run_dir: str) -> list[str]:
34193422 # set in normal usage.
34203423 import queue as _queue_mod
34213424
3422- poll_interval = 60 # seconds: crash-detection granularity.
3423- # Smaller values detect crashes sooner at the cost
3424- # of slightly more CPU wakeups.
3425- elapsed = 0.0
3426-
3425+ poll_interval = 60
3426+ start_time = time .time ()
3427+
34273428 while True :
34283429 try :
34293430 tag , payload = result_q .get (timeout = poll_interval )
@@ -3448,7 +3449,7 @@ def _run_autots(self, task: ExplorationTask, run_dir: str) -> list[str]:
34483449 "exception in AutoTSWorkflow."
34493450 )
34503451
3451- elapsed += poll_interval
3452+ elapsed = time . time () - start_time
34523453
34533454 # ── Optional hard timeout (disabled by default) ───────────────
34543455 # Activated only when mapper_settings["worker_timeout_s"] is set
@@ -3713,6 +3714,7 @@ def _find_or_register_node(
37133714 "(exclude_bond_rearrangement=True)." ,
37143715 node_id ,
37153716 )
3717+ return node_id
37163718
37173719 ref_e = self .graph .reference_energy ()
37183720 if ref_e is None or new_node .energy is None :
@@ -3806,44 +3808,47 @@ def _process_profile(self, profile_dir: str, run_dir: str) -> None:
38063808 )
38073809
38083810 # ── Step 3: TS duplicate check (TS-vs-TS only) ────────────────────
3809- if ts_coords .size > 0 :
3810- # Narrow candidates to edges within the energy window.
3811- # When ts_energy is None (profile parse failed) fall back to
3812- # all_edges() so those structures still undergo geometric check.
3813- ts_candidates : list [TSEdge ] = (
3814- self .graph .edges_in_energy_window (ts_energy , self .energy_tolerance )
3815- if ts_energy is not None
3816- else self .graph .all_edges ()
3817- )
3818- for existing_edge in ts_candidates :
3819- if not existing_edge .has_coords :
3820- # Geometry is missing for this edge — geometric duplicate
3821- # detection cannot be performed. Log a warning so the
3822- # operator knows the check was skipped; the incoming TS
3823- # will be registered as a new edge rather than suppressed.
3824- logger .warning (
3825- "_process_profile: TS%06d has no cached coordinates — "
3826- "cannot compare with incoming TS (profile_dir=%s). "
3827- "Skipping duplicate check for this edge." ,
3828- existing_edge .edge_id , profile_dir ,
3829- )
3830- continue
3831-
3832- # Geometric check via RMSD.
3833- # Energy pre-filter is already enforced by the index window,
3834- # so we proceed directly to the (more expensive) RMSD test.
3835- if self .checker .are_similar (
3836- ts_sym , ts_coords ,
3837- existing_edge .symbols , existing_edge .coords ,
3838- ):
3839- logger .info (
3840- "Duplicate TS skipped (matches TS%06d, RMSD < %.3f A) "
3841- "EQ%d -- EQ%d" ,
3842- existing_edge .edge_id , self .checker .rmsd_threshold ,
3843- node_id_1 , node_id_2 ,
3844- )
3845- return
3846-
3811+ if ts_coords .size == 0 :
3812+ logger .warning ("...: TS XYZ unavailable, skipping this profile to avoid duplicate edge." )
3813+ return
3814+
3815+ # Narrow candidates to edges within the energy window.
3816+ # When ts_energy is None (profile parse failed) fall back to
3817+ # all_edges() so those structures still undergo geometric check.
3818+ ts_candidates : list [TSEdge ] = (
3819+ self .graph .edges_in_energy_window (ts_energy , self .energy_tolerance )
3820+ if ts_energy is not None
3821+ else self .graph .all_edges ()
3822+ )
3823+ for existing_edge in ts_candidates :
3824+ if not existing_edge .has_coords :
3825+ # Geometry is missing for this edge — geometric duplicate
3826+ # detection cannot be performed. Log a warning so the
3827+ # operator knows the check was skipped; the incoming TS
3828+ # will be registered as a new edge rather than suppressed.
3829+ logger .warning (
3830+ "_process_profile: TS%06d has no cached coordinates — "
3831+ "cannot compare with incoming TS (profile_dir=%s). "
3832+ "Skipping duplicate check for this edge." ,
3833+ existing_edge .edge_id , profile_dir ,
3834+ )
3835+ continue
3836+
3837+ # Geometric check via RMSD.
3838+ # Energy pre-filter is already enforced by the index window,
3839+ # so we proceed directly to the (more expensive) RMSD test.
3840+ if self .checker .are_similar (
3841+ ts_sym , ts_coords ,
3842+ existing_edge .symbols , existing_edge .coords ,
3843+ ):
3844+ logger .info (
3845+ "Duplicate TS skipped (matches TS%06d, RMSD < %.3f A) "
3846+ "EQ%d -- EQ%d" ,
3847+ existing_edge .edge_id , self .checker .rmsd_threshold ,
3848+ node_id_1 , node_id_2 ,
3849+ )
3850+ return
3851+
38473852 # ── Step 4: unique TS — persist XYZ and register edge ─────────────
38483853 edge_id = self .graph .next_edge_id ()
38493854 saved_ts_xyz = self ._persist_ts_xyz (ts_xyz_path , edge_id ) if ts_xyz_path else None
0 commit comments