-
Notifications
You must be signed in to change notification settings - Fork 136
Experiment run hook methods #380
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
c0dcd99
5724afa
c0cd0a7
9bb746a
be9ce8c
8492bc2
f1a1bcb
88f1304
4baca73
80a689e
3fa62e1
2abc6d8
b7b1128
ad0074f
d5ca18d
502da20
8fca8e9
26c6dd4
c238504
4319d70
690d2ec
d56612c
b968434
64a6469
003b9be
91c8de9
e234afa
9250b6e
f24f5cb
bf34b77
5768915
b7f8ac0
1df06b2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
|
|
@@ -109,43 +104,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 +144,114 @@ def _initialize_experiment_data( | |
|
|
||
| return experiment_data._copy_metadata() | ||
|
|
||
| def run_analysis(self, experiment_data, **options) -> ExperimentData: | ||
| def _pre_transpile_hook(self, backend: Backend): | ||
|
yaelbh marked this conversation as resolved.
Outdated
|
||
| """An extra subroutine executed before transpilation. | ||
|
nkanazawa1989 marked this conversation as resolved.
|
||
|
|
||
| Args: | ||
| backend: Target backend. | ||
| """ | ||
| pass | ||
|
|
||
| # pylint: disable = unused-argument | ||
| def _post_transpile_hook( | ||
| self, circuits: List[QuantumCircuit], backend: Backend | ||
| ) -> List[QuantumCircuit]: | ||
| """An extra subroutine executed after transpilation. | ||
|
eggerdj marked this conversation as resolved.
|
||
|
|
||
| Args: | ||
| circuits: List of transpiled circuits. | ||
| backend: Target backend. | ||
|
|
||
| Returns: | ||
| List of circuits to execute. | ||
| """ | ||
| return circuits | ||
|
|
||
| def run_transpile(self, backend: Backend, **options) -> List[QuantumCircuit]: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's better to have this interface instead: def run_transpile(self, circuits, backend: Backend, **options) -> List[QuantumCircuit]: That is to have the the circuits generation phase called explicitly in the
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This causes a problem in Another advantage of this signature would be #380 (comment). User can easily check what will be executed. If we assume circuit is always generated by
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. According to your comment, probably
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there any reason to have this function instead of just having a def _circuits(self, backend=None):
# equivalent to existing circuit method for current experiments
def circuits(self, backend=None, transpile=True, **options):
circuits = self._circuits(backend)
if transpile:
transpile_options = ...
circuits = transpile(self._circuits(backend), backend, **transpile_options)
self._post_transpile_action(circuits, backend)
return circuitsMaybe you don't need the transpile kwarg and can just always transpile
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like this approach. However, this always returns transpiled circuit, i.e. even single qubit experiment returns full qubit circuits. Sometime this make it difficult to understand what is happening in the experiment. |
||
| """Run transpile and returns transpiled circuits. | ||
|
nkanazawa1989 marked this conversation as resolved.
Outdated
|
||
|
|
||
| Args: | ||
| backend: Target backend. | ||
| options: User provided runtime options. | ||
|
|
||
| Returns: | ||
| Transpiled circuit to execute. | ||
| """ | ||
| # Run pre transpile. This is implemented by each experiment subclass. | ||
|
nkanazawa1989 marked this conversation as resolved.
Outdated
|
||
| self._pre_transpile_hook(backend) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think those calls should be at the
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't assume any situation we need to customize The another important point in my mind is exp1 = Experiment1(**options1)
exp2 = Experiment2(**options2)
data1 = exp1.run(backend)
data2 = exp2.run(backend)
par_exp = ParallelExperiment(exp1, exp2)
par_data = par_exp.run(backend)
assert parallel_result.component_experiment_data(0) == data1
assert parallel_result.component_experiment_data(1) == data2This example means a composite experiment is just a macro that combines experiments for efficient execution, and it should NOT implement own pre/post methods not to override the experimental configuration of sub experiments. I think the composite experiment should always call pre/post methods of nested experiments. |
||
|
|
||
| # 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): | ||
|
eggerdj marked this conversation as resolved.
Outdated
|
||
| """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. | ||
| """ | ||
| pass | ||
|
|
||
| def run_analysis( | ||
| self, experiment_data: ExperimentData, job: BaseJob = None, **options | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do you need to add the
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think your PR #398 will improve this. I wanted to tie analysis to post analysis so that composite experiment can easily handle entire analysis logic. Otherwise composite needs to guarantee the matching of analysis and post analysis, and the logic will be bit more complicated. |
||
| ) -> 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() 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.run(experiment_data, **analysis_options) | ||
| else: | ||
| # Run analysis when job is completed | ||
| experiment_data.add_data( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This gives this function a confusing signature, if i call it with data and a job it is going to try and add that job to the data, but adding job data to experiment data should probably always be done by the
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is fixed in bf34b77 |
||
| data=job, | ||
| post_processing_callback=run_analysis.run, | ||
| **analysis_options, | ||
| ) | ||
|
|
||
| # Run post analysis. This is implemented by each experiment subclass. | ||
| self._post_analysis_hook(experiment_data) | ||
|
|
||
| return experiment_data | ||
|
|
||
| @property | ||
|
|
@@ -335,10 +397,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. | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,62 +13,80 @@ | |
| 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. | ||
|
nkanazawa1989 marked this conversation as resolved.
Outdated
|
||
| 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. | ||
|
|
||
| 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], []))) | ||
|
nkanazawa1989 marked this conversation as resolved.
Outdated
|
||
| super().__init__(experiments, qubits) | ||
|
|
||
| def circuits(self, backend=None): | ||
| def _flatten_circuits( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not keeping the name
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh this is due to poor gitdiff. The Actually this is one of my concerns in this PR. Previously, this I think an inner-loop of a variational algorithm can be implemented as a |
||
| self, | ||
| circuits: List[List[QuantumCircuit]], | ||
| num_qubits: int, | ||
| ) -> List[QuantumCircuit]: | ||
| """Flatten circuits. | ||
|
|
||
| Note: | ||
| This experiment concatenates sub experiment circuits. | ||
|
nkanazawa1989 marked this conversation as resolved.
Outdated
|
||
| """ | ||
| 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 | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This block looks strange, that if you are doing analysis
run_analysishandles adding job to data, but if you dont, this function handles adding data.