# Copyright Quantinuum
#
# 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 collections import Counter, defaultdict
from collections.abc import Iterator, Sequence
from typing import (
Any,
)
import numpy as np
from pytket.backends.backendresult import BackendResult
from pytket.circuit import Bit, Qubit, UnitID
from pytket.utils.outcomearray import OutcomeArray
from qiskit.result import Result # type: ignore
from qiskit.result.models import ExperimentResult # type: ignore
def _get_registers_from_uids(uids: list[UnitID]) -> dict[str, set[UnitID]]:
registers: dict[str, set[UnitID]] = defaultdict(set)
for uid in uids:
registers[uid.reg_name].add(uid)
return registers
LabelsType = list[tuple[str, int]]
def _get_header_info(uids: list[UnitID]) -> tuple[LabelsType, LabelsType]:
registers = _get_registers_from_uids(uids)
reg_sizes = [(name, max(uids).index[0] + 1) for name, uids in registers.items()]
reg_labels = [
(name, uid.index[0]) for name, indices in registers.items() for uid in uids
]
return reg_sizes, reg_labels
def _qiskit_ordered_uids(uids: list[UnitID]) -> list[UnitID]:
registers = _get_registers_from_uids(uids)
names = sorted(registers.keys())
return [uid for name in names for uid in sorted(registers[name], reverse=True)]
def _hex_to_outar(hexes: Sequence[str], width: int) -> OutcomeArray:
ints = [int(hexst, 16) for hexst in hexes]
return OutcomeArray.from_ints(ints, width, big_endian=False)
# An empty ExperimentResult can be an empty dict, but it can also be a dict
# filled with empty values.
def _result_is_empty_shots(result: ExperimentResult) -> bool:
if not result.shots > 0:
# 0-shots results don't count as empty; they are simply ignored
return False
datadict = result.data.to_dict()
return bool(
len(datadict) == 0
or ("memory" in datadict and len(datadict["memory"]) == 0)
or ("counts" in datadict and len(datadict["counts"]) == 0)
)
# In some cases, Qiskit returns a result with fields we don't expect -
# for example, a circuit with classical bits run on AerStateBackend will
# return counts (whether or not there were measurements). The include_foo
# arguments should be set based on what the backend supports.
[docs]
def qiskit_experimentresult_to_backendresult( # noqa: PLR0912, PLR0913
result: ExperimentResult,
include_counts: bool = True,
include_shots: bool = True,
include_state: bool = True,
include_unitary: bool = True,
include_density_matrix: bool = True,
) -> BackendResult:
if not result.success:
raise RuntimeError(result.status)
header = result.header
width = header["memory_slots"]
c_bits, q_bits = None, None
if "creg_sizes" in header:
c_bits = []
for name, size in header["creg_sizes"]:
for index in range(size):
c_bits.append(Bit(name, index))
if "qreg_sizes" in header:
q_bits = []
for name, size in header["qreg_sizes"]:
for index in range(size):
q_bits.append(Qubit(name, index))
shots, counts, state, unitary, density_matrix = (None,) * 5
datadict = result.data.to_dict()
if _result_is_empty_shots(result) and include_shots:
n_bits = len(c_bits) if c_bits else 0
shots = OutcomeArray.from_readouts(
np.zeros((result.shots, n_bits), dtype=np.uint8)
)
else:
if "memory" in datadict and include_shots:
memory = datadict["memory"]
shots = _hex_to_outar(memory, width)
elif "counts" in datadict and include_counts:
qis_counts = datadict["counts"]
counts = Counter(
{
_hex_to_outar([hexst], width): count
for hexst, count in qis_counts.items()
}
)
if "statevector" in datadict and include_state:
state = datadict["statevector"].reverse_qargs().data
if "unitary" in datadict and include_unitary:
unitary = datadict["unitary"].reverse_qargs().data
if "density_matrix" in datadict and include_density_matrix:
density_matrix = datadict["density_matrix"].reverse_qargs().data
return BackendResult(
c_bits=c_bits,
q_bits=q_bits,
shots=shots,
counts=counts,
state=state,
unitary=unitary,
density_matrix=density_matrix,
ppcirc=None,
)
[docs]
def qiskit_result_to_backendresult( # noqa: PLR0913
res: Result,
include_counts: bool = True,
include_shots: bool = True,
include_state: bool = True,
include_unitary: bool = True,
include_density_matrix: bool = True,
) -> Iterator[BackendResult]:
for result in res.results:
yield qiskit_experimentresult_to_backendresult(
result,
include_counts,
include_shots,
include_state,
include_unitary,
include_density_matrix,
)
[docs]
def backendresult_to_qiskit_resultdata(
res: BackendResult,
cbits: list[UnitID],
qbits: list[UnitID],
final_map: dict[UnitID, UnitID] | None,
) -> dict[str, Any]:
data: dict[str, Any] = dict() # noqa: C408
if res.contains_state_results:
qbits = _qiskit_ordered_uids(qbits)
qbits.sort(reverse=True)
if final_map:
qbits = [final_map[q] for q in qbits]
stored_res = res.get_result(qbits)
if stored_res.state is not None:
data["statevector"] = stored_res.state
if stored_res.unitary is not None:
data["unitary"] = stored_res.unitary
if res.contains_measured_results:
cbits = _qiskit_ordered_uids(cbits)
if final_map:
cbits = [final_map.get(c, c) for c in cbits]
stored_res = res.get_result(cbits)
if stored_res.shots is not None:
data["memory"] = [hex(i) for i in stored_res.shots.to_intlist()]
data["counts"] = dict(Counter(data["memory"]))
elif stored_res.counts is not None:
data["counts"] = {
hex(i.to_intlist()[0]): f for i, f in stored_res.counts.items()
}
return data