Source code for divi.qprog._observable_measuring_mixin

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

"""Mixin adding Pauli-observable measurement configuration knobs.

Provides ``grouping_strategy`` and ``shot_distribution`` kwargs to any
:class:`~divi.qprog.QuantumProgram` subclass that mixes it in. Programs
that hit the expval branch of
:class:`~divi.pipeline.stages.MeasurementStage` honour both knobs;
sampling-only programs hit the probs branch (which ignores both) and the
kwargs become silent no-ops.
"""

from warnings import warn

from divi.pipeline import GroupingStrategy, ShotDistStrategy

# Distinguishes "user accepted the default" from "user explicitly passed
# a value" so the override warning only fires for explicit choices.
_UNSET: object = object()

# User-facing subset of ``GroupingStrategy``.  ``"_backend_expval"`` is an
# internal MeasurementStage strategy and is excluded.
_ALLOWED_GROUPING_STRATEGIES: frozenset = frozenset({"qwc", "default", "wires", None})


[docs] class ObservableMeasuringMixin: """Mixin adding measurement-stage configuration to a quantum program. Mix in alongside :class:`~divi.qprog.QuantumProgram` (the mixin must come first so its cooperative ``super().__init__()`` resolves to the program base) — for example:: class MyProgram(ObservableMeasuringMixin, QuantumProgram): ... The mixin stores the two measurement-stage knobs verbatim. Runtime shape-aware decisions — including the auto-flip from ``"qwc"`` to the internal ``"_backend_expval"`` strategy when the backend supports analytic expvals and every MetaCircuit carries a single observable — live inside :class:`~divi.pipeline.stages.MeasurementStage`. """ def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) from divi.qprog.quantum_program import QuantumProgram mro = cls.__mro__ if ObservableMeasuringMixin in mro and QuantumProgram in mro: mixin_idx = mro.index(ObservableMeasuringMixin) base_idx = mro.index(QuantumProgram) if mixin_idx > base_idx: raise TypeError( f"{cls.__name__}: ObservableMeasuringMixin must precede " f"QuantumProgram in the base list so the mixin's " f"__init__ runs before QuantumProgram's strict-kwargs " f"check rejects ``grouping_strategy=`` / " f"``shot_distribution=``." ) def __init__( self, *args, grouping_strategy: GroupingStrategy | None = _UNSET, # type: ignore[assignment] shot_distribution: ShotDistStrategy | None = None, **kwargs, ): """Initialize the measurement-config layer. Args: grouping_strategy: Strategy for partitioning Hamiltonian terms into compatible measurement groups; one circuit is executed per group. Options: ``"qwc"`` (qubit-wise- commuting — most compact, default), ``"wires"`` (group by support wires), or ``None`` (one circuit per term). :class:`~divi.pipeline.stages.MeasurementStage` may further auto-flip ``"qwc"`` to its internal ``"_backend_expval"`` mode at runtime when the backend supports analytic expvals and the batch is single- observable; ``"_backend_expval"`` is not user-passable. shot_distribution: Focus the backend's shot budget on the Hamiltonian terms that matter most. Without this option, every measurement group is sampled with the backend's full shot count, even tiny terms with little impact on the final energy. With ``shot_distribution`` set, the same total budget is split across groups according to their importance — reducing variance without spending more shots. Available strategies: - ``"uniform"`` — equal split across groups. - ``"weighted"`` — proportional to per-group coefficient L1 norm; dominant Hamiltonian terms get more shots. - ``"weighted_random"`` — multinomial sample of the same probabilities; may drop more low-weight groups than the deterministic ``"weighted"`` for the same budget. - A callable ``(group_l1_norms, total_shots) -> per_group_shots`` for fully custom allocation. Defaults to ``None`` (every group receives the full shot budget). ``*args``, ``**kwargs``: Forwarded to the next class in the MRO (typically :class:`~divi.qprog.QuantumProgram`). """ super().__init__(*args, **kwargs) if ( grouping_strategy is not _UNSET and grouping_strategy not in _ALLOWED_GROUPING_STRATEGIES ): raise ValueError( f"Invalid grouping_strategy={grouping_strategy!r}. " f"Choose 'qwc', 'wires', 'default', or None." ) if ( grouping_strategy not in (_UNSET, None) and shot_distribution is None and self.backend.supports_expval # type: ignore[attr-defined] ): warn( "Backend supports analytic expectation values; " f"grouping_strategy={grouping_strategy!r} may be auto-" "overridden to use the backend's native expval path for " "single-observable expval-mode circuits.", UserWarning, stacklevel=2, ) self._grouping_strategy: GroupingStrategy = ( "qwc" if grouping_strategy is _UNSET else grouping_strategy ) self._shot_distribution = shot_distribution