From 3bb8e772b7c37f98ab1506049ff37231dd3abb4d Mon Sep 17 00:00:00 2001 From: Jens Hedegaard Nielsen Date: Thu, 4 Jul 2024 07:52:07 +0000 Subject: [PATCH 1/3] Merge pull request #6192 from trevormorgan/trmorgan/keyboard-interrupt Trmorgan/keyboard interrupt --- docs/changes/0.46.0.rst | 4 + src/qcodes/dataset/dond/do_nd_utils.py | 20 +++- .../dond/test_dond_keyboard_interrupts.py | 100 ++++++++++++++++++ 3 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 tests/dataset/dond/test_dond_keyboard_interrupts.py diff --git a/docs/changes/0.46.0.rst b/docs/changes/0.46.0.rst index f9703c8a523..d42ff915603 100644 --- a/docs/changes/0.46.0.rst +++ b/docs/changes/0.46.0.rst @@ -27,6 +27,10 @@ Breaking Changes: If the attribute is not a ParameterBase this will instead warn. It is the intention that this becomes an error in the future. (:pr:`6174`) (:pr:`6211`) +- Updated dond functions to to re-raise KeyboardInterrupt for better interrupt handling making it easier to stop long-running measurement + loops and reducing the need for kernel restarts. This meas that if you interrupt a `dond`` function with a keyboard interrupt not only + the measurement but any pending code to execute will be interrupted. In the process logging for interrupted measurements has been improved. (:pr:`6192`) + Improved: --------- diff --git a/src/qcodes/dataset/dond/do_nd_utils.py b/src/qcodes/dataset/dond/do_nd_utils.py index 7dbb862be32..62922ab7560 100644 --- a/src/qcodes/dataset/dond/do_nd_utils.py +++ b/src/qcodes/dataset/dond/do_nd_utils.py @@ -120,14 +120,26 @@ def _register_actions( @contextmanager -def catch_interrupts() -> Iterator[Callable[[], MeasInterruptT]]: - interrupt_exception = None +def catch_interrupts() -> Iterator[Callable[[], MeasInterruptT | None]]: + interrupt_exception: MeasInterruptT | None = None + interrupt_raised = False - def get_interrupt_exception() -> MeasInterruptT: + def get_interrupt_exception() -> MeasInterruptT | None: nonlocal interrupt_exception return interrupt_exception try: yield get_interrupt_exception - except (KeyboardInterrupt, BreakConditionInterrupt) as e: + except KeyboardInterrupt as e: interrupt_exception = e + interrupt_raised = True + raise # Re-raise KeyboardInterrupt + except BreakConditionInterrupt as e: + interrupt_exception = e + interrupt_raised = True + # Don't re-raise BreakConditionInterrupt + finally: + if interrupt_raised: + log.warning( + f"Measurement has been interrupted, data may be incomplete: {interrupt_exception}" + ) diff --git a/tests/dataset/dond/test_dond_keyboard_interrupts.py b/tests/dataset/dond/test_dond_keyboard_interrupts.py new file mode 100644 index 00000000000..7d73ba45d9a --- /dev/null +++ b/tests/dataset/dond/test_dond_keyboard_interrupts.py @@ -0,0 +1,100 @@ + +import pytest + +from qcodes.dataset.dond.do_nd_utils import BreakConditionInterrupt, catch_interrupts + + +def test_catch_interrupts(): + # Test normal execution (no interrupt) + with catch_interrupts() as get_interrupt: + assert get_interrupt() is None + + # Test KeyboardInterrupt + with pytest.raises(KeyboardInterrupt): + with catch_interrupts() as get_interrupt: + raise KeyboardInterrupt() + + # Test BreakConditionInterrupt + with catch_interrupts() as get_interrupt: + raise BreakConditionInterrupt() + assert isinstance(get_interrupt(), BreakConditionInterrupt) + + # Test that cleanup code runs for KeyboardInterrupt + cleanup_ran = False + with pytest.raises(KeyboardInterrupt): + with catch_interrupts(): + try: + raise KeyboardInterrupt() + finally: + cleanup_ran = True + assert cleanup_ran + + # Test that cleanup code runs for BreakConditionInterrupt + cleanup_ran = False + with catch_interrupts(): + try: + raise BreakConditionInterrupt() + finally: + cleanup_ran = True + assert cleanup_ran + + # Test that BreakConditionInterrupt is caught and doesn't raise + with catch_interrupts() as get_interrupt: + raise BreakConditionInterrupt() + assert isinstance(get_interrupt(), BreakConditionInterrupt) + + +def test_catch_interrupts_in_loops(): + # Test interruption in a simple loop + loop_count = 0 + with pytest.raises(KeyboardInterrupt): + for i in range(5): + with catch_interrupts(): + loop_count += 1 + if i == 2: + raise KeyboardInterrupt() + assert loop_count == 3 # Loop should stop at the third iteration + + # Test interruption in nested loops + outer_count = 0 + inner_count = 0 + with pytest.raises(KeyboardInterrupt): + for i in range(3): + with catch_interrupts(): + outer_count += 1 + for j in range(3): + with catch_interrupts(): + inner_count += 1 + if i == 1 and j == 1: + raise KeyboardInterrupt() + assert outer_count == 2 + assert inner_count == 5 + + +def test_catch_interrupts_simulated_sweeps(): + def simulated_sweep(interrupt_at=None): + for i in range(5): + with catch_interrupts(): + if i == interrupt_at: + raise KeyboardInterrupt() + yield i + + # Test interruption in a single sweep + results = [] + with pytest.raises(KeyboardInterrupt): + for value in simulated_sweep(interrupt_at=3): + results.append(value) + assert results == [0, 1, 2] + + # Test interruption in nested sweeps + outer_results = [] + inner_results = [] + with pytest.raises(KeyboardInterrupt): + for outer_value in simulated_sweep(interrupt_at=None): + outer_results.append(outer_value) + for inner_value in simulated_sweep( + interrupt_at=2 if outer_value == 1 else None + ): + inner_results.append(inner_value) + assert outer_results == [0, 1] + assert inner_results == [0, 1, 2, 3, 4, 0, 1] From 4327cf5fc1003d9ef93ce71c044d44f1d27f2cfc Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 4 Jul 2024 10:18:51 +0200 Subject: [PATCH 2/3] Fix typo --- docs/changes/0.46.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changes/0.46.0.rst b/docs/changes/0.46.0.rst index d42ff915603..8ed2fdf02c3 100644 --- a/docs/changes/0.46.0.rst +++ b/docs/changes/0.46.0.rst @@ -11,7 +11,7 @@ Breaking Changes: documented as TypedDics classes that can be used to type `**kwargs` in the subclass constructors. See `Creating QCoDeS instrument drivers` for usage examples. - This also means that the these arguments **must** be passed as keyword arguments, and not as positional arguments. + This also means that these arguments **must** be passed as keyword arguments, and not as positional arguments. This specifically includeds passing ``label`` and ``metadata`` to direct subclasses of ``Instrument`` as well as ``terminator`` to subclasses of ``VisaInstrument``. From 2dbbd945fa617bca76439a012fec6dec7a3afc8b Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 4 Jul 2024 10:19:30 +0200 Subject: [PATCH 3/3] Update date --- docs/changes/0.46.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changes/0.46.0.rst b/docs/changes/0.46.0.rst index 8ed2fdf02c3..0af2a4955f3 100644 --- a/docs/changes/0.46.0.rst +++ b/docs/changes/0.46.0.rst @@ -1,4 +1,4 @@ -QCoDeS 0.46.0 (2024-06-27) +QCoDeS 0.46.0 (2024-07-04) ========================== Breaking Changes: