From cecdec35dfc53c6db5c5af2977eba7bfb973cf11 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Thu, 19 Aug 2021 18:05:14 +0900 Subject: [PATCH] event hook mechanism --- .../framework/base_experiment.py | 122 +++++------------- .../framework/events/__init__.py | 17 +++ .../framework/events/events_execute.py | 29 +++++ .../framework/events/events_postprocess.py | 47 +++++++ .../framework/events/events_preprocessing.py | 44 +++++++ .../framework/events/events_transpiler.py | 82 ++++++++++++ qiskit_experiments/framework/events/runner.py | 71 ++++++++++ .../library/characterization/t1.py | 3 + .../library/characterization/t2ramsey.py | 3 + .../randomized_benchmarking/rb_experiment.py | 13 +- .../randomized_benchmarking/rb_utils.py | 32 ----- 11 files changed, 333 insertions(+), 130 deletions(-) create mode 100644 qiskit_experiments/framework/events/__init__.py create mode 100644 qiskit_experiments/framework/events/events_execute.py create mode 100644 qiskit_experiments/framework/events/events_postprocess.py create mode 100644 qiskit_experiments/framework/events/events_preprocessing.py create mode 100644 qiskit_experiments/framework/events/events_transpiler.py create mode 100644 qiskit_experiments/framework/events/runner.py diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index 4240ae91c1..b4e3ddeba6 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -13,20 +13,19 @@ Base Experiment class. """ -from abc import ABC, abstractmethod -from typing import Iterable, Optional, Tuple, List, Dict import copy +from abc import ABC, abstractmethod from numbers import Integral +from typing import Iterable, Optional, Tuple, List, Dict -from qiskit import transpile, assemble, QuantumCircuit -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 import QuantumCircuit from qiskit.exceptions import QiskitError +from qiskit.providers.backend import Backend from qiskit.qobj.utils import MeasLevel + from qiskit_experiments.framework import Options from qiskit_experiments.framework.experiment_data import ExperimentData +from .events import ExperimentRunner class BaseExperiment(ABC): @@ -47,6 +46,18 @@ class BaseExperiment(ABC): # ExperimentData class for experiment __experiment_data__ = ExperimentData + # Execute hooks + __execute_events__ = ["backend_run"] + + # Transpile hooks + __transpile_events__ = ["transpile_circuits"] + + # Pre-processing hooks + __pre_processing_events__ = ["initialize_experiment_data", "update_run_options"] + + # Post-processing hooks + __post_processing_events__ = ["add_job_metadata", "set_analysis"] + def __init__(self, qubits: Iterable[int], experiment_type: Optional[str] = None): """Initialize the experiment object. @@ -101,71 +112,27 @@ def run( QiskitError: if experiment is run with an incompatible existing ExperimentData container. """ - # Create experiment data container - experiment_data = self._initialize_experiment_data(backend, experiment_data) - - # Run options - run_opts = copy.copy(self.run_options) - 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) + runner = ExperimentRunner(self) - 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 + # add pre-processing events + for handler in self.__pre_processing_events__: + runner.add_handler(handler, module="events_preprocessing") - experiment_data.add_data(job, post_processing_callback=run_analysis) + # add transpiler events + for handler in self.__transpile_events__: + runner.add_handler(handler, module="events_transpiler") - # Return the ExperimentData future - return experiment_data + # add execution events + for handler in self.__execute_events__: + runner.add_handler(handler, module="events_execute") - def _initialize_experiment_data( - self, backend: Backend, experiment_data: Optional[ExperimentData] = None - ) -> ExperimentData: - """Initialize the return data container for the experiment run""" - if experiment_data is None: - return self.__experiment_data__(experiment=self, backend=backend) - - # Validate experiment is compatible with existing data - if not isinstance(experiment_data, ExperimentData): - raise QiskitError("Input `experiment_data` is not a valid ExperimentData.") - if experiment_data.experiment_type != self._type: - raise QiskitError("Existing ExperimentData contains data from a different experiment.") - if experiment_data.metadata.get("physical_qubits") != list(self.physical_qubits): - raise QiskitError( - "Existing ExperimentData contains data for a different set of physical qubits." - ) + # add post-processing events + for handler in self.__post_processing_events__: + runner.add_handler(handler, module="events_postprocess") - return experiment_data._copy_metadata() + return runner.run( + backend=backend, analysis=analysis, experiment_data=experiment_data, **run_options + ) def run_analysis(self, experiment_data, **options) -> ExperimentData: """Run analysis and update ExperimentData with analysis result. @@ -335,10 +302,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. @@ -364,20 +327,3 @@ def _additional_metadata(self) -> Dict[str, any]: additional experiment metadata in ExperimentData. """ return {} - - def _add_job_metadata(self, experiment_data: ExperimentData, job: BaseJob, **run_options): - """Add runtime job metadata to ExperimentData. - - Args: - experiment_data: the experiment data container. - job: the job object. - run_options: backend run options for the job. - """ - metadata = { - "job_id": job.job_id(), - "experiment_options": copy.copy(self.experiment_options.__dict__), - "transpile_options": copy.copy(self.transpile_options.__dict__), - "analysis_options": copy.copy(self.analysis_options.__dict__), - "run_options": copy.copy(run_options), - } - experiment_data._metadata["job_metadata"].append(metadata) diff --git a/qiskit_experiments/framework/events/__init__.py b/qiskit_experiments/framework/events/__init__.py new file mode 100644 index 0000000000..ffcd778f1b --- /dev/null +++ b/qiskit_experiments/framework/events/__init__.py @@ -0,0 +1,17 @@ +# 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. + +""" +Event hooks to run experiment. +""" + +from .runner import ExperimentRunner diff --git a/qiskit_experiments/framework/events/events_execute.py b/qiskit_experiments/framework/events/events_execute.py new file mode 100644 index 0000000000..deb96c3cf9 --- /dev/null +++ b/qiskit_experiments/framework/events/events_execute.py @@ -0,0 +1,29 @@ +# 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. + +""" +Execute events. +""" + +from qiskit import assemble +from qiskit.providers.basebackend import BaseBackend as LegacyBackend + + +def backend_run(experiment, backend, circuits, run_options, **kwargs): + + if isinstance(backend, LegacyBackend): + qobj = assemble(circuits, backend=backend, **run_options) + job = backend.run(qobj) + else: + job = backend.run(circuits, **run_options) + + return {"job": job} diff --git a/qiskit_experiments/framework/events/events_postprocess.py b/qiskit_experiments/framework/events/events_postprocess.py new file mode 100644 index 0000000000..efa4f33f69 --- /dev/null +++ b/qiskit_experiments/framework/events/events_postprocess.py @@ -0,0 +1,47 @@ +# 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. + +""" +Post processing events. +""" + +import copy + + +def set_analysis(experiment, analysis, job, experiment_data, **kwargs): + + if analysis and experiment.__analysis_class__ is not None: + run_analysis_callback = experiment.run_analysis + else: + run_analysis_callback = None + + experiment_data.add_data(job, post_processing_callback=run_analysis_callback) + + +def add_job_metadata(experiment, experiment_data, job, run_options, **kwargs): + """Add runtime job metadata to ExperimentData. + + Args: + experiment: Experiment object. + experiment_data: The experiment data container. + job: The job object. + run_options: Backend run options for the job. + """ + metadata = { + "job_id": job.job_id(), + "experiment_options": copy.copy(experiment.experiment_options.__dict__), + "transpile_options": copy.copy(experiment.transpile_options.__dict__), + "analysis_options": copy.copy(experiment.analysis_options.__dict__), + "run_options": copy.copy(run_options) + } + + experiment_data._metadata["job_metadata"].append(metadata) diff --git a/qiskit_experiments/framework/events/events_preprocessing.py b/qiskit_experiments/framework/events/events_preprocessing.py new file mode 100644 index 0000000000..5edd0c80e3 --- /dev/null +++ b/qiskit_experiments/framework/events/events_preprocessing.py @@ -0,0 +1,44 @@ +# 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. + +""" +Pre processing events. +""" +import copy + +from qiskit_experiments.framework import ExperimentData +from qiskit.exceptions import QiskitError + + +def initialize_experiment_data(experiment, backend, expeirment_data, **kwargs): + if expeirment_data is not None: + init_data = experiment.__experiment_data__(experiment=experiment, backend=backend) + else: + # Validate experiment is compatible with existing data + if not isinstance(expeirment_data, ExperimentData): + raise QiskitError("Input `experiment_data` is not a valid ExperimentData.") + if expeirment_data.experiment_type != experiment.experiment_type: + raise QiskitError("Existing ExperimentData contains data from a different experiment.") + if expeirment_data.metadata.get("physical_qubits") != list(experiment.physical_qubits): + raise QiskitError( + "Existing ExperimentData contains data for a different set of physical qubits." + ) + init_data = expeirment_data._copy_metadata() + + return {"expeirment_data": init_data} + + +def update_run_options(experiment, run_options, **kwargs): + options = copy.copy(experiment.run_options).__dict__ + options.update(**run_options) + + return {"run_options": options} diff --git a/qiskit_experiments/framework/events/events_transpiler.py b/qiskit_experiments/framework/events/events_transpiler.py new file mode 100644 index 0000000000..69817ace02 --- /dev/null +++ b/qiskit_experiments/framework/events/events_transpiler.py @@ -0,0 +1,82 @@ +# 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. + +""" +Transpile events. +""" + +import copy + +from qiskit import transpile +from qiskit.test.mock import FakeBackend + +from qiskit_experiments.framework import ParallelExperiment + + +def count_transpiled_ops(experiment, circuits, **kwargs): + + def get_metadata(circuit): + if circuit.metadata["experiment_type"] == experiment.experiment_type: + return circuit.metadata + if circuit.metadata["experiment_type"] == ParallelExperiment.__name__: + for meta in circuit.metadata["composite_metadata"]: + if meta["physical_qubits"] == experiment.physical_qubits: + return meta + return dict() + + for circ in circuits: + meta = get_metadata(circ) + if meta: + qubits = range(len(circ.qubits)) + c_count_ops = {} + for instr, qargs, _ in circ: + instr_qubits = [] + skip_instr = False + for qubit in qargs: + qubit_index = circ.qubits.index(qubit) + if qubit_index not in qubits: + skip_instr = True + instr_qubits.append(qubit_index) + if not skip_instr: + instr_qubits = tuple(instr_qubits) + c_count_ops[(instr_qubits, instr.name)] = ( + c_count_ops.get((instr_qubits, instr.name), 0) + 1 + ) + circuit_length = meta["xval"] + count_ops = [(key, (value, circuit_length)) for key, value in c_count_ops.items()] + meta.update({"count_ops": count_ops}) + + +def set_scheduling_contraints(expeirment, backend, **kwargs): + if not backend.configuration().simulator and not isinstance(backend, FakeBackend): + timing_constraints = getattr( + expeirment.transpile_options.__dict__, + "timing_constraints", + {} + ) + timing_constraints["acquire_alignment"] = getattr( + timing_constraints, "acquire_alignment", 16 + ) + scheduling_method = getattr( + expeirment.transpile_options.__dict__, "scheduling_method", "alap" + ) + expeirment.set_transpile_options( + timing_constraints=timing_constraints, scheduling_method=scheduling_method + ) + + +def transpile_circuits(experiment, backend, **kwargs): + options = copy.copy(experiment.transpile_options.__dict__) + options["initial_layout"] = list(experiment.physical_qubits) + circuits = transpile(experiment.circuits(backend), backend, **options) + + return {"circuits": circuits} diff --git a/qiskit_experiments/framework/events/runner.py b/qiskit_experiments/framework/events/runner.py new file mode 100644 index 0000000000..8db3cd5ecd --- /dev/null +++ b/qiskit_experiments/framework/events/runner.py @@ -0,0 +1,71 @@ +# 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. + +""" +EventHook for experiment runner. +""" + +import importlib + +from qiskit.exceptions import QiskitError +from qiskit_experiments.framework.experiment_data import ExperimentData + + +class ExperimentRunner: + """Event handler of the experiment run.""" + + def __init__(self, experiment: "BaseExperiment"): + """Create new runner. + + Args: + experiment: Associated experiment instance. + """ + self.__experiment = experiment + + self.__handlers = list() + self.__scope_vars = dict() + + def add_handler(self, handler: str, module: str): + """Add new event handler. + + Args: + handler: Name of event handler. + module: Name of event module. + """ + try: + callback = importlib.import_module(f"{module}.{handler}") + self.__handlers.append(callback) + except ModuleNotFoundError: + QiskitError(f"Event handler {module}.{handler} is not found.") + + def run(self, **kwargs) -> ExperimentData: + """Run experiment. + + Args: + kwargs: User provided runtime options. + + Returns: + Experiment data. + """ + self.__scope_vars.update(**kwargs) + + for handler in self.__handlers: + scope_args = handler(self.__experiment, **self.__scope_vars) + if scope_args: + self.__scope_vars.update(scope_args) + + # return experiment data + try: + return self.__scope_vars["experiment_data"] + except KeyError: + # TODO no logger + raise QiskitError("Experiment result data is not generated. Check log.") diff --git a/qiskit_experiments/library/characterization/t1.py b/qiskit_experiments/library/characterization/t1.py index d03b9761c1..d6a6e529cc 100644 --- a/qiskit_experiments/library/characterization/t1.py +++ b/qiskit_experiments/library/characterization/t1.py @@ -46,6 +46,9 @@ class T1(BaseExperiment): __analysis_class__ = T1Analysis + # update transpile events + __transpile_events__ = ["set_scheduling_contraints", "transpile_circuits"] + @classmethod def _default_experiment_options(cls) -> Options: """Default experiment options. diff --git a/qiskit_experiments/library/characterization/t2ramsey.py b/qiskit_experiments/library/characterization/t2ramsey.py index 3b1750c138..08f221fa5f 100644 --- a/qiskit_experiments/library/characterization/t2ramsey.py +++ b/qiskit_experiments/library/characterization/t2ramsey.py @@ -63,6 +63,9 @@ class T2Ramsey(BaseExperiment): """ __analysis_class__ = T2RamseyAnalysis + # update transpile events + __transpile_events__ = ["set_scheduling_contraints", "transpile_circuits"] + @classmethod def _default_experiment_options(cls) -> Options: """Default experiment options. diff --git a/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py b/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py index bf9f3b29e1..0c0b4ae71d 100644 --- a/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py +++ b/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py @@ -59,6 +59,9 @@ class StandardRB(BaseExperiment): # Analysis class for experiment __analysis_class__ = RBAnalysis + # Update transpile hook + __transpile_events__ = ["transpile_circuits", "count_transpiled_ops"] + def __init__( self, qubits: Union[int, Iterable[int]], @@ -219,13 +222,3 @@ def _get_circuit_metadata(self, circuit): if meta["physical_qubits"] == self.physical_qubits: 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) - if meta is not None: - c_count_ops = RBUtils.count_ops(c, 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}) diff --git a/qiskit_experiments/library/randomized_benchmarking/rb_utils.py b/qiskit_experiments/library/randomized_benchmarking/rb_utils.py index fa99c7e08c..acdd2d5a5d 100644 --- a/qiskit_experiments/library/randomized_benchmarking/rb_utils.py +++ b/qiskit_experiments/library/randomized_benchmarking/rb_utils.py @@ -59,38 +59,6 @@ def get_error_dict_from_backend( return None return error_dict - @staticmethod - def count_ops( - circuit: QuantumCircuit, qubits: Optional[Iterable[int]] = None - ) -> Dict[Tuple[Iterable[int], str], int]: - """Counts occurrences of each gate in the given circuit - - Args: - circuit: The quantum circuit whose gates are counted - qubits: A list of qubits to filter irrelevant gates - - Returns: - A dictionary of the form (qubits, gate) -> value where value - is the number of occurrences of the gate on the given qubits - """ - if qubits is None: - qubits = range(len(circuit.qubits)) - count_ops_result = {} - for instr, qargs, _ in circuit: - instr_qubits = [] - skip_instr = False - for qubit in qargs: - qubit_index = circuit.qubits.index(qubit) - if qubit_index not in qubits: - skip_instr = True - instr_qubits.append(qubit_index) - if not skip_instr: - instr_qubits = tuple(instr_qubits) - count_ops_result[(instr_qubits, instr.name)] = ( - count_ops_result.get((instr_qubits, instr.name), 0) + 1 - ) - return count_ops_result - @staticmethod def gates_per_clifford( ops_count: List,