# This code is part of Qiskit.
#
# (C) Copyright IBM 2022.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
"""
Circuit basis for tomography preparation and measurement circuits
"""
from typing import Sequence, Optional, Tuple, Union, List, Dict
import numpy as np
from qiskit.circuit import QuantumCircuit, Instruction
from qiskit.quantum_info.states.quantum_state import QuantumState
from qiskit.quantum_info.operators.base_operator import BaseOperator
from qiskit.quantum_info.operators.channel.quantum_channel import QuantumChannel
from qiskit.quantum_info import DensityMatrix, Statevector, Operator, SuperOp
from qiskit.exceptions import QiskitError
from .base_basis import PreparationBasis, MeasurementBasis
from .cache_method import cache_method, _method_cache_name
# Typing
Povm = Union[List[Statevector], List[DensityMatrix], QuantumChannel]
States = Union[List[QuantumState], Dict[Tuple[int, ...], QuantumState]]
[docs]
class LocalPreparationBasis(PreparationBasis):
"""Local tensor-product preparation basis.
This basis consists of a set of 1-qubit instructions which
are used to define a tensor-product basis on N-qubits.
"""
def __init__(
self,
name: str,
instructions: Optional[Sequence[Instruction]] = None,
default_states: Optional[States] = None,
qubit_states: Optional[Dict[Tuple[int, ...], States]] = None,
):
"""Initialize a fitter preparation basis.
Args:
name: a name to identify the basis.
instructions: list of 1-qubit instructions for preparing states
from the :math:`|0\\rangle` state.
default_states: Optional, default density matrices prepared by the
input instructions. If None these will be determined by
ideal simulation of the preparation instructions.
qubit_states: Optional, a dict with physical qubit keys and a list of
density matrices prepared by the list of basis instructions
for a specific qubit. The default states will be used for any
qubits not specified in this dict.
Raises:
QiskitError: If input states or instructions are not valid, or no
instructions or states are provided.
"""
if instructions is None and default_states is None and qubit_states is None:
raise QiskitError(
"LocalPreparationBasis must define at least one of instructions, "
"default_states, or qubit_states."
)
super().__init__(name)
# POVM element variables
self._instructions = _format_instructions(instructions)
self._default_states = _format_states(default_states, (0,), self._instructions)
self._qubit_states = _format_qubit_states(qubit_states)
self._custom_defaults = bool(default_states)
# Other attributes derived from povms and instructions
# that need initializing
self._qubits = set()
self._size = None
self._default_dim = None
self._qubit_dim = {}
self._hash = None
# Initialize attributes
self._initialize()
def __repr__(self):
return f"<{type(self).__name__}: {self.name}>"
def __hash__(self):
return self._hash
def __eq__(self, value):
return (
super().__eq__(value)
and self._size == getattr(value, "_size", None)
and self._default_dim == getattr(value, "_default_dim", None)
and self._custom_defaults == getattr(value, "_custom_defaults", None)
and self._qubits == getattr(value, "_qubits", None)
and self._qubit_dim == getattr(value, "_qubit_dim", None)
and self._instructions == getattr(value, "_instructions", None)
)
[docs]
def index_shape(self, qubits: Sequence[int]) -> Tuple[int, ...]:
return len(qubits) * (self._size,)
[docs]
def matrix_shape(self, qubits: Sequence[int]) -> Tuple[int, ...]:
qubits = tuple(qubits)
if len(qubits) > 1 and qubits in self._qubit_dim:
return self._qubit_dim[qubits]
dims = tuple()
for i in qubits:
dims += self._qubit_dim.get((i,), (self._default_dim,))
return dims
[docs]
def circuit(
self, index: Sequence[int], qubits: Optional[Sequence[int]] = None
) -> QuantumCircuit:
# pylint: disable = unused-argument
if not self._instructions:
raise NotImplementedError(
f"Basis {self.name} does not define circuits so can only be "
" used as a fitter basis for analysis."
)
return _tensor_product_circuit(self._instructions, index, self._name)
[docs]
def matrix(self, index: Sequence[int], qubits: Optional[Sequence[int]] = None):
# Convert args to hashable tuples
if qubits is None:
qubits = tuple(range(len(index)))
else:
qubits = tuple(qubits)
index = tuple(index)
try:
# Look for custom POVM for specified qubits
state = self._generate_qubits_state(index, qubits)
if state is not None:
return state.data
mat = np.eye(1)
for idx, qubit in zip(index, qubits):
qubit_state = self._generate_qubits_state((idx,), (qubit,))
mat = np.kron(qubit_state, mat)
return mat
except TypeError as ex:
# This occurs if basis is constructed with qubit_states
# kwarg but no default_states or instructions and is called for
# a qubit not in the specified kwargs.
raise ValueError(f"Invalid qubits for basis {self.name}") from ex
@cache_method()
def _generate_qubits_state(self, index: Tuple[int, ...], qubits: Tuple[int, ...]):
"""LRU cached function for returning POVMS"""
num_qubits = len(qubits)
# Check for N-qubit states
if qubits in self._qubit_states:
# Get states for specified qubits
# TODO: In the future we could add support for different orderings
# of qubits by permuting the returned POVMS
states = self._qubit_states[qubits]
if index in states:
return states[index]
# Look up custom 0 init state for specified qubits
# TODO: Add support for noisy instructions
if not self._instructions:
raise NotImplementedError(
f"Basis {self.name} does not define circuits to construct POVMs from"
)
key0 = num_qubits * (0,)
if key0 in states:
circuit = _tensor_product_circuit(self._instructions, index, self._name)
return _generate_state(circuit, states[key0])
# No match, so if 1-qubit use default, otherwise return None
if num_qubits == 1 and self._default_states:
return self._default_states[index]
return None
def _initialize(self):
"""Initialize dimension and num outcomes"""
if self._instructions:
self._size = len(self._instructions)
# Format default POVMs
if self._default_states:
default_state = next(iter(self._default_states.values()))
self._default_dim = np.prod(default_state.dims())
if self._size is None:
self._size = len(self._default_states)
elif len(self._default_states) != self._size:
raise QiskitError("Number of instructions and number of states must be equal.")
# Format qubit states
for qubits, states in self._qubit_states.items():
state = next(iter(states.values()))
num_qubits = len(qubits)
dims = state.dims()
if num_qubits == 1:
qubit_dim = (np.prod(dims),)
elif len(dims) == num_qubits:
qubit_dim = tuple(dims)
else:
# Assume all subsystems have the same dimension if the provided
# state dimension don't match number of qubits
ave_dim = np.prod(dims) ** (1 / num_qubits)
if int(ave_dim) != ave_dim:
raise QiskitError("Cannot infer unequal subsystem dimensions from input states")
qubit_dim = num_qubits * (int(ave_dim),)
self._qubit_dim[qubits] = qubit_dim
self._qubits.update(qubits)
# Pseudo hash value to make basis hashable for LRU cached functions
self._hash = hash(
(
type(self),
self._name,
self._size,
self._default_dim,
self._custom_defaults,
tuple(self._qubits),
tuple(sorted(self._qubit_dim.items())),
tuple(type(i) for i in self._instructions),
)
)
def __json_encode__(self):
value = {
"name": self._name,
"instructions": list(self._instructions) if self._instructions else None,
}
if self._custom_defaults:
value["default_states"] = self._default_states
if self._qubit_states:
value["qubit_states"] = self._qubit_states
return value
def __getstate__(self):
# override get state to skip class cache when pickling
state = self.__dict__.copy()
state.pop(_method_cache_name(self), None)
return state
[docs]
class LocalMeasurementBasis(MeasurementBasis):
"""Local tensor-product measurement basis.
This basis consists of a set of 1-qubit instructions which
are used to define a tensor-product basis on N-qubits to
rotate a desired multi-qubit measurement basis to the Z-basis
measurement.
"""
def __init__(
self,
name: str,
instructions: Optional[Sequence[Instruction]] = None,
default_povms: Optional[Sequence[Povm]] = None,
qubit_povms: Optional[Dict[Tuple[int, ...], Sequence[Povm]]] = None,
):
"""Initialize a fitter preparation basis.
Args:
name: a name to identity the basis.
instructions: list of instructions for rotating a desired
measurement basis to the standard :math:`Z^{\\otimes n}`
computational basis measurement.
default_povms: Optional, list if positive operators valued measures (POVM)
for of the measurement basis instructions. A POVM can be
input as a list of effects (Statevector or DensityMatrix)
for each possible measurement outcome of that basis, or as
a single QuantumChannel. For the channel case the effects
will be calculated by evolving the computation basis states
by the adjoint of the channel. If None the input instructions
will be used as the POVM channel.
qubit_povms: Optional, a dict with physical qubit keys and a list of POVMs
corresponding to each basis measurement instruction for the
specific qubit. The default POVMs will be used for any qubits
not specified in this dict.
Raises:
QiskitError: If the input instructions or POVMs are not valid, or if no
instructions or POVMs are provided.
"""
if instructions is None and default_povms is None and qubit_povms is None:
raise QiskitError(
"LocalMeasurementBasis must define at least one of instructions, "
"default_povms, or qubit_povms."
)
super().__init__(name)
# POVM element variables
self._instructions = _format_instructions(instructions)
self._default_povms = _format_default_povms(default_povms, self._instructions)
self._qubit_povms = _format_qubit_povms(qubit_povms)
self._custom_defaults = bool(default_povms)
# Other attributes derived from povms and instructions
# that need initializing
self._qubits = set()
self._size = None
self._default_num_outcomes = None
self._default_dim = None
self._qubit_num_outcomes = {}
self._qubit_dim = {}
self._hash = None
# Initialize attributes
self._initialize()
def __repr__(self):
return f"<{type(self).__name__}: {self.name}>"
def __hash__(self):
return self._hash
def __eq__(self, value):
return (
super().__eq__(value)
and self._size == getattr(value, "_size", None)
and self._default_dim == getattr(value, "_default_dim", None)
and self._default_num_outcomes == getattr(value, "_default_num_outcomes", None)
and self._custom_defaults == getattr(value, "_custom_defaults", None)
and self._qubit_dim == getattr(value, "_qubit_dim", None)
and self._qubit_num_outcomes == getattr(value, "_qubit_num_outcomes", None)
and self._qubits == getattr(value, "_qubits", None)
and self._instructions == getattr(value, "_instructions", None)
)
[docs]
def index_shape(self, qubits: Sequence[int]) -> Tuple[int, ...]:
return len(qubits) * (self._size,)
[docs]
def matrix_shape(self, qubits: Sequence[int]) -> Tuple[int, ...]:
qubits = tuple(qubits)
if len(qubits) > 1 and qubits in self._qubit_dim:
return self._qubit_dim[qubits]
dims = tuple()
for i in qubits:
dims += self._qubit_dim.get((i,), (self._default_dim,))
return dims
[docs]
def outcome_shape(self, qubits: Sequence[int]) -> Tuple[int, ...]:
qubits = tuple(qubits)
if qubits in self._qubit_num_outcomes:
return self._qubit_num_outcomes[qubits]
shape = tuple()
for i in qubits:
shape += self._qubit_num_outcomes.get((i,), (self._default_num_outcomes,))
return shape
[docs]
def circuit(self, index: Sequence[int], qubits: Optional[Sequence[int]] = None):
# pylint: disable = unused-argument
if not self._instructions:
raise NotImplementedError(
f"Basis {self.name} does not define circuits so can only be "
" used as a fitter basis for analysis."
)
circuit = _tensor_product_circuit(self._instructions, index, self._name)
circuit.measure_all()
return circuit
[docs]
def matrix(self, index: Sequence[int], outcome: int, qubits: Optional[Sequence[int]] = None):
# Convert args to hashable tuples
if qubits is None:
qubits = tuple(range(len(index)))
else:
qubits = tuple(qubits)
index = tuple(index)
try:
# Look for custom POVM for specified qubits
qubit_povm = self._generate_qubits_povm(index, qubits)
if qubit_povm:
return qubit_povm[outcome].data
# Otherwise construct tensor product POVM
outcome_index = self._outcome_indices(outcome, qubits)
mat = np.eye(1)
for idx, odx, qubit in zip(index, outcome_index, qubits):
povm = self._generate_qubits_povm((idx,), (qubit,))
mat = np.kron(povm[odx], mat)
return mat
except TypeError as ex:
# This occurs if basis is constructed with qubit_states
# kwarg but no default_states or instructions and is called for
# a qubit not in the specified kwargs.
raise ValueError(f"Invalid qubits for basis {self.name}") from ex
def _initialize(self):
"""Initialize dimension and num outcomes"""
if self._instructions:
self._size = len(self._instructions)
# Format default POVMs
if self._default_povms:
default_povm = next(iter(self._default_povms.values()))
self._default_num_outcomes = len(default_povm)
self._default_dim = np.prod(default_povm[0].dims())
if self._size is None:
self._size = len(self._default_povms)
elif len(self._default_povms) != self._size:
raise QiskitError("Number of instructions and number of states must be equal.")
if any(
len(povm) != self._default_num_outcomes for povm in self._default_povms.values()
):
raise QiskitError(
"LocalMeasurementBasis default POVM elements must all have "
"the same number of outcomes."
)
# Format qubit POVMS
for qubits, povms in self._qubit_povms.items():
povm = next(iter(povms.values()))
num_povms = len(povm)
if any(len(povm) != num_povms for povm in povms.values()):
raise QiskitError(
"LocalMeasurementBasis POVM elements must all have the "
"same number of outcomes."
)
num_qubits = len(qubits)
dims = povm[0].dims()
dim = np.prod(dims)
if num_qubits == 1:
qubit_dim = (dim,)
num_outcomes = (num_povms,)
elif len(dims) == num_qubits:
qubit_dim = tuple(dims)
if dim != num_povms:
raise QiskitError("POVMs dimensions don't match number of outcomes")
num_outcomes = qubit_dim
else:
# Assume all subsystems have the same dimension if the provided
# operator dimension don't match number of qubits
ave_dim = np.prod(dims) ** (1 / num_qubits)
if int(ave_dim) != ave_dim:
raise QiskitError("Cannot infer unequal subsystem dimensions from input POVMs")
qubit_dim = num_qubits * (int(ave_dim),)
ave_num_outcomes = num_povms ** (1 / num_qubits)
if int(ave_num_outcomes) != ave_num_outcomes:
raise QiskitError("Cannot infer unequal subsystem num_outcome from input POVMs")
num_outcomes = num_qubits * (int(ave_dim),)
self._qubit_num_outcomes[qubits] = num_outcomes
self._qubit_dim[qubits] = qubit_dim
self._qubits.update(qubits)
# Pseudo hash value to make basis hashable for LRU cached functions
self._hash = hash(
(
type(self),
self._name,
self._size,
self._default_dim,
self._default_num_outcomes,
self._custom_defaults,
tuple(self._qubits),
tuple(sorted(self._qubit_dim.items())),
tuple(sorted(self._qubit_num_outcomes.items())),
tuple(type(i) for i in self._instructions),
)
)
@cache_method()
def _outcome_indices(self, outcome: int, qubits: Tuple[int, ...]) -> Tuple[int, ...]:
"""Convert an outcome integer to a tuple of single-qubit outcomes"""
num_outcomes = np.prod(self._qubit_num_outcomes.get(qubits[:1], self._default_num_outcomes))
try:
value = (outcome % num_outcomes,)
if len(qubits) == 1:
return value
return value + self._outcome_indices(outcome // num_outcomes, qubits[1:])
except TypeError as ex:
raise ValueError("Invalid qubits for basis") from ex
@cache_method()
def _generate_qubits_povm(self, index: Tuple[int, ...], qubits: Tuple[int, ...]):
"""LRU cached function for returning POVMS"""
num_qubits = len(qubits)
if qubits in self._qubit_povms:
# Get POVMS for specified qubits
# TODO: In the future we could add support for different orderings
# of qubits by permuting the returned POVMS
povms = self._qubit_povms[qubits]
if index in povms:
return povms[index]
# Look up custom Z-default POVM for specified qubits
# TODO: Add support for noisy instructions
if not self._instructions:
raise NotImplementedError(
f"Basis {self.name} does not define circuits to construct POVMs from"
)
key0 = num_qubits * (0,)
if key0 in povms:
circuit = _tensor_product_circuit(self._instructions, index, self._name)
return _generate_povm(circuit, povms[key0])
# No match, so if 1-qubit use default, otherwise return None
if num_qubits == 1 and self._default_povms:
return self._default_povms[index[0]]
return None
def __json_encode__(self):
value = {
"name": self._name,
"instructions": self._instructions if self._instructions else None,
}
if self._custom_defaults:
value["default_povms"] = self._default_povms
if self._qubit_povms:
value["qubit_povms"] = self._qubit_povms
return value
def __getstate__(self):
# override get state to skip class cache when pickling
state = self.__dict__.copy()
state.pop(_method_cache_name(self), None)
return state
def _tensor_product_circuit(
instructions: Sequence[Instruction],
index: Sequence[int],
name: str = "",
) -> QuantumCircuit:
"""Return tensor product of 1-qubit basis instructions"""
size = len(instructions)
circuit = QuantumCircuit(len(index), name=f"{name}{list(index)}")
for i, elt in enumerate(index):
if elt >= size:
raise QiskitError("Invalid basis element index")
circuit.append(instructions[elt], [i])
return circuit
def _format_instructions(instructions: Sequence[any]) -> List[Instruction]:
"""Parse multiple input formats for list of instructions"""
ret = []
if instructions is None:
return ret
for inst in instructions:
# Convert to instructions if object is not an instruction
# This allows converting raw unitary matrices and other operator
# types like Pauli or Clifford into instructions.
if not isinstance(inst, Instruction):
if hasattr(inst, "to_instruction"):
inst = inst.to_instruction()
else:
inst = Operator(inst).to_instruction()
# Validate that instructions are single qubit
if inst.num_qubits != 1:
raise QiskitError(f"Input instruction {inst.name} is not a 1-qubit instruction.")
ret.append(inst)
return ret
def _generate_povm(
value: Union[List[DensityMatrix], Instruction, Operator, SuperOp],
default_z: Optional[List[DensityMatrix]] = None,
dims: Optional[Tuple[int, ...]] = None,
) -> List[DensityMatrix]:
"""Format a POVM into list of density matrix effects"""
# If already a list convert to DensityMatrix objects
if isinstance(value, (list, tuple)):
return [DensityMatrix(i, dims=dims) for i in value]
# Otherwise convert from operator/channel to POVM effects
try:
chan = Operator(value)
except QiskitError:
chan = SuperOp(value)
adjoint = chan.adjoint()
if dims is None:
dims = adjoint.input_dims()
if default_z is not None:
z_states = [DensityMatrix(i, dims) for i in default_z]
else:
z_states = [DensityMatrix.from_int(i, dims) for i in range(np.prod(dims))]
return [state.evolve(adjoint) for state in z_states]
def _format_default_povms(
default_povms: any, instructions: Optional[Sequence[Instruction]] = None
) -> Dict[Tuple[int, ...], List[DensityMatrix]]:
"Format default POVM data"
# Parse data into a dict
# Legacy data handling
if isinstance(default_povms, (list, tuple, np.ndarray)):
povms = dict(enumerate(default_povms))
elif isinstance(default_povms, dict):
povms = {int(key): val for key, val in default_povms.items()}
elif not default_povms:
povms = {}
# Add instructions to POVM dict if not specified in data
if instructions and len(povms) < len(instructions):
for i, inst in enumerate(instructions):
if i not in povms:
povms[i] = inst
# Look for default POVMs for Z
if 0 in povms and isinstance(povms[0], (list, tuple)):
default_z = povms[0]
else:
default_z = None
# Format remaining POVM values and update attribute
return {key: _generate_povm(val, default_z) for key, val in povms.items()}
def _format_qubit_povms(
qubit_povms: any,
) -> Dict[Tuple[int, ...], Dict[Tuple[int, ...], List[DensityMatrix]]]:
"""Format qubit POVMs dict"""
povms = {}
if not qubit_povms:
return povms
# Format POVM keys to be a tuple of (qubits, basis)
for qubits, povm in qubit_povms.items():
if isinstance(qubits, int):
qubits = (qubits,)
# Convert value to dict if not already
if not isinstance(povm, dict):
formatted_povm = {(i,): val for i, val in enumerate(povm)}
else:
# Format dict keys
formatted_povm = {}
for index, value in povm.items():
if isinstance(index, int):
index = (index,)
formatted_povm[tuple(index)] = value
# Add qubit POVM dict to povms
povms[qubits] = formatted_povm
# Format POVM values
for qubits, povm in povms.items():
# Convert any Z-povm value if present
key0 = len(qubits) * (0,)
if key0 in povm and isinstance(povm[key0], (list, tuple)):
default_z = povm[key0]
else:
default_z = None
# Convert any values from instructions/channels
# By applying to Z-povm
for key, value in povm.items():
povm[key] = _generate_povm(value, default_z)
return povms
def _generate_state(
value: Union[Statevector, DensityMatrix, Instruction, Operator, SuperOp],
init_state: Optional[QuantumState] = None,
dims: Optional[Tuple[int, ...]] = None,
) -> DensityMatrix:
"""Format a state into list of DensityMatrix"""
# If already a quantum state convert to a density matrix
if isinstance(value, (QuantumState, np.ndarray, list)):
return DensityMatrix(value, dims=dims)
# Otherwise convert from operator/channel to POVM effects
try:
chan = Operator(value)
except QiskitError:
chan = SuperOp(value)
if dims is None:
dims = chan.input_dims()
# Default |0> state density matrix
if init_state is None:
init_state = DensityMatrix.from_int(0, dims)
elif not isinstance(init_state, DensityMatrix):
init_state = DensityMatrix(init_state, dims)
return init_state.evolve(chan)
def _format_states(
states: Optional[Union[List[any], Dict[Tuple[int, ...], any]]],
init_key: Tuple[int, ...] = (0,),
instructions: Optional[Sequence[Instruction]] = None,
) -> Dict[Tuple[int, ...], DensityMatrix]:
"Format default state data"
# Parse data into dict to include legacy handling
states = _format_data_dict(states, instructions)
# Look for default state
init_val = states.get(init_key, None)
if isinstance(init_val, (QuantumState, np.ndarray, list)):
init_state = DensityMatrix(init_val)
else:
init_state = None
# Format remaining states and update attribute
return {key: _generate_state(val, init_state) for key, val in states.items()}
def _format_qubit_states(
qubit_states: any,
) -> Dict[Tuple[int, ...], Dict[Tuple[int, ...], DensityMatrix]]:
"""Format qubit POVMs dict"""
if not qubit_states:
return {}
# Format POVM keys to be a tuple of (qubits, basis)
formatted_states = {}
for qubits, states in qubit_states.items():
if isinstance(qubits, int):
qubits = (qubits,)
else:
qubits = tuple(qubits)
init_key = len(qubits) * (0,)
formatted_states[qubits] = _format_states(states, init_key)
return formatted_states
def _format_data_dict(
data: Optional[any], instructions: Optional[Sequence[Union[Instruction, BaseOperator]]] = None
) -> Dict[Tuple[int, ...], any]:
"Format default state data"
# Parse data into dict to include legacy handling
if isinstance(data, (list, tuple, np.ndarray)):
iter_data = enumerate(data)
elif isinstance(data, dict):
iter_data = data.items()
elif not data:
iter_data = {}.items()
else:
iter_data = data
# Format arg to tuple keys
dict_data = {((key,) if isinstance(key, int) else tuple(key)): val for key, val in iter_data}
# Add instructions for unspecified data
if instructions and len(dict_data) < len(instructions):
for i, inst in enumerate(instructions):
if (i,) not in dict_data:
dict_data[(i,)] = inst
return dict_data