# Copyright 2019-2023 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 typing import Dict, List, Set, Tuple
from pytket import Bit, Circuit, OpType, Qubit
from pytket.backends import Backend
from pytket.backends.backendresult import BackendResult
from pytket.unit_id import Node
from qermit import (
CircuitShots,
MitTask,
)
from qermit.spam.full_transition_tomography import (
CorrectionMethod,
StateInfo,
calculate_correlation_matrices,
correct_transition_noise,
get_full_transition_tomography_circuits,
)
[docs]
def gen_full_tomography_spam_circuits_task(
backend: Backend, shots: int, qubit_subsets: List[List[Node]]
) -> MitTask:
"""Generate MitTask for calibration circuits according to the specified correlation and given backend.
:param backend: Backend on which the experiments are run.
:param qubit_subsets: A list of lists of correlated Nodes of a `Device`.
Qubits within the same list are assumed to only have SPAM errors correlated
with each other. Thus to allow SPAM errors between all qubits you should
provide a single list. The qubits in `qubit_subsets` must be nodes in the
backend's associated `Device`.
:param shots: An int corresponding to the number of shots of each calibration circuit required.
:return: A MitTask object, requiring 1 List[CircuitShots] wire and returning (List[CircuitShots], List[StateInfo])
corresponding to Calibration Circuits and corresponding states.
"""
def task(
obj, wire: List[CircuitShots]
) -> Tuple[List[CircuitShots], List[CircuitShots], List[StateInfo]]:
if "FullCorrelatedSpamCorrection" in obj.characterisation:
# check correlations distance
if (
obj.characterisation["FullCorrelatedSpamCorrection"].CorrelatedNodes
== qubit_subsets
):
return (wire, [], [])
process_circuit = Circuit(
len([qb for subset in qubit_subsets for qb in subset])
)
tomo_circuit_states = get_full_transition_tomography_circuits(
process_circuit, backend, qubit_subsets
)
tomo_circuit_shots = [
CircuitShots(Circuit=c, Shots=shots) for c in tomo_circuit_states[0]
]
return (wire, tomo_circuit_shots, tomo_circuit_states[1])
return MitTask(
_label="SPAMFullTomographyCircuits", _n_in_wires=1, _n_out_wires=3, _method=task
)
[docs]
def gen_full_tomography_spam_characterisation_task(
qubit_subsets: List[List[Node]],
) -> MitTask:
"""
Uses results from device for characterisation circuits to characterise transition matrices
for different qubit subsets and stores them in backend.
:param qubit_subsets: Subsets of qubits in backend corresponding to different correlated subsets.
"""
def task(
obj, results: List[BackendResult], state_infos: List[StateInfo]
) -> Tuple[bool]:
"""
:param results: Results from characterisation circuits run on backend.
:param state_infos: Corresponding state prepared in the circuit run for each result and qubit to bit map.
:return: bool confirming characterisation complete.
"""
if len(results) != len(state_infos):
raise ValueError(
"SPAM Characterisation requires the same number of prepared states and results."
)
if len(results) > 0:
obj.characterisation["FullCorrelatedSpamCorrection"] = (
calculate_correlation_matrices(results, state_infos, qubit_subsets)
)
return (True,)
return MitTask(
_label="SPAMFullCharacterisationCircuits",
_n_in_wires=2,
_n_out_wires=1,
_method=task,
)
[docs]
def gen_full_tomography_spam_correction_task(corr_method: CorrectionMethod) -> MitTask:
"""
Uses characterisation result held in backend to correct for SPAM noise in passed
BackendResult objects. Method used to invert SPAM characteriastion matrices
and correct results given by CorrectionMethod enum.
:param corr_method: Method used to invert matrices and correct results.
"""
def task(
obj,
results: List[BackendResult],
bit_qb_maps: List[Tuple[Dict[Qubit, Bit], Dict[Bit, Qubit]]],
characterised: bool,
) -> Tuple[List[BackendResult]]:
"""
:param results: Results from experiment circuits run on backend.
:param bit_qb_maps: Map between Bits measurement outcomes are assigned to and Qubits in each experiment Circuit. Separate dicts for
results end of and mid-circuit measurements.
:return: Corrected Results
"""
if "FullCorrelatedSpamCorrection" in obj.characterisation:
char = obj.characterisation["FullCorrelatedSpamCorrection"]
else:
raise ValueError(
"'FullCorrelatedSpamCorrection' not characterised for Backend."
)
if len(results) != len(bit_qb_maps):
raise ValueError(
"Number of experiment results and Qubit to Bit maps do not match."
)
corrected_results = [
correct_transition_noise(r, qbm, char, corr_method)
for r, qbm in zip(results, bit_qb_maps)
]
return (corrected_results,)
return MitTask(
_label="SPAMFullCorrection", _n_in_wires=3, _n_out_wires=1, _method=task
)
def get_mid_circuit_measure_map(
circuit: Circuit, used_bits: Set[Bit] = set()
) -> Dict[Bit, Qubit]:
"""
For each circuit, gets all Measure commands and uses them to construct a dictionary
between Bit and Qubit.
:param circuit: Circuit to get dict between Bit measured and Qubit measured on.
:return: A dict between Bit measured and Qubit measured on.
"""
bit_to_qubit_map = dict()
for mc in circuit.commands_of_type(OpType.Measure):
bit = mc.bits[0]
if bit not in used_bits:
bit_to_qubit_map[bit] = mc.qubits[0]
return bit_to_qubit_map
[docs]
def gen_get_bit_maps_task() -> MitTask:
"""
Returns a task that takes a list of circuits and returns the circuits, and a map betwen
each circuit bit and the qubit it is measured on.
"""
def task(
obj, circuit_shots: List[CircuitShots]
) -> Tuple[List[CircuitShots], List[Tuple[Dict[Qubit, Bit], Dict[Bit, Qubit]]]]:
"""
:param circuits: Circuits to retrieve bit maps from.
:return: A tuple comprising the original circuits, and each circuits bit map.
"""
bq_maps = []
for c in circuit_shots:
qb_map = c[0].qubit_to_bit_map
# if condition met, implies that mid circuit measurement has ocurred and not accounted for
# in this case, iterate through circuit commands to get Qubits for all Bits
if len(qb_map) != len(c[0].bits):
bq_maps.append(
(qb_map, get_mid_circuit_measure_map(c[0], set(qb_map.values())))
)
else:
# else, just invert map for later correction
bq_maps.append((qb_map, dict()))
return (circuit_shots, bq_maps)
return MitTask(
_label="GetBitQubitMaps", _n_in_wires=1, _n_out_wires=2, _method=task
)