diff --git a/qiskit_algorithms/gradients/base_estimator_gradient.py b/qiskit_algorithms/gradients/base_estimator_gradient.py index 3e04605b..44143de2 100644 --- a/qiskit_algorithms/gradients/base_estimator_gradient.py +++ b/qiskit_algorithms/gradients/base_estimator_gradient.py @@ -36,19 +36,20 @@ class BaseEstimatorGradient(ABC): def __init__( self, estimator: BaseEstimator, - run_options: dict | None = None, + options: Options | None = None, ): """ Args: estimator: The estimator used to compute the gradients. - run_options: Backend runtime options used for circuit execution. The order of priority is: - run_options in ``run`` method > gradient's default run_options > primitive's default - setting. Higher priority setting overrides lower priority setting. + options: Primitive backend runtime options used for circuit execution. + The order of priority is: options in ``run`` method > gradient's + default options > primitive's default setting. + Higher priority setting overrides lower priority setting """ self._estimator: BaseEstimator = estimator - self._default_run_options = Options() - if run_options is not None: - self._default_run_options.update_options(**run_options) + self._default_options = Options() + if options is not None: + self._default_options.update_options(**options) def run( self, @@ -56,7 +57,7 @@ def run( observables: Sequence[BaseOperator | PauliSumOp], parameter_values: Sequence[Sequence[float]], parameters: Sequence[Sequence[Parameter] | None] | None = None, - **run_options, + **options, ) -> AlgorithmJob: """Run the job of the estimator gradient on the given circuits. @@ -68,9 +69,10 @@ def run( the specified parameters. Each sequence of parameters corresponds to a circuit in ``circuits``. Defaults to None, which means that the gradients of all parameters in each circuit are calculated. - run_options: Backend runtime options used for circuit execution. The order of priority is: - run_options in ``run`` method > gradient's default run_options > primitive's default - setting. Higher priority setting overrides lower priority setting. + options: Primitive backend runtime options used for circuit execution. + The order of priority is: options in ``run`` method > gradient's + default options > primitive's default setting. + Higher priority setting overrides lower priority setting Returns: The job object of the gradients of the expectation values. The i-th result corresponds to @@ -87,11 +89,11 @@ def run( # Validate the arguments. self._validate_arguments(circuits, observables, parameter_values, parameters) # The priority of run option is as follows: - # run_options in ``run`` method > gradient's default run_options > primitive's default setting. - run_opts = copy(self._default_run_options) - run_opts.update_options(**run_options) + # options in ``run`` method > gradient's default options > primitive's default setting. + opts = copy(self._default_options) + opts.update_options(**options) job = AlgorithmJob( - self._run, circuits, observables, parameter_values, parameters, **run_opts.__dict__ + self._run, circuits, observables, parameter_values, parameters, **opts.__dict__ ) job.submit() return job @@ -103,7 +105,7 @@ def _run( observables: Sequence[BaseOperator | PauliSumOp], parameter_values: Sequence[Sequence[float]], parameters: Sequence[Sequence[Parameter] | None], - **run_options, + **options, ) -> EstimatorGradientResult: """Compute the estimator gradients on the given circuits.""" raise NotImplementedError() @@ -166,15 +168,38 @@ def _validate_arguments( f"({observable.num_qubits})." ) - def _get_local_run_options(self, run_options: dict) -> Options: - """Update the run options in the results. + @property + def options(self) -> Options: + """Return the union of estimator options setting and gradient default options, + where, if the same field is set in both, the gradient's default options override + the primitive's default setting. + + Returns: + The gradient default + estimator options. + """ + return self._get_local_options(self._default_options.__dict__) + + def update_default_options(self, **options): + """Update the gradient's default options setting. + + Args: + **options: The fields to update the default options. + """ + + self._default_options.update_options(**options) + + def _get_local_options(self, options: Options) -> Options: + """Return the union of the primitive's default setting, + the gradient default options, and the options in the ``run`` method. + The order of priority is: options in ``run`` method > gradient's + default options > primitive's default setting. Args: - run_options: The run options to update. + options: The fields to update the options Returns: - The updated run options. + The gradient default + estimator + run options. """ - run_opts = copy(self._estimator.options) - run_opts.update_options(**run_options) - return run_opts + opts = copy(self._estimator.options) + opts.update_options(**options) + return opts diff --git a/qiskit_algorithms/gradients/base_sampler_gradient.py b/qiskit_algorithms/gradients/base_sampler_gradient.py index 40284c17..8812b0d1 100644 --- a/qiskit_algorithms/gradients/base_sampler_gradient.py +++ b/qiskit_algorithms/gradients/base_sampler_gradient.py @@ -30,25 +30,26 @@ class BaseSamplerGradient(ABC): """Base class for a ``SamplerGradient`` to compute the gradients of the sampling probability.""" - def __init__(self, sampler: BaseSampler, run_options: dict | None = None): + def __init__(self, sampler: BaseSampler, options: Options | None = None): """ Args: sampler: The sampler used to compute the gradients. - run_options: Backend runtime options used for circuit execution. The order of priority is: - run_options in `run` method > gradient's default run_options > primitive's default - setting. Higher priority setting overrides lower priority setting. + options: Primitive backend runtime options used for circuit execution. + The order of priority is: options in ``run`` method > gradient's + default options > primitive's default setting. + Higher priority setting overrides lower priority setting """ self._sampler: BaseSampler = sampler - self._default_run_options = Options() - if run_options is not None: - self._default_run_options.update_options(**run_options) + self._default_options = Options() + if options is not None: + self._default_options.update_options(**options) def run( self, circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Sequence[float]], parameters: Sequence[Sequence[Parameter] | None] | None = None, - **run_options, + **options, ) -> AlgorithmJob: """Run the job of the sampler gradient on the given circuits. @@ -59,10 +60,10 @@ def run( the specified parameters. Each sequence of parameters corresponds to a circuit in ``circuits``. Defaults to None, which means that the gradients of all parameters in each circuit are calculated. - run_options: Backend runtime options used for circuit execution. The order of priority is: - run_options in ``run`` method > gradient's default run_options > primitive's default - setting. Higher priority setting overrides lower priority setting. - + options: Primitive backend runtime options used for circuit execution. + The order of priority is: options in ``run`` method > gradient's + default options > primitive's default setting. + Higher priority setting overrides lower priority setting Returns: The job object of the gradients of the sampling probability. The i-th result corresponds to ``circuits[i]`` evaluated with parameters bound as ``parameter_values[i]``. @@ -78,10 +79,10 @@ def run( # Validate the arguments. self._validate_arguments(circuits, parameter_values, parameters) # The priority of run option is as follows: - # run_options in `run` method > gradient's default run_options > primitive's default run_options. - run_opts = copy(self._default_run_options) - run_opts.update_options(**run_options) - job = AlgorithmJob(self._run, circuits, parameter_values, parameters, **run_opts.__dict__) + # options in `run` method > gradient's default options > primitive's default options. + opts = copy(self._default_options) + opts.update_options(**options) + job = AlgorithmJob(self._run, circuits, parameter_values, parameters, **opts.__dict__) job.submit() return job @@ -91,7 +92,7 @@ def _run( circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Sequence[float]], parameters: Sequence[Sequence[Parameter] | None], - **run_options, + **options, ) -> SamplerGradientResult: """Compute the sampler gradients on the given circuits.""" raise NotImplementedError() @@ -138,15 +139,38 @@ def _validate_arguments( f"the number of parameters ({circuit.num_parameters}) for the {i}-th circuit." ) - def _get_local_run_options(self, run_options: dict) -> dict: - """Update the run options in the results. + @property + def options(self) -> Options: + """Return the union of sampler options setting and gradient default options, + where, if the same field is set in both, the gradient's default options override + the primitive's default setting. + + Returns: + The gradient default + sampler options. + """ + return self._get_local_options(self._default_options.__dict__) + + def update_default_options(self, **options): + """Update the gradient's default options setting. + + Args: + **options: The fields to update the default options. + """ + + self._default_options.update_options(**options) + + def _get_local_options(self, options: Options) -> Options: + """Return the union of the primitive's default setting, + the gradient default options, and the options in the ``run`` method. + The order of priority is: options in ``run`` method > gradient's + default options > primitive's default setting. Args: - run_options: The run options to update. + options: The fields to update the options Returns: - The updated run options. + The gradient default + sampler + run options. """ - run_opts = copy(self._sampler.options) - run_opts.update_options(**run_options) - return run_opts + opts = copy(self._sampler.options) + opts.update_options(**options) + return opts diff --git a/qiskit_algorithms/gradients/estimator_gradient_result.py b/qiskit_algorithms/gradients/estimator_gradient_result.py index 910e64e6..ada3bdb2 100644 --- a/qiskit_algorithms/gradients/estimator_gradient_result.py +++ b/qiskit_algorithms/gradients/estimator_gradient_result.py @@ -31,5 +31,5 @@ class EstimatorGradientResult: """The gradients of the expectation values.""" metadata: list[dict[str, Any]] """Additional information about the job.""" - run_options: Options - """run_options for the job.""" + options: Options + """Primitive runtime options for the execution of the job.""" diff --git a/qiskit_algorithms/gradients/finite_diff_estimator_gradient.py b/qiskit_algorithms/gradients/finite_diff_estimator_gradient.py index a2a52446..98c955f5 100644 --- a/qiskit_algorithms/gradients/finite_diff_estimator_gradient.py +++ b/qiskit_algorithms/gradients/finite_diff_estimator_gradient.py @@ -33,14 +33,15 @@ class FiniteDiffEstimatorGradient(BaseEstimatorGradient): Compute the gradients of the expectation values by finite difference method. """ - def __init__(self, estimator: BaseEstimator, epsilon: float, **run_options): + def __init__(self, estimator: BaseEstimator, epsilon: float, **options): """ Args: estimator: The estimator used to compute the gradients. epsilon: The offset size for the finite difference gradients. - run_options: Backend runtime options used for circuit execution. The order of priority is: - run_options in ``run`` method > gradient's default run_options > primitive's default - setting. Higher priority setting overrides lower priority setting. + options: Primitive backend runtime options used for circuit execution. + The order of priority is: options in ``run`` method > gradient's + default options > primitive's default setting. + Higher priority setting overrides lower priority setting Raises: ValueError: If ``epsilon`` is not positive. @@ -49,7 +50,7 @@ def __init__(self, estimator: BaseEstimator, epsilon: float, **run_options): raise ValueError(f"epsilon ({epsilon}) should be positive.") self._epsilon = epsilon self._base_parameter_values_dict = {} - super().__init__(estimator, **run_options) + super().__init__(estimator, **options) def _run( self, @@ -57,7 +58,7 @@ def _run( observables: Sequence[BaseOperator | PauliSumOp], parameter_values: Sequence[Sequence[float]], parameters: Sequence[Sequence[Parameter] | None], - **run_options, + **options, ) -> EstimatorGradientResult: """Compute the estimator gradients on the given circuits.""" jobs, metadata_ = [], [] @@ -76,7 +77,7 @@ def _run( minus = parameter_values_ - self._epsilon * offset n = 2 * len(indices) job = self._estimator.run( - [circuit] * n, [observable] * n, plus.tolist() + minus.tolist(), **run_options + [circuit] * n, [observable] * n, plus.tolist() + minus.tolist(), **options ) jobs.append(job) @@ -91,5 +92,5 @@ def _run( n = len(result.values) // 2 # is always a multiple of 2 gradient_ = (result.values[:n] - result.values[n:]) / (2 * self._epsilon) gradients.append(gradient_) - run_opt = self._get_local_run_options(run_options) - return EstimatorGradientResult(gradients=gradients, metadata=metadata_, run_options=run_opt) + opt = self._get_local_options(options) + return EstimatorGradientResult(gradients=gradients, metadata=metadata_, options=opt) diff --git a/qiskit_algorithms/gradients/finite_diff_sampler_gradient.py b/qiskit_algorithms/gradients/finite_diff_sampler_gradient.py index bdf79cbf..d64cad85 100644 --- a/qiskit_algorithms/gradients/finite_diff_sampler_gradient.py +++ b/qiskit_algorithms/gradients/finite_diff_sampler_gradient.py @@ -33,15 +33,16 @@ def __init__( self, sampler: BaseSampler, epsilon: float, - **run_options, + **options, ): """ Args: sampler: The sampler used to compute the gradients. epsilon: The offset size for the finite difference gradients. - run_options: Backend runtime options used for circuit execution. The order of priority is: - run_options in ``run`` method > gradient's default run_options > primitive's default - setting. Higher priority setting overrides lower priority setting. + options: Primitive backend runtime options used for circuit execution. + The order of priority is: options in ``run`` method > gradient's + default options > primitive's default setting. + Higher priority setting overrides lower priority setting Raises: ValueError: If ``epsilon`` is not positive. @@ -49,14 +50,14 @@ def __init__( if epsilon <= 0: raise ValueError(f"epsilon ({epsilon}) should be positive.") self._epsilon = epsilon - super().__init__(sampler, **run_options) + super().__init__(sampler, **options) def _run( self, circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Sequence[float]], parameters: Sequence[Sequence[Parameter] | None], - **run_options, + **options, ) -> SamplerGradientResult: """Compute the sampler gradients on the given circuits.""" jobs, metadata_ = [], [] @@ -71,7 +72,7 @@ def _run( plus = parameter_values_ + self._epsilon * offset minus = parameter_values_ - self._epsilon * offset n = 2 * len(indices) - job = self._sampler.run([circuit] * n, plus.tolist() + minus.tolist(), **run_options) + job = self._sampler.run([circuit] * n, plus.tolist() + minus.tolist(), **options) jobs.append(job) # combine the results @@ -92,5 +93,5 @@ def _run( gradient_.append(dict(enumerate(grad_dist))) gradients.append(gradient_) - run_opt = self._get_local_run_options(run_options) - return SamplerGradientResult(gradients=gradients, metadata=metadata_, run_options=run_opt) + opt = self._get_local_options(options) + return SamplerGradientResult(gradients=gradients, metadata=metadata_, options=opt) diff --git a/qiskit_algorithms/gradients/lin_comb_estimator_gradient.py b/qiskit_algorithms/gradients/lin_comb_estimator_gradient.py index 357db3c6..80567ab8 100644 --- a/qiskit_algorithms/gradients/lin_comb_estimator_gradient.py +++ b/qiskit_algorithms/gradients/lin_comb_estimator_gradient.py @@ -44,16 +44,17 @@ class LinCombEstimatorGradient(BaseEstimatorGradient): `arXiv:1811.11184 `_ """ - def __init__(self, estimator: BaseEstimator, **run_options): + def __init__(self, estimator: BaseEstimator, **options): """ Args: estimator: The estimator used to compute the gradients. - run_options: Backend runtime options used for circuit execution. The order of priority is: - run_options in ``run`` method > gradient's default run_options > primitive's default - setting. Higher priority setting overrides lower priority setting. + options: Primitive backend runtime options used for circuit execution. + The order of priority is: options in ``run`` method > gradient's + default options > primitive's default setting. + Higher priority setting overrides lower priority setting """ self._gradient_circuits = {} - super().__init__(estimator, **run_options) + super().__init__(estimator, **options) def _run( self, @@ -61,14 +62,14 @@ def _run( observables: Sequence[BaseOperator | PauliSumOp], parameter_values: Sequence[Sequence[float]], parameters: Sequence[Sequence[Parameter] | None], - **run_options, + **options, ) -> EstimatorGradientResult: """Compute the estimator gradients on the given circuits.""" jobs, result_indices_all, coeffs_all, metadata_ = [], [], [], [] for circuit, observable, parameter_values_, parameters_ in zip( circuits, observables, parameter_values, parameters ): - # Make the observable as observable as :class:`~qiskit.quantum_info.SparsePauliOp`. + # Make the observable as :class:`~qiskit.quantum_info.SparsePauliOp`. observable = init_observable(observable) # a set of parameters to be differentiated if parameters_ is None: @@ -110,7 +111,7 @@ def _run( n = len(gradient_circuits) job = self._estimator.run( - gradient_circuits, [observable_] * n, [parameter_values_] * n, **run_options + gradient_circuits, [observable_] * n, [parameter_values_] * n, **options ) jobs.append(job) result_indices_all.append(result_indices) @@ -129,5 +130,5 @@ def _run( gradient_[idx] += coeff * grad_ gradients.append(gradient_) - run_opt = self._get_local_run_options(run_options) - return EstimatorGradientResult(gradients=gradients, metadata=metadata_, run_options=run_opt) + opt = self._get_local_options(options) + return EstimatorGradientResult(gradients=gradients, metadata=metadata_, options=opt) diff --git a/qiskit_algorithms/gradients/lin_comb_sampler_gradient.py b/qiskit_algorithms/gradients/lin_comb_sampler_gradient.py index 440e083d..37f4df7a 100644 --- a/qiskit_algorithms/gradients/lin_comb_sampler_gradient.py +++ b/qiskit_algorithms/gradients/lin_comb_sampler_gradient.py @@ -37,24 +37,25 @@ class LinCombSamplerGradient(BaseSamplerGradient): `arXiv:1811.11184 `_ """ - def __init__(self, sampler: BaseSampler, **run_options): + def __init__(self, sampler: BaseSampler, **options): """ Args: sampler: The sampler used to compute the gradients. - run_options: Backend runtime options used for circuit execution. The order of priority is: - run_options in ``run`` method > gradient's default run_options > primitive's default - setting. Higher priority setting overrides lower priority setting. + options: Primitive backend runtime options used for circuit execution. + The order of priority is: options in ``run`` method > gradient's + default options > primitive's default setting. + Higher priority setting overrides lower priority setting """ self._gradient_circuits = {} - super().__init__(sampler, **run_options) + super().__init__(sampler, **options) def _run( self, circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Sequence[float]], parameters: Sequence[Sequence[Parameter] | None], - **run_options, + **options, ) -> SamplerGradientResult: """Compute the sampler gradients on the given circuits.""" jobs, result_indices_all, coeffs_all, metadata_ = [], [], [], [] @@ -96,7 +97,7 @@ def _run( coeffs.append(bound_coeff) n = len(gradient_circuits) - job = self._sampler.run(gradient_circuits, [parameter_values_] * n, **run_options) + job = self._sampler.run(gradient_circuits, [parameter_values_] * n, **options) jobs.append(job) result_indices_all.append(result_indices) coeffs_all.append(coeffs) @@ -120,5 +121,5 @@ def _run( gradient_.append(dict(enumerate(grad_dist))) gradients.append(gradient_) - run_opt = self._get_local_run_options(run_options) - return SamplerGradientResult(gradients=gradients, metadata=metadata_, run_options=run_opt) + opt = self._get_local_options(options) + return SamplerGradientResult(gradients=gradients, metadata=metadata_, options=opt) diff --git a/qiskit_algorithms/gradients/param_shift_estimator_gradient.py b/qiskit_algorithms/gradients/param_shift_estimator_gradient.py index e1a6d300..e1c25ad3 100644 --- a/qiskit_algorithms/gradients/param_shift_estimator_gradient.py +++ b/qiskit_algorithms/gradients/param_shift_estimator_gradient.py @@ -33,16 +33,17 @@ class ParamShiftEstimatorGradient(BaseEstimatorGradient): """Compute the gradients of the expectation values by the parameter shift rule""" - def __init__(self, estimator: BaseEstimator, **run_options): + def __init__(self, estimator: BaseEstimator, **options): """ Args: estimator: The estimator used to compute the gradients. - run_options: Backend runtime options used for circuit execution. The order of priority is: - run_options in ``run`` method > gradient's default run_options > primitive's default - setting. Higher priority setting overrides lower priority setting. + options: Primitive backend runtime options used for circuit execution. + The order of priority is: options in ``run`` method > gradient's + default options > primitive's default setting. + Higher priority setting overrides lower priority setting """ self._gradient_circuits = {} - super().__init__(estimator, **run_options) + super().__init__(estimator, **options) def _run( self, @@ -50,7 +51,7 @@ def _run( observables: Sequence[BaseOperator | PauliSumOp], parameter_values: Sequence[Sequence[float]], parameters: Sequence[Sequence[Parameter] | None], - **run_options, + **options, ) -> EstimatorGradientResult: """Compute the estimator gradients on the given circuits.""" jobs, result_indices_all, coeffs_all, metadata_ = [], [], [], [] @@ -89,7 +90,7 @@ def _run( [gradient_circuit.gradient_circuit] * n, [observable] * n, gradient_parameter_values_plus + gradient_parameter_values_minus, - **run_options, + **options, ) jobs.append(job) result_indices_all.append(result_indices) @@ -110,5 +111,5 @@ def _run( values[idx] += coeff * grad_ gradients.append(values) - run_opt = self._get_local_run_options(run_options) - return EstimatorGradientResult(gradients=gradients, metadata=metadata_, run_options=run_opt) + opt = self._get_local_options(options) + return EstimatorGradientResult(gradients=gradients, metadata=metadata_, options=opt) diff --git a/qiskit_algorithms/gradients/param_shift_sampler_gradient.py b/qiskit_algorithms/gradients/param_shift_sampler_gradient.py index 1f0e254b..a55e372b 100644 --- a/qiskit_algorithms/gradients/param_shift_sampler_gradient.py +++ b/qiskit_algorithms/gradients/param_shift_sampler_gradient.py @@ -31,23 +31,24 @@ class ParamShiftSamplerGradient(BaseSamplerGradient): """Compute the gradients of the sampling probability by the parameter shift rule.""" - def __init__(self, sampler: BaseSampler, **run_options): + def __init__(self, sampler: BaseSampler, **options): """ Args: sampler: The sampler used to compute the gradients. - run_options: Backend runtime options used for circuit execution. The order of priority is: - run_options in ``run`` method > gradient's default run_options > primitive's default - setting. Higher priority setting overrides lower priority setting. + options: Primitive backend runtime options used for circuit execution. + The order of priority is: options in ``run`` method > gradient's + default options > primitive's default setting. + Higher priority setting overrides lower priority setting """ self._gradient_circuits = {} - super().__init__(sampler, **run_options) + super().__init__(sampler, **options) def _run( self, circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Sequence[float]], parameters: Sequence[Sequence[Parameter] | None], - **run_options, + **options, ) -> SamplerGradientResult: """Compute the sampler gradients on the given circuits.""" jobs, result_indices_all, coeffs_all, metadata_ = [], [], [], [] @@ -84,7 +85,7 @@ def _run( job = self._sampler.run( [gradient_circuit.gradient_circuit] * n, gradient_parameter_values_plus + gradient_parameter_values_minus, - **run_options, + **options, ) jobs.append(job) result_indices_all.append(result_indices) @@ -115,5 +116,5 @@ def _run( gradient_.append(dict(enumerate(grad_dist))) gradients.append(gradient_) - run_opt = self._get_local_run_options(run_options) - return SamplerGradientResult(gradients=gradients, metadata=metadata_, run_options=run_opt) + opt = self._get_local_options(options) + return SamplerGradientResult(gradients=gradients, metadata=metadata_, options=opt) diff --git a/qiskit_algorithms/gradients/sampler_gradient_result.py b/qiskit_algorithms/gradients/sampler_gradient_result.py index 89b94bf1..b78b468f 100644 --- a/qiskit_algorithms/gradients/sampler_gradient_result.py +++ b/qiskit_algorithms/gradients/sampler_gradient_result.py @@ -29,5 +29,5 @@ class SamplerGradientResult: """The gradients of the sample probabilities.""" metadata: list[dict[str, Any]] """Additional information about the job.""" - run_options: Options - """run_options for the job.""" + options: Options + """Primitive runtime options for the execution of the job.""" diff --git a/qiskit_algorithms/gradients/spsa_estimator_gradient.py b/qiskit_algorithms/gradients/spsa_estimator_gradient.py index 84681dfb..89e7d063 100644 --- a/qiskit_algorithms/gradients/spsa_estimator_gradient.py +++ b/qiskit_algorithms/gradients/spsa_estimator_gradient.py @@ -40,7 +40,7 @@ def __init__( epsilon: float, batch_size: int = 1, seed: int | None = None, - **run_options, + **options, ): """ Args: @@ -48,9 +48,10 @@ def __init__( epsilon: The offset size for the SPSA gradients. batch_size: The number of gradients to average. seed: The seed for a random perturbation vector. - run_options: Backend runtime options used for circuit execution. The order of priority is: - run_options in ``run`` method > gradient's default run_options > primitive's default - setting. Higher priority setting overrides lower priority setting. + options: Primitive backend runtime options used for circuit execution. + The order of priority is: options in ``run`` method > gradient's + default options > primitive's default setting. + Higher priority setting overrides lower priority setting Raises: ValueError: If ``epsilon`` is not positive. @@ -61,7 +62,7 @@ def __init__( self._batch_size = batch_size self._seed = np.random.default_rng(seed) - super().__init__(estimator, **run_options) + super().__init__(estimator, **options) def _run( self, @@ -69,7 +70,7 @@ def _run( observables: Sequence[BaseOperator | PauliSumOp], parameter_values: Sequence[Sequence[float]], parameters: Sequence[Sequence[Parameter] | None], - **run_options, + **options, ) -> EstimatorGradientResult: """Compute the estimator gradients on the given circuits.""" jobs, offsets, metadata_ = [], [], [] @@ -96,7 +97,7 @@ def _run( [circuit] * 2 * self._batch_size, [observable] * 2 * self._batch_size, plus + minus, - **run_options, + **options, ) jobs.append(job) @@ -119,5 +120,5 @@ def _run( indices = [circuits[i].parameters.data.index(p) for p in metadata_[i]["parameters"]] gradients.append(gradient[indices]) - run_opt = self._get_local_run_options(run_options) - return EstimatorGradientResult(gradients=gradients, metadata=metadata_, run_options=run_opt) + opt = self._get_local_options(options) + return EstimatorGradientResult(gradients=gradients, metadata=metadata_, options=opt) diff --git a/qiskit_algorithms/gradients/spsa_sampler_gradient.py b/qiskit_algorithms/gradients/spsa_sampler_gradient.py index b5426b0b..94a359a8 100644 --- a/qiskit_algorithms/gradients/spsa_sampler_gradient.py +++ b/qiskit_algorithms/gradients/spsa_sampler_gradient.py @@ -38,7 +38,7 @@ def __init__( epsilon: float, batch_size: int = 1, seed: int | None = None, - **run_options, + **options, ): """ Args: @@ -46,9 +46,10 @@ def __init__( epsilon: The offset size for the SPSA gradients. batch_size: number of gradients to average. seed: The seed for a random perturbation vector. - run_options: Backend runtime options used for circuit execution. The order of priority is: - run_options in `run` method > gradient's default run_options > primitive's default - setting. Higher priority setting overrides lower priority setting. + options: Primitive backend runtime options used for circuit execution. + The order of priority is: options in ``run`` method > gradient's + default options > primitive's default setting. + Higher priority setting overrides lower priority setting Raises: ValueError: If ``epsilon`` is not positive. @@ -59,14 +60,14 @@ def __init__( self._epsilon = epsilon self._seed = np.random.default_rng(seed) - super().__init__(sampler, **run_options) + super().__init__(sampler, **options) def _run( self, circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Sequence[float]], parameters: Sequence[Sequence[Parameter] | None], - **run_options, + **options, ) -> SamplerGradientResult: """Compute the sampler gradients on the given circuits.""" jobs, offsets, metadata_ = [], [], [] @@ -88,7 +89,7 @@ def _run( minus = [parameter_values_ - self._epsilon * offset_ for offset_ in offset] offsets.append(offset) - job = self._sampler.run([circuit] * 2 * self._batch_size, plus + minus, **run_options) + job = self._sampler.run([circuit] * 2 * self._batch_size, plus + minus, **options) jobs.append(job) # combine the results @@ -120,5 +121,5 @@ def _run( gradient.append(dict(enumerate(gradient_j))) gradients.append(gradient) - run_opt = self._get_local_run_options(run_options) - return SamplerGradientResult(gradients=gradients, metadata=metadata_, run_options=run_opt) + opt = self._get_local_options(options) + return SamplerGradientResult(gradients=gradients, metadata=metadata_, options=opt) diff --git a/qiskit_algorithms/state_fidelities/base_state_fidelity.py b/qiskit_algorithms/state_fidelities/base_state_fidelity.py index a711adac..cec48e3c 100644 --- a/qiskit_algorithms/state_fidelities/base_state_fidelity.py +++ b/qiskit_algorithms/state_fidelities/base_state_fidelity.py @@ -94,8 +94,9 @@ def _preprocess_values( ) # ensure 2d - if len(values) > 0 and not isinstance(values[0], Sequence): + if len(values) > 0 and not isinstance(values[0], Sequence) or len(values) == 0: values = [values] + return values def _check_qubits_match(self, circuit_1: QuantumCircuit, circuit_2: QuantumCircuit) -> None: @@ -236,7 +237,7 @@ def _run( circuits_2: QuantumCircuit | Sequence[QuantumCircuit], values_1: Sequence[float] | Sequence[Sequence[float]] | None = None, values_2: Sequence[float] | Sequence[Sequence[float]] | None = None, - **run_options, + **options, ) -> StateFidelityResult: r""" Computes the state overlap (fidelity) calculation between two @@ -248,9 +249,9 @@ def _run( circuits_2: (Parametrized) quantum circuits preparing :math:`|\phi\rangle`. values_1: Numerical parameters to be bound to the first set of circuits values_2: Numerical parameters to be bound to the second set of circuits. - run_options: Backend runtime options used for circuit execution. The order - of priority is\: run_options in ``run`` method > fidelity's default - run_options > primitive's default setting. + options: Primitive backend runtime options used for circuit execution. The order + of priority is\: options in ``run`` method > fidelity's default + options > primitive's default setting. Higher priority setting overrides lower priority setting. Returns: @@ -264,7 +265,7 @@ def run( circuits_2: QuantumCircuit | Sequence[QuantumCircuit], values_1: Sequence[float] | Sequence[Sequence[float]] | None = None, values_2: Sequence[float] | Sequence[Sequence[float]] | None = None, - **run_options, + **options, ) -> AlgorithmJob: r""" Runs asynchronously the state overlap (fidelity) calculation between two @@ -277,9 +278,9 @@ def run( circuits_2: (Parametrized) quantum circuits preparing :math:`|\phi\rangle`. values_1: Numerical parameters to be bound to the first set of circuits. values_2: Numerical parameters to be bound to the second set of circuits. - run_options: Backend runtime options used for circuit execution. The order - of priority is\: run_options in ``run`` method > fidelity's default - run_options > primitive's default setting. + options: Primitive backend runtime options used for circuit execution. The order + of priority is\: options in ``run`` method > fidelity's default + options > primitive's default setting. Higher priority setting overrides lower priority setting. Returns: @@ -287,7 +288,7 @@ def run( The job's result is an instance of ``StateFidelityResult``. """ - job = AlgorithmJob(self._run, circuits_1, circuits_2, values_1, values_2, **run_options) + job = AlgorithmJob(self._run, circuits_1, circuits_2, values_1, values_2, **options) job.submit() return job diff --git a/qiskit_algorithms/state_fidelities/compute_uncompute.py b/qiskit_algorithms/state_fidelities/compute_uncompute.py index ff9080e5..e2798fcc 100644 --- a/qiskit_algorithms/state_fidelities/compute_uncompute.py +++ b/qiskit_algorithms/state_fidelities/compute_uncompute.py @@ -20,6 +20,7 @@ from qiskit import QuantumCircuit from qiskit.algorithms import AlgorithmError from qiskit.primitives import BaseSampler +from qiskit.providers import Options from .base_state_fidelity import BaseStateFidelity from .state_fidelity_result import StateFidelityResult @@ -48,11 +49,14 @@ class ComputeUncompute(BaseStateFidelity): """ - def __init__(self, sampler: BaseSampler, **run_options) -> None: + def __init__(self, sampler: BaseSampler, options: Options | None = None) -> None: """ Args: sampler: Sampler primitive instance. - run_options: Backend runtime options used for circuit execution. + options: Primitive backend runtime options used for circuit execution. + The order of priority is: options in ``run`` method > fidelity's + default options > primitive's default setting. + Higher priority setting overrides lower priority setting. Raises: ValueError: If the sampler is not an instance of ``BaseSampler``. @@ -62,7 +66,9 @@ def __init__(self, sampler: BaseSampler, **run_options) -> None: f"The sampler should be an instance of BaseSampler, " f"but got {type(sampler)}" ) self._sampler: BaseSampler = sampler - self._default_run_options = run_options + self._default_options = Options() + if options is not None: + self._default_options.update_options(**options) super().__init__() def create_fidelity_circuit( @@ -79,6 +85,11 @@ def create_fidelity_circuit( Returns: The fidelity quantum circuit corresponding to circuit_1 and circuit_2. """ + if len(circuit_1.clbits) > 0: + circuit_1.remove_final_measurements() + if len(circuit_2.clbits) > 0: + circuit_2.remove_final_measurements() + circuit = circuit_1.compose(circuit_2.inverse()) circuit.measure_all() return circuit @@ -89,7 +100,7 @@ def _run( circuits_2: QuantumCircuit | Sequence[QuantumCircuit], values_1: Sequence[float] | Sequence[Sequence[float]] | None = None, values_2: Sequence[float] | Sequence[Sequence[float]] | None = None, - **run_options, + **options, ) -> StateFidelityResult: r""" Computes the state overlap (fidelity) calculation between two @@ -101,10 +112,10 @@ def _run( circuits_2: (Parametrized) quantum circuits preparing :math:`|\phi\rangle`. values_1: Numerical parameters to be bound to the first circuits. values_2: Numerical parameters to be bound to the second circuits. - run_options: Backend runtime options used for circuit execution. The order - of priority is\: run_options in ``run`` method > fidelity's default - run_options > primitive's default setting. - Higher priority setting overrides lower priority setting. + options: Primitive backend runtime options used for circuit execution. + The order of priority is: options in ``run`` method > fidelity's + default options > primitive's default setting. + Higher priority setting overrides lower priority setting. Returns: The result of the fidelity calculation. @@ -122,12 +133,12 @@ def _run( values = self._construct_value_list(circuits_1, circuits_2, values_1, values_2) # The priority of run options is as follows: - # run_options in `evaluate` method > fidelity's default run_options > - # primitive's default run_options. - run_opts = copy(self._default_run_options) - run_opts.update(**run_options) + # options in `evaluate` method > fidelity's default options > + # primitive's default options. + opts = copy(self._default_options) + opts.update_options(**options) - job = self._sampler.run(circuits=circuits, parameter_values=values, **run_opts) + job = self._sampler.run(circuits=circuits, parameter_values=values, **opts.__dict__) try: result = job.result() @@ -141,5 +152,41 @@ def _run( fidelities=fidelities, raw_fidelities=raw_fidelities, metadata=result.metadata, - run_options=run_opts, + options=self._get_local_options(opts.__dict__), ) + + @property + def options(self) -> Options: + """Return the union of estimator options setting and fidelity default options, + where, if the same field is set in both, the fidelity's default options override + the primitive's default setting. + + Returns: + The fidelity default + estimator options. + """ + return self._get_local_options(self._default_options.__dict__) + + def update_default_options(self, **options): + """Update the fidelity's default options setting. + + Args: + **options: The fields to update the default options. + """ + + self._default_options.update_options(**options) + + def _get_local_options(self, options: Options) -> Options: + """Return the union of the primitive's default setting, + the fidelity default options, and the options in the ``run`` method. + The order of priority is: options in ``run`` method > fidelity's + default options > primitive's default setting. + + Args: + options: The fields to update the options + + Returns: + The fidelity default + estimator + run options. + """ + opts = copy(self._sampler.options) + opts.update_options(**options) + return opts diff --git a/qiskit_algorithms/state_fidelities/state_fidelity_result.py b/qiskit_algorithms/state_fidelities/state_fidelity_result.py index 04d4aa0c..88dca035 100644 --- a/qiskit_algorithms/state_fidelities/state_fidelity_result.py +++ b/qiskit_algorithms/state_fidelities/state_fidelity_result.py @@ -19,6 +19,8 @@ from typing import Any from dataclasses import dataclass +from qiskit.providers import Options + @dataclass(frozen=True) class StateFidelityResult: @@ -31,5 +33,5 @@ class StateFidelityResult: depending on the error mitigation method used.""" metadata: Sequence[Mapping[str, Any]] """Additional information about the fidelity calculation.""" - run_options: Mapping[str, Any] - """Runtime options for the execution of the fidelity job.""" + options: Options + """Primitive runtime options for the execution of the fidelity job.""" diff --git a/test/state_fidelities/test_compute_uncompute.py b/test/state_fidelities/test_compute_uncompute.py index cfadd0ba..99eceb25 100644 --- a/test/state_fidelities/test_compute_uncompute.py +++ b/test/state_fidelities/test_compute_uncompute.py @@ -92,6 +92,10 @@ def test_no_params(self): results = job.result() np.testing.assert_allclose(results.fidelities, np.array([0.25]), atol=1e-16) + job = fidelity.run([self._circuit[2]], [self._circuit[3]], [], []) + results = job.result() + np.testing.assert_allclose(results.fidelities, np.array([0.25]), atol=1e-16) + def test_left_param(self): """test for fidelity with only left parameters""" fidelity = ComputeUncompute(self._sampler) @@ -183,6 +187,61 @@ def test_input_format(self): np.testing.assert_allclose(result_1.fidelities, result_3.fidelities, atol=1e-16) np.testing.assert_allclose(result_1.fidelities, result_4.fidelities, atol=1e-16) + def test_input_measurements(self): + """test for fidelity with measurements on input circuits""" + fidelity = ComputeUncompute(self._sampler) + circuit_1 = self._circuit[0] + circuit_1.measure_all() + circuit_2 = self._circuit[1] + circuit_2.measure_all() + + job = fidelity.run(circuit_1, circuit_2, self._left_params[0], self._right_params[0]) + result = job.result() + np.testing.assert_allclose(result.fidelities, np.array([1.0])) + + def test_options(self): + """Test fidelity's run options""" + sampler_shots = Sampler(options={"shots": 1024}) + + with self.subTest("sampler"): + # Only options in sampler + fidelity = ComputeUncompute(sampler_shots) + options = fidelity.options + job = fidelity.run(self._circuit[2], self._circuit[3]) + result = job.result() + self.assertEqual(options.__dict__, {"shots": 1024}) + self.assertEqual(result.options.__dict__, {"shots": 1024}) + + with self.subTest("fidelity init"): + # Fidelity default options override sampler + # options and add new fields + fidelity = ComputeUncompute(sampler_shots, options={"shots": 2048, "dummy": 100}) + options = fidelity.options + job = fidelity.run(self._circuit[2], self._circuit[3]) + result = job.result() + self.assertEqual(options.__dict__, {"shots": 2048, "dummy": 100}) + self.assertEqual(result.options.__dict__, {"shots": 2048, "dummy": 100}) + + with self.subTest("fidelity update"): + # Update fidelity options + fidelity = ComputeUncompute(sampler_shots, options={"shots": 2048, "dummy": 100}) + fidelity.update_default_options(shots=100) + options = fidelity.options + job = fidelity.run(self._circuit[2], self._circuit[3]) + result = job.result() + self.assertEqual(options.__dict__, {"shots": 100, "dummy": 100}) + self.assertEqual(result.options.__dict__, {"shots": 100, "dummy": 100}) + + with self.subTest("fidelity run"): + # Run options override fidelity options + fidelity = ComputeUncompute(sampler_shots, options={"shots": 2048, "dummy": 100}) + job = fidelity.run(self._circuit[2], self._circuit[3], shots=50, dummy=None) + options = fidelity.options + result = job.result() + # Only default + sampler options. Not run. + self.assertEqual(options.__dict__, {"shots": 2048, "dummy": 100}) + self.assertEqual(result.options.__dict__, {"shots": 50, "dummy": None}) + if __name__ == "__main__": unittest.main() diff --git a/test/test_estimator_gradient.py b/test/test_estimator_gradient.py index b4b2c520..e82383a5 100644 --- a/test/test_estimator_gradient.py +++ b/test/test_estimator_gradient.py @@ -394,24 +394,42 @@ def test_options(self, grad): gradient = grad(estimator, epsilon=1e-6) else: gradient = grad(estimator) + options = gradient.options result = gradient.run([qc], [op], [[1]]).result() - self.assertEqual(result.run_options.get("shots"), 100) + self.assertEqual(result.options.get("shots"), 100) + self.assertEqual(options.get("shots"), 100) with self.subTest("gradient init"): if grad is FiniteDiffEstimatorGradient or grad is SPSAEstimatorGradient: - gradient = grad(estimator, epsilon=1e-6, run_options={"shots": 200}) + gradient = grad(estimator, epsilon=1e-6, options={"shots": 200}) else: - gradient = grad(estimator, run_options={"shots": 200}) + gradient = grad(estimator, options={"shots": 200}) + options = gradient.options result = gradient.run([qc], [op], [[1]]).result() - self.assertEqual(result.run_options.get("shots"), 200) + self.assertEqual(result.options.get("shots"), 200) + self.assertEqual(options.get("shots"), 200) + + with self.subTest("gradient update"): + if grad is FiniteDiffEstimatorGradient or grad is SPSAEstimatorGradient: + gradient = grad(estimator, epsilon=1e-6, options={"shots": 200}) + else: + gradient = grad(estimator, options={"shots": 200}) + gradient.update_default_options(shots=100) + options = gradient.options + result = gradient.run([qc], [op], [[1]]).result() + self.assertEqual(result.options.get("shots"), 100) + self.assertEqual(options.get("shots"), 100) with self.subTest("gradient run"): if grad is FiniteDiffEstimatorGradient or grad is SPSAEstimatorGradient: - gradient = grad(estimator, epsilon=1e-6, run_options={"shots": 200}) + gradient = grad(estimator, epsilon=1e-6, options={"shots": 200}) else: - gradient = grad(estimator, run_options={"shots": 200}) + gradient = grad(estimator, options={"shots": 200}) + options = gradient.options result = gradient.run([qc], [op], [[1]], shots=300).result() - self.assertEqual(result.run_options.get("shots"), 300) + self.assertEqual(result.options.get("shots"), 300) + # Only default + estimator options. Not run. + self.assertEqual(options.get("shots"), 200) if __name__ == "__main__": diff --git a/test/test_sampler_gradient.py b/test/test_sampler_gradient.py index 47a82249..43ac68c0 100644 --- a/test/test_sampler_gradient.py +++ b/test/test_sampler_gradient.py @@ -515,7 +515,7 @@ def test_gradient_random_parameters(self, grad): SPSASamplerGradient, ], ) - def test_run_options(self, grad): + def test_options(self, grad): """Test sampler gradient's run options""" a = Parameter("a") qc = QuantumCircuit(1) @@ -527,24 +527,42 @@ def test_run_options(self, grad): gradient = grad(sampler, epsilon=1e-6) else: gradient = grad(sampler) + options = gradient.options result = gradient.run([qc], [[1]]).result() - self.assertEqual(result.run_options.get("shots"), 100) + self.assertEqual(result.options.get("shots"), 100) + self.assertEqual(options.get("shots"), 100) with self.subTest("gradient init"): if grad is FiniteDiffSamplerGradient or grad is SPSASamplerGradient: - gradient = grad(sampler, epsilon=1e-6, run_options={"shots": 200}) + gradient = grad(sampler, epsilon=1e-6, options={"shots": 200}) else: - gradient = grad(sampler, run_options={"shots": 200}) + gradient = grad(sampler, options={"shots": 200}) + options = gradient.options result = gradient.run([qc], [[1]]).result() - self.assertEqual(result.run_options.get("shots"), 200) + self.assertEqual(result.options.get("shots"), 200) + self.assertEqual(options.get("shots"), 200) + + with self.subTest("gradient update"): + if grad is FiniteDiffSamplerGradient or grad is SPSASamplerGradient: + gradient = grad(sampler, epsilon=1e-6, options={"shots": 200}) + else: + gradient = grad(sampler, options={"shots": 200}) + gradient.update_default_options(shots=100) + options = gradient.options + result = gradient.run([qc], [[1]]).result() + self.assertEqual(result.options.get("shots"), 100) + self.assertEqual(options.get("shots"), 100) with self.subTest("gradient run"): if grad is FiniteDiffSamplerGradient or grad is SPSASamplerGradient: - gradient = grad(sampler, epsilon=1e-6, run_options={"shots": 200}) + gradient = grad(sampler, epsilon=1e-6, options={"shots": 200}) else: - gradient = grad(sampler, run_options={"shots": 200}) + gradient = grad(sampler, options={"shots": 200}) + options = gradient.options result = gradient.run([qc], [[1]], shots=300).result() - self.assertEqual(result.run_options.get("shots"), 300) + self.assertEqual(result.options.get("shots"), 300) + # Only default + sampler options. Not run. + self.assertEqual(options.get("shots"), 200) def _quasi2array(quasis: List[QuasiDistribution], num_qubits: int) -> np.ndarray: