Source code for qilisdk.functionals.functional_result

# Copyright 2025 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 typing import TYPE_CHECKING, Generic, overload

from loguru import logger

from qilisdk.core.result import Result
from qilisdk.readout.readout_result import (
    E,
    ExpectationReadoutResult,
    ReadoutCompositeResults,
    S,
    SamplingReadoutResult,
    StateTomographyReadoutResult,
    T,
    has_expectation_values,
    has_sampling,
    has_state_tomography,
)
from qilisdk.yaml import yaml

if TYPE_CHECKING:
    from collections.abc import Iterator
    from typing import Any, Never

    from qilisdk.core import QTensor


@yaml.register_class
[docs] class FunctionalResult(Result, Generic[S, E, T]): """Container for the outputs produced by executing a functional on a backend. Instances of this class are returned by :meth:`~qilisdk.backends.Backend.execute`. The three type parameters correspond directly to the readout types declared in the :class:`~qilisdk.readout.Readout` passed at execution time: * ``S``: :class:`~qilisdk.readout.SamplingReadoutResult` when :meth:`~qilisdk.readout.Readout.with_sampling` was used, ``None`` otherwise. * ``E``: :class:`~qilisdk.readout.ExpectationReadoutResult` when :meth:`~qilisdk.readout.Readout.with_expectation` was used, ``None`` otherwise. * ``T``: :class:`~qilisdk.readout.StateTomographyReadoutResult` when :meth:`~qilisdk.readout.Readout.with_state_tomography` was used, ``None`` otherwise. **Typed access** — use the forwarding properties that return the readout result objects directly. The type checker knows whether each is ``None`` or populated based on the type parameters:: result.sampling.samples # ✅ when S = SamplingReadoutResult result.state_tomography.state # ✅ when T = StateTomographyReadoutResult result.expectation # None when E = None → .expectation_values is a type error **Convenience shortcuts** — ``result.get_samples()``, ``result.get_state()``, ``result.get_expectation_values()``, etc. remain available for interactive / notebook use. They raise ``ValueError`` at runtime when the corresponding readout is absent. """ def __init__( self, readout_results: ReadoutCompositeResults[S, E, T], intermediate_results: list[ReadoutCompositeResults[S, E, T]] | None = None, ) -> None: """Initialise a functional result from readout outputs. Args: readout_results: Final readout results. intermediate_results: Optional per-step intermediate readout results. Defaults to ``None``. """ self._readout_results = readout_results self._intermediate_results = intermediate_results or [] logger.debug( "[FunctionalResult] Created FunctionalResult with {} intermediate results", len(self._intermediate_results), ) # -- forwarding properties (typed safe path) -------------------------- # These return the generic type parameters S, E, T directly, so the # type checker knows the exact type from the FunctionalResult's # parameterisation. No descriptors or overloads needed. @property
[docs] def sampling(self) -> S: """The sampling readout result, or ``None`` if not requested. When ``S = SamplingReadoutResult``, this returns the result object with typed access to ``.samples``, ``.probabilities``, etc. When ``S = None``, the type checker sees ``None``. """ return self._readout_results.sampling
@property
[docs] def expectation(self) -> E: """The expectation-value readout result, or ``None`` if not requested. When ``E = ExpectationReadoutResult``, this returns the result object with typed access to ``.expectation_values``. When ``E = None``, the type checker sees ``None``. """ return self._readout_results.expectation_values
@property
[docs] def state_tomography(self) -> T: """The state-tomography readout result, or ``None`` if not requested. When ``T = StateTomographyReadoutResult``, this returns the result object with typed access to ``.state``, ``.probabilities``, etc. When ``T = None``, the type checker sees ``None``. """ return self._readout_results.state_tomography
@property
[docs] def readout_results(self) -> ReadoutCompositeResults[S, E, T]: """Composite readout results from the final execution step.""" return self._readout_results
@property
[docs] def intermediate_results(self) -> list[ReadoutCompositeResults[S, E, T]]: """Intermediate readout results for each time-step, if stored.""" return self._intermediate_results
# -- convenience methods (type-narrowed via self-type overloads) ------ # Each method has overloads keyed on the *type of self*, so the type # checker returns the concrete type when the readout is present and # ``Never`` when it is absent (flagging the call as unreachable). @overload
[docs] def get_samples(self: FunctionalResult[SamplingReadoutResult, Any, Any]) -> dict[str, int]: ...
@overload def get_samples(self: FunctionalResult[None, Any, Any]) -> Never: ... def get_samples(self) -> dict[str, int]: """Measurement samples from the final execution step. Returns: dict[str, int]: Mapping of measured bitstring to count. Raises: ValueError: If sampling readout was not provided. """ logger.trace("[FunctionalResult] Retrieving final samples") if has_sampling(self._readout_results): return self._readout_results.sampling.samples raise ValueError("Sampling readout was not provided.") @overload
[docs] def get_probabilities(self: FunctionalResult[SamplingReadoutResult, Any, Any]) -> dict[str, float]: ...
@overload def get_probabilities(self: FunctionalResult[None, Any, StateTomographyReadoutResult]) -> dict[str, float]: ... @overload def get_probabilities(self: FunctionalResult[None, Any, None]) -> Never: ... def get_probabilities(self) -> dict[str, float]: """Outcome probabilities from the final execution step. Returns: dict[str, float]: Mapping of bitstring to probability. Raises: ValueError: If neither sampling nor state-tomography readout was provided. """ logger.trace("[FunctionalResult] Retrieving final probabilities") if has_sampling(self._readout_results): return self._readout_results.sampling.probabilities if has_state_tomography(self._readout_results): return self._readout_results.state_tomography.probabilities raise ValueError("Can't compute probabilities if Sampling or State tomography readouts are not specified.") @overload
[docs] def get_state(self: FunctionalResult[Any, Any, StateTomographyReadoutResult]) -> QTensor: ...
@overload def get_state(self: FunctionalResult[Any, Any, None]) -> Never: ... def get_state(self) -> QTensor: """Quantum state vector from the final execution step. Returns: QTensor: The final quantum state. Raises: ValueError: If state-tomography readout was not provided. """ logger.trace("[FunctionalResult] Retrieving final state") if has_state_tomography(self._readout_results): return self._readout_results.state_tomography.state raise ValueError("Can't obtain the final state if State Tomography readout is not specified.") @overload
[docs] def get_expectation_values(self: FunctionalResult[Any, ExpectationReadoutResult, Any]) -> list[float]: ...
@overload def get_expectation_values(self: FunctionalResult[Any, None, Any]) -> Never: ... def get_expectation_values(self) -> list[float]: """Expectation values from the final execution step. Returns: list[float]: One expectation value per observable. Raises: ValueError: If expectation readout was not provided. """ logger.trace("[FunctionalResult] Retrieving final expectation values") if has_expectation_values(self._readout_results): return self._readout_results.expectation_values.expectation_values raise ValueError("Can't compute expectation values because Expectation readout was not specified.") # -- intermediate accessors (runtime-checked) ------------------------- @overload
[docs] def get_intermediate_samples(self: FunctionalResult[SamplingReadoutResult, Any, Any]) -> list[dict[str, int]]: ...
@overload def get_intermediate_samples(self: FunctionalResult[None, Any, Any]) -> Never: ... def get_intermediate_samples(self) -> list[dict[str, int]]: """Measurement samples for every intermediate time-step. Raises: ValueError: If no intermediate results were stored or no ``SamplingReadout`` was provided. Returns: list[dict[str, int]]: List of mappings of measured bitstring to count, one per intermediate step. """ logger.trace("[FunctionalResult] Retrieving intermediate samples for {} steps", len(self._intermediate_results)) if self._intermediate_results: results = [] for res in self._intermediate_results: if has_sampling(res): results.append(res.sampling.samples) return results raise ValueError("Can't find intermediate samples because intermediate Results were not stored.") @overload
[docs] def get_intermediate_probabilities( self: FunctionalResult[SamplingReadoutResult | None, Any, StateTomographyReadoutResult | None], ) -> list[dict[str, float]]: ...
@overload def get_intermediate_probabilities(self: FunctionalResult[None, Any, None]) -> Never: ... def get_intermediate_probabilities(self) -> list[dict[str, float]]: """Outcome probabilities for every intermediate time-step. Raises: ValueError: If no intermediate results were stored or no ``SamplingReadout`` / ``StateTomographyReadout`` was provided. Returns: list[dict[str, float]]: List of mappings of bitstring to probability, one per intermediate step. """ logger.trace( "[FunctionalResult] Retrieving intermediate probabilities for {} steps", len(self._intermediate_results) ) if self._intermediate_results: results = [] for res in self._intermediate_results: if has_state_tomography(res): results.append(res.state_tomography.probabilities) elif has_sampling(res): results.append(res.sampling.probabilities) return results raise ValueError("Can't find intermediate probabilities because intermediate Results were not stored.") @overload
[docs] def get_intermediate_states(self: FunctionalResult[Any, Any, StateTomographyReadoutResult]) -> list[QTensor]: ...
@overload def get_intermediate_states(self: FunctionalResult[Any, Any, None]) -> Never: ... def get_intermediate_states(self) -> list[QTensor]: """Quantum state vectors for every intermediate time-step. Raises: ValueError: If no intermediate results were stored or no ``StateTomographyReadout`` was provided. Returns: list[QTensor]: List of quantum states, one per intermediate step. """ logger.trace("[FunctionalResult] Retrieving intermediate states for {} steps", len(self._intermediate_results)) if self._intermediate_results: results = [] for res in self._intermediate_results: if has_state_tomography(res): results.append(res.state_tomography.state) return results raise ValueError("Can't find intermediate states because intermediate Results were not stored.") @overload
[docs] def get_intermediate_expectation_values( self: FunctionalResult[Any, ExpectationReadoutResult, Any], ) -> list[list[float]]: ...
@overload def get_intermediate_expectation_values(self: FunctionalResult[Any, None, Any]) -> Never: ... def get_intermediate_expectation_values(self) -> list[list[float]]: """Expectation values for every intermediate time-step. Raises: ValueError: If no intermediate results were stored or no ``ExpectationReadout`` was provided. Returns: list[list[float]]: List of expectation values, one per intermediate step. """ logger.trace( "[FunctionalResult] Retrieving intermediate expectation values for {} steps", len(self._intermediate_results), ) if self._intermediate_results: results = [] for res in self._intermediate_results: if has_expectation_values(res): results.append(res.expectation_values.expectation_values) return results raise ValueError("Can't find intermediate expectation values because intermediate Results were not stored.") # -- has_* helpers (useful for runtime checks) ------------------------
[docs] def has_state(self) -> bool: """Check whether the result contains a final quantum state. Returns: bool: True if the Functional result has the state. False Otherwise. """ return has_state_tomography(self._readout_results)
[docs] def has_samples(self) -> bool: """Check whether the result contains measurement samples. Returns: bool: True if the Functional result has samples. False Otherwise. """ return has_sampling(self._readout_results)
[docs] def has_probabilities(self) -> bool: """Check whether the result contains outcome probabilities. Returns: bool: True if the Functional result has probabilities. False Otherwise. """ return has_state_tomography(self._readout_results) or has_sampling(self._readout_results)
[docs] def has_expectation_values(self) -> bool: """Check whether the result contains expectation values. Returns: bool: True if the Functional result has expectation values. False Otherwise. """ return has_expectation_values(self._readout_results)
# -- container protocol ----------------------------------------------- def __len__(self) -> int: return len(self._intermediate_results) + 1 if self._intermediate_results else 1 def __iter__(self) -> Iterator[ReadoutCompositeResults[S, E, T]]: if self._intermediate_results: yield from self._intermediate_results yield self._readout_results def __getitem__(self, index: int) -> ReadoutCompositeResults[S, E, T]: if index >= len(self): raise IndexError(f"Index {index} out of range for FunctionalResult of length {len(self)}") if index < len(self._intermediate_results): return self._intermediate_results[index] return self._readout_results def __repr__(self) -> str: LIMIT = 10 out = "" if self.execution_time is not None: out += f"- Execution Time: {self.execution_time:.6g} s\n\n" out += "- Functional Results: [\n\n" out += str(self._readout_results) out += "]" if self._intermediate_results: out += "\n\n\n- Intermediate Results: [\n\n" for i, res in enumerate(self._intermediate_results[:LIMIT]): out += str(res) if i < len(self._intermediate_results) - 1 and i < LIMIT - 1: out += "\n" + "-" * 20 + "\n" if len(self._intermediate_results) > LIMIT: out += "\n...\n\n" out += "]" return out