55import time
66import threading
77import warnings
8+ import weakref
89from decimal import ROUND_HALF_EVEN , Decimal , localcontext
910from functools import partial
1011from typing import Optional , Tuple
2021from ..tools .util import _autorange_srs , is_numeric_parameter , safe_get , safe_set
2122from .progress import ProgressState , SweepState
2223
24+ _ACTIVE_SWEEPS = weakref .WeakSet ()
25+ _ACTIVE_SWEEPS_LOCK = threading .Lock ()
26+
27+
28+ def _register_active_sweep (sweep : "BaseSweep" ) -> None :
29+ with _ACTIVE_SWEEPS_LOCK :
30+ _ACTIVE_SWEEPS .add (sweep )
31+
32+
33+ def _deregister_active_sweep (sweep : "BaseSweep" ) -> None :
34+ with _ACTIVE_SWEEPS_LOCK :
35+ _ACTIVE_SWEEPS .discard (sweep )
36+
37+
38+ def _iter_parent_chain (sweep : "BaseSweep" ):
39+ seen = set ()
40+ current = getattr (sweep , "parent" , None )
41+ while current is not None and current not in seen :
42+ yield current
43+ seen .add (current )
44+ current = getattr (current , "parent" , None )
45+
46+
47+ def _is_related_sweep (a : "BaseSweep" , b : "BaseSweep" ) -> bool :
48+ if a is b :
49+ return True
50+ for parent in _iter_parent_chain (a ):
51+ if parent is b :
52+ return True
53+ for parent in _iter_parent_chain (b ):
54+ if parent is a :
55+ return True
56+ return False
57+
58+
59+ def _has_other_active_sweep (sweep : "BaseSweep" ) -> bool :
60+ with _ACTIVE_SWEEPS_LOCK :
61+ active = list (_ACTIVE_SWEEPS )
62+ for other in active :
63+ if other is sweep :
64+ continue
65+ state = getattr (getattr (other , "progressState" , None ), "state" , None )
66+ if state in (SweepState .RUNNING , SweepState .RAMPING ):
67+ if _is_related_sweep (sweep , other ):
68+ continue
69+ return True
70+ return False
71+
72+
73+ def _kill_other_active_sweeps (sweep : "BaseSweep" ) -> int :
74+ """Kill all unrelated active sweeps and return how many were killed."""
75+ with _ACTIVE_SWEEPS_LOCK :
76+ active = list (_ACTIVE_SWEEPS )
77+ killed = 0
78+ for other in active :
79+ if other is sweep :
80+ continue
81+ if _is_related_sweep (sweep , other ):
82+ continue
83+ try :
84+ other .kill ()
85+ killed += 1
86+ except Exception :
87+ pass
88+ return killed
89+
2390
2491class BaseSweep (QObject ):
2592 """The parent class for the 0D, 1D and 2D sweep classes.
@@ -376,6 +443,7 @@ def _enter_running_state(self, *, reset_elapsed: bool) -> float:
376443 self ._mark_done_deferred = False
377444 self ._run_started_at = now
378445 self .progressState .state = SweepState .RUNNING
446+ _register_active_sweep (self )
379447 return now
380448
381449 def pause (self ):
@@ -388,6 +456,15 @@ def pause(self):
388456 self ._add_runtime_since_last_resume ()
389457 self .progressState .state = SweepState .PAUSED
390458 self .send_updates ()
459+ # If this is an inner/child sweep, pause the parent as well to keep states consistent
460+ parent = getattr (self , "parent" , None )
461+ if parent is not None and parent is not self :
462+ try :
463+ parent_state = getattr (getattr (parent , "progressState" , None ), "state" , None )
464+ if parent_state in (SweepState .RUNNING , SweepState .RAMPING ):
465+ parent .pause ()
466+ except Exception :
467+ pass
391468
392469 def stop (self ):
393470 """Stop/pause the sweep. Alias for pause() for backward compatibility.
@@ -411,6 +488,7 @@ def kill(self):
411488 # ERROR state transitions to KILLED since user explicitly called kill()
412489 if progress_state is not None and progress_state .state not in (SweepState .DONE , SweepState .KILLED ):
413490 self .progressState .state = SweepState .KILLED
491+ _deregister_active_sweep (self )
414492 if hasattr (self , "_error_completion_pending" ):
415493 self ._error_completion_pending = False # Clear to prevent stale flag
416494
@@ -454,6 +532,29 @@ def check_running(self):
454532 """Returns the status of the sweep."""
455533 return self .progressState .state in (SweepState .RUNNING , SweepState .RAMPING )
456534
535+ def start_force (self , * args , ** kwargs ):
536+ """Kill other unrelated active sweeps, then start this sweep."""
537+ if not self .progressState .is_queued :
538+ _kill_other_active_sweeps (self )
539+ return self .start (* args , ** kwargs )
540+
541+ @staticmethod
542+ def list_active_sweeps ():
543+ """Return a snapshot of active sweeps for debugging."""
544+ with _ACTIVE_SWEEPS_LOCK :
545+ active = list (_ACTIVE_SWEEPS )
546+ info = []
547+ for sweep in active :
548+ state = getattr (getattr (sweep , "progressState" , None ), "state" , None )
549+ info .append (
550+ {
551+ "type" : sweep .__class__ .__name__ ,
552+ "state" : state .name if state is not None else None ,
553+ "sweep" : sweep ,
554+ }
555+ )
556+ return info
557+
457558 def start (self , persist_data = None , ramp_to_start = False ):
458559 """Starts the sweep by creating and running the worker threads.
459560
@@ -465,6 +566,10 @@ def start(self, persist_data=None, ramp_to_start=False):
465566 Optional argument which gradually ramps each parameter to the starting
466567 point of its sweep. Default is true for Sweep1D and Sweep2D.
467568 """
569+ if not self .progressState .is_queued and _has_other_active_sweep (self ):
570+ raise RuntimeError (
571+ "Another sweep is already running. Stop or kill it before starting a new sweep, use start_force() to kill others, or use SweepQueue to stack sweeps."
572+ )
468573 if self .progressState .state in (SweepState .RUNNING , SweepState .RAMPING ):
469574 self .print_main .emit ("We are already running, can't start while running." )
470575 return
@@ -540,6 +645,10 @@ def start(self, persist_data=None, ramp_to_start=False):
540645 def resume (self ):
541646 """Restarts the sweep after it has been paused."""
542647 if self .progressState .state == SweepState .PAUSED :
648+ if not self .progressState .is_queued and _has_other_active_sweep (self ):
649+ raise RuntimeError (
650+ "Another sweep is already running. Stop or kill it before resuming this sweep, use start_force() to kill others, or use SweepQueue to stack sweeps."
651+ )
543652 self ._enter_running_state (reset_elapsed = False )
544653 self .send_updates (no_sp = True )
545654 else :
@@ -614,6 +723,7 @@ def mark_done(self) -> None:
614723 if self .progressState .state == SweepState .RUNNING :
615724 self ._add_runtime_since_last_resume ()
616725 self .progressState .state = SweepState .DONE
726+ _deregister_active_sweep (self )
617727 self .send_updates ()
618728 self .completed .emit ()
619729
@@ -639,6 +749,7 @@ def mark_error(self, error_message: str, _from_runner: bool = False) -> None:
639749 self ._add_runtime_since_last_resume ()
640750 self .progressState .state = SweepState .ERROR
641751 self .progressState .error_message = error_message
752+ _deregister_active_sweep (self )
642753
643754 # Propagate error to parent sweep (e.g., Sweep2D when inner Sweep1D fails)
644755 parent = getattr (self , "parent" , None )
0 commit comments