# Copyright 2026 Qilimanjaro Quantum Tech
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from copy import copy
from typing import TYPE_CHECKING, Generic, Literal, cast
from loguru import logger
from qilisdk.yaml import yaml
from .readout import ExpectationReadout, SamplingReadout, StateTomographyReadout
from .readout_result import E, ExpectationReadoutResult, S, SamplingReadoutResult, StateTomographyReadoutResult, T
if TYPE_CHECKING:
from qilisdk.analog import Hamiltonian
from qilisdk.core import QTensor
from .readout import ReadoutMethod
@yaml.register_class
[documents]
class Readout(Generic[S, E, T]):
"""Type-safe specification of which readout methods to apply during execution.
:class:`Readout` is the **primary interface** for declaring what information to extract from the quantum
backend after execution. Build a specification by chaining ``with_*`` builder methods, then pass it to
:meth:`~qilisdk.backends.Backend.execute`.
Each ``with_*`` method returns a **new** :class:`Readout` whose type parameters encode which readout
slots are populated, allowing the type checker to verify result access at compile time. The original
instance is not mutated.
Type parameters:
S: :class:`~qilisdk.readout.SamplingReadoutResult` when :meth:`with_sampling` was called, ``None``
otherwise.
E: :class:`~qilisdk.readout.ExpectationReadoutResult` when :meth:`with_expectation` was called,
``None`` otherwise.
T: :class:`~qilisdk.readout.StateTomographyReadoutResult` when :meth:`with_state_tomography` was
called, ``None`` otherwise.
Examples:
Sampling — measure in the computational basis and collect bitstring counts::
spec = Readout().with_sampling(nshots=1000)
result = backend.execute(functional, readout=spec)
counts = result.samples # dict[str, int]
probs = result.probabilities # dict[str, float]
Expectation values — compute ``<psi|O|psi>`` for one or more observables::
from qilisdk.analog import Z
spec = Readout().with_expectation(observables=[Z(0)], nshots=0)
result = backend.execute(functional, readout=spec)
ev = result.expectation_values # list[float], one entry per observable
State tomography — retrieve the full quantum state vector after execution::
spec = Readout().with_state_tomography()
result = backend.execute(functional, readout=spec)
state = result.state # QTensor
Multiple readout types can be combined in a single specification::
from qilisdk.analog import Z
spec = Readout().with_sampling(nshots=500).with_expectation(observables=[Z(0)])
result = backend.execute(functional, readout=spec)
counts = result.samples # dict[str, int]
ev = result.expectation_values # list[float]
"""
def __init__(self: Readout[None, None, None]) -> None:
self._sampling: SamplingReadout | None = None
self._expectation: ExpectationReadout | None = None
self._state_tomography: StateTomographyReadout | None = None
# -- builder methods -- each one transforms one type parameter --------
[documents]
def with_sampling(self, nshots: int = 1000, expand_samples: bool = True) -> Readout[SamplingReadoutResult, E, T]:
"""Add a sampling readout to the specification.
If expand_samples is True, partial samples will be displayed with underscores for better readability.
For example, "00_0" instead of "000" for a 4-qubit system where the third qubit's state is not measured.
Setting this to false can cause ambiguity, especially if you have a variety of mid-circuit measurements.
Args:
nshots (int): Number of measurement shots. Must be >= 0.
expand_samples (bool): Whether to display partial samples as "00_0" instead of "000" for better readability.
Returns:
A new :class:`Readout` with the sampling slot populated.
Raises:
ValueError: If a sampling readout was already set.
"""
if self._sampling is not None:
raise ValueError("Sampling readout already set in this specification.")
logger.debug("[Readout] Adding sampling readout (nshots={})", nshots)
readout = copy(self)
readout._sampling = SamplingReadout( # ruff: ignore[private-member-access]
nshots=nshots, expand_samples=expand_samples
)
return cast("Readout[SamplingReadoutResult, E, T]", readout)
[documents]
def with_expectation(
self,
observables: list[Hamiltonian | QTensor],
nshots: int = 0,
) -> Readout[S, ExpectationReadoutResult, T]:
"""Add an expectation-value readout to the specification.
Args:
observables (list[Hamiltonian | QTensor]): Observables whose expectation values will be evaluated.
nshots (int): Number of measurement shots. Use ``0`` for exact (state-vector-based) evaluation.
Returns:
A new :class:`Readout` with the expectation slot populated.
Raises:
ValueError: If an expectation readout was already set.
"""
if self._expectation is not None:
raise ValueError("Expectation readout already set in this specification.")
logger.debug("[Readout] Adding expectation readout ({} observables, nshots={})", len(observables), nshots)
readout = copy(self)
readout._expectation = ExpectationReadout( # ruff: ignore[private-member-access]
observables=observables, nshots=nshots
)
return cast("Readout[S, ExpectationReadoutResult, T]", readout)
[documents]
def with_state_tomography(self, method: Literal["exact"] = "exact") -> Readout[S, E, StateTomographyReadoutResult]:
"""Add a state-tomography readout to the specification.
Args:
method (Literal): Tomography method identifier. Currently only 'exact' is supported.
Returns:
A new :class:`Readout` with the state-tomography slot populated.
Raises:
ValueError: If a state-tomography readout was already set.
"""
if self._state_tomography is not None:
raise ValueError("State-tomography readout already set in this specification.")
logger.debug("[Readout] Adding state-tomography readout (method={})", method)
readout = copy(self)
readout._state_tomography = StateTomographyReadout(method=method) # ruff: ignore[private-member-access]
return cast("Readout[S, E, StateTomographyReadoutResult]", readout)
# -- accessors --------------------------------------------------------
@property
[documents]
def sampling(self) -> SamplingReadout | None:
"""The sampling readout, or ``None`` if not set."""
return self._sampling
@property
[documents]
def expectation(self) -> ExpectationReadout | None:
"""The expectation-value readout, or ``None`` if not set."""
return self._expectation
@property
[documents]
def state_tomography(self) -> StateTomographyReadout | None:
"""The state-tomography readout, or ``None`` if not set."""
return self._state_tomography
[documents]
def to_list(self) -> list[ReadoutMethod]:
"""Return the readout methods as a flat list.
Useful for backward compatibility with APIs that accept
``list[ReadoutMethod]``.
"""
return [ro for ro in (self._sampling, self._expectation, self._state_tomography) if ro is not None]
def __repr__(self) -> str:
parts: list[str] = []
if self._sampling is not None:
parts.append(f"sampling={self._sampling!r}")
if self._expectation is not None:
parts.append(f"expectation={self._expectation!r}")
if self._state_tomography is not None:
parts.append(f"state_tomography={self._state_tomography!r}")
return f"Readout({', '.join(parts)})"
def __bool__(self) -> bool:
return self._sampling is not None or self._expectation is not None or self._state_tomography is not None