diff --git a/qiskit_experiments/library/tomography/qpt_analysis.py b/qiskit_experiments/library/tomography/qpt_analysis.py index 08bffeee77..06f1eafc6e 100644 --- a/qiskit_experiments/library/tomography/qpt_analysis.py +++ b/qiskit_experiments/library/tomography/qpt_analysis.py @@ -83,11 +83,9 @@ def _default_options(cls) -> Options: rescale_trace (bool): If True rescale the state returned by the fitter have either trace 1 (Default: True). target (Union[str, :class:`~qiskit.quantum_info.operators.channel.quantum_channel`, - :class:`~qiskit.quantum_info.Operator`]): Set a custom target quantum + :class:`~qiskit.quantum_info.Operator`]): Optional, Set a custom target quantum channel for computing the :func:~qiskit.quantum_info.process_fidelity` of the - fitted process against. If ``"default"`` the ideal process corresponding for - the input circuit will be used. If ``None`` no fidelity will be computed - (Default: "default"). + fitted process against (Default: None). """ options = super()._default_options() options.measurement_basis = PauliMeasurementBasis() diff --git a/qiskit_experiments/library/tomography/qpt_experiment.py b/qiskit_experiments/library/tomography/qpt_experiment.py index f585345a2f..514c9f8b9f 100644 --- a/qiskit_experiments/library/tomography/qpt_experiment.py +++ b/qiskit_experiments/library/tomography/qpt_experiment.py @@ -14,8 +14,11 @@ """ from typing import Union, Optional, Iterable, List, Tuple, Sequence +import numpy as np from qiskit.circuit import QuantumCircuit, Instruction from qiskit.quantum_info.operators.base_operator import BaseOperator +from qiskit.quantum_info import Choi, Operator, Statevector, partial_trace +from qiskit_experiments.exceptions import QiskitError from .tomography_experiment import TomographyExperiment from .qpt_analysis import ProcessTomographyAnalysis from . import basis @@ -88,5 +91,50 @@ def __init__( preparation_qubits=preparation_qubits, basis_indices=basis_indices, qubits=qubits, + analysis=ProcessTomographyAnalysis(), ) - self.analysis = ProcessTomographyAnalysis() + + # Set target quantum channel + self.analysis.set_options(target=self._target_quantum_channel()) + + def _target_quantum_channel(self) -> Union[Choi, Operator]: + """Return the process tomography target""" + # Check if circuit contains measure instructions + # If so we cannot return target state + circuit_ops = self._circuit.count_ops() + if "measure" in circuit_ops: + return None + perm_circ = self._permute_circuit() + try: + if "reset" in circuit_ops or "kraus" in circuit_ops or "superop" in circuit_ops: + channel = Choi(perm_circ) + else: + channel = Operator(perm_circ) + except QiskitError: + # Circuit couldn't be simulated + return None + + total_qubits = self._circuit.num_qubits + if self._meas_qubits: + num_meas = len(self._meas_qubits) + else: + num_meas = total_qubits + if self._prep_qubits: + num_prep = len(self._prep_qubits) + else: + num_prep = total_qubits + + if num_prep == total_qubits and num_meas == total_qubits: + return channel + + # Trace out non-measurement subsystems + tr_qargs = [] + if self._prep_qubits: + tr_qargs += list(range(num_prep, total_qubits)) + if self._meas_qubits: + tr_qargs += list(range(total_qubits + num_meas, 2 * total_qubits)) + + chan_state = Statevector(np.ravel(channel, order="F")) + chan_state = partial_trace(chan_state, tr_qargs) / 2 ** (total_qubits - num_meas) + channel = Choi(chan_state.data, input_dims=[2] * num_prep, output_dims=[2] * num_meas) + return channel diff --git a/qiskit_experiments/library/tomography/qst_analysis.py b/qiskit_experiments/library/tomography/qst_analysis.py index 64cc0136a7..484913e158 100644 --- a/qiskit_experiments/library/tomography/qst_analysis.py +++ b/qiskit_experiments/library/tomography/qst_analysis.py @@ -79,11 +79,9 @@ def _default_options(cls) -> Options: rescale_trace (bool): If True rescale the state returned by the fitter have either trace 1 (Default: True). target (Union[str, :class:`~qiskit.quantum_info.DensityMatrix`, - :class:`~qiskit.quantum_info.Statevector`]): Set a custom target quantum - state for computing the :func:~qiskit.quantum_info.state_fidelity` of the - fitted state against. If ``"default"`` the ideal state corresponding for - the input circuit will be used. If ``None`` no fidelity will be computed - (Default: "default"). + :class:`~qiskit.quantum_info.Statevector`]): Optional, et a custom target + quantum state for computing the :func:~qiskit.quantum_info.state_fidelity` + of the fitted state against (Default: None). """ options = super()._default_options() options.measurement_basis = PauliMeasurementBasis() diff --git a/qiskit_experiments/library/tomography/qst_experiment.py b/qiskit_experiments/library/tomography/qst_experiment.py index 8ea6202ab6..e3a37d4547 100644 --- a/qiskit_experiments/library/tomography/qst_experiment.py +++ b/qiskit_experiments/library/tomography/qst_experiment.py @@ -16,7 +16,8 @@ from typing import Union, Optional, Iterable, List, Sequence from qiskit.circuit import QuantumCircuit, Instruction from qiskit.quantum_info.operators.base_operator import BaseOperator -from qiskit.quantum_info import Statevector +from qiskit.quantum_info import Statevector, DensityMatrix, partial_trace +from qiskit_experiments.exceptions import QiskitError from .tomography_experiment import TomographyExperiment from .qst_analysis import StateTomographyAnalysis from . import basis @@ -87,5 +88,38 @@ def __init__( measurement_qubits=measurement_qubits, basis_indices=basis_indices, qubits=qubits, + analysis=StateTomographyAnalysis(), ) - self.analysis = StateTomographyAnalysis() + + # Set target quantum state + self.analysis.set_options(target=self._target_quantum_state()) + + def _target_quantum_state(self) -> Union[Statevector, DensityMatrix]: + """Return the state tomography target""" + # Check if circuit contains measure instructions + # If so we cannot return target state + circuit_ops = self._circuit.count_ops() + if "measure" in circuit_ops: + return None + + perm_circ = self._permute_circuit() + try: + if "reset" in circuit_ops or "kraus" in circuit_ops or "superop" in circuit_ops: + state = DensityMatrix(perm_circ) + else: + state = Statevector(perm_circ) + except QiskitError: + # Circuit couldn't be simulated + return None + + total_qubits = self._circuit.num_qubits + if self._meas_qubits: + num_meas = len(self._meas_qubits) + else: + num_meas = total_qubits + if num_meas == total_qubits: + return state + + # Trace out non-measurement qubits + tr_qargs = range(num_meas, total_qubits) + return partial_trace(state, tr_qargs) diff --git a/qiskit_experiments/library/tomography/tomography_analysis.py b/qiskit_experiments/library/tomography/tomography_analysis.py index f5d95c7bf1..fbf780ac5c 100644 --- a/qiskit_experiments/library/tomography/tomography_analysis.py +++ b/qiskit_experiments/library/tomography/tomography_analysis.py @@ -77,7 +77,8 @@ def _default_options(cls) -> Options: rescale_trace (bool): If True rescale the state returned by the fitter have either trace 1 for :class:`~qiskit.quantum_info.DensityMatrix`, or trace dim for :class:`~qiskit.quantum_info.Choi` matrices (Default: True). - target (Any): depends on subclass. + target (Any): Optional, target object for fidelity comparison of the fit + (Default: None). """ options = super()._default_options() @@ -87,7 +88,7 @@ def _default_options(cls) -> Options: options.fitter_options = {} options.rescale_positive = True options.rescale_trace = True - options.target = "default" + options.target = None return options @classmethod @@ -107,12 +108,6 @@ def _run_analysis(self, experiment_data): experiment_data.data() ) - # Get target state - target_state = self.options.target - if target_state == "default": - metadata = experiment_data.metadata - target_state = metadata.get("target", None) - # Get tomography fitter function fitter = self._get_fitter(self.options.fitter) try: @@ -136,7 +131,7 @@ def _run_analysis(self, experiment_data): analysis_results = self._postprocess_fit( state, metadata=fitter_metadata, - target_state=target_state, + target_state=self.options.target, rescale_positive=self.options.rescale_positive, rescale_trace=self.options.rescale_trace, qpt=bool(self.options.preparation_basis), diff --git a/qiskit_experiments/library/tomography/tomography_experiment.py b/qiskit_experiments/library/tomography/tomography_experiment.py index 13534e1b3b..2966c47410 100644 --- a/qiskit_experiments/library/tomography/tomography_experiment.py +++ b/qiskit_experiments/library/tomography/tomography_experiment.py @@ -13,15 +13,12 @@ Quantum Tomography experiment """ -import copy from typing import Union, Optional, Iterable, List, Tuple, Sequence from itertools import product -import numpy as np from qiskit.circuit import QuantumCircuit, Instruction from qiskit.circuit.library import Permutation from qiskit.providers.backend import Backend from qiskit.quantum_info.operators.base_operator import BaseOperator -import qiskit.quantum_info as qi from qiskit_experiments.exceptions import QiskitError from qiskit_experiments.framework import BaseExperiment, Options from .basis import BaseTomographyMeasurementBasis, BaseTomographyPreparationBasis @@ -63,6 +60,7 @@ def __init__( preparation_qubits: Optional[Sequence[int]] = None, basis_indices: Optional[Iterable[Tuple[List[int], List[int]]]] = None, qubits: Optional[Sequence[int]] = None, + analysis: Optional[TomographyAnalysis] = None, ): """Initialize a tomography experiment. @@ -79,6 +77,8 @@ def __init__( basis_indices: Optional, the basis elements to be measured. If None All basis elements will be measured. qubits: Optional, the physical qubits for the initial state circuit. + analysis: Optional, analysis class to use for experiment. If None the default + tomography analysis will be used. Raises: QiskitError: if input params are invalid. @@ -86,7 +86,9 @@ def __init__( # Initialize BaseExperiment if qubits is None: qubits = range(circuit.num_qubits) - super().__init__(qubits, analysis=TomographyAnalysis(), backend=backend) + if analysis is None: + analysis = TomographyAnalysis() + super().__init__(qubits, analysis=analysis, backend=backend) # Get the target tomography circuit if isinstance(circuit, QuantumCircuit): @@ -128,19 +130,6 @@ def __init__( if basis_indices: self.set_experiment_options(basis_indices=basis_indices) - # Compute target state - self._target = None - if self._prep_circ_basis: - self._target = self._target_quantum_channel( - self._circuit, - measurement_qubits=self._meas_qubits, - preparation_qubits=self._prep_qubits, - ) - else: - self._target = self._target_quantum_state( - self._circuit, measurement_qubits=self._meas_qubits - ) - # Configure analysis basis options analysis_options = {} if measurement_basis: @@ -149,12 +138,6 @@ def __init__( analysis_options["preparation_basis"] = preparation_basis self.analysis.set_options(**analysis_options) - def _metadata(self): - metadata = super()._metadata() - if self._target: - metadata["target"] = copy.copy(self._target) - return metadata - def circuits(self): # Get qubits and clbits @@ -221,131 +204,41 @@ def _basis_indices(self): return product(prep_elements, meas_elements) - @staticmethod - def _permute_circuit( - circuit: QuantumCircuit, - measurement_qubits: Optional[Sequence[int]] = None, - preparation_qubits: Optional[Sequence[int]] = None, - ): + def _permute_circuit(self) -> QuantumCircuit: """Permute circuit qubits. This permutes the circuit so that the specified preparation and measurement qubits correspond to input and output qubits [0, ..., N-1] respectively for the returned circuit. """ - if measurement_qubits is None and preparation_qubits is None: - return circuit + if self._meas_qubits is None and self._prep_qubits is None: + return self._circuit - total_qubits = circuit.num_qubits - total_clbits = circuit.num_clbits + total_qubits = self._circuit.num_qubits + total_clbits = self._circuit.num_clbits if total_clbits: perm_circ = QuantumCircuit(total_qubits, total_clbits) else: perm_circ = QuantumCircuit(total_qubits) # Apply permutation to put prep qubits as [0, ..., M-1] - if preparation_qubits: - prep_qargs = list(preparation_qubits) - if len(preparation_qubits) != total_qubits: - prep_qargs += [i for i in range(total_qubits) if i not in preparation_qubits] + if self._prep_qubits: + prep_qargs = list(self._prep_qubits) + if len(self._prep_qubits) != total_qubits: + prep_qargs += [i for i in range(total_qubits) if i not in self._prep_qubits] perm_circ.append(Permutation(total_qubits, prep_qargs).inverse(), range(total_qubits)) # Apply original circuit if total_clbits: - perm_circ = perm_circ.compose(circuit, range(total_qubits), range(total_clbits)) + perm_circ = perm_circ.compose(self._circuit, range(total_qubits), range(total_clbits)) else: - perm_circ = perm_circ.compose(circuit, range(total_qubits)) + perm_circ = perm_circ.compose(self._circuit, range(total_qubits)) # Apply permutation to put meas qubits as [0, ..., M-1] - if measurement_qubits: - meas_qargs = list(measurement_qubits) - if len(measurement_qubits) != total_qubits: - meas_qargs += [i for i in range(total_qubits) if i not in measurement_qubits] + if self._meas_qubits: + meas_qargs = list(self._meas_qubits) + if len(self._meas_qubits) != total_qubits: + meas_qargs += [i for i in range(total_qubits) if i not in self._meas_qubits] perm_circ.append(Permutation(total_qubits, meas_qargs), range(total_qubits)) return perm_circ - - @classmethod - def _target_quantum_state( - cls, circuit: QuantumCircuit, measurement_qubits: Optional[Sequence[int]] = None - ): - """Return the state tomography target""" - # Check if circuit contains measure instructions - # If so we cannot return target state - circuit_ops = circuit.count_ops() - if "measure" in circuit_ops: - return None - - perm_circ = cls._permute_circuit(circuit, measurement_qubits=measurement_qubits) - - try: - if "reset" in circuit_ops or "kraus" in circuit_ops or "superop" in circuit_ops: - state = qi.DensityMatrix(perm_circ) - else: - state = qi.Statevector(perm_circ) - except QiskitError: - # Circuit couldn't be simulated - return None - - total_qubits = circuit.num_qubits - if measurement_qubits: - num_meas = len(measurement_qubits) - else: - num_meas = total_qubits - if num_meas == total_qubits: - return state - - # Trace out non-measurement qubits - tr_qargs = range(num_meas, total_qubits) - return qi.partial_trace(state, tr_qargs) - - @classmethod - def _target_quantum_channel( - cls, - circuit: QuantumCircuit, - measurement_qubits: Optional[Sequence[int]] = None, - preparation_qubits: Optional[Sequence[int]] = None, - ): - """Return the process tomography target""" - # Check if circuit contains measure instructions - # If so we cannot return target state - circuit_ops = circuit.count_ops() - if "measure" in circuit_ops: - return None - - perm_circ = cls._permute_circuit( - circuit, measurement_qubits=measurement_qubits, preparation_qubits=preparation_qubits - ) - try: - if "reset" in circuit_ops or "kraus" in circuit_ops or "superop" in circuit_ops: - channel = qi.Choi(perm_circ) - else: - channel = qi.Operator(perm_circ) - except QiskitError: - # Circuit couldn't be simulated - return None - - total_qubits = circuit.num_qubits - if measurement_qubits: - num_meas = len(measurement_qubits) - else: - num_meas = total_qubits - if preparation_qubits: - num_prep = len(preparation_qubits) - else: - num_prep = total_qubits - - if num_prep == total_qubits and num_meas == total_qubits: - return channel - - # Trace out non-measurement subsystems - tr_qargs = [] - if preparation_qubits: - tr_qargs += list(range(num_prep, total_qubits)) - if measurement_qubits: - tr_qargs += list(range(total_qubits + num_meas, 2 * total_qubits)) - - chan_state = qi.Statevector(np.ravel(channel, order="F")) - chan_state = qi.partial_trace(chan_state, tr_qargs) / 2 ** (total_qubits - num_meas) - channel = qi.Choi(chan_state.data, input_dims=[2] * num_prep, output_dims=[2] * num_meas) - return channel diff --git a/test/test_tomography.py b/test/test_tomography.py index 8cd8b80ce2..a05ead80f8 100644 --- a/test/test_tomography.py +++ b/test/test_tomography.py @@ -136,9 +136,8 @@ def test_exp_circuits_measurement_qubits(self, meas_qubits): clbits = meta.get("clbits") self.assertEqual(clbits, list(range(num_meas)), msg="metadata clbits is incorrect") - # Check experiment target metadata is correct - exp_meta = exp._metadata() - target_state = exp_meta.get("target") + # Check analysis target is correct + target_state = exp.analysis.options.target target_circ = QuantumCircuit(num_meas) for i, qubit in enumerate(meas_qubits): @@ -344,9 +343,8 @@ def test_exp_measurement_preparation_qubits(self, qubits): clbits = meta.get("clbits") self.assertEqual(clbits, list(range(num_meas)), msg="metadata clbits is incorrect") - # Check experiment target metadata is correct - exp_meta = exp._metadata() - target_state = exp_meta.get("target") + # Check analysis target is correct + target_state = exp.analysis.options.target target_circ = QuantumCircuit(num_meas) for i, qubit in enumerate(qubits):