88
99from __future__ import annotations
1010
11- import bisect
1211import glob
1312import heapq
1413import json
@@ -642,15 +641,19 @@ def pop(self) -> ExplorationTask | None:
642641
643642 Also removes the task from ``_tasks`` so subclasses that iterate
644643 ``_tasks`` see a consistent state.
644+
645+ Stale heap entries (tasks that exist in the heap but have already
646+ been removed from ``_tasks`` by a prior rebuild) are silently
647+ skipped so that only valid tasks are ever returned to the caller.
645648 """
646- if not self ._heap :
647- return None
648- _ , _ , task = heapq . heappop ( self . _heap )
649- try :
650- self . _tasks . remove ( task ) # O(n) but only called in base-class path
651- except ValueError :
652- pass
653- return task
649+ while self ._heap :
650+ _ , _ , task = heapq . heappop ( self . _heap )
651+ try :
652+ self . _tasks . remove ( task )
653+ return task
654+ except ValueError :
655+ continue # stale entry — skip and try the next one
656+ return None
654657
655658 def should_add (self , node : "EQNode" , reference_energy : float , ** kwargs ) -> bool :
656659 """Decide probabilistically whether to enqueue a node.
@@ -723,9 +726,13 @@ def refresh_priorities(self, ref_e: float | None) -> None:
723726 task .priority = self .compute_priority (task )
724727
725728 # Rebuild the heap from the updated _tasks list.
726- self ._heap = [(- t .priority , i , t ) for i , t in enumerate (self ._tasks )]
729+ # Use _push_counter-based indices to guarantee unique tie-breakers,
730+ # then advance _push_counter so that subsequent push() calls never
731+ # reuse a counter value (monotonic increase requirement).
732+ base = self ._push_counter
733+ self ._heap = [(- t .priority , base + i , t ) for i , t in enumerate (self ._tasks )]
727734 heapq .heapify (self ._heap )
728- self ._push_counter = len (self ._heap )
735+ self ._push_counter = base + len (self ._tasks )
729736
730737 def export_queue_status (self ) -> list [dict ]:
731738 return [
@@ -1551,8 +1558,6 @@ def _validate_mapper_settings(cls, config: dict) -> None:
15511558 ("max_pairs" , int , lambda v : v > 0 , "must be a positive integer" ),
15521559 ("dist_lower_ang" , (int , float ), lambda v : v >= 0 , "must be >= 0" ),
15531560 ("dist_upper_ang" , (int , float ), lambda v : v > 0 , "must be > 0" ),
1554- ("rcmc_temperature_K" , (int , float ), lambda v : v > 0 , "must be > 0" ),
1555- ("rcmc_reaction_time_s" , (int , float ), lambda v : v > 0 , "must be > 0" ),
15561561 ("rng_seed" , int , lambda v : v >= 0 , "must be a non-negative integer" ),
15571562 ]
15581563
@@ -1581,18 +1586,18 @@ def _validate_mapper_settings(cls, config: dict) -> None:
15811586 f"string (got { type (ms ['output_dir' ]).__name__ } )."
15821587 )
15831588
1584- # ── exclude_nodes : optional, must be list[int] when present ──────
1585- if "exclude_nodes " in ms :
1586- en = ms ["exclude_nodes " ]
1589+ # ── excluded_node_ids : optional, must be list[int] when present ──────
1590+ if "excluded_node_ids " in ms :
1591+ en = ms ["excluded_node_ids " ]
15871592 if not isinstance (en , list ):
15881593 raise ValueError (
1589- f"Config validation failed: 'mapper_settings.exclude_nodes ' must be "
1594+ f"Config validation failed: 'mapper_settings.excluded_node_ids ' must be "
15901595 f"a list of integers (got { type (en ).__name__ } )."
15911596 )
15921597 for idx , item in enumerate (en ):
15931598 if not isinstance (item , int ):
15941599 raise ValueError (
1595- f"Config validation failed: 'mapper_settings.exclude_nodes [{ idx } ]' "
1600+ f"Config validation failed: 'mapper_settings.excluded_node_ids [{ idx } ]' "
15961601 f"must be an integer (got { type (item ).__name__ } : { item !r} )."
15971602 )
15981603
@@ -2328,13 +2333,17 @@ def _process_profile(self, profile_dir: str, run_dir: str) -> None:
23282333 if not existing_edge .has_coords :
23292334 continue
23302335
2331- # Energy pre-filter
2332- energy_diff = abs (ts_energy - (existing_edge .ts_energy or 0.0 ))
2333- if energy_diff >= self .energy_tolerance :
2334- continue
2336+ # Energy pre-filter: skip RMSD computation when energies
2337+ # differ by more than the tolerance (fast path).
2338+ # When either energy is None the filter is skipped so that
2339+ # structures without parsed energies still undergo the
2340+ # geometric check.
2341+ if ts_energy is not None and existing_edge .ts_energy is not None :
2342+ energy_diff = abs (ts_energy - existing_edge .ts_energy )
2343+ if energy_diff >= self .energy_tolerance :
2344+ continue
23352345
2336-
2337- # Geometric check via RMSD (evaluated only if topological check fails)
2346+ # Geometric check via RMSD
23382347 if self .checker .are_similar (
23392348 ts_sym , ts_coords ,
23402349 existing_edge .symbols , existing_edge .coords ,
@@ -2346,17 +2355,6 @@ def _process_profile(self, profile_dir: str, run_dir: str) -> None:
23462355 node_id_1 , node_id_2 ,
23472356 )
23482357 return
2349-
2350- # Topological check (order-independent node matching)
2351- nodes_match = {node_id_1 , node_id_2 } == {existing_edge .node_id_1 , existing_edge .node_id_2 }
2352- if nodes_match :
2353- logger .info (
2354- "Duplicate TS skipped (matches topological connection of TS%06d, "
2355- "energy diff=%.6f Ha) EQ%d -- EQ%d" ,
2356- existing_edge .edge_id , energy_diff , node_id_1 , node_id_2
2357- )
2358- return # Not registered anywhere
2359-
23602358
23612359 # ── Step 4: unique TS — persist XYZ and register edge ─────────────
23622360 edge_id = self .graph .next_edge_id ()
0 commit comments