Source code for divi.pipeline._preprocessor

# SPDX-FileCopyrightText: 2025-2026 Qoro Quantum Ltd <divi@qoroquantum.de>
#
# SPDX-License-Identifier: Apache-2.0

"""Circuit preprocessors: post-spec MetaCircuit transforms + the readout they target.

A :class:`CircuitPreprocessor` is a named transform of the seed circuit emitted
by the spec stage — ``MetaCircuit -> MetaCircuit`` — paired with the readout that
transform prepares for (a :class:`~divi.pipeline.ResultFormat` and an optional
custom terminal stage). :meth:`~divi.qprog.QuantumProgram.evaluate` applies the
preprocessor (via :class:`~divi.pipeline.stages.PreprocessStage`) and runs the
program's one pipeline, so callers (optimizers, metric estimators) select a
routine by passing a preprocessor instead of assembling pipelines themselves.

These default preprocessors are factory *functions* (``cost_preprocessor``,
``sample_preprocessor``); each program surfaces one through a method that
returns it — the public, overridable ``VariationalQuantumAlgorithm.cost_preprocessor``
(PCE overrides it with its counts-based variant) and the internal
``_sample_preprocessor``. The metric/overlap preprocessors live in
``divi.qprog._metrics`` as factory functions too, parameterized by
per-evaluation state (the overlap closure, the Fubini-Study block id) rather
than overridden.
"""

from collections.abc import Callable, Hashable
from dataclasses import dataclass, replace

from divi.circuits import MetaCircuit
from divi.pipeline.abc import ResultFormat, Stage


def _identity(meta: MetaCircuit) -> MetaCircuit:
    return meta


def _clear_observable(meta: MetaCircuit) -> MetaCircuit:
    """Drop the observable and measure every wire — turns an expval seed into a
    computational-basis sampling circuit."""
    return replace(
        meta,
        observable=None,
        measured_wires=tuple(range(meta.n_qubits)),
        measurement_qasms=(),
        measurement_groups=(),
    )


[docs] @dataclass(frozen=True) class CircuitPreprocessor: """A named seed transform and the readout it targets. Attributes: name: Identifier for the routine (``"cost"``, ``"sample"``, ...). preprocess: Transform applied to each post-spec ``MetaCircuit`` before mitigation and the terminal measurement. Defaults to identity. result_format: Format the raw backend results convert into; also drives error-mitigation applicability when the pipeline is assembled. Defaults to expectation values. terminal_stage: Optional custom ``handles_measurement`` terminal. ``None`` means the program supplies its default :class:`~divi.pipeline.stages.MeasurementStage` (configured with the program's grouping / shot strategy); PCE supplies its own :class:`~divi.pipeline.stages.PCECostStage`. consumes_dag_bodies: Whether ``preprocess`` reads or replaces circuit DAG bodies. Metadata-only transforms leave this ``False`` so dry runs can keep analytic shortcuts. cache_key: Identity under which :meth:`~divi.qprog.QuantumProgram._build_preprocessor_pipeline` memoizes the assembled pipeline (so its forward-pass cache survives across optimizer iterations). ``None`` (the default) means *do not cache* — the pipeline is rebuilt per call and discarded. Every built-in routine (cost/sample/evolution/overlap and the metric estimators) passes a constant key, because each one's ``preprocess`` is a pure transform; per-iteration freshness where it is needed (the QDrift stochastic resampling) comes from the spec stage's ``cache_key_extras`` invalidating the forward-pass cache, not from leaving this ``None``. """ name: str preprocess: Callable[[MetaCircuit], MetaCircuit] = _identity result_format: ResultFormat = ResultFormat.EXPVALS terminal_stage: Stage | None = None consumes_dag_bodies: bool = False cache_key: Hashable | None = None
[docs] def cost_preprocessor() -> CircuitPreprocessor: """Measure the seed's cost observable as expectation values (identity transform).""" return CircuitPreprocessor("cost", cache_key="cost")
[docs] def sample_preprocessor() -> CircuitPreprocessor: """Sample the prepared state in the computational basis (clears the observable).""" return CircuitPreprocessor( "sample", preprocess=_clear_observable, result_format=ResultFormat.PROBS, cache_key="sample", )