Source code for qilisdk.readout.readout
# 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.
"""Low-level readout method classes for quantum backend execution.
This module defines the :class:`ReadoutMethod` base class and its concrete subclasses -
:class:`SamplingReadout`, :class:`ExpectationReadout`, and :class:`StateTomographyReadout` - that
describe *how* results are extracted from the quantum backend after execution.
**The recommended way to compose readout for a functional is** :class:`~qilisdk.readout.readout_spec.Readout`.
The classes in this module are typically constructed internally by :class:`~qilisdk.readout.readout_spec.Readout`
and do not need to be instantiated directly in user code.
"""
from __future__ import annotations
from typing import Literal
from qilisdk.analog import Hamiltonian
from qilisdk.core import QTensor
from qilisdk.yaml import yaml
@yaml.register_class
[docs]
class ReadoutMethod:
"""Base type for readout configurations.
:class:`ReadoutMethod` is not meant to be instantiated directly. Use
:class:`~qilisdk.readout.Readout` to compose a readout specification and pass it to
:meth:`~qilisdk.backends.Backend.execute`.
"""
[docs]
def is_sampling_readout(self) -> bool:
"""Check whether this readout is a :class:`SamplingReadout`.
Returns:
bool: ``True`` if this instance is a :class:`SamplingReadout`,
``False`` otherwise.
"""
return isinstance(self, SamplingReadout)
[docs]
def is_expectation_readout(self) -> bool:
"""Check whether this readout is an :class:`ExpectationReadout`.
Returns:
bool: ``True`` if this instance is an :class:`ExpectationReadout`,
``False`` otherwise.
"""
return isinstance(self, ExpectationReadout)
[docs]
def is_state_tomography_readout(self) -> bool:
"""Check whether this readout is a :class:`StateTomographyReadout`.
Returns:
bool: ``True`` if this instance is a
:class:`StateTomographyReadout`, ``False`` otherwise.
"""
return isinstance(self, StateTomographyReadout)
@yaml.register_class
[docs]
class SamplingReadout(ReadoutMethod):
"""Sampling readout configuration.
Instructs the backend to perform repeated measurement shots and return bitstring counts.
Typically constructed via :meth:`Readout.with_sampling <qilisdk.readout.Readout.with_sampling>`.
Args:
nshots (int): Number of measurement shots. Must be a positive integer.
Examples:
>>> from qilisdk.readout import Readout
>>> spec = Readout().with_sampling(nshots=1000)
"""
def __init__(self, nshots: int, expand_samples: bool = True) -> None:
"""
Args:
nshots (int): The number of shots to use during sampling. Needs to be a positive integer.
expand_samples (bool): Whether to display partial samples as "00_0" instead of "000" for better readability.
Raises:
ValueError: If the number of shots is not a positive integer.
"""
if nshots <= 0 or not isinstance(nshots, int):
raise ValueError("The number of shots has to be a positive integer")
self._nshots: int = nshots
self._expand_samples: bool = expand_samples
@property
[docs]
def nshots(self) -> int:
return self._nshots
@property
[docs]
def expand_samples(self) -> bool:
return self._expand_samples
@yaml.register_class
[docs]
class ExpectationReadout(ReadoutMethod):
"""Expectation-value readout configuration.
Instructs the backend to compute ``<psi|O|psi>`` for one or more observables.
Typically constructed via
:meth:`Readout.with_expectation <qilisdk.readout.Readout.with_expectation>`.
Args:
observables (list[Hamiltonian | QTensor]): Observables whose expectation values are requested.
nshots (int): Number of measurement shots. Use ``0`` (the default) for exact state-vector
evaluation.
Examples:
>>> from qilisdk.analog import Z
>>> from qilisdk.readout import Readout
>>> spec = Readout().with_expectation(observables=[Z(0)], nshots=0)
"""
def __init__(self, observables: list[Hamiltonian | QTensor], nshots: int = 0) -> None:
if nshots < 0 or not isinstance(nshots, int):
raise ValueError("The number of shots has to be a positive integer")
if any(not isinstance(o, (Hamiltonian, QTensor)) for o in observables):
raise ValueError("Invalid Observable: All observables need to be QTensors or a Hamiltonian.")
self._nshots: int = nshots
self._observables: list[Hamiltonian | QTensor] = observables
self._qtensor_observables: list[QTensor] = [
(o if isinstance(o, QTensor) else o.to_qtensor()) for o in self.observables
]
self._scaled_nqubits: int | None = None
@property
[docs]
def nshots(self) -> int:
"""
The number of shots to use when estimating the expectation values.
"""
return self._nshots
@property
[docs]
def observables(self) -> list[Hamiltonian | QTensor]:
"""
The observables whose expectation values are requested, in their original form as provided by the user.
"""
return self._observables
@property
[docs]
def qtensor_observables(self) -> list[QTensor]:
"""
The observables converted to :class:`~qilisdk.core.QTensor` form, populated automatically; not intended to be set manually.
"""
return self._qtensor_observables
[docs]
def expand_observables(self, nqubits: int) -> None:
"""Scale each observable to match a given number of qubits.
The conversion is cached: calling this method again with the same
``nqubits`` value is a no-op.
Args:
nqubits (int): Target qubit count to scale the observables to.
"""
if self._scaled_nqubits == nqubits:
return
self._qtensor_observables = [
(o.expand(nqubits) if isinstance(o, QTensor) else o.to_qtensor(nqubits)) for o in self.observables
]
self._scaled_nqubits = nqubits
@yaml.register_class
[docs]
class StateTomographyReadout(ReadoutMethod):
"""State-tomography readout configuration.
Instructs the backend to return the full quantum state (ket or density matrix) after execution.
Typically constructed via
:meth:`Readout.with_state_tomography <qilisdk.readout.Readout.with_state_tomography>`.
Args:
method (Literal): Tomography method identifier. Currently only 'exact' is supported.
Examples:
>>> from qilisdk.readout import Readout
>>> spec = Readout().with_state_tomography()
"""
def __init__(self, method: Literal["exact"] = "exact") -> None:
self._method: Literal["exact"] = method
@property
[docs]
def method(self) -> Literal["exact"]:
return self._method