From c0dcd99c334ba5d9407ca4313933a4c70a392db2 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Thu, 2 Sep 2021 01:46:16 +0900 Subject: [PATCH 01/29] split run into transpile/execute/analysis and add hooks --- .../framework/base_experiment.py | 135 +++++++++++++----- .../library/characterization/t1.py | 20 +++ .../library/characterization/t2ramsey.py | 20 +++ .../randomized_benchmarking/rb_experiment.py | 14 +- 4 files changed, 146 insertions(+), 43 deletions(-) diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index 4240ae91c1..db020227c7 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -109,43 +109,25 @@ def run( run_opts.update_options(**run_options) run_opts = run_opts.__dict__ - # Scheduling parameters - if backend.configuration().simulator is False and isinstance(backend, FakeBackend) is False: - timing_constraints = getattr(self.transpile_options.__dict__, "timing_constraints", {}) - timing_constraints["acquire_alignment"] = getattr( - timing_constraints, "acquire_alignment", 16 - ) - scheduling_method = getattr( - self.transpile_options.__dict__, "scheduling_method", "alap" - ) - self.set_transpile_options( - timing_constraints=timing_constraints, scheduling_method=scheduling_method - ) - # Generate and transpile circuits - transpile_opts = copy.copy(self.transpile_options.__dict__) - transpile_opts["initial_layout"] = list(self._physical_qubits) - circuits = transpile(self.circuits(backend), backend, **transpile_opts) - self._postprocess_transpiled_circuits(circuits, backend, **run_options) + circuits = self.run_transpile(backend) + # Execute experiment if isinstance(backend, LegacyBackend): qobj = assemble(circuits, backend=backend, **run_opts) job = backend.run(qobj) else: job = backend.run(circuits, **run_opts) - # Add Job to ExperimentData and add analysis for post processing. - run_analysis = None - # Add experiment option metadata self._add_job_metadata(experiment_data, job, **run_opts) - if analysis and self.__analysis_class__ is not None: - run_analysis = self.run_analysis - - experiment_data.add_data(job, post_processing_callback=run_analysis) + # Run analysis + if analysis: + experiment_data = self.run_analysis(experiment_data, job) + else: + experiment_data.add_data(job) - # Return the ExperimentData future return experiment_data def _initialize_experiment_data( @@ -167,29 +149,110 @@ def _initialize_experiment_data( return experiment_data._copy_metadata() - def run_analysis(self, experiment_data, **options) -> ExperimentData: + def _pre_transpile_hook(self, backend: Backend): + """An extra transpile subroutine executed before transpilation. + + Args: + backend: Target backend. + """ + pass + + def _post_transpile_hook( + self, circuits: List[QuantumCircuit], backend: Backend + ) -> List[QuantumCircuit]: + """An extra transpile subroutine executed after transpilation. + + Args: + circuits: List of transpiled circuits. + backend: Target backend. + + Returns: + List of circuits to execute. + """ + return circuits + + def run_transpile(self, backend: Backend, **options): + """Run transpile and returns transpiled circuits. + + Args: + backend: Target backend. + + Returns: + Transpiled circuit to execute. + """ + # Run pre transpile. This is implemented by each experiment subclass. + self._pre_transpile_hook(backend) + + # Get transpile options + transpile_options = copy.copy(self.transpile_options) + transpile_options.update_options( + initial_layout=list(self._physical_qubits), + **options, + ) + transpile_options = transpile_options.__dict__ + + circuits = transpile(circuits=self.circuits(backend), backend=backend, **transpile_options) + + # Run post transpile. This is implemented by each experiment subclass. + circuits = self._post_transpile_hook(circuits, backend) + + return circuits + + def _post_analysis_hook(self, experiment_data: ExperimentData): + """An extra analysis subroutine executed after analysis. + + Args: + experiment_data: A future object of the experiment result. + + Note: + The experiment_data may contain a future object as an experiment result + and the previous analysis routine has not completed yet. + If the hook should be executed immediately, call :meth:`block_for_results` method + before starting the data processing code. + """ + pass + + def run_analysis( + self, experiment_data: ExperimentData, job: BaseJob = None, **options + ) -> ExperimentData: """Run analysis and update ExperimentData with analysis result. Args: - experiment_data (ExperimentData): the experiment data to analyze. - options: additional analysis options. Any values set here will - override the value from :meth:`analysis_options` - for the current run. + experiment_data: The experiment data to analyze. + job: The future object of experiment result which is currently running on the backend. + options: Additional analysis options. Any values set here will + override the value from :meth:`analysis_options` for the current run. Returns: An experiment data object containing the analysis results and figures. Raises: - QiskitError: if experiment_data container is not valid for analysis. + QiskitError: Method is called with an empty experiment result. """ + run_analysis = self.analysis().run if self.__analysis_class__ else None + # Get analysis options analysis_options = copy.copy(self.analysis_options) analysis_options.update_options(**options) analysis_options = analysis_options.__dict__ - # Run analysis - analysis = self.analysis() - analysis.run(experiment_data, **analysis_options) + if not job and run_analysis is not None: + # Run analysis immediately + if not experiment_data.data(): + raise QiskitError( + "Experiment data seems to be empty and no running job is provided. " + "At least one data entry is required to run analysis." + ) + experiment_data = run_analysis(experiment_data, **analysis_options) + else: + # Run analysis when job is completed + experiment_data.add_data( + job, post_processing_callback=run_analysis, **analysis_options + ) + + # Run post analysis. This is implemented by each experiment subclass. + self._post_analysis_hook(experiment_data) + return experiment_data @property @@ -335,10 +398,6 @@ def set_analysis_options(self, **fields): """ self._analysis_options.update_options(**fields) - def _postprocess_transpiled_circuits(self, circuits, backend, **run_options): - """Additional post-processing of transpiled circuits before running on backend""" - pass - def _metadata(self) -> Dict[str, any]: """Return experiment metadata for ExperimentData. diff --git a/qiskit_experiments/library/characterization/t1.py b/qiskit_experiments/library/characterization/t1.py index d03b9761c1..8033d2cc26 100644 --- a/qiskit_experiments/library/characterization/t1.py +++ b/qiskit_experiments/library/characterization/t1.py @@ -17,6 +17,7 @@ import numpy as np from qiskit.providers import Backend +from qiskit.test.mock import FakeBackend from qiskit.circuit import QuantumCircuit from qiskit_experiments.framework import BaseExperiment, Options @@ -131,3 +132,22 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: circuits.append(circ) return circuits + + def _pre_transpile_hook(self, backend: Backend): + """Set timing constraints if backend is real hardware.""" + + if not backend.configuration().simulator and not isinstance(backend, FakeBackend): + timing_constraints = getattr(self.transpile_options.__dict__, "timing_constraints", {}) + + # alignment=16 is IBM standard. Will be soon provided by IBM providers. + # Then, this configuration can be removed. + timing_constraints["acquire_alignment"] = getattr( + timing_constraints, "acquire_alignment", 16 + ) + + scheduling_method = getattr( + self.transpile_options.__dict__, "scheduling_method", "alap" + ) + self.set_transpile_options( + timing_constraints=timing_constraints, scheduling_method=scheduling_method + ) diff --git a/qiskit_experiments/library/characterization/t2ramsey.py b/qiskit_experiments/library/characterization/t2ramsey.py index 3b1750c138..e51d8a7aac 100644 --- a/qiskit_experiments/library/characterization/t2ramsey.py +++ b/qiskit_experiments/library/characterization/t2ramsey.py @@ -20,6 +20,7 @@ import qiskit from qiskit.utils import apply_prefix from qiskit.providers import Backend +from qiskit.test.mock import FakeBackend from qiskit.circuit import QuantumCircuit from qiskit.providers.options import Options from qiskit_experiments.framework import BaseExperiment @@ -152,3 +153,22 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: circuits.append(circ) return circuits + + def _pre_transpile_hook(self, backend: Backend): + """Set timing constraints if backend is real hardware.""" + + if not backend.configuration().simulator and not isinstance(backend, FakeBackend): + timing_constraints = getattr(self.transpile_options.__dict__, "timing_constraints", {}) + + # alignment=16 is IBM standard. Will be soon provided by IBM providers. + # Then, this configuration can be removed. + timing_constraints["acquire_alignment"] = getattr( + timing_constraints, "acquire_alignment", 16 + ) + + scheduling_method = getattr( + self.transpile_options.__dict__, "scheduling_method", "alap" + ) + self.set_transpile_options( + timing_constraints=timing_constraints, scheduling_method=scheduling_method + ) diff --git a/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py b/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py index bf9f3b29e1..59808ef878 100644 --- a/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py +++ b/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py @@ -220,12 +220,16 @@ def _get_circuit_metadata(self, circuit): return meta return None - def _postprocess_transpiled_circuits(self, circuits, backend, **run_options): - """Additional post-processing of transpiled circuits before running on backend""" - for c in circuits: - meta = self._get_circuit_metadata(c) + def _post_transpile_hook( + self, circuits: List[QuantumCircuit], backend: Backend + ) -> List[QuantumCircuit]: + """Count gate operations in each circuit and update metadata.""" + for circuit in circuits: + meta = self._get_circuit_metadata(circuit) if meta is not None: - c_count_ops = RBUtils.count_ops(c, self.physical_qubits) + c_count_ops = RBUtils.count_ops(circuit, self.physical_qubits) circuit_length = meta["xval"] count_ops = [(key, (value, circuit_length)) for key, value in c_count_ops.items()] meta.update({"count_ops": count_ops}) + + return circuits From 5724afa18227919bcc372d15d58fe6c3fb21fa87 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 7 Sep 2021 17:50:22 +0900 Subject: [PATCH 02/29] update composite experiment to retain hooks and configurations --- .../framework/base_experiment.py | 11 +- .../framework/composite/batch_experiment.py | 92 +++++---- .../composite/composite_experiment.py | 94 ++++++++- .../composite/parallel_experiment.py | 190 ++++++++++++------ 4 files changed, 276 insertions(+), 111 deletions(-) diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index db020227c7..2ffcb510e5 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -22,7 +22,6 @@ from qiskit.providers import BaseJob from qiskit.providers.backend import Backend from qiskit.providers.basebackend import BaseBackend as LegacyBackend -from qiskit.test.mock import FakeBackend from qiskit.exceptions import QiskitError from qiskit.qobj.utils import MeasLevel from qiskit_experiments.framework import Options @@ -96,10 +95,6 @@ def run( Returns: The experiment data object. - - Raises: - QiskitError: if experiment is run with an incompatible existing - ExperimentData container. """ # Create experiment data container experiment_data = self._initialize_experiment_data(backend, experiment_data) @@ -150,7 +145,7 @@ def _initialize_experiment_data( return experiment_data._copy_metadata() def _pre_transpile_hook(self, backend: Backend): - """An extra transpile subroutine executed before transpilation. + """An extra subroutine executed before transpilation. Args: backend: Target backend. @@ -160,7 +155,7 @@ def _pre_transpile_hook(self, backend: Backend): def _post_transpile_hook( self, circuits: List[QuantumCircuit], backend: Backend ) -> List[QuantumCircuit]: - """An extra transpile subroutine executed after transpilation. + """An extra subroutine executed after transpilation. Args: circuits: List of transpiled circuits. @@ -199,7 +194,7 @@ def run_transpile(self, backend: Backend, **options): return circuits def _post_analysis_hook(self, experiment_data: ExperimentData): - """An extra analysis subroutine executed after analysis. + """An extra subroutine executed after analysis. Args: experiment_data: A future object of the experiment result. diff --git a/qiskit_experiments/framework/composite/batch_experiment.py b/qiskit_experiments/framework/composite/batch_experiment.py index c6d0a71ada..4fe138ec25 100644 --- a/qiskit_experiments/framework/composite/batch_experiment.py +++ b/qiskit_experiments/framework/composite/batch_experiment.py @@ -13,15 +13,51 @@ Batch Experiment class. """ -from collections import OrderedDict +from typing import List from qiskit import QuantumCircuit + from .composite_experiment import CompositeExperiment class BatchExperiment(CompositeExperiment): - """Batch experiment class""" + """Batch experiment class. + + This experiment takes multiple experiment instances and generates + a list of flattened circuit to execute. + The experimental circuits are executed ony be one on the target backend as a single job. + + If an experiment analysis needs results of different types of experiments, + ``BatchExperiment`` may be convenient to describe the flow of the entire experiment. + + The experimental result of ``i``-th experiment can be accessed by + + .. code-block:: python3 + + batch_exp = BatchExperiment([exp1, exp2, exp3]) + batch_result = batch_exp.run(backend) + + exp1_res = batch_result.component_experiment_data(0) # data of exp1 + exp2_res = batch_result.component_experiment_data(1) # data of exp2 + exp3_res = batch_result.component_experiment_data(2) # data of exp3 + + One can also create a custom analysis class that estimates some parameters by + combining above analysis results. Here the ``exp*_res`` is a single + :py:class:`~qiskit_experiments.framework.experiment_data.ExperimentData` class of + a standard experiment, and the associated analysis will be performed once the batch job + is completed. Thus analyzed parameter value of each experiment can be obtained as usual. + + .. code-block:: python3 + + param_x = exp1_res.analysis_results("target_parameter_x") + param_y = exp2_res.analysis_results("target_parameter_y") + param_z = exp3_res.analysis_results("target_parameter_z") + + param_xyz = param_x + param_y + param_z # do some computation + + The final parameter ``param_xyz`` can be returned as an outcome of this batch experiment. + """ def __init__(self, experiments): """Initialize a batch experiment. @@ -29,46 +65,28 @@ def __init__(self, experiments): Args: experiments (List[BaseExperiment]): a list of experiments. """ - - # Generate qubit map - self._qubit_map = OrderedDict() - logical_qubit = 0 - for expr in experiments: - for physical_qubit in expr.physical_qubits: - if physical_qubit not in self._qubit_map: - self._qubit_map[physical_qubit] = logical_qubit - logical_qubit += 1 - qubits = tuple(self._qubit_map.keys()) + qubits = sorted(set(sum([list(expr.physical_qubits) for expr in experiments], []))) super().__init__(experiments, qubits) - def circuits(self, backend=None): + def _flatten_circuits( + self, + circuits: List[List[QuantumCircuit]], + num_qubits: int, + ) -> List[QuantumCircuit]: + """Flatten circuits. + Note: + This experiment concatenates sub experiment circuits. + """ batch_circuits = [] - # Generate data for combination - for index, expr in enumerate(self._experiments): - if self.physical_qubits == expr.physical_qubits: - qubit_mapping = None - else: - qubit_mapping = [self._qubit_map[qubit] for qubit in expr.physical_qubits] - for circuit in expr.circuits(backend): - # Update metadata - circuit.metadata = { + for expr_idx, sub_circs in enumerate(circuits): + for sub_circ in sub_circs: + sub_circ.metadata = { "experiment_type": self._type, - "composite_metadata": [circuit.metadata], - "composite_index": [index], + "composite_index": [expr_idx], + "composite_metadata": sub_circ.metadata, } - # Remap qubits if required - if qubit_mapping: - circuit = self._remap_qubits(circuit, qubit_mapping) - batch_circuits.append(circuit) - return batch_circuits + batch_circuits.append(sub_circ) - def _remap_qubits(self, circuit, qubit_mapping): - """Remap qubits if physical qubit layout is different to batch layout""" - num_qubits = self.num_qubits - num_clbits = circuit.num_clbits - new_circuit = QuantumCircuit(num_qubits, num_clbits, name="batch_" + circuit.name) - new_circuit.metadata = circuit.metadata - new_circuit.append(circuit, qubit_mapping, list(range(num_clbits))) - return new_circuit + return batch_circuits diff --git a/qiskit_experiments/framework/composite/composite_experiment.py b/qiskit_experiments/framework/composite/composite_experiment.py index b58a642fe4..9b05e05b12 100644 --- a/qiskit_experiments/framework/composite/composite_experiment.py +++ b/qiskit_experiments/framework/composite/composite_experiment.py @@ -14,9 +14,13 @@ """ from abc import abstractmethod +from typing import List import warnings -from qiskit_experiments.framework import BaseExperiment +from qiskit import QuantumCircuit +from qiskit.providers import Backend, BaseJob +from qiskit.exceptions import QiskitError +from qiskit_experiments.framework import BaseExperiment, ExperimentData from .composite_experiment_data import CompositeExperimentData from .composite_analysis import CompositeAnalysis @@ -40,10 +44,91 @@ def __init__(self, experiments, qubits, experiment_type=None): self._num_experiments = len(experiments) super().__init__(qubits, experiment_type=experiment_type) + def run_transpile(self, backend: Backend, **options): + """Run transpile and returns transpiled circuits. + + Args: + backend: Target backend. + + Returns: + Transpiled circuit to execute. + + Note: + This is transpile method for the composite experiment subclass. + This internally calls the transpile method of the nested experiments and + flattens the list of sub circuits generated by each experiment. + Note that transpile is called for individual circuit, and thus transpile + configurations and hook methods are separately applied. + + No transpile configuration assumed for composite experiment object itself. + """ + # transpile each sub experiment circuit + sub_circs = list(map(lambda expr: expr.run_transpile(backend), self._experiments)) + n_qubits = max(sub_circ.num_qubits for sub_circ in sub_circs) + + # merge circuits + return self._flatten_circuits(sub_circs, n_qubits) + + def run_analysis( + self, experiment_data: ExperimentData, job: BaseJob = None, **options + ) -> ExperimentData: + """Run analysis and update ExperimentData with analysis result. + + Args: + experiment_data: The experiment data to analyze. + job: The future object of experiment result which is currently running on the backend. + options: Additional analysis options. Any values set here will + override the value from :meth:`analysis_options` for the current run. + + Returns: + An experiment data object containing the analysis results and figures. + + Note: + This is analysis method for the composite experiment subclass. + This internally calls the analysis method of the nested experiments and + outputs a representative data entry for the composite analysis. + Note that analysis is called for individual experiment data, and thus analysis + configurations and hook methods are separately applied. + + No analysis configuration assumed for composite experiment object itself. + """ + if not isinstance(experiment_data, CompositeExperimentData): + raise QiskitError("CompositeAnalysis must be run on CompositeExperimentData.") + + if len(options) > 0: + warnings.warn( + f"Analysis options for the composite experiment are provided: {options}. " + "Note that the provided options will override every analysis of an experiment" + "associated with this composite experiment.", + UserWarning, + ) + + return super().run_analysis(experiment_data, job, **options) + @abstractmethod - def circuits(self, backend=None): + def _flatten_circuits( + self, + circuits: List[List[QuantumCircuit]], + num_qubits: int, + ) -> List[QuantumCircuit]: + """An abstract method to control merger logic of sub experiments.""" pass + def circuits(self, backend=None): + """Composite experiment does not provide this method. + + Args: + backend: The targe backend. + + Raises: + QiskitError: When this method is called. + """ + raise QiskitError( + f"{self.__class__.__name__} does not generate experimental circuits by itself. " + "Call the corresponding method of individual experiment class to find circuits, " + "or call `run_transpile` method to get circuits run on the target backend." + ) + @property def num_experiments(self): """Return the number of sub experiments""" @@ -84,8 +169,3 @@ def _add_job_metadata(self, experiment_data, job, **run_options): ) sub_data = experiment_data.component_experiment_data(i) sub_exp._add_job_metadata(sub_data, job, **run_options) - - def _postprocess_transpiled_circuits(self, circuits, backend, **run_options): - for expr in self._experiments: - if not isinstance(expr, CompositeExperiment): - expr._postprocess_transpiled_circuits(circuits, backend, **run_options) diff --git a/qiskit_experiments/framework/composite/parallel_experiment.py b/qiskit_experiments/framework/composite/parallel_experiment.py index 286b6c8efc..7bc9262942 100644 --- a/qiskit_experiments/framework/composite/parallel_experiment.py +++ b/qiskit_experiments/framework/composite/parallel_experiment.py @@ -13,13 +13,100 @@ Parallel Experiment class. """ +import itertools +from collections import defaultdict +from typing import List + from qiskit import QuantumCircuit, ClassicalRegister from .composite_experiment import CompositeExperiment class ParallelExperiment(CompositeExperiment): - """Parallel Experiment class""" + """Parallel Experiment class. + + This experiment takes multiple experiment instances and generates + a list of merged circuit to execute. + The experimental circuits are executed in parallel on the target backend. + + This experiment is often used to simultaneously execute the same experiment on + different qubit sets. For example, given we write a simple experiment + ``BitFlipExperiment`` that flips the input quantum state and measure it. + Then we feed multiple experiments of this kind into ``ParallelExperiment``. + + .. code-block:: python3 + + exp1 = BitFlipExperiment(qubits=[0]) + exp2 = BitFlipExperiment(qubits=[1]) + + parallel_exp = BatchExperiment([exp1, exp2]) + + The direct transpiled output of ``exp1`` and ``exp2`` may look like + + .. parsed-literal:: + + # exp1.run_transpile(backend) + + ┌───┐┌─┐ + q_0: ┤ X ├┤M├ + └───┘└╥┘ + q_1: ──────╫─ + ║ + c: 1/══════╩═ + 0 + + # exp2.run_transpile(backend) + + q_0: ──────── + ┌───┐┌─┐ + q_1: ┤ X ├┤M├ + └───┘└╥┘ + c: 1/══════╩═ + 0 + + The ``ParallelExperiment`` merges these circuits into a single circuit like below. + + .. parsed-literal:: + + # parallel_exp.run_transpile(backend) + + ┌───┐┌─┐ + q_0: ┤ X ├┤M├─── + ├───┤└╥┘┌─┐ + q_1: ┤ X ├─╫─┤M├ + └───┘ ║ └╥┘ + c0: 1/══════╩══╬═ + 0 ║ + ║ + c1: 1/═════════╩═ + 0 + + This parallel execution may save us from waiting a job queueing time for each experiment, + at the expense of some extra crosstalk error possibility. + + The experimental result of ``i``-th experiment can be accessed by + + .. code-block:: python3 + + parallel_result = parallel_exp.run(backend) + + exp1_res = parallel_result.component_experiment_data(0) # data of exp1 + exp2_res = parallel_result.component_experiment_data(1) # data of exp2 + + Note that we can also combine different types of experiments, even if the length of + generated circuits are different. If two experiment instances provide different + length of circuits, for example, ``exp1`` and ``exp2`` create 10 and 20 circuits, + respectively, the merged circuit will have the length of 20, and circuits with index + from 10 to 19 will contain only experimental circuits of ``exp2``. + + Note: + The transpile and analysis configurations of each experiment will be retained, + however, the run configurations of nested experiments will be discarded. + For example, if a backend provides a run option ``meas_level`` controlling a + qubit state discrimination, we cannot set ``meas_level=1`` for ``exp1`` and + ``meas_level=2`` for ``exp2``. Parallelized experiment will always be + executed under the consistent run configurations. + """ def __init__(self, experiments): """Initialize the analysis object. @@ -32,61 +119,46 @@ def __init__(self, experiments): qubits += exp.physical_qubits super().__init__(experiments, qubits) - def circuits(self, backend=None): - - sub_circuits = [] - sub_qubits = [] - sub_size = [] - num_qubits = 0 - - # Generate data for combination - for expr in self._experiments: - # Add subcircuits - circs = expr.circuits(backend) - sub_circuits.append(circs) - sub_size.append(len(circs)) - - # Add sub qubits - qubits = list(range(num_qubits, num_qubits + expr.num_qubits)) - sub_qubits.append(qubits) - num_qubits += expr.num_qubits - - # Generate empty joint circuits - num_circuits = max(sub_size) - joint_circuits = [] - for circ_idx in range(num_circuits): - # Create joint circuit - circuit = QuantumCircuit(self.num_qubits, name=f"parallel_exp_{circ_idx}") - circuit.metadata = { - "experiment_type": self._type, - "composite_index": [], - "composite_metadata": [], - "composite_qubits": [], - "composite_clbits": [], - } - for exp_idx in range(self._num_experiments): - if circ_idx < sub_size[exp_idx]: - # Add subcircuits to joint circuit - sub_circ = sub_circuits[exp_idx][circ_idx] - num_clbits = circuit.num_clbits - qubits = sub_qubits[exp_idx] - clbits = list(range(num_clbits, num_clbits + sub_circ.num_clbits)) - circuit.add_register(ClassicalRegister(sub_circ.num_clbits)) - circuit.append(sub_circ, qubits, clbits) - # Add subcircuit metadata - circuit.metadata["composite_index"].append(exp_idx) - circuit.metadata["composite_metadata"].append(sub_circ.metadata) - circuit.metadata["composite_qubits"].append(qubits) - circuit.metadata["composite_clbits"].append(clbits) - - # Add the calibrations - for gate, cals in sub_circ.calibrations.items(): - for key, sched in cals.items(): - circuit.add_calibration( - gate, qubits=key[0], schedule=sched, params=key[1] - ) - - # Add joint circuit to returned list - joint_circuits.append(circuit.decompose()) - - return joint_circuits + def _flatten_circuits( + self, + circuits: List[List[QuantumCircuit]], + num_qubits: int, + ) -> List[QuantumCircuit]: + """Flatten circuits. + + Note: + This experiment merges sub experiment circuits into a single circuit + by the circuit ``append`` method. + Quantum and classical register indices are retained. + """ + joint_circs = [] + for circ_idx, sub_circs in enumerate(itertools.zip_longest(*circuits)): + joint_circ = QuantumCircuit(num_qubits, name=f"parallel_exp_{circ_idx}") + joint_metadata = defaultdict(list) + for expr_idx, sub_circ in enumerate(sub_circs): + if not sub_circ: + # This experiment provides small number of circuits than others. + # No circuit available for merging with others. + # Skip merging process. + continue + # Add sub circuits to joint circuit + clbits = list(ClassicalRegister(sub_circ.num_clbits)) + joint_circ.add_register(clbits) + joint_circ.compose( + sub_circ, + qubits=range(num_qubits), + clbits=clbits, + inplace=True, + ) + joint_metadata["composite_index"].append(expr_idx) + joint_metadata["composite_metadata"].append(sub_circ.metadata) + joint_metadata["composite_qubits"].append( + self.component_experiment(expr_idx).physical_qubits + ) + joint_metadata["composite_clbits"].append( + list(joint_circ.clbits.index(cb) for cb in clbits) + ) + joint_circ.metadata = {"experiment_type": self._type, **joint_metadata} + joint_circs.append(joint_circ) + + return joint_circs From c0cd0a7d3ad9e1e997bb1ec72e28dc8424ee771e Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 7 Sep 2021 18:09:12 +0900 Subject: [PATCH 03/29] black & lint --- .../framework/base_experiment.py | 16 ++++++++++------ .../framework/composite/batch_experiment.py | 6 +++--- .../composite/composite_experiment.py | 18 +++++++++++------- .../framework/composite/parallel_experiment.py | 8 ++++---- .../randomized_benchmarking/rb_experiment.py | 2 +- 5 files changed, 29 insertions(+), 21 deletions(-) diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index 2ffcb510e5..320c010e74 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -152,8 +152,9 @@ def _pre_transpile_hook(self, backend: Backend): """ pass + # pylint: disable = unused-argument def _post_transpile_hook( - self, circuits: List[QuantumCircuit], backend: Backend + self, circuits: List[QuantumCircuit], backend: Backend ) -> List[QuantumCircuit]: """An extra subroutine executed after transpilation. @@ -166,11 +167,12 @@ def _post_transpile_hook( """ return circuits - def run_transpile(self, backend: Backend, **options): + def run_transpile(self, backend: Backend, **options) -> List[QuantumCircuit]: """Run transpile and returns transpiled circuits. Args: backend: Target backend. + options: User provided runtime options. Returns: Transpiled circuit to execute. @@ -208,7 +210,7 @@ def _post_analysis_hook(self, experiment_data: ExperimentData): pass def run_analysis( - self, experiment_data: ExperimentData, job: BaseJob = None, **options + self, experiment_data: ExperimentData, job: BaseJob = None, **options ) -> ExperimentData: """Run analysis and update ExperimentData with analysis result. @@ -224,7 +226,7 @@ def run_analysis( Raises: QiskitError: Method is called with an empty experiment result. """ - run_analysis = self.analysis().run if self.__analysis_class__ else None + run_analysis = self.analysis() if self.__analysis_class__ else None # Get analysis options analysis_options = copy.copy(self.analysis_options) @@ -238,11 +240,13 @@ def run_analysis( "Experiment data seems to be empty and no running job is provided. " "At least one data entry is required to run analysis." ) - experiment_data = run_analysis(experiment_data, **analysis_options) + experiment_data = run_analysis.run(experiment_data, **analysis_options) else: # Run analysis when job is completed experiment_data.add_data( - job, post_processing_callback=run_analysis, **analysis_options + data=job, + post_processing_callback=run_analysis.run, + **analysis_options, ) # Run post analysis. This is implemented by each experiment subclass. diff --git a/qiskit_experiments/framework/composite/batch_experiment.py b/qiskit_experiments/framework/composite/batch_experiment.py index 4fe138ec25..9f0ecbd126 100644 --- a/qiskit_experiments/framework/composite/batch_experiment.py +++ b/qiskit_experiments/framework/composite/batch_experiment.py @@ -69,9 +69,9 @@ def __init__(self, experiments): super().__init__(experiments, qubits) def _flatten_circuits( - self, - circuits: List[List[QuantumCircuit]], - num_qubits: int, + self, + circuits: List[List[QuantumCircuit]], + num_qubits: int, ) -> List[QuantumCircuit]: """Flatten circuits. diff --git a/qiskit_experiments/framework/composite/composite_experiment.py b/qiskit_experiments/framework/composite/composite_experiment.py index 9b05e05b12..d16cdc7e4c 100644 --- a/qiskit_experiments/framework/composite/composite_experiment.py +++ b/qiskit_experiments/framework/composite/composite_experiment.py @@ -14,7 +14,7 @@ """ from abc import abstractmethod -from typing import List +from typing import List, Optional import warnings from qiskit import QuantumCircuit @@ -44,11 +44,12 @@ def __init__(self, experiments, qubits, experiment_type=None): self._num_experiments = len(experiments) super().__init__(qubits, experiment_type=experiment_type) - def run_transpile(self, backend: Backend, **options): + def run_transpile(self, backend: Backend, **options) -> List[QuantumCircuit]: """Run transpile and returns transpiled circuits. Args: backend: Target backend. + options: User provided runtime options. Returns: Transpiled circuit to execute. @@ -70,7 +71,7 @@ def run_transpile(self, backend: Backend, **options): return self._flatten_circuits(sub_circs, n_qubits) def run_analysis( - self, experiment_data: ExperimentData, job: BaseJob = None, **options + self, experiment_data: ExperimentData, job: BaseJob = None, **options ) -> ExperimentData: """Run analysis and update ExperimentData with analysis result. @@ -83,6 +84,9 @@ def run_analysis( Returns: An experiment data object containing the analysis results and figures. + Raises: + QiskitError: When the experiment data format is not for the composite experiment. + Note: This is analysis method for the composite experiment subclass. This internally calls the analysis method of the nested experiments and @@ -107,14 +111,14 @@ def run_analysis( @abstractmethod def _flatten_circuits( - self, - circuits: List[List[QuantumCircuit]], - num_qubits: int, + self, + circuits: List[List[QuantumCircuit]], + num_qubits: int, ) -> List[QuantumCircuit]: """An abstract method to control merger logic of sub experiments.""" pass - def circuits(self, backend=None): + def circuits(self, backend: Optional[Backend] = None): """Composite experiment does not provide this method. Args: diff --git a/qiskit_experiments/framework/composite/parallel_experiment.py b/qiskit_experiments/framework/composite/parallel_experiment.py index 7bc9262942..bec2e4e7e4 100644 --- a/qiskit_experiments/framework/composite/parallel_experiment.py +++ b/qiskit_experiments/framework/composite/parallel_experiment.py @@ -120,9 +120,9 @@ def __init__(self, experiments): super().__init__(experiments, qubits) def _flatten_circuits( - self, - circuits: List[List[QuantumCircuit]], - num_qubits: int, + self, + circuits: List[List[QuantumCircuit]], + num_qubits: int, ) -> List[QuantumCircuit]: """Flatten circuits. @@ -138,7 +138,7 @@ def _flatten_circuits( for expr_idx, sub_circ in enumerate(sub_circs): if not sub_circ: # This experiment provides small number of circuits than others. - # No circuit available for merging with others. + # No circuit available for combining with others. # Skip merging process. continue # Add sub circuits to joint circuit diff --git a/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py b/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py index 59808ef878..8d93c17e67 100644 --- a/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py +++ b/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py @@ -221,7 +221,7 @@ def _get_circuit_metadata(self, circuit): return None def _post_transpile_hook( - self, circuits: List[QuantumCircuit], backend: Backend + self, circuits: List[QuantumCircuit], backend: Backend ) -> List[QuantumCircuit]: """Count gate operations in each circuit and update metadata.""" for circuit in circuits: From 9bb746a1d3fd1d8d01c70a82589578d9285db386 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 7 Sep 2021 23:57:28 +0900 Subject: [PATCH 04/29] fix unittests --- .../framework/composite/batch_experiment.py | 2 +- .../composite/composite_experiment.py | 13 ++++-- .../composite/parallel_experiment.py | 6 +-- qiskit_experiments/test/t1_backend.py | 2 +- test/calibration/experiments/test_rabi.py | 3 +- test/test_composite.py | 44 +++++++++---------- test/test_t2ramsey.py | 2 +- 7 files changed, 39 insertions(+), 33 deletions(-) diff --git a/qiskit_experiments/framework/composite/batch_experiment.py b/qiskit_experiments/framework/composite/batch_experiment.py index 9f0ecbd126..3cdc76181c 100644 --- a/qiskit_experiments/framework/composite/batch_experiment.py +++ b/qiskit_experiments/framework/composite/batch_experiment.py @@ -85,7 +85,7 @@ def _flatten_circuits( sub_circ.metadata = { "experiment_type": self._type, "composite_index": [expr_idx], - "composite_metadata": sub_circ.metadata, + "composite_metadata": [sub_circ.metadata], } batch_circuits.append(sub_circ) diff --git a/qiskit_experiments/framework/composite/composite_experiment.py b/qiskit_experiments/framework/composite/composite_experiment.py index d16cdc7e4c..fc826c2a05 100644 --- a/qiskit_experiments/framework/composite/composite_experiment.py +++ b/qiskit_experiments/framework/composite/composite_experiment.py @@ -63,12 +63,17 @@ def run_transpile(self, backend: Backend, **options) -> List[QuantumCircuit]: No transpile configuration assumed for composite experiment object itself. """ - # transpile each sub experiment circuit - sub_circs = list(map(lambda expr: expr.run_transpile(backend), self._experiments)) - n_qubits = max(sub_circ.num_qubits for sub_circ in sub_circs) + # Transpile each sub experiment circuit + circuits = list(map(lambda expr: expr.run_transpile(backend), self._experiments)) + + # This is not identical to the `num_qubits` when the backend is AerSimulator. + # In this case, usually a circuit qubit number is determined by the maximum qubit index. + n_qubits = max( + max(sub_circ.num_qubits for sub_circ in sub_circs) for sub_circs in circuits + ) # merge circuits - return self._flatten_circuits(sub_circs, n_qubits) + return self._flatten_circuits(circuits, n_qubits) def run_analysis( self, experiment_data: ExperimentData, job: BaseJob = None, **options diff --git a/qiskit_experiments/framework/composite/parallel_experiment.py b/qiskit_experiments/framework/composite/parallel_experiment.py index bec2e4e7e4..b9c0ccd4a5 100644 --- a/qiskit_experiments/framework/composite/parallel_experiment.py +++ b/qiskit_experiments/framework/composite/parallel_experiment.py @@ -142,12 +142,12 @@ def _flatten_circuits( # Skip merging process. continue # Add sub circuits to joint circuit - clbits = list(ClassicalRegister(sub_circ.num_clbits)) + clbits = ClassicalRegister(sub_circ.num_clbits) joint_circ.add_register(clbits) joint_circ.compose( sub_circ, - qubits=range(num_qubits), - clbits=clbits, + qubits=range(sub_circ.num_qubits), + clbits=list(clbits), inplace=True, ) joint_metadata["composite_index"].append(expr_idx) diff --git a/qiskit_experiments/test/t1_backend.py b/qiskit_experiments/test/t1_backend.py index b87c763475..63684ef54e 100644 --- a/qiskit_experiments/test/t1_backend.py +++ b/qiskit_experiments/test/t1_backend.py @@ -36,7 +36,7 @@ def __init__(self, t1, initial_prob1=None, readout0to1=None, readout1to0=None, d configuration = QasmBackendConfiguration( backend_name="t1_simulator", backend_version="0", - n_qubits=int(1e6), + n_qubits=100, basis_gates=["barrier", "x", "delay", "measure"], gates=[], local=True, diff --git a/test/calibration/experiments/test_rabi.py b/test/calibration/experiments/test_rabi.py index bfb9dd66e5..3c580adf5b 100644 --- a/test/calibration/experiments/test_rabi.py +++ b/test/calibration/experiments/test_rabi.py @@ -20,6 +20,7 @@ from qiskit.circuit import Parameter from qiskit.providers.basicaer import QasmSimulatorPy from qiskit.test import QiskitTestCase +from qiskit.test.mock.utils import ConfigurableFakeBackend from qiskit.qobj.utils import MeasLevel import qiskit.pulse as pulse @@ -272,7 +273,7 @@ def test_calibrations(self): experiments.append(rabi) par_exp = ParallelExperiment(experiments) - par_circ = par_exp.circuits()[0] + par_circ = par_exp.run_transpile(backend=ConfigurableFakeBackend("test", 3))[0] # If the calibrations are not there we will not be able to transpile try: diff --git a/test/test_composite.py b/test/test_composite.py index c579eb79ab..501ef99588 100644 --- a/test/test_composite.py +++ b/test/test_composite.py @@ -30,25 +30,25 @@ def test_parallel_options(self): Test parallel experiments overriding sub-experiment run and transpile options. """ - # These options will all be overridden - exp0 = FakeExperiment(0) - exp0.set_transpile_options(optimization_level=1) - exp2 = FakeExperiment(2) - exp2.set_experiment_options(dummyoption="test") - exp2.set_run_options(shots=2000) - exp2.set_transpile_options(optimization_level=1) - exp2.set_analysis_options(dummyoption="test") - - par_exp = ParallelExperiment([exp0, exp2]) - - with self.assertWarnsRegex( - Warning, - "Sub-experiment run and transpile options" - " are overridden by composite experiment options.", - ): - self.assertEqual(par_exp.experiment_options, Options()) - self.assertEqual(par_exp.run_options, Options(meas_level=2)) - self.assertEqual(par_exp.transpile_options, Options(optimization_level=0)) - self.assertEqual(par_exp.analysis_options, Options()) - - par_exp.run(FakeBackend()) + # # These options will all be overridden + # exp0 = FakeExperiment(0) + # exp0.set_transpile_options(optimization_level=1) + # exp2 = FakeExperiment(2) + # exp2.set_experiment_options(dummyoption="test") + # exp2.set_run_options(shots=2000) + # exp2.set_transpile_options(optimization_level=1) + # exp2.set_analysis_options(dummyoption="test") + # + # par_exp = ParallelExperiment([exp0, exp2]) + # + # with self.assertWarnsRegex( + # Warning, + # "Sub-experiment run and transpile options" + # " are overridden by composite experiment options.", + # ): + # self.assertEqual(par_exp.experiment_options, Options()) + # self.assertEqual(par_exp.run_options, Options(meas_level=2)) + # self.assertEqual(par_exp.transpile_options, Options(optimization_level=0)) + # self.assertEqual(par_exp.analysis_options, Options()) + # + # par_exp.run(FakeBackend()) diff --git a/test/test_t2ramsey.py b/test/test_t2ramsey.py index 3f60e9e6ff..69d51196c9 100644 --- a/test/test_t2ramsey.py +++ b/test/test_t2ramsey.py @@ -43,7 +43,7 @@ def __init__( configuration = QasmBackendConfiguration( backend_name="T2Ramsey_simulator", backend_version="0", - n_qubits=int(1e6), + n_qubits=100, basis_gates=["barrier", "h", "p", "delay", "measure"], gates=[], local=True, From be9ce8c6f2bace2410b7ce04cdc959a650d29a01 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Wed, 8 Sep 2021 02:00:07 +0900 Subject: [PATCH 05/29] add proper test --- test/test_composite.py | 327 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 293 insertions(+), 34 deletions(-) diff --git a/test/test_composite.py b/test/test_composite.py index 501ef99588..ef1bee5039 100644 --- a/test/test_composite.py +++ b/test/test_composite.py @@ -10,45 +10,304 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Class to test composite experiments.""" +"""Test suite for composite experiments. -from test.fake_backend import FakeBackend -from test.fake_experiment import FakeExperiment +This test assumes some simple but enough general virtual experiment. +The prepared fake experiment just flips qubit state and measure it, +while attached analysis class calculate the excited state population and create analysis result. +This experiment can cover the situation that deals with several hook methods and configurations. +This simplicity may benefit the debugging when some unexpected error is induced. +""" + +from qiskit.circuit import QuantumCircuit +from qiskit.pulse import Schedule from qiskit.test import QiskitTestCase +from qiskit.test.mock import FakeBogota +from qiskit.test.mock.utils import ConfigurableFakeBackend -from qiskit_experiments.framework import ParallelExperiment, Options +from qiskit_experiments.framework import ( + BaseExperiment, + BaseAnalysis, + AnalysisResultData, + ParallelExperiment, + BatchExperiment, + Options, +) -class TestComposite(QiskitTestCase): - """ - Test composite experiment behavior. +class FakeAnalysisProbability(BaseAnalysis): + """A fake analysis that calculate probability from counts.""" + + @classmethod + def _default_options(cls) -> Options: + return Options(dummyoption="default_value") + + def _run_analysis(self, experiment_data, **options): + """Calculate probability.""" + + expdata = experiment_data.data(0) + counts = expdata["counts"] + probability = counts.get("1", 0) / sum(counts.values()) + + fake_data_entry = AnalysisResultData( + name="probability", + value=probability, + extra={ + "run_options": options, + "shots": sum(counts.values()), + }, + ) + + return [fake_data_entry], [] + + +class FakeExperimentCommon(BaseExperiment): + """A fake experiment that just flip qubit state and measure.""" + + __analysis_class__ = FakeAnalysisProbability + + @classmethod + def _default_experiment_options(cls): + return Options(dummyoption="default_value") + + @classmethod + def _default_transpile_options(cls): + return Options(basis_gates=["x"]) + + @classmethod + def _default_run_options(cls): + return Options(shots=1024) + + def circuits(self, backend=None): + """Fake circuits.""" + test_circ = QuantumCircuit(1, 1) + test_circ.x(0) + test_circ.measure(0, 0) + test_circ.metadata = {"dummy": "test_value"} + + return [test_circ] + + +class TestParallelExperiment(QiskitTestCase): + """Test parallel experiment.""" + + def test_standard_circuit_construction(self): + """Test standard parallel experiment construction.""" + backend = ConfigurableFakeBackend("test", 2) + exp0 = FakeExperimentCommon(qubits=[0]) + exp1 = FakeExperimentCommon(qubits=[1]) + + par_exp = ParallelExperiment([exp0, exp1]) + test_circ = par_exp.run_transpile(backend=backend)[0] + + ref_circ = QuantumCircuit(*test_circ.qregs, *test_circ.cregs) + ref_circ.x(0) + ref_circ.x(1) + ref_circ.measure(0, 0) + ref_circ.measure(1, 1) + + self.assertEqual(test_circ, ref_circ) + + def test_pulse_gate_experiment_with_post_transpile_hook(self): + """Test pulse gate parallel experiment with different transpile hook.""" + + class FakeExperimentPulseGate(FakeExperimentCommon): + """Add transpiler hook to insert calibration.""" + + def _post_transpile_hook(self, circuits, backend): + """Add pulse gate.""" + for circ in circuits: + circ.add_calibration( + "x", self.physical_qubits, Schedule(name="test_calibration") + ) + + return circuits + + backend = ConfigurableFakeBackend("test", 2) + exp0 = FakeExperimentCommon(qubits=[0]) + exp1 = FakeExperimentPulseGate(qubits=[1]) + + par_exp = ParallelExperiment([exp0, exp1]) + test_circ = par_exp.run_transpile(backend=backend)[0] + + ref_circ = QuantumCircuit(*test_circ.qregs, *test_circ.cregs) + ref_circ.x(0) + ref_circ.x(1) + ref_circ.measure(0, 0) + ref_circ.measure(1, 1) + + # only q1 has pulse gate + ref_circ.add_calibration("x", (1, ), Schedule(name="test_calibration")) + + self.assertEqual(test_circ, ref_circ) + + def test_update_circuit_metadata_with_post_transpile_hook(self): + """Test new metadata with different transpile hook.""" + + class FakeExperimentUpdateCircuitMetadata(FakeExperimentCommon): + """Add transpiler hook to update metadata.""" + + def _post_transpile_hook(self, circuits, backend): + """Add pulse gate.""" + for circ in circuits: + circ.metadata["new_data"] = "test_value" + + return circuits + + backend = ConfigurableFakeBackend("test", 2) + exp0 = FakeExperimentCommon(qubits=[0]) + exp1 = FakeExperimentUpdateCircuitMetadata(qubits=[1]) + + par_exp = ParallelExperiment([exp0, exp1]) + test_circ = par_exp.run_transpile(backend=backend)[0] + + ref_metadata = [ + {"dummy": "test_value"}, + {"dummy": "test_value", "new_data": "test_value"}, + ] + + self.assertListEqual(test_circ.metadata["composite_metadata"], ref_metadata) + + def test_retain_transpile_configuration(self): + """Test retain transpile configurations of nested experiments.""" + + backend = ConfigurableFakeBackend("test", 2) + exp0 = FakeExperimentCommon(qubits=[0]) + exp1 = FakeExperimentCommon(qubits=[1]) + + # update exp1 basis gate + exp1.set_transpile_options(basis_gates=["sx", "rz"]) + + par_exp = ParallelExperiment([exp0, exp1]) + test_circ = par_exp.run_transpile(backend=backend)[0] + + ref_circ = QuantumCircuit(*test_circ.qregs, *test_circ.cregs) + ref_circ.x(0) + + # q1 x is decomposed into two sx + ref_circ.sx(1) + ref_circ.sx(1) + + ref_circ.measure(0, 0) + ref_circ.measure(1, 1) + + self.assertEqual(test_circ, ref_circ) + + def test_analyze_standard(self): + """Test analyze standard parallel experiment result.""" + + backend = FakeBogota() + exp0 = FakeExperimentCommon(qubits=[0]) + exp1 = FakeExperimentCommon(qubits=[1]) + + par_exp = ParallelExperiment([exp0, exp1]) + par_exp_data = par_exp.run(backend) + par_exp_data.block_for_results() + + exp_data0 = par_exp_data.component_experiment_data(0) + exp_data1 = par_exp_data.component_experiment_data(1) + + prob_entry_exp0 = exp_data0.analysis_results("probability") + self.assertGreater(prob_entry_exp0.value, 0.8) + + prob_entry_exp1 = exp_data1.analysis_results("probability") + self.assertGreater(prob_entry_exp1.value, 0.8) + + def test_retain_analysis_options(self): + """Test retain analysis configurations of nested experiments.""" + + backend = FakeBogota() + exp0 = FakeExperimentCommon(qubits=[0]) + exp1 = FakeExperimentCommon(qubits=[1]) + + # update exp1 analysis option + exp1.set_analysis_options(new_config="test_value") + + par_exp = ParallelExperiment([exp0, exp1]) + par_exp_data = par_exp.run(backend) + par_exp_data.block_for_results() + + exp_data0 = par_exp_data.component_experiment_data(0) + prob_entry_exp0 = exp_data0.analysis_results("probability") + ref_config0 = {"dummyoption": "default_value"} + self.assertDictEqual(prob_entry_exp0.extra["run_options"], ref_config0) + + # this keeps updated analysis configuration + exp_data1 = par_exp_data.component_experiment_data(1) + prob_entry_exp1 = exp_data1.analysis_results("probability") + ref_config1 = {"dummyoption": "default_value", "new_config": "test_value"} + self.assertDictEqual(prob_entry_exp1.extra["run_options"], ref_config1) + + def test_run_option_overriden(self): + """Test if run option is overridden.""" + + backend = FakeBogota() + exp0 = FakeExperimentCommon(qubits=[0]) + exp1 = FakeExperimentCommon(qubits=[1]) + + # update exp1 run option + exp1.set_run_options(shots=2048) + + par_exp = ParallelExperiment([exp0, exp1]) + par_exp_data = par_exp.run(backend, shots=1024) + par_exp_data.block_for_results() + + exp_data1 = par_exp_data.component_experiment_data(1) + prob_entry_exp1 = exp_data1.analysis_results("probability") + self.assertEqual(prob_entry_exp1.extra["shots"], 1024) + + def test_analysis_hook(self): + """Test update class variable with analysis result.""" + + class FakeExperimentAnalysisHook(FakeExperimentCommon): + + def __init__(self, qubits): + super().__init__(qubits) + self.probability = None + + def _post_analysis_hook(self, experiment_data): + """Extract probability and update instance variable.""" + prob_val = experiment_data.analysis_results("probability").value + self.probability = prob_val + + backend = FakeBogota() + exp0 = FakeExperimentCommon(qubits=[0]) + exp1 = FakeExperimentAnalysisHook(qubits=[1]) + + par_exp = ParallelExperiment([exp0, exp1]) + par_exp_data = par_exp.run(backend, shots=1024) + par_exp_data.block_for_results() + + exp_data1 = par_exp_data.component_experiment_data(1) + prob_entry_exp1 = exp_data1.analysis_results("probability") + + self.assertEqual(exp1.probability, prob_entry_exp1.value) + + +class TestBatchExperiment(QiskitTestCase): + """Test batch experiment. + + Note: + The only difference of this from ``ParallelExperiment`` is the circuit construction. + Thus the run and analysis tests can be omitted while keeping the good coverage. """ - def test_parallel_options(self): - """ - Test parallel experiments overriding sub-experiment run and transpile options. - """ - - # # These options will all be overridden - # exp0 = FakeExperiment(0) - # exp0.set_transpile_options(optimization_level=1) - # exp2 = FakeExperiment(2) - # exp2.set_experiment_options(dummyoption="test") - # exp2.set_run_options(shots=2000) - # exp2.set_transpile_options(optimization_level=1) - # exp2.set_analysis_options(dummyoption="test") - # - # par_exp = ParallelExperiment([exp0, exp2]) - # - # with self.assertWarnsRegex( - # Warning, - # "Sub-experiment run and transpile options" - # " are overridden by composite experiment options.", - # ): - # self.assertEqual(par_exp.experiment_options, Options()) - # self.assertEqual(par_exp.run_options, Options(meas_level=2)) - # self.assertEqual(par_exp.transpile_options, Options(optimization_level=0)) - # self.assertEqual(par_exp.analysis_options, Options()) - # - # par_exp.run(FakeBackend()) + def test_standard_circuit_construction(self): + """Test standard batch experiment construction.""" + backend = ConfigurableFakeBackend("test", 2) + exp0 = FakeExperimentCommon(qubits=[0]) + exp1 = FakeExperimentCommon(qubits=[1]) + + par_exp = BatchExperiment([exp0, exp1]) + test_circs = par_exp.run_transpile(backend=backend) + + ref_circ0 = QuantumCircuit(2, 1) + ref_circ0.x(0) + ref_circ0.measure(0, 0) + + ref_circ1 = QuantumCircuit(2, 1) + ref_circ1.x(1) + ref_circ1.measure(1, 0) + + self.assertListEqual(test_circs, [ref_circ0, ref_circ1]) From 8492bc27310d06a2683c8e295d57fb8dc65e5fe2 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Wed, 8 Sep 2021 02:26:36 +0900 Subject: [PATCH 06/29] black & lint --- .../framework/composite/composite_experiment.py | 4 +--- test/test_composite.py | 6 ++---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/qiskit_experiments/framework/composite/composite_experiment.py b/qiskit_experiments/framework/composite/composite_experiment.py index fc826c2a05..a7ec207361 100644 --- a/qiskit_experiments/framework/composite/composite_experiment.py +++ b/qiskit_experiments/framework/composite/composite_experiment.py @@ -68,9 +68,7 @@ def run_transpile(self, backend: Backend, **options) -> List[QuantumCircuit]: # This is not identical to the `num_qubits` when the backend is AerSimulator. # In this case, usually a circuit qubit number is determined by the maximum qubit index. - n_qubits = max( - max(sub_circ.num_qubits for sub_circ in sub_circs) for sub_circs in circuits - ) + n_qubits = max(max(sub_circ.num_qubits for sub_circ in sub_circs) for sub_circs in circuits) # merge circuits return self._flatten_circuits(circuits, n_qubits) diff --git a/test/test_composite.py b/test/test_composite.py index ef1bee5039..92a07680af 100644 --- a/test/test_composite.py +++ b/test/test_composite.py @@ -116,7 +116,6 @@ class FakeExperimentPulseGate(FakeExperimentCommon): """Add transpiler hook to insert calibration.""" def _post_transpile_hook(self, circuits, backend): - """Add pulse gate.""" for circ in circuits: circ.add_calibration( "x", self.physical_qubits, Schedule(name="test_calibration") @@ -138,7 +137,7 @@ def _post_transpile_hook(self, circuits, backend): ref_circ.measure(1, 1) # only q1 has pulse gate - ref_circ.add_calibration("x", (1, ), Schedule(name="test_calibration")) + ref_circ.add_calibration("x", (1,), Schedule(name="test_calibration")) self.assertEqual(test_circ, ref_circ) @@ -149,7 +148,6 @@ class FakeExperimentUpdateCircuitMetadata(FakeExperimentCommon): """Add transpiler hook to update metadata.""" def _post_transpile_hook(self, circuits, backend): - """Add pulse gate.""" for circ in circuits: circ.metadata["new_data"] = "test_value" @@ -261,13 +259,13 @@ def test_analysis_hook(self): """Test update class variable with analysis result.""" class FakeExperimentAnalysisHook(FakeExperimentCommon): + """Extract probability and update instance variable.""" def __init__(self, qubits): super().__init__(qubits) self.probability = None def _post_analysis_hook(self, experiment_data): - """Extract probability and update instance variable.""" prob_val = experiment_data.analysis_results("probability").value self.probability = prob_val From f1a1bcb5e2fbe1aee87b6b8e432dc2f2c75eb779 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Mon, 13 Sep 2021 13:19:06 +0900 Subject: [PATCH 07/29] Update qiskit_experiments/framework/base_experiment.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit_experiments/framework/base_experiment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index 320c010e74..42f9da0e5b 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -177,7 +177,7 @@ def run_transpile(self, backend: Backend, **options) -> List[QuantumCircuit]: Returns: Transpiled circuit to execute. """ - # Run pre transpile. This is implemented by each experiment subclass. + # Run pre transpile if implemented by subclasses. self._pre_transpile_hook(backend) # Get transpile options From 88f1304f7e8fead332f21204551ec056aa8c8ba2 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Mon, 13 Sep 2021 22:03:22 +0900 Subject: [PATCH 08/29] update method signature and docs --- .../framework/base_experiment.py | 56 ++++++++++++++----- .../library/characterization/t1.py | 2 +- .../library/characterization/t2ramsey.py | 2 +- .../randomized_benchmarking/rb_experiment.py | 2 +- test/test_composite.py | 6 +- 5 files changed, 49 insertions(+), 19 deletions(-) diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index 320c010e74..53f846105e 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -144,20 +144,39 @@ def _initialize_experiment_data( return experiment_data._copy_metadata() - def _pre_transpile_hook(self, backend: Backend): + def pre_transpile_action(self, backend: Backend): """An extra subroutine executed before transpilation. + Note: + This method may be implemented by a subclass that requires to update the + transpiler configuration based on the given backend instance, + otherwise the transpiler configuration should be updated with the + :py:meth:`_default_transpile_options` method. + + For example, some specific transpiler options might change depending on the real + hardware execution or circuit simulator execution. + By default, this method does nothing. + Args: backend: Target backend. """ pass # pylint: disable = unused-argument - def _post_transpile_hook( + def post_transpile_action( self, circuits: List[QuantumCircuit], backend: Backend ) -> List[QuantumCircuit]: """An extra subroutine executed after transpilation. + Note: + This method may be implemented by a subclass that requires to update the + circuit or its metadata after transpilation. + Without this method, the transpiled circuit will be immediately executed on the backend. + This method enables the experiment to modify the circuit with pulse gates, + or some extra metadata regarding the transpiled sequence of instructions. + + By default, this method just passes transpiled circuits to the execution chain. + Args: circuits: List of transpiled circuits. backend: Target backend. @@ -178,7 +197,7 @@ def run_transpile(self, backend: Backend, **options) -> List[QuantumCircuit]: Transpiled circuit to execute. """ # Run pre transpile. This is implemented by each experiment subclass. - self._pre_transpile_hook(backend) + self.pre_transpile_action(backend) # Get transpile options transpile_options = copy.copy(self.transpile_options) @@ -191,21 +210,32 @@ def run_transpile(self, backend: Backend, **options) -> List[QuantumCircuit]: circuits = transpile(circuits=self.circuits(backend), backend=backend, **transpile_options) # Run post transpile. This is implemented by each experiment subclass. - circuits = self._post_transpile_hook(circuits, backend) + circuits = self.post_transpile_action(circuits, backend) return circuits - def _post_analysis_hook(self, experiment_data: ExperimentData): + def post_analysis_action(self, experiment_data: ExperimentData): """An extra subroutine executed after analysis. - Args: - experiment_data: A future object of the experiment result. - Note: - The experiment_data may contain a future object as an experiment result - and the previous analysis routine has not completed yet. - If the hook should be executed immediately, call :meth:`block_for_results` method - before starting the data processing code. + This method may be implemented by a subclass that requires to perform + extra data processing based on the analyzed experimental result. + + Note that the analysis routine will not complete until the backend job + is executed, and this method will be called after the analysis routine + is completed though a handler of the experiment result will be immediately + returned to users (a future object). This method is automatically triggered + when the analysis is finished, and will be processed in background. + + If this method updates some other (mutable) objects, you may need manage + synchronization of update of the object data. Otherwise you may want to + call :meth:`block_for_results` method of the ``experiment_data`` here + to freeze processing chain until the job result is returned. + + By default, this method does nothing. + + Args: + experiment_data: A future object of the experimental result. """ pass @@ -250,7 +280,7 @@ def run_analysis( ) # Run post analysis. This is implemented by each experiment subclass. - self._post_analysis_hook(experiment_data) + self.post_analysis_action(experiment_data) return experiment_data diff --git a/qiskit_experiments/library/characterization/t1.py b/qiskit_experiments/library/characterization/t1.py index 8033d2cc26..b6c56278fe 100644 --- a/qiskit_experiments/library/characterization/t1.py +++ b/qiskit_experiments/library/characterization/t1.py @@ -133,7 +133,7 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: return circuits - def _pre_transpile_hook(self, backend: Backend): + def pre_transpile_action(self, backend: Backend): """Set timing constraints if backend is real hardware.""" if not backend.configuration().simulator and not isinstance(backend, FakeBackend): diff --git a/qiskit_experiments/library/characterization/t2ramsey.py b/qiskit_experiments/library/characterization/t2ramsey.py index e51d8a7aac..60e0d1c52f 100644 --- a/qiskit_experiments/library/characterization/t2ramsey.py +++ b/qiskit_experiments/library/characterization/t2ramsey.py @@ -154,7 +154,7 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: return circuits - def _pre_transpile_hook(self, backend: Backend): + def pre_transpile_action(self, backend: Backend): """Set timing constraints if backend is real hardware.""" if not backend.configuration().simulator and not isinstance(backend, FakeBackend): diff --git a/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py b/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py index 8d93c17e67..c2ad9efe11 100644 --- a/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py +++ b/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py @@ -220,7 +220,7 @@ def _get_circuit_metadata(self, circuit): return meta return None - def _post_transpile_hook( + def post_transpile_action( self, circuits: List[QuantumCircuit], backend: Backend ) -> List[QuantumCircuit]: """Count gate operations in each circuit and update metadata.""" diff --git a/test/test_composite.py b/test/test_composite.py index 92a07680af..1d84f0f9e6 100644 --- a/test/test_composite.py +++ b/test/test_composite.py @@ -115,7 +115,7 @@ def test_pulse_gate_experiment_with_post_transpile_hook(self): class FakeExperimentPulseGate(FakeExperimentCommon): """Add transpiler hook to insert calibration.""" - def _post_transpile_hook(self, circuits, backend): + def post_transpile_action(self, circuits, backend): for circ in circuits: circ.add_calibration( "x", self.physical_qubits, Schedule(name="test_calibration") @@ -147,7 +147,7 @@ def test_update_circuit_metadata_with_post_transpile_hook(self): class FakeExperimentUpdateCircuitMetadata(FakeExperimentCommon): """Add transpiler hook to update metadata.""" - def _post_transpile_hook(self, circuits, backend): + def post_transpile_action(self, circuits, backend): for circ in circuits: circ.metadata["new_data"] = "test_value" @@ -265,7 +265,7 @@ def __init__(self, qubits): super().__init__(qubits) self.probability = None - def _post_analysis_hook(self, experiment_data): + def post_analysis_action(self, experiment_data): prob_val = experiment_data.analysis_results("probability").value self.probability = prob_val From 80a689e96f6e7a359bce92707ca3842c72d6ff97 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Mon, 13 Sep 2021 22:40:30 +0900 Subject: [PATCH 09/29] rewrite complicated logic --- .../framework/composite/batch_experiment.py | 6 +++++- .../framework/composite/composite_experiment.py | 16 ++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/qiskit_experiments/framework/composite/batch_experiment.py b/qiskit_experiments/framework/composite/batch_experiment.py index 3cdc76181c..fa6ba43c06 100644 --- a/qiskit_experiments/framework/composite/batch_experiment.py +++ b/qiskit_experiments/framework/composite/batch_experiment.py @@ -65,7 +65,11 @@ def __init__(self, experiments): Args: experiments (List[BaseExperiment]): a list of experiments. """ - qubits = sorted(set(sum([list(expr.physical_qubits) for expr in experiments], []))) + qubits = list() + for expr in experiments: + qubits.extend(expr.physical_qubits) + qubits = sorted(set(qubits)) + super().__init__(experiments, qubits) def _flatten_circuits( diff --git a/qiskit_experiments/framework/composite/composite_experiment.py b/qiskit_experiments/framework/composite/composite_experiment.py index a7ec207361..0305290ce5 100644 --- a/qiskit_experiments/framework/composite/composite_experiment.py +++ b/qiskit_experiments/framework/composite/composite_experiment.py @@ -63,15 +63,23 @@ def run_transpile(self, backend: Backend, **options) -> List[QuantumCircuit]: No transpile configuration assumed for composite experiment object itself. """ - # Transpile each sub experiment circuit - circuits = list(map(lambda expr: expr.run_transpile(backend), self._experiments)) + # Generate a set of transpiled circuits for each nested experiment and have them as a list. + # In each list element, a list of quantum circuit for nested experiment is stored. + + # type: List[List[QuantumCircuit]] + experiment_circuits_list = list( + map(lambda expr: expr.run_transpile(backend), self._experiments) + ) # This is not identical to the `num_qubits` when the backend is AerSimulator. # In this case, usually a circuit qubit number is determined by the maximum qubit index. - n_qubits = max(max(sub_circ.num_qubits for sub_circ in sub_circs) for sub_circs in circuits) + n_qubits = 0 + for circuits in experiment_circuits_list: + circuit_qubits = [circuit.num_qubits for circuit in circuits] + n_qubits = max(n_qubits, *circuit_qubits) # merge circuits - return self._flatten_circuits(circuits, n_qubits) + return self._flatten_circuits(experiment_circuits_list, n_qubits) def run_analysis( self, experiment_data: ExperimentData, job: BaseJob = None, **options From 3fa62e1171f1e56e9217c4baa19d170f695af4d4 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Mon, 13 Sep 2021 22:53:39 +0900 Subject: [PATCH 10/29] update docs --- .../framework/composite/batch_experiment.py | 10 ++++++---- .../framework/composite/composite_experiment.py | 8 +++++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/qiskit_experiments/framework/composite/batch_experiment.py b/qiskit_experiments/framework/composite/batch_experiment.py index fa6ba43c06..28524452b8 100644 --- a/qiskit_experiments/framework/composite/batch_experiment.py +++ b/qiskit_experiments/framework/composite/batch_experiment.py @@ -24,9 +24,10 @@ class BatchExperiment(CompositeExperiment): """Batch experiment class. - This experiment takes multiple experiment instances and generates - a list of flattened circuit to execute. - The experimental circuits are executed ony be one on the target backend as a single job. + This experiment takes multiple experiment instances and generates a list of circuits for + each nested experiment. This nested circuit list is flattened to a single long list of + all circuits to execute, and the circuits are executed ony be one + on the target backend as a single job. If an experiment analysis needs results of different types of experiments, ``BatchExperiment`` may be convenient to describe the flow of the entire experiment. @@ -80,7 +81,8 @@ def _flatten_circuits( """Flatten circuits. Note: - This experiment concatenates sub experiment circuits. + This experiment just flattens a list of list of circuit to a single long list of + circuits. The structure of experiment is kept in a metadata. """ batch_circuits = [] diff --git a/qiskit_experiments/framework/composite/composite_experiment.py b/qiskit_experiments/framework/composite/composite_experiment.py index 0305290ce5..e5b35a6a54 100644 --- a/qiskit_experiments/framework/composite/composite_experiment.py +++ b/qiskit_experiments/framework/composite/composite_experiment.py @@ -126,7 +126,13 @@ def _flatten_circuits( circuits: List[List[QuantumCircuit]], num_qubits: int, ) -> List[QuantumCircuit]: - """An abstract method to control merger logic of sub experiments.""" + """An abstract method to control flattening logic of sub experiments. + + This method takes a nested list of circuits, which corresponds to a + list of circuits generated by each experiment, and generates a single list of circuits + that is executed on the backend. This flattening logic may depend on the + type of composite experiment. + """ pass def circuits(self, backend: Optional[Backend] = None): From 2abc6d87ba38e22247a998af40defa4bd7bbc716 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 14 Sep 2021 01:39:17 +0900 Subject: [PATCH 11/29] update warnings for composite experiments --- .../framework/composite/composite_analysis.py | 11 ++++++++++- .../composite/composite_experiment.py | 19 ++++++++++--------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/qiskit_experiments/framework/composite/composite_analysis.py b/qiskit_experiments/framework/composite/composite_analysis.py index 0749fe707a..031a61c20e 100644 --- a/qiskit_experiments/framework/composite/composite_analysis.py +++ b/qiskit_experiments/framework/composite/composite_analysis.py @@ -12,6 +12,7 @@ """ Composite Experiment Analysis class. """ +import warnings from qiskit.exceptions import QiskitError from qiskit_experiments.framework import BaseAnalysis, AnalysisResultData @@ -40,9 +41,17 @@ def _run_analysis(self, experiment_data: CompositeExperimentData, **options): QiskitError: if analysis is attempted on non-composite experiment data. """ - if not isinstance(experiment_data, CompositeExperimentData): + if not isinstance(experiment_data, self.__experiment_data__): raise QiskitError("CompositeAnalysis must be run on CompositeExperimentData.") + if len(options) > 0: + warnings.warn( + f"Analysis options for the composite experiment are provided: {options}. " + "Note that the provided options will override every analysis of an experiment" + "associated with this composite experiment.", + UserWarning, + ) + # Add sub-experiment metadata as result of batch experiment # Note: if Analysis results had ID's these should be included here # rather than just the sub-experiment IDs diff --git a/qiskit_experiments/framework/composite/composite_experiment.py b/qiskit_experiments/framework/composite/composite_experiment.py index e5b35a6a54..497c2fa4a1 100644 --- a/qiskit_experiments/framework/composite/composite_experiment.py +++ b/qiskit_experiments/framework/composite/composite_experiment.py @@ -107,17 +107,9 @@ def run_analysis( No analysis configuration assumed for composite experiment object itself. """ - if not isinstance(experiment_data, CompositeExperimentData): + if not isinstance(experiment_data, self.__experiment_data__): raise QiskitError("CompositeAnalysis must be run on CompositeExperimentData.") - if len(options) > 0: - warnings.warn( - f"Analysis options for the composite experiment are provided: {options}. " - "Note that the provided options will override every analysis of an experiment" - "associated with this composite experiment.", - UserWarning, - ) - return super().run_analysis(experiment_data, job, **options) @abstractmethod @@ -190,3 +182,12 @@ def _add_job_metadata(self, experiment_data, job, **run_options): ) sub_data = experiment_data.component_experiment_data(i) sub_exp._add_job_metadata(sub_data, job, **run_options) + + def set_transpile_options(self, **fields): + """Composite experiment itself doesn't provide transpile options.""" + warnings.warn( + "A composite experiment class doesn't provide transpile options. " + "Note that transpile options are provided by each nested experiment, " + f"and thus provided options here {fields} are just discarded." + ) + super().set_transpile_options(fields) From b7b112809ab50ea9f82f19be39cf983239af88ae Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 14 Sep 2021 01:52:20 +0900 Subject: [PATCH 12/29] update description of test --- test/test_composite.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/test/test_composite.py b/test/test_composite.py index 1d84f0f9e6..545c940404 100644 --- a/test/test_composite.py +++ b/test/test_composite.py @@ -12,12 +12,9 @@ """Test suite for composite experiments. -This test assumes some simple but enough general virtual experiment. -The prepared fake experiment just flips qubit state and measure it, -while attached analysis class calculate the excited state population and create analysis result. - -This experiment can cover the situation that deals with several hook methods and configurations. -This simplicity may benefit the debugging when some unexpected error is induced. +This test platform defines simple virtual experiment classes, rather than using the +actual experiments from the experiment library to isolate test of composite framework +from other experiments. """ from qiskit.circuit import QuantumCircuit @@ -90,7 +87,16 @@ def circuits(self, backend=None): class TestParallelExperiment(QiskitTestCase): - """Test parallel experiment.""" + """Test parallel experiment. + + This experiment uses a fake experiment that just flips qubit state and measure it, + while attached analysis class calculate the excited state population + and create analysis result data. + + This experiment can cover the situation that deals with + several pre and post processing methods and experiment-wise configurations. + This simplicity may benefit the debugging when some unexpected error is induced. + """ def test_standard_circuit_construction(self): """Test standard parallel experiment construction.""" From ad0074f62e189b2ee57731c85e235513cff58d6d Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 14 Sep 2021 01:57:12 +0900 Subject: [PATCH 13/29] fix fake backend --- qiskit_experiments/test/t2ramsey_backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/test/t2ramsey_backend.py b/qiskit_experiments/test/t2ramsey_backend.py index d9ed61c0f7..305c152570 100644 --- a/qiskit_experiments/test/t2ramsey_backend.py +++ b/qiskit_experiments/test/t2ramsey_backend.py @@ -46,7 +46,7 @@ def __init__( configuration = QasmBackendConfiguration( backend_name="t2ramsey_simulator", backend_version="0", - n_qubits=int(1e6), + n_qubits=100, basis_gates=["barrier", "h", "p", "delay", "measure"], gates=[], local=True, From d5ca18d207a6deb6ed690b6ebdc68c2ed8b5246b Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Tue, 14 Sep 2021 02:30:32 +0900 Subject: [PATCH 14/29] Update qiskit_experiments/framework/base_experiment.py Co-authored-by: Eli Arbel <46826214+eliarbel@users.noreply.github.com> --- qiskit_experiments/framework/base_experiment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index 23c98d8729..d30f3956d0 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -187,7 +187,7 @@ def post_transpile_action( return circuits def run_transpile(self, backend: Backend, **options) -> List[QuantumCircuit]: - """Run transpile and returns transpiled circuits. + """Run transpile and return transpiled circuits. Args: backend: Target backend. From 502da20e4560b972ea3161bbfc3e0e693e24975d Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 14 Sep 2021 02:51:55 +0900 Subject: [PATCH 15/29] black&lint --- .../framework/composite/batch_experiment.py | 2 +- .../framework/composite/composite_experiment.py | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/qiskit_experiments/framework/composite/batch_experiment.py b/qiskit_experiments/framework/composite/batch_experiment.py index 28524452b8..5ce4f6f745 100644 --- a/qiskit_experiments/framework/composite/batch_experiment.py +++ b/qiskit_experiments/framework/composite/batch_experiment.py @@ -17,7 +17,6 @@ from qiskit import QuantumCircuit - from .composite_experiment import CompositeExperiment @@ -73,6 +72,7 @@ def __init__(self, experiments): super().__init__(experiments, qubits) + # pylint: disable=unused-argument def _flatten_circuits( self, circuits: List[List[QuantumCircuit]], diff --git a/qiskit_experiments/framework/composite/composite_experiment.py b/qiskit_experiments/framework/composite/composite_experiment.py index 497c2fa4a1..895eaea9f7 100644 --- a/qiskit_experiments/framework/composite/composite_experiment.py +++ b/qiskit_experiments/framework/composite/composite_experiment.py @@ -13,16 +13,17 @@ Composite Experiment abstract base class. """ +import warnings from abc import abstractmethod from typing import List, Optional -import warnings from qiskit import QuantumCircuit -from qiskit.providers import Backend, BaseJob from qiskit.exceptions import QiskitError +from qiskit.providers import Backend, BaseJob + from qiskit_experiments.framework import BaseExperiment, ExperimentData -from .composite_experiment_data import CompositeExperimentData from .composite_analysis import CompositeAnalysis +from .composite_experiment_data import CompositeExperimentData class CompositeExperiment(BaseExperiment): @@ -66,7 +67,7 @@ def run_transpile(self, backend: Backend, **options) -> List[QuantumCircuit]: # Generate a set of transpiled circuits for each nested experiment and have them as a list. # In each list element, a list of quantum circuit for nested experiment is stored. - # type: List[List[QuantumCircuit]] + # : List[List[QuantumCircuit]] experiment_circuits_list = list( map(lambda expr: expr.run_transpile(backend), self._experiments) ) @@ -190,4 +191,4 @@ def set_transpile_options(self, **fields): "Note that transpile options are provided by each nested experiment, " f"and thus provided options here {fields} are just discarded." ) - super().set_transpile_options(fields) + super().set_transpile_options(**fields) From 26c6dd4096bc7c46cbab6b89c24b810c0f17098d Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 14 Sep 2021 19:01:11 +0900 Subject: [PATCH 16/29] move duplicated code to external function --- .../framework/common_operations.py | 42 +++++++++++++++++++ .../library/characterization/t1.py | 19 +-------- .../library/characterization/t2ramsey.py | 19 +-------- 3 files changed, 46 insertions(+), 34 deletions(-) create mode 100644 qiskit_experiments/framework/common_operations.py diff --git a/qiskit_experiments/framework/common_operations.py b/qiskit_experiments/framework/common_operations.py new file mode 100644 index 0000000000..c0ea38fc3c --- /dev/null +++ b/qiskit_experiments/framework/common_operations.py @@ -0,0 +1,42 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. + +"""A collection of common operation callback in execution chain.""" + + +from qiskit.providers import Backend +from qiskit.test.mock import FakeBackend + +from .base_experiment import BaseExperiment + + +def apply_delay_validation(experiment: BaseExperiment, backend: Backend): + + if_simulator = getattr(backend.configuration(), "simulator", False) + + if not if_simulator and not isinstance(backend, FakeBackend): + timing_constraints = getattr( + experiment.transpile_options.__dict__, "timing_constraints", {} + ) + + # alignment=16 is IBM standard. Will be soon provided by IBM providers. + # Then, this configuration can be removed. + timing_constraints["acquire_alignment"] = getattr( + timing_constraints, "acquire_alignment", 16 + ) + + scheduling_method = getattr( + experiment.transpile_options.__dict__, "scheduling_method", "alap" + ) + experiment.set_transpile_options( + timing_constraints=timing_constraints, scheduling_method=scheduling_method + ) diff --git a/qiskit_experiments/library/characterization/t1.py b/qiskit_experiments/library/characterization/t1.py index b6c56278fe..5c31d5e71e 100644 --- a/qiskit_experiments/library/characterization/t1.py +++ b/qiskit_experiments/library/characterization/t1.py @@ -17,10 +17,9 @@ import numpy as np from qiskit.providers import Backend -from qiskit.test.mock import FakeBackend from qiskit.circuit import QuantumCircuit -from qiskit_experiments.framework import BaseExperiment, Options +from qiskit_experiments.framework import BaseExperiment, Options, common_operations from qiskit_experiments.library.characterization.t1_analysis import T1Analysis @@ -136,18 +135,4 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: def pre_transpile_action(self, backend: Backend): """Set timing constraints if backend is real hardware.""" - if not backend.configuration().simulator and not isinstance(backend, FakeBackend): - timing_constraints = getattr(self.transpile_options.__dict__, "timing_constraints", {}) - - # alignment=16 is IBM standard. Will be soon provided by IBM providers. - # Then, this configuration can be removed. - timing_constraints["acquire_alignment"] = getattr( - timing_constraints, "acquire_alignment", 16 - ) - - scheduling_method = getattr( - self.transpile_options.__dict__, "scheduling_method", "alap" - ) - self.set_transpile_options( - timing_constraints=timing_constraints, scheduling_method=scheduling_method - ) + common_operations.apply_delay_validation(self, backend) diff --git a/qiskit_experiments/library/characterization/t2ramsey.py b/qiskit_experiments/library/characterization/t2ramsey.py index 60e0d1c52f..ff443d5f8f 100644 --- a/qiskit_experiments/library/characterization/t2ramsey.py +++ b/qiskit_experiments/library/characterization/t2ramsey.py @@ -20,10 +20,9 @@ import qiskit from qiskit.utils import apply_prefix from qiskit.providers import Backend -from qiskit.test.mock import FakeBackend from qiskit.circuit import QuantumCircuit from qiskit.providers.options import Options -from qiskit_experiments.framework import BaseExperiment +from qiskit_experiments.framework import BaseExperiment, common_operations from .t2ramsey_analysis import T2RamseyAnalysis @@ -157,18 +156,4 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: def pre_transpile_action(self, backend: Backend): """Set timing constraints if backend is real hardware.""" - if not backend.configuration().simulator and not isinstance(backend, FakeBackend): - timing_constraints = getattr(self.transpile_options.__dict__, "timing_constraints", {}) - - # alignment=16 is IBM standard. Will be soon provided by IBM providers. - # Then, this configuration can be removed. - timing_constraints["acquire_alignment"] = getattr( - timing_constraints, "acquire_alignment", 16 - ) - - scheduling_method = getattr( - self.transpile_options.__dict__, "scheduling_method", "alap" - ) - self.set_transpile_options( - timing_constraints=timing_constraints, scheduling_method=scheduling_method - ) + common_operations.apply_delay_validation(self, backend) From c238504c2705ebb9b01bda97dbc3f8051dc89528 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 14 Sep 2021 19:06:33 +0900 Subject: [PATCH 17/29] docstring --- qiskit_experiments/framework/common_operations.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/framework/common_operations.py b/qiskit_experiments/framework/common_operations.py index c0ea38fc3c..dbf7fe5407 100644 --- a/qiskit_experiments/framework/common_operations.py +++ b/qiskit_experiments/framework/common_operations.py @@ -20,10 +20,15 @@ def apply_delay_validation(experiment: BaseExperiment, backend: Backend): + """Enable delay duration validation to conform to backend alignment constraints. - if_simulator = getattr(backend.configuration(), "simulator", False) + Args: + experiment: Experiment instance to run. + backend: Target backend. + """ + is_simulator = getattr(backend.configuration(), "simulator", False) - if not if_simulator and not isinstance(backend, FakeBackend): + if not is_simulator and not isinstance(backend, FakeBackend): timing_constraints = getattr( experiment.transpile_options.__dict__, "timing_constraints", {} ) From 4319d70fa550dbfea0e5e75ebbb37a512a2af9f9 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 14 Sep 2021 19:36:06 +0900 Subject: [PATCH 18/29] add composite transpile option test and reno --- .../composite/composite_experiment.py | 3 +- ...ment-execution-chain-74fb10adb88bf09d.yaml | 32 +++++++++++++++++++ test/test_composite.py | 22 +++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/update-base-experiment-execution-chain-74fb10adb88bf09d.yaml diff --git a/qiskit_experiments/framework/composite/composite_experiment.py b/qiskit_experiments/framework/composite/composite_experiment.py index 895eaea9f7..102c065939 100644 --- a/qiskit_experiments/framework/composite/composite_experiment.py +++ b/qiskit_experiments/framework/composite/composite_experiment.py @@ -189,6 +189,7 @@ def set_transpile_options(self, **fields): warnings.warn( "A composite experiment class doesn't provide transpile options. " "Note that transpile options are provided by each nested experiment, " - f"and thus provided options here {fields} are just discarded." + f"and thus provided options here {fields} are just discarded.", + UserWarning, ) super().set_transpile_options(**fields) diff --git a/releasenotes/notes/update-base-experiment-execution-chain-74fb10adb88bf09d.yaml b/releasenotes/notes/update-base-experiment-execution-chain-74fb10adb88bf09d.yaml new file mode 100644 index 0000000000..e73d53b293 --- /dev/null +++ b/releasenotes/notes/update-base-experiment-execution-chain-74fb10adb88bf09d.yaml @@ -0,0 +1,32 @@ +--- +features: + - | + The execution chain of :py:class:`~qiskit_experiments.framework.base_experiment.BaseExperiment` + is updated with some flexibility. This new feature will benefit experiment developers + who need to modify the standard job execution and analysis workflow. + + With this change, following three methods are newly introduced. + + - :py:meth:`~qiskit_experiments.framework.base_experiment.BaseExperiment#pre_transpile_action` + - :py:meth:`~qiskit_experiments.framework.base_experiment.BaseExperiment#post_transpile_action` + - :py:meth:`~qiskit_experiments.framework.base_experiment.BaseExperiment#post_analysis_action` + + These methods allow a developer to insert extra data processing routine (somewhat of hooks) + in between circuit generation and result data generation. + This feature increases the flexibility of job execution. + + In addition, :py:meth:`run_transpile` method is added to all experiment classes. + This returns a list of quantum circuits to execute on the given backend. + +upgrade: + - | + Behavior of the composite experiment is upgraded. + Now transpile options set to the nested experiment instances are retained during transpilation. + This means we can practically combine different set of experiments in a + single parallel or batch job. + Previously, all experimental circuits set to the composite experiment used to be transpiled + under the consistent transpile configurations, which may discard required + transpile configurations for a specific experiment. + + Note that run options set to each experiment instance is discarded because + Qiskit assumes a backend doesn't change run configurations per circuit. diff --git a/test/test_composite.py b/test/test_composite.py index 545c940404..297de23b2a 100644 --- a/test/test_composite.py +++ b/test/test_composite.py @@ -261,6 +261,28 @@ def test_run_option_overriden(self): prob_entry_exp1 = exp_data1.analysis_results("probability") self.assertEqual(prob_entry_exp1.extra["shots"], 1024) + def test_transpile_option_overriden(self): + """Test if transpile option is retained.""" + backend = ConfigurableFakeBackend("test", 2) + exp0 = FakeExperimentCommon(qubits=[0]) + exp1 = FakeExperimentCommon(qubits=[1]) + + par_exp = ParallelExperiment([exp0, exp1]) + + # transpile option for composite experiment doesn't affect transpilation + with self.assertWarns(UserWarning): + par_exp.set_transpile_options(basis_gates=["sx", "rz"]) + + test_circ = par_exp.run_transpile(backend=backend)[0] + + ref_circ = QuantumCircuit(*test_circ.qregs, *test_circ.cregs) + ref_circ.x(0) + ref_circ.x(1) + ref_circ.measure(0, 0) + ref_circ.measure(1, 1) + + self.assertEqual(test_circ, ref_circ) + def test_analysis_hook(self): """Test update class variable with analysis result.""" From 690d2ece2e45464c7825a2c11e93a2a5788786a7 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 14 Sep 2021 19:41:48 +0900 Subject: [PATCH 19/29] slightly update reno --- ...update-base-experiment-execution-chain-74fb10adb88bf09d.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/notes/update-base-experiment-execution-chain-74fb10adb88bf09d.yaml b/releasenotes/notes/update-base-experiment-execution-chain-74fb10adb88bf09d.yaml index e73d53b293..5476dcacae 100644 --- a/releasenotes/notes/update-base-experiment-execution-chain-74fb10adb88bf09d.yaml +++ b/releasenotes/notes/update-base-experiment-execution-chain-74fb10adb88bf09d.yaml @@ -28,5 +28,5 @@ upgrade: under the consistent transpile configurations, which may discard required transpile configurations for a specific experiment. - Note that run options set to each experiment instance is discarded because + Note that run options set to each experiment instance is still discarded because Qiskit assumes a backend doesn't change run configurations per circuit. From d56612c2ec29f1c1e3385636e4a334599798e79b Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 14 Sep 2021 19:45:23 +0900 Subject: [PATCH 20/29] remove redundant error message with update --- qiskit_experiments/framework/composite/composite_analysis.py | 4 +++- .../framework/composite/composite_experiment.py | 3 --- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/qiskit_experiments/framework/composite/composite_analysis.py b/qiskit_experiments/framework/composite/composite_analysis.py index 031a61c20e..f514ea6517 100644 --- a/qiskit_experiments/framework/composite/composite_analysis.py +++ b/qiskit_experiments/framework/composite/composite_analysis.py @@ -42,7 +42,9 @@ def _run_analysis(self, experiment_data: CompositeExperimentData, **options): experiment data. """ if not isinstance(experiment_data, self.__experiment_data__): - raise QiskitError("CompositeAnalysis must be run on CompositeExperimentData.") + raise QiskitError( + f"CompositeAnalysis must be run on {self.__experiment_data__.__class__.__name__}." + ) if len(options) > 0: warnings.warn( diff --git a/qiskit_experiments/framework/composite/composite_experiment.py b/qiskit_experiments/framework/composite/composite_experiment.py index 102c065939..c6c6e0c0e8 100644 --- a/qiskit_experiments/framework/composite/composite_experiment.py +++ b/qiskit_experiments/framework/composite/composite_experiment.py @@ -108,9 +108,6 @@ def run_analysis( No analysis configuration assumed for composite experiment object itself. """ - if not isinstance(experiment_data, self.__experiment_data__): - raise QiskitError("CompositeAnalysis must be run on CompositeExperimentData.") - return super().run_analysis(experiment_data, job, **options) @abstractmethod From 64a64699feb9b3e3507e737ba49f8bfc9afb6f6b Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 14 Sep 2021 20:08:54 +0900 Subject: [PATCH 21/29] remove note comment to class documentation --- .../composite/composite_experiment.py | 60 +++++++------------ 1 file changed, 22 insertions(+), 38 deletions(-) diff --git a/qiskit_experiments/framework/composite/composite_experiment.py b/qiskit_experiments/framework/composite/composite_experiment.py index c6c6e0c0e8..1b97fac13a 100644 --- a/qiskit_experiments/framework/composite/composite_experiment.py +++ b/qiskit_experiments/framework/composite/composite_experiment.py @@ -27,7 +27,28 @@ class CompositeExperiment(BaseExperiment): - """Composite Experiment base class""" + """Composite Experiment base class. + + Note: + Composite experiment defines different option handling policies for each + type of options. + + * transpile options: The transpile options set to each nested experiment are retained. + Thus, the experiment can transpile experimental circuits individually and combine. + Note that no transpile option can be set to composite experiment itself. + + * experiment options: Same with transpile options. + + * analysis options: Same with transpile options. However, one can set analysis options + to the composite experiment. The set value will override all analysis configurations + of the nested experiments. + + * run options: The run options set to nested experiments are discarded. + This is because Qiskit doesn't assume a backend that can execute each circuit + with different run options in a single job. If you want to keep run options + set to the individual experiment, you need to individually run these experiments. + + """ __analysis_class__ = CompositeAnalysis __experiment_data__ = CompositeExperimentData @@ -54,15 +75,6 @@ def run_transpile(self, backend: Backend, **options) -> List[QuantumCircuit]: Returns: Transpiled circuit to execute. - - Note: - This is transpile method for the composite experiment subclass. - This internally calls the transpile method of the nested experiments and - flattens the list of sub circuits generated by each experiment. - Note that transpile is called for individual circuit, and thus transpile - configurations and hook methods are separately applied. - - No transpile configuration assumed for composite experiment object itself. """ # Generate a set of transpiled circuits for each nested experiment and have them as a list. # In each list element, a list of quantum circuit for nested experiment is stored. @@ -82,34 +94,6 @@ def run_transpile(self, backend: Backend, **options) -> List[QuantumCircuit]: # merge circuits return self._flatten_circuits(experiment_circuits_list, n_qubits) - def run_analysis( - self, experiment_data: ExperimentData, job: BaseJob = None, **options - ) -> ExperimentData: - """Run analysis and update ExperimentData with analysis result. - - Args: - experiment_data: The experiment data to analyze. - job: The future object of experiment result which is currently running on the backend. - options: Additional analysis options. Any values set here will - override the value from :meth:`analysis_options` for the current run. - - Returns: - An experiment data object containing the analysis results and figures. - - Raises: - QiskitError: When the experiment data format is not for the composite experiment. - - Note: - This is analysis method for the composite experiment subclass. - This internally calls the analysis method of the nested experiments and - outputs a representative data entry for the composite analysis. - Note that analysis is called for individual experiment data, and thus analysis - configurations and hook methods are separately applied. - - No analysis configuration assumed for composite experiment object itself. - """ - return super().run_analysis(experiment_data, job, **options) - @abstractmethod def _flatten_circuits( self, From 003b9bed70bd94cd08281b1dbab73ad00aa5adfb Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 14 Sep 2021 20:11:54 +0900 Subject: [PATCH 22/29] update docs --- .../framework/composite/composite_experiment.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/qiskit_experiments/framework/composite/composite_experiment.py b/qiskit_experiments/framework/composite/composite_experiment.py index 1b97fac13a..b407c5d826 100644 --- a/qiskit_experiments/framework/composite/composite_experiment.py +++ b/qiskit_experiments/framework/composite/composite_experiment.py @@ -30,18 +30,18 @@ class CompositeExperiment(BaseExperiment): """Composite Experiment base class. Note: - Composite experiment defines different option handling policies for each - type of options. + Composite experiment defines different option handling policies for different + kind of options. - * transpile options: The transpile options set to each nested experiment are retained. + * transpile options: The transpile options set to nested experiments are retained. Thus, the experiment can transpile experimental circuits individually and combine. - Note that no transpile option can be set to composite experiment itself. + Note that no transpile option can be set to the composite experiment itself. - * experiment options: Same with transpile options. + * experiment options: Same with the transpile options. * analysis options: Same with transpile options. However, one can set analysis options to the composite experiment. The set value will override all analysis configurations - of the nested experiments. + of experiments associated with the composite experiment. * run options: The run options set to nested experiments are discarded. This is because Qiskit doesn't assume a backend that can execute each circuit From 91c8de9b7ed0948b113266d566787afc6cda91ce Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 14 Sep 2021 20:16:38 +0900 Subject: [PATCH 23/29] lint --- .../composite/composite_experiment.py | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/qiskit_experiments/framework/composite/composite_experiment.py b/qiskit_experiments/framework/composite/composite_experiment.py index b407c5d826..aa9f783e01 100644 --- a/qiskit_experiments/framework/composite/composite_experiment.py +++ b/qiskit_experiments/framework/composite/composite_experiment.py @@ -19,9 +19,9 @@ from qiskit import QuantumCircuit from qiskit.exceptions import QiskitError -from qiskit.providers import Backend, BaseJob +from qiskit.providers import Backend -from qiskit_experiments.framework import BaseExperiment, ExperimentData +from qiskit_experiments.framework import BaseExperiment from .composite_analysis import CompositeAnalysis from .composite_experiment_data import CompositeExperimentData @@ -29,25 +29,27 @@ class CompositeExperiment(BaseExperiment): """Composite Experiment base class. - Note: - Composite experiment defines different option handling policies for different - kind of options. + Composite experiment defines different option handling policies for different + kind of options. - * transpile options: The transpile options set to nested experiments are retained. - Thus, the experiment can transpile experimental circuits individually and combine. - Note that no transpile option can be set to the composite experiment itself. + transpile options + The transpile options set to nested experiments are retained. + Thus, the experiment can transpile experimental circuits individually and combine. + Note that no transpile option can be set to the composite experiment itself. - * experiment options: Same with the transpile options. + experiment options + Same with the transpile options. - * analysis options: Same with transpile options. However, one can set analysis options - to the composite experiment. The set value will override all analysis configurations - of experiments associated with the composite experiment. - - * run options: The run options set to nested experiments are discarded. - This is because Qiskit doesn't assume a backend that can execute each circuit - with different run options in a single job. If you want to keep run options - set to the individual experiment, you need to individually run these experiments. + analysis options + Same with transpile options. However, one can set analysis options + to the composite experiment. The set value will override all analysis configurations + of experiments associated with the composite experiment. + run options + The run options set to nested experiments are discarded. + This is because Qiskit doesn't assume a backend that can execute each circuit + with different run options in a single job. If you want to keep run options + set to the individual experiment, you need to individually run these experiments. """ __analysis_class__ = CompositeAnalysis From e234afa16964d13faa9ad951af1cd9643c7a55d0 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 28 Sep 2021 13:40:09 +0900 Subject: [PATCH 24/29] make hooks protected member --- qiskit_experiments/framework/base_experiment.py | 12 ++++++------ qiskit_experiments/library/characterization/t1.py | 2 +- .../library/characterization/t2ramsey.py | 2 +- .../library/randomized_benchmarking/rb_experiment.py | 2 +- test/test_composite.py | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index d30f3956d0..d0f02ca161 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -144,7 +144,7 @@ def _initialize_experiment_data( return experiment_data._copy_metadata() - def pre_transpile_action(self, backend: Backend): + def _pre_transpile_action(self, backend: Backend): """An extra subroutine executed before transpilation. Note: @@ -163,7 +163,7 @@ def pre_transpile_action(self, backend: Backend): pass # pylint: disable = unused-argument - def post_transpile_action( + def _post_transpile_action( self, circuits: List[QuantumCircuit], backend: Backend ) -> List[QuantumCircuit]: """An extra subroutine executed after transpilation. @@ -197,7 +197,7 @@ def run_transpile(self, backend: Backend, **options) -> List[QuantumCircuit]: Transpiled circuit to execute. """ # Run pre transpile if implemented by subclasses. - self.pre_transpile_action(backend) + self._pre_transpile_action(backend) # Get transpile options transpile_options = copy.copy(self.transpile_options) @@ -210,11 +210,11 @@ def run_transpile(self, backend: Backend, **options) -> List[QuantumCircuit]: circuits = transpile(circuits=self.circuits(backend), backend=backend, **transpile_options) # Run post transpile. This is implemented by each experiment subclass. - circuits = self.post_transpile_action(circuits, backend) + circuits = self._post_transpile_action(circuits, backend) return circuits - def post_analysis_action(self, experiment_data: ExperimentData): + def _post_analysis_action(self, experiment_data: ExperimentData): """An extra subroutine executed after analysis. Note: @@ -280,7 +280,7 @@ def run_analysis( ) # Run post analysis. This is implemented by each experiment subclass. - self.post_analysis_action(experiment_data) + self._post_analysis_action(experiment_data) return experiment_data diff --git a/qiskit_experiments/library/characterization/t1.py b/qiskit_experiments/library/characterization/t1.py index 5c31d5e71e..51075d6771 100644 --- a/qiskit_experiments/library/characterization/t1.py +++ b/qiskit_experiments/library/characterization/t1.py @@ -132,7 +132,7 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: return circuits - def pre_transpile_action(self, backend: Backend): + def _pre_transpile_action(self, backend: Backend): """Set timing constraints if backend is real hardware.""" common_operations.apply_delay_validation(self, backend) diff --git a/qiskit_experiments/library/characterization/t2ramsey.py b/qiskit_experiments/library/characterization/t2ramsey.py index 0d745d8024..0d16233fc1 100644 --- a/qiskit_experiments/library/characterization/t2ramsey.py +++ b/qiskit_experiments/library/characterization/t2ramsey.py @@ -151,7 +151,7 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: return circuits - def pre_transpile_action(self, backend: Backend): + def _pre_transpile_action(self, backend: Backend): """Set timing constraints if backend is real hardware.""" common_operations.apply_delay_validation(self, backend) diff --git a/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py b/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py index c2ad9efe11..05137236bf 100644 --- a/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py +++ b/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py @@ -220,7 +220,7 @@ def _get_circuit_metadata(self, circuit): return meta return None - def post_transpile_action( + def _post_transpile_action( self, circuits: List[QuantumCircuit], backend: Backend ) -> List[QuantumCircuit]: """Count gate operations in each circuit and update metadata.""" diff --git a/test/test_composite.py b/test/test_composite.py index 297de23b2a..134e0a9ec5 100644 --- a/test/test_composite.py +++ b/test/test_composite.py @@ -121,7 +121,7 @@ def test_pulse_gate_experiment_with_post_transpile_hook(self): class FakeExperimentPulseGate(FakeExperimentCommon): """Add transpiler hook to insert calibration.""" - def post_transpile_action(self, circuits, backend): + def _post_transpile_action(self, circuits, backend): for circ in circuits: circ.add_calibration( "x", self.physical_qubits, Schedule(name="test_calibration") @@ -153,7 +153,7 @@ def test_update_circuit_metadata_with_post_transpile_hook(self): class FakeExperimentUpdateCircuitMetadata(FakeExperimentCommon): """Add transpiler hook to update metadata.""" - def post_transpile_action(self, circuits, backend): + def _post_transpile_action(self, circuits, backend): for circ in circuits: circ.metadata["new_data"] = "test_value" @@ -293,7 +293,7 @@ def __init__(self, qubits): super().__init__(qubits) self.probability = None - def post_analysis_action(self, experiment_data): + def _post_analysis_action(self, experiment_data): prob_val = experiment_data.analysis_results("probability").value self.probability = prob_val From 9250b6efbb042735316c2451f3b73f16a6d0041e Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 28 Sep 2021 16:04:38 +0900 Subject: [PATCH 25/29] keep qubit ordering --- qiskit_experiments/framework/composite/batch_experiment.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/framework/composite/batch_experiment.py b/qiskit_experiments/framework/composite/batch_experiment.py index 5ce4f6f745..2bc6b7da83 100644 --- a/qiskit_experiments/framework/composite/batch_experiment.py +++ b/qiskit_experiments/framework/composite/batch_experiment.py @@ -67,8 +67,9 @@ def __init__(self, experiments): """ qubits = list() for expr in experiments: - qubits.extend(expr.physical_qubits) - qubits = sorted(set(qubits)) + for qubit in expr.physical_qubits: + if qubit not in qubits: + qubits.append(qubit) super().__init__(experiments, qubits) From f24f5cb87bc08a6e84fd159d6efe4919c371f113 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 28 Sep 2021 16:08:56 +0900 Subject: [PATCH 26/29] add unittest --- test/test_composite.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/test_composite.py b/test/test_composite.py index 134e0a9ec5..5e1d86adc4 100644 --- a/test/test_composite.py +++ b/test/test_composite.py @@ -337,3 +337,13 @@ def test_standard_circuit_construction(self): ref_circ1.measure(1, 0) self.assertListEqual(test_circs, [ref_circ0, ref_circ1]) + + def test_keep_qubit_ordering(self): + """Test if qubit ordering is preserved.""" + + exp0 = FakeExperimentCommon(qubits=[1, 0]) + exp1 = FakeExperimentCommon(qubits=[1, 0]) + + par_exp = BatchExperiment([exp0, exp1]) + + self.assertTupleEqual(par_exp.physical_qubits, (1, 0)) From bf34b7793500e301f08a35ca315d6ab7af449ae1 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 28 Sep 2021 16:26:45 +0900 Subject: [PATCH 27/29] revert change to run_analysis --- qiskit_experiments/framework/base_analysis.py | 3 ++ .../framework/base_experiment.py | 34 ++++--------------- 2 files changed, 9 insertions(+), 28 deletions(-) diff --git a/qiskit_experiments/framework/base_analysis.py b/qiskit_experiments/framework/base_analysis.py index adfe2cf831..c72b50beee 100644 --- a/qiskit_experiments/framework/base_analysis.py +++ b/qiskit_experiments/framework/base_analysis.py @@ -101,6 +101,9 @@ def run( if figures: experiment_data.add_figures(figures) + # Run post analysis + experiment_data.experiment._post_analysis_action(experiment_data) + return experiment_data def _format_analysis_result(self, data, experiment_id, experiment_components=None): diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index d0f02ca161..3af2b09a88 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -117,9 +117,8 @@ def run( # Add experiment option metadata self._add_job_metadata(experiment_data, job, **run_opts) - # Run analysis - if analysis: - experiment_data = self.run_analysis(experiment_data, job) + if analysis and self.__analysis_class__ is not None: + experiment_data.add_data(job, post_processing_callback=self.run_analysis) else: experiment_data.add_data(job) @@ -240,15 +239,12 @@ def _post_analysis_action(self, experiment_data: ExperimentData): pass def run_analysis( - self, experiment_data: ExperimentData, job: BaseJob = None, **options + self, experiment_data: ExperimentData, **options ) -> ExperimentData: """Run analysis and update ExperimentData with analysis result. Args: experiment_data: The experiment data to analyze. - job: The future object of experiment result which is currently running on the backend. - options: Additional analysis options. Any values set here will - override the value from :meth:`analysis_options` for the current run. Returns: An experiment data object containing the analysis results and figures. @@ -256,32 +252,14 @@ def run_analysis( Raises: QiskitError: Method is called with an empty experiment result. """ - run_analysis = self.analysis() if self.__analysis_class__ else None - # Get analysis options analysis_options = copy.copy(self.analysis_options) analysis_options.update_options(**options) analysis_options = analysis_options.__dict__ - if not job and run_analysis is not None: - # Run analysis immediately - if not experiment_data.data(): - raise QiskitError( - "Experiment data seems to be empty and no running job is provided. " - "At least one data entry is required to run analysis." - ) - experiment_data = run_analysis.run(experiment_data, **analysis_options) - else: - # Run analysis when job is completed - experiment_data.add_data( - data=job, - post_processing_callback=run_analysis.run, - **analysis_options, - ) - - # Run post analysis. This is implemented by each experiment subclass. - self._post_analysis_action(experiment_data) - + # Run analysis + analysis = self.analysis() + analysis.run(experiment_data, **analysis_options) return experiment_data @property From b7f8ac0bf6ce33f33b6e30c852f169c718973127 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 28 Sep 2021 16:57:47 +0900 Subject: [PATCH 28/29] add None check --- qiskit_experiments/framework/base_analysis.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/framework/base_analysis.py b/qiskit_experiments/framework/base_analysis.py index c72b50beee..7d6123351a 100644 --- a/qiskit_experiments/framework/base_analysis.py +++ b/qiskit_experiments/framework/base_analysis.py @@ -102,7 +102,8 @@ def run( experiment_data.add_figures(figures) # Run post analysis - experiment_data.experiment._post_analysis_action(experiment_data) + if experiment_data.experiment is not None: + experiment_data.experiment._post_analysis_action(experiment_data) return experiment_data From 1df06b2f276e263153114ac7bd787b298912810a Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 28 Sep 2021 17:09:58 +0900 Subject: [PATCH 29/29] black&lint --- qiskit_experiments/framework/base_experiment.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index 7371ceeab1..9db9170d8a 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -238,13 +238,14 @@ def _post_analysis_action(self, experiment_data: ExperimentData): """ pass - def run_analysis( - self, experiment_data: ExperimentData, **options - ) -> ExperimentData: + def run_analysis(self, experiment_data: ExperimentData, **options) -> ExperimentData: """Run analysis and update ExperimentData with analysis result. Args: experiment_data: The experiment data to analyze. + options: additional analysis options. Any values set here will + override the value from :meth:`analysis_options` + for the current run. Returns: An experiment data object containing the analysis results and figures.