Feedback control concepts
==========================

Parallel noise sweeps (v0.7.1)
------------------------------

The library function ``qubitos.feedback.analysis.noise_sweep_comparison``
evaluates each pair of sweep method and scaled noise level in an independent
``cell``. From v0.7.1 onward, cells run in parallel by default using a
process pool (``concurrent.futures.ProcessPoolExecutor`` with the
``spawn`` start method). Pass ``max_workers=1`` to force a traditional
single-process loop.

Reproducibility is defined by a base integer ``seed`` and a deterministic
per-cell seed: NumPy's ``SeedSequence(seed).spawn(n_methods * n_noise)``
produces one child sequence per cell; the flat index is
``k = method_idx * n_noise + noise_idx``, and the SME runtime receives the
32-bit integer collapsed from that child. The same cell therefore sees the
same random streams whether you use one worker or many, or whether you
resume from per-cell checkpoint files.

Example::

    from pathlib import Path
    from qubitos.feedback.analysis import noise_sweep_comparison

    result = noise_sweep_comparison(
        noise_range=[0.5, 1.0, 2.0],
        methods=("gaussian", "lyapunov_feedback"),
        num_trajectories=64,
        seed=12345,
        max_workers=8,
        checkpoint_dir=Path("./sweep_ckpt"),
    )

With ``checkpoint_dir`` set, each completed cell writes a compressed
``.npz`` file; a later call with the same arguments skips cells that
already have files and prints how many checkpoints were found.

Structured logging
------------------

For long sweeps, silent failure is the worst kind of failure. The
``log_path`` keyword takes a path to a JSONL file that the sweep appends
one record to per ``resume``, ``start``, ``done``, and ``error`` event.
Each record carries a UTC timestamp (``ts``), the cell coordinates
(``noise_idx``, ``method_idx``, ``method``, ``noise_level``), the wall
time spent on the cell (``wall_s``), and for completions the cell's
``mean_fidelity``. Failures additionally record an ``error`` string with
the exception type and message. The same events are emitted at ``INFO``
to the ``qubitos.feedback.analysis.sweep`` logger, which is silent by
default (no handlers attached); callers that prefer the stdlib
``logging`` route can attach their own handler instead of writing to
disk::

    import logging
    logging.getLogger("qubitos.feedback.analysis.sweep").addHandler(
        logging.StreamHandler()
    )

Disk-based logging is intended for long jobs where ``tail -f`` of the
JSONL stream gives live progress and post-mortem evidence of where a
run stalled or failed.
