Source code for qiskit_experiments.library.tomography.basis.local_basis

# 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