"""QDK/Chemistry Circuit Executor implementation using QDK.
This module provides a CircuitExecutor implementation that uses the QDK backends
to execute quantum circuits. It accepts QDK/Chemistry Circuit and QuantumErrorProfile
data classes and returns measurement bitstring results via CircuitExecutorData.
Supported QDK backends include:
* QDK Full State Simulator
* QDK Sparse State Simulator
"""
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE.txt in the project root for license information.
# --------------------------------------------------------------------------------------------
from typing import Literal
from qdk import qsharp
from qdk.openqasm import run as sparse_state_run_qasm
try:
from qdk.simulation import run_qir
except ImportError:
from qsharp._simulation import run_qir
from qdk_chemistry.algorithms.circuit_executor.base import CircuitExecutor
from qdk_chemistry.data import Circuit, CircuitExecutorData, QuantumErrorProfile, Settings
from qdk_chemistry.utils import Logger
__all__: list[str] = ["QdkFullStateSimulator", "QdkFullStateSimulatorSettings"]
def _process_raw_results(raw_results: list) -> tuple[dict[str, int], dict[str, int] | None]:
"""Convert raw measurement results into bitstring counts, separating clean and lost shots.
Reorders bits to Little Endian convention and uses 'L' to mark lost qubits.
Args:
raw_results: Raw measurement results from the simulator.
Returns:
A tuple of (bitstring_counts, loss_bitstrings). loss_bitstrings is None
if no shots experienced qubit loss.
"""
_map = {"One": "1", "Loss": "L", "Zero": "0"}
clean_counts: dict[str, int] = {}
loss_counts: dict[str, int] = {}
for one_run in raw_results:
has_loss = False
chars = []
for x in reversed(one_run):
label = str(x)
if label not in _map:
raise ValueError(f"Unexpected measurement result '{label}'; expected one of {set(_map)}")
c = _map[label]
if c == "L":
has_loss = True
chars.append(c)
key = "".join(chars)
if has_loss:
loss_counts[key] = loss_counts.get(key, 0) + 1
else:
clean_counts[key] = clean_counts.get(key, 0) + 1
return clean_counts, loss_counts if loss_counts else None
[docs]
class QdkFullStateSimulatorSettings(Settings):
"""Settings for the QDK Full State Simulator circuit executor."""
[docs]
def __init__(self) -> None:
"""Initialize QDK Full State Simulator settings."""
super().__init__()
self._set_default(
"type", "string", "cpu", "Type of simulator to use: 'cpu', 'gpu', or 'clifford'", ["cpu", "gpu", "clifford"]
)
self._set_default("seed", "int", 42, "Random seed for simulation reproducibility")
[docs]
class QdkFullStateSimulator(CircuitExecutor):
"""QDK Full State Simulator circuit executor implementation."""
[docs]
def __init__(
self,
simulator_type: Literal["cpu", "gpu", "clifford"] = "cpu",
seed: int = 42,
) -> None:
"""Initialize the QDK Full State Simulator circuit executor.
Args:
simulator_type: The type of simulator to use.
seed: The random seed for simulation reproducibility.
"""
super().__init__()
self._settings = QdkFullStateSimulatorSettings()
self._settings.set("type", simulator_type)
self._settings.set("seed", seed)
def _run_impl(
self,
circuit: Circuit,
shots: int,
noise: QuantumErrorProfile | None = None,
) -> CircuitExecutorData:
"""Execute the given quantum circuit using the QDK Full State Simulator.
Args:
circuit: The quantum circuit to execute.
shots: The number of shots to execute the circuit.
noise: Optional noise profile to apply during execution.
Returns:
CircuitExecutorData: Object containing the results of the circuit execution.
"""
Logger.trace_entering()
qir = circuit.get_qir()
Logger.debug("QIR compiled")
noise_config = noise.to_qdk_noise_config() if noise is not None else None
raw_results = run_qir(
qir, shots=shots, noise=noise_config, seed=self._settings.get("seed"), type=self._settings.get("type")
)
bitstring_counts, loss_bitstrings = _process_raw_results(raw_results)
return CircuitExecutorData(
bitstring_counts=bitstring_counts,
total_shots=shots,
executor=self.name(),
executor_metadata=raw_results,
loss_bitstrings=loss_bitstrings,
)
[docs]
def name(self) -> str:
"""Return the algorithm name as qdk_full_state_simulator."""
return "qdk_full_state_simulator"
class QdkSparseStateSimulatorSettings(Settings):
"""Settings for the QDK Sparse State Simulator circuit executor."""
def __init__(self) -> None:
"""Initialize QDK Sparse State Simulator settings."""
Logger.trace_entering()
super().__init__()
self._set_default("seed", "int", 42, "Random seed for simulation reproducibility")
class QdkSparseStateSimulator(CircuitExecutor):
"""QDK Sparse State Simulator circuit executor implementation."""
def __init__(self) -> None:
"""Initialize the QDK Sparse State Simulator circuit executor."""
Logger.trace_entering()
super().__init__()
self._settings = QdkSparseStateSimulatorSettings()
def _run_impl(
self,
circuit: Circuit,
shots: int,
noise: QuantumErrorProfile | None = None,
) -> CircuitExecutorData:
"""Execute the given quantum circuit using the QDK Sparse State Simulator.
Args:
circuit: The quantum circuit to execute.
shots: The number of shots to execute the circuit.
noise: Optional noise profile to apply during execution.
Returns:
CircuitExecutorData: Object containing the results of the circuit execution.
"""
Logger.trace_entering()
noise_config = noise.to_qdk_noise_config() if noise is not None else None
if circuit._qsharp_factory is not None: # noqa: SLF001
raw_results = qsharp.run(
circuit._qsharp_factory.program, # noqa: SLF001
shots,
*circuit._qsharp_factory.parameter.values(), # noqa: SLF001
noise=noise_config,
seed=self._settings.get("seed"),
)
bitstring_counts, loss_bitstrings = _process_raw_results(raw_results)
else:
qasm = circuit.get_qasm()
raw_results = sparse_state_run_qasm(
qasm,
shots=shots,
noise=noise_config,
seed=self._settings.get("seed"),
)
bitstring_counts, loss_bitstrings = _process_raw_results(raw_results)
return CircuitExecutorData(
bitstring_counts=bitstring_counts,
total_shots=shots,
executor=self.name(),
executor_metadata=raw_results,
loss_bitstrings=loss_bitstrings,
)
def name(self) -> str:
"""Return the algorithm name as qdk_sparse_state_simulator."""
return "qdk_sparse_state_simulator"