stabilizer_qec 2.1.0

Quantum error-correction simulation and decoding: Stim-format circuits and error models, a bit-parallel sampler, sparse-blossom, correlated and belief matching, BP+OSD, window decoders, bivariate bicycle codes, lattice surgery, Clifford+T circuits with coherent noise, and sampling and BP on the GPU.
Documentation
//! Python signals need manual checking, otherwise we cannot Ctrl-C to stop the program.
//! It is better to allow the user to stop it early if a decoding task takes too long, say tens of seconds,
//! rather than having to kill the process in the task manager (often killing the whole terminal)
//!
//! The signal can only be checked in the main thread (see https://pyo3.rs/main/doc/pyo3/marker/struct.python#method.check_signals),
//! but we need to do computation in the main thread, so we cannot afford to get the GIL frequently to query the signal.
//! Here we use another thread to set an atomic bool every 100ms.
//! The main loop will only get GIL and query for interrupt signal when this atomic bool is asserted, and then clear it.
//! To avoid this behavior affecting some very short tasks, we set another atomic flag called `skip_next`.
//! If the ticker sees the `skip_next` flag, it will clear it and do not set the `should_check` flag.

#[allow(unused_imports)]
use crate::mwpf::shim::prelude::*;
use pyo3::prelude::*;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::{sleep, spawn, JoinHandle};
use std::time::Duration;

pub fn force_check_signals() -> PyResult<()> {
    Python::attach(|py| py.check_signals())
}

pub struct PythonSignalChecker {
    pub should_check: &'static AtomicBool,
    pub is_skipping_next: &'static AtomicBool,
    pub thread_handler: JoinHandle<()>,
}

pub static DEFAULT_SHOULD_CHECK: AtomicBool = AtomicBool::new(false);
pub static DEFAULT_IS_SKIPPING_NEXT: AtomicBool = AtomicBool::new(false);
pub static DEFAULT_SLEEP_INTERVAL_MILLIS: u64 = 100;
pub static PYTHON_SIGNAL_CHECKER: std::sync::LazyLock<PythonSignalChecker> = std::sync::LazyLock::new(|| {
    PythonSignalChecker::new(&DEFAULT_SHOULD_CHECK, &DEFAULT_IS_SKIPPING_NEXT, DEFAULT_SLEEP_INTERVAL_MILLIS)
});

impl PythonSignalChecker {
    pub fn new(
        should_check: &'static AtomicBool,
        is_skipping_next: &'static AtomicBool,
        sleep_interval_millis: u64,
    ) -> Self {
        let thread_handler = spawn(move || {
            loop {
                if is_skipping_next.swap(false, Ordering::Relaxed) {
                    continue;
                }
                should_check.store(true, Ordering::Relaxed);
                sleep(Duration::from_millis(sleep_interval_millis));
            }
        });
        Self {
            should_check,
            is_skipping_next,
            thread_handler,
        }
    }

    #[inline]
    pub fn check(&self) -> PyResult<()> {
        if self.should_check.swap(false, Ordering::Relaxed) {
            force_check_signals()?;
        }
        Ok(())
    }

    #[inline]
    pub fn skip_next(&self) {
        self.is_skipping_next.store(true, Ordering::Relaxed);
        self.should_check.store(false, Ordering::Relaxed);
    }
}