Skip to content

Commit 20f1d16

Browse files
committed
Add killed state handling to SweepQueue and internal sweep flag
Introduces a '_last_killed' flag to SweepQueue to track when the queue is stopped via kill() or kill_all(), and updates status reporting to include a 'killed' effective state. Internal ramp and inner sweeps are now marked with a '_internal_sweep' attribute to prevent sweep collision checks. Unit tests are added to verify the new killed state behavior.
1 parent 0784297 commit 20f1d16

File tree

7 files changed

+74
-13
lines changed

7 files changed

+74
-13
lines changed

src/measureit/sweep/base_sweep.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,11 @@ def start(self, persist_data=None, ramp_to_start=False):
566566
Optional argument which gradually ramps each parameter to the starting
567567
point of its sweep. Default is true for Sweep1D and Sweep2D.
568568
"""
569-
if not self.progressState.is_queued and _has_other_active_sweep(self):
569+
if (
570+
not self.progressState.is_queued
571+
and not getattr(self, "_internal_sweep", False)
572+
and _has_other_active_sweep(self)
573+
):
570574
raise RuntimeError(
571575
"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."
572576
)
@@ -645,7 +649,11 @@ def start(self, persist_data=None, ramp_to_start=False):
645649
def resume(self):
646650
"""Restarts the sweep after it has been paused."""
647651
if self.progressState.state == SweepState.PAUSED:
648-
if not self.progressState.is_queued and _has_other_active_sweep(self):
652+
if (
653+
not self.progressState.is_queued
654+
and not getattr(self, "_internal_sweep", False)
655+
and _has_other_active_sweep(self)
656+
):
649657
raise RuntimeError(
650658
"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."
651659
)

src/measureit/sweep/simul_sweep.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,7 @@ def ramp_to(self, vals_dict, start_on_finish=False, persist=None, multiplier=1):
406406
suppress_output=self.suppress_output,
407407
)
408408
self.ramp_sweep.parent = self
409+
self.ramp_sweep._internal_sweep = True
409410
# Only follow parameters that are not being ramped to avoid circular dependencies
410411
follow_params = [p for p in self._params if p not in ramp_params_dict.keys()]
411412
if follow_params:

src/measureit/sweep/sweep1d.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,7 @@ def ramp_to(self, value, start_on_finish=False, persist=None, multiplier=1):
507507
plot_data=self.plot_data,
508508
)
509509
self.ramp_sweep.parent = self
510+
self.ramp_sweep._internal_sweep = True
510511
self.ramp_sweep.follow_param(self._params)
511512

512513
self.progressState.state = SweepState.RAMPING

src/measureit/sweep/sweep1d_listening.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,7 @@ def ramp_to(self, value, start_on_finish=False, persist=None, multiplier=1):
402402
plot_data=self.plot_data,
403403
)
404404
self.ramp_sweep.parent = self
405+
self.ramp_sweep._internal_sweep = True
405406

406407
self.progressState.state = SweepState.RAMPING
407408
self.ramp_sweep.start(ramp_to_start=False)

src/measureit/sweep/sweep2d.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ def __init__(
216216
)
217217
# Set parent reference so UI actions (e.g., ESC) can stop both inner and outer sweeps
218218
self.in_sweep.parent = self
219+
self.in_sweep._internal_sweep = True
219220
# Ensure the inner sweep writes metadata for the composite (outer) sweep
220221
self.in_sweep.metadata_provider = self
221222
# We set our outer sweep parameter as a follow param for the inner sweep, so that
@@ -674,6 +675,7 @@ def ramp_to(self, value, start_on_finish=False, multiplier=1):
674675
plot_data=True,
675676
)
676677
self.ramp_sweep.parent = self
678+
self.ramp_sweep._internal_sweep = True
677679
for p in self._params:
678680
if p is not self.set_param:
679681
self.ramp_sweep.follow_param(p)
@@ -707,6 +709,7 @@ def ramp_to_zero(self):
707709
complete_func=self.done_ramping,
708710
)
709711
self.ramp_sweep.parent = self
712+
self.ramp_sweep._internal_sweep = True
710713
self.ramp_sweep.follow_param(self._params)
711714
self.progressState.state = SweepState.RAMPING
712715
self.outer_ramp = True

src/measureit/tools/sweep_queue.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@ def __init__(self, inter_delay=1, post_db_delay=1.0, debug=False):
199199
self.previous_action = None
200200
# Queue-level error state (persists after sweep cleanup)
201201
self._last_error: Optional[QueueError] = None
202+
# Queue-level killed flag (persists after sweep cleanup)
203+
self._last_killed = False
202204

203205
def _exec_in_kernel(self, fn):
204206
"""Execute a callable synchronously.
@@ -511,6 +513,8 @@ def start(self, rts=True):
511513

512514
self.log.info("Starting sweeps")
513515
self.current_action = self.queue.popleft()
516+
# Clear kill flag once new work begins
517+
self._last_killed = False
514518
if isinstance(self.current_action, BaseSweep):
515519
self.current_sweep = self.current_action
516520
# Ensure metadata shows this sweep was launched by SweepQueue
@@ -592,6 +596,7 @@ def resume(self):
592596
def kill(self):
593597
"""Kills the current sweep. Use kill_all() to also clear the queue."""
594598
if self.current_sweep is not None:
599+
self._last_killed = True
595600
# Clear current_sweep AND current_action before kill() to prevent
596601
# begin_next() from processing if kill() emits completion synchronously
597602
sweep_to_kill = self.current_sweep
@@ -609,6 +614,11 @@ def kill_all(self):
609614
Note: If a DatabaseEntry or callable is currently executing, it cannot be
610615
interrupted, but the queue will not continue after it completes.
611616
"""
617+
had_work = (
618+
self.current_sweep is not None
619+
or self.current_action is not None
620+
or len(self.queue) > 0
621+
)
612622
# Save reference to sweep before clearing state
613623
sweep_to_kill = self.current_sweep
614624

@@ -625,6 +635,8 @@ def kill_all(self):
625635

626636
# Clear error state for full reset
627637
self._last_error = None
638+
# Mark queue as killed if there was work to stop
639+
self._last_killed = had_work
628640

629641
# Now safe to kill - even if completion fires, queue is empty
630642
if sweep_to_kill is not None:
@@ -650,7 +662,7 @@ def status(self):
650662
651663
Returns a dictionary with:
652664
- effective_state: Overall queue state accounting for pending items
653-
("idle", "pending", "running", "paused", "error", "stopped")
665+
("idle", "pending", "running", "paused", "killed", "error", "stopped")
654666
- current_sweep_state: State name of the currently executing sweep (or None)
655667
- queue_length: Number of items waiting in the queue
656668
- current_sweep_type: Class name of current sweep (or None)
@@ -667,6 +679,7 @@ def status(self):
667679
- "running": Current sweep is actively running/ramping, or a DatabaseEntry/callable
668680
is executing
669681
- "paused": Current sweep is paused
682+
- "killed": Queue stopped due to a kill() or kill_all() call. Call start() to resume.
670683
- "error": Current sweep is in error state (actively erroring)
671684
- "stopped": Queue stopped due to a previous error (check last_error for details).
672685
Call clear_error() and start() to resume.
@@ -704,12 +717,16 @@ def status(self):
704717
effective_state = "running"
705718
elif self._last_error is not None:
706719
effective_state = "stopped"
720+
elif self._last_killed:
721+
effective_state = "killed"
707722
elif queue_length > 0:
708723
effective_state = "pending"
709724
else:
710725
effective_state = "idle"
711726
elif current_sweep_state == SweepState.ERROR:
712727
effective_state = "error"
728+
elif current_sweep_state == SweepState.KILLED:
729+
effective_state = "killed"
713730
elif current_sweep_state == SweepState.PAUSED:
714731
effective_state = "paused"
715732
elif current_sweep_state in (SweepState.RUNNING, SweepState.RAMPING):
@@ -724,7 +741,7 @@ def status(self):
724741
else:
725742
effective_state = "idle"
726743
else:
727-
# KILLED or unexpected states with a current_sweep still set
744+
# Unexpected states with a current_sweep still set
728745
if queue_length > 0:
729746
effective_state = "pending"
730747
else:
@@ -799,11 +816,12 @@ def begin_next(self):
799816
self._processing = True
800817
self._pending_begin_next = False
801818

802-
# Guard: if queue is in error state, don't process further
803-
# This prevents re-entrancy from draining the queue after an error
804-
if self._last_error is not None:
819+
# Guard: if queue is in error or killed state, don't process further
820+
# This prevents re-entrancy from draining the queue after a stop
821+
if self._last_error is not None or self._last_killed:
805822
if self.debug:
806-
self.log.debug("Queue in error state, not processing further")
823+
state = "error" if self._last_error is not None else "killed"
824+
self.log.debug("Queue in %s state, not processing further", state)
807825
self._processing = False
808826
return
809827

tests/unit/test_sweep_queue.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
os.environ.setdefault("MEASUREIT_FAKE_QT", "1")
1818

1919
from measureit.tools import sweep_queue as sq
20+
from measureit.sweep.progress import SweepState
2021

2122

2223
class DummySignal:
@@ -54,7 +55,7 @@ def __init__(
5455
self.killed: int = 0
5556
self.resumed: int = 0
5657
self.metadata_provider = None
57-
self.progressState = type("State", (), {"state": "READY"})()
58+
self.progressState = type("State", (), {"state": SweepState.READY})()
5859
self.completed = DummySignal()
5960
self._complete_func: Optional[Callable[[], None]] = None
6061
self.begin = start_value
@@ -81,15 +82,15 @@ def set_complete_func(self, func: Callable[..., None], *args: Any, **kwargs: Any
8182

8283
def start(self, persist_data: Any = None, ramp_to_start: bool = False) -> None:
8384
self.started = True
84-
self.progressState.state = "RUNNING"
85+
self.progressState.state = SweepState.RUNNING
8586

8687
def kill(self) -> None:
8788
self.killed += 1
88-
self.progressState.state = "KILLED"
89+
self.progressState.state = SweepState.KILLED
8990

9091
def resume(self) -> None:
9192
self.resumed += 1
92-
self.progressState.state = "RUNNING"
93+
self.progressState.state = SweepState.RUNNING
9394

9495
def export_json(self, fn: Optional[str] = None) -> dict[str, Any]:
9596
return dict(self.export_payload)
@@ -98,7 +99,7 @@ def export_json(self, fn: Optional[str] = None) -> dict[str, Any]:
9899

99100
def trigger_complete(self) -> None:
100101
"""Simulate the sweep finishing and emitting its completed signal."""
101-
self.progressState.state = "DONE"
102+
self.progressState.state = SweepState.DONE
102103
self.completed.emit()
103104

104105
def __repr__(self) -> str: # pragma: no cover - debugging helper
@@ -235,3 +236,31 @@ def test_export_json_includes_queue_configuration(queue: sq.SweepQueue) -> None:
235236
assert data["inter_delay"] == queue.inter_delay
236237
assert isinstance(data["queue"], list)
237238
assert data["queue"][0]["attributes"]["name"] == "json-test"
239+
240+
241+
def test_status_reports_killed_after_kill(queue: sq.SweepQueue) -> None:
242+
first = DummySweep1D("first")
243+
second = DummySweep("second")
244+
245+
queue.append(first, second)
246+
queue.start()
247+
248+
queue.kill()
249+
250+
status = queue.status()
251+
assert status["effective_state"] == "killed"
252+
assert status["queue_length"] == 1
253+
254+
255+
def test_start_clears_killed_state(queue: sq.SweepQueue) -> None:
256+
first = DummySweep1D("first")
257+
second = DummySweep("second")
258+
259+
queue.append(first, second)
260+
queue.start()
261+
queue.kill()
262+
263+
queue.start()
264+
265+
status = queue.status()
266+
assert status["effective_state"] == "running"

0 commit comments

Comments
 (0)