diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index af722a06ca..c852f6849f 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -245,30 +245,33 @@ def run( stacklevel=2, ) - if backend is not None or analysis != "default": + if backend is not None or analysis != "default" or run_options: # Make a copy to update analysis or backend if one is provided at runtime experiment = self.copy() if backend: experiment._set_backend(backend) if isinstance(analysis, BaseAnalysis): experiment.analysis = analysis + if run_options: + experiment.set_run_options(**run_options) else: experiment = self if experiment.backend is None: raise QiskitError("Cannot run experiment, no backend has been set.") + # Finalize experiment before executions + experiment._finalize() + # Initialize result container experiment_data = experiment._initialize_experiment_data() - # Run options - run_opts = copy.copy(experiment.run_options) - run_opts.update_options(**run_options) - run_opts = run_opts.__dict__ - # Generate and transpile circuits transpiled_circuits = experiment._transpiled_circuits() + # Run options + run_opts = experiment.run_options.__dict__ + # Run jobs jobs = experiment._run_jobs(transpiled_circuits, **run_opts) experiment_data.add_jobs(jobs, timeout=timeout) @@ -319,6 +322,15 @@ def run_analysis( ) return self.analysis.run(experiment_data, replace_results=replace_results, **options) + def _finalize(self): + """Finalize experiment object before running jobs. + + Subclasses can override this method to set any final option + values derived from other options or attributes of the + experiment before `_run` is called. + """ + pass + def _run_jobs(self, circuits: List[QuantumCircuit], **run_options) -> List[BaseJob]: """Run circuits on backend as 1 or more jobs.""" # Run experiment jobs diff --git a/qiskit_experiments/framework/composite/composite_experiment.py b/qiskit_experiments/framework/composite/composite_experiment.py index 63aa510c8e..efd4b14f8e 100644 --- a/qiskit_experiments/framework/composite/composite_experiment.py +++ b/qiskit_experiments/framework/composite/composite_experiment.py @@ -98,11 +98,50 @@ def copy(self) -> "BaseExperiment": ret.analysis._analyses[i] = ret._experiments[i].analysis return ret + def set_run_options(self, **fields): + super().set_run_options(**fields) + for subexp in self._experiments: + subexp.set_run_options(**fields) + def _set_backend(self, backend): super()._set_backend(backend) for subexp in self._experiments: subexp._set_backend(backend) + def _finalize(self): + # NOTE: When CompositeAnalysis is updated to support level-1 + # measurements this method should be updated to validate that all + # sub-experiments have the same meas level and meas return types, + # and update the composite experiment run option to that value. + + for i, subexp in enumerate(self._experiments): + # Validate set and default run options in component experiment + # against and component experiment run options and raise a + # warning if any are different and will be overridden + overridden_keys = [] + sub_vals = [] + comp_vals = [] + for key, sub_val in subexp.run_options.__dict__.items(): + comp_val = getattr(self.run_options, key, None) + if sub_val != comp_val: + overridden_keys.append(key) + sub_vals.append(sub_val) + comp_vals.append(comp_val) + + if overridden_keys: + warnings.warn( + f"Component {i} {subexp.experiment_type} experiment run options" + f" {overridden_keys} values {sub_vals} will be overridden with" + f" {self.experiment_type} values {comp_vals}.", + UserWarning, + ) + # Update sub-experiment options with actual run option values so + # they can be used by that sub experiments _finalize method. + subexp.set_run_options(**dict(zip(overridden_keys, comp_vals))) + + # Call sub-experiments finalize method + subexp._finalize() + def _initialize_experiment_data(self): """Initialize the return data container for the experiment run""" experiment_data = ExperimentData(experiment=self) @@ -125,9 +164,4 @@ def _add_job_metadata(self, metadata, jobs, **run_options): for sub_metadata, sub_exp in zip( metadata["component_metadata"], self.component_experiment() ): - # Run and transpile options are always overridden - if sub_exp.run_options != sub_exp._default_run_options(): - warnings.warn( - "Sub-experiment run options" " are overridden by composite experiment options." - ) sub_exp._add_job_metadata(sub_metadata, jobs, **run_options) diff --git a/releasenotes/notes/exp-finalize-b7ca0a139ad5f872.yaml b/releasenotes/notes/exp-finalize-b7ca0a139ad5f872.yaml new file mode 100644 index 0000000000..a9a01e880c --- /dev/null +++ b/releasenotes/notes/exp-finalize-b7ca0a139ad5f872.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + Adds a :meth:`.BaseExperiment._finalize` method to :class:`.BaseExperiment` + which is after configuring any runtime options, backend, or analysis + classes but before generation and execution of experiment + circuits during :class:`.BaseExperiment.run`. + + This method is intended to be overridden in experiment subclasses if they + need to configure any analysis or runtime options based on a combination + of properties of the experiment, for example some combination of backend, + experiment and run options. diff --git a/test/test_composite.py b/test/test_composite.py index b17061515b..fbdd640445 100644 --- a/test/test_composite.py +++ b/test/test_composite.py @@ -58,17 +58,14 @@ def test_parallel_options(self): par_exp = ParallelExperiment([exp0, exp2]) - with self.assertWarnsRegex( - Warning, - "Sub-experiment run 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()) + 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()) + with self.assertWarns(UserWarning): expdata = par_exp.run(FakeBackend()) - self.assertExperimentDone(expdata) + self.assertExperimentDone(expdata) def test_experiment_config(self): """Test converting to and from config works"""