diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1e3eeb01f..b4cc1e962 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -312,4 +312,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: coveralls --service=github - shell: bash + shell: bash \ No newline at end of file diff --git a/.pylintdict b/.pylintdict index 93892d47d..70e2bb17f 100644 --- a/.pylintdict +++ b/.pylintdict @@ -312,6 +312,7 @@ monte mosca mpl mprev +msg multiclass multinomial multioutput @@ -501,6 +502,7 @@ sparsearray spedalieri spsa sqrt +stacklevel statefn statevector statevectors diff --git a/qiskit_machine_learning/algorithms/inference/qbayesian.py b/qiskit_machine_learning/algorithms/inference/qbayesian.py index 9621ba5e4..2d3736eac 100644 --- a/qiskit_machine_learning/algorithms/inference/qbayesian.py +++ b/qiskit_machine_learning/algorithms/inference/qbayesian.py @@ -15,11 +15,17 @@ import copy from typing import Tuple, Dict, Set, List + from qiskit import QuantumCircuit, ClassicalRegister from qiskit.quantum_info import Statevector -from qiskit.circuit.library import GroverOperator -from qiskit.primitives import BaseSampler, Sampler from qiskit.circuit import Qubit +from qiskit.circuit.library import GroverOperator +from qiskit.primitives import BaseSampler, Sampler, BaseSamplerV2, BaseSamplerV1 +from qiskit.transpiler.passmanager import BasePassManager +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager +from qiskit.providers.fake_provider import GenericBackendV2 + +from ...utils.deprecation import issue_deprecation_msg class QBayesian: @@ -62,7 +68,8 @@ def __init__( *, limit: int = 10, threshold: float = 0.9, - sampler: BaseSampler | None = None, + sampler: BaseSampler | BaseSamplerV2 | None = None, + pass_manager: BasePassManager | None = None, ): """ Args: @@ -83,15 +90,30 @@ def __init__( # Test valid input for qrg in circuit.qregs: if qrg.size > 1: - raise ValueError("Every register needs to be mapped to exactly one unique qubit") + raise ValueError("Every register needs to be mapped to exactly one unique qubit.") + # Initialize parameter self._circ = circuit self._limit = limit self._threshold = threshold if sampler is None: sampler = Sampler() + + if isinstance(sampler, BaseSamplerV1): + issue_deprecation_msg( + msg="V1 Primitives are deprecated", + version="0.8.0", + remedy="Use V2 primitives for continued compatibility and support.", + period="4 months", + ) + self._sampler = sampler + if pass_manager is None: + _backend = GenericBackendV2(num_qubits=max(circuit.num_qubits, 2)) + pass_manager = generate_preset_pass_manager(optimization_level=1, backend=_backend) + self._pass_manager = pass_manager + # Label of register mapped to its qubit self._label2qubit = {qrg.name: qrg[0] for qrg in self._circ.qregs} # Label of register mapped to its qubit index bottom up in significance @@ -139,11 +161,34 @@ def _get_grover_op(self, evidence: Dict[str, int]) -> GroverOperator: def _run_circuit(self, circuit: QuantumCircuit) -> Dict[str, float]: """Run the quantum circuit with the sampler.""" - # Sample from circuit - job = self._sampler.run(circuit) - result = job.result() - # Get the counts of quantum state results - counts = result.quasi_dists[0].nearest_probability_distribution().binary_probabilities() + counts = {} + + if isinstance(self._sampler, BaseSampler): + # Sample from circuit + job = self._sampler.run(circuit) + result = job.result() + + # Get the counts of quantum state results + counts = result.quasi_dists[0].nearest_probability_distribution().binary_probabilities() + + elif isinstance(self._sampler, BaseSamplerV2): + + # Sample from circuit + circuit_isa = self._pass_manager.run(circuit) + job = self._sampler.run([circuit_isa]) + result = job.result() + + bit_array = list(result[0].data.values())[0] + bitstring_counts = bit_array.get_counts() + + # Normalize the counts to probabilities + total_shots = result[0].metadata["shots"] + counts = {k: v / total_shots for k, v in bitstring_counts.items()} + + # Convert to quasi-probabilities + # counts = QuasiDistribution(probabilities) + # counts = {k: v for k, v in counts.items()} + return counts def __power_grover( @@ -360,12 +405,12 @@ def limit(self, limit: int): self._limit = limit @property - def sampler(self) -> BaseSampler: + def sampler(self) -> BaseSampler | BaseSamplerV2: """Returns the sampler primitive used to compute the samples.""" return self._sampler @sampler.setter - def sampler(self, sampler: BaseSampler): + def sampler(self, sampler: BaseSampler | BaseSamplerV2): """Set the sampler primitive used to compute the samples.""" self._sampler = sampler diff --git a/qiskit_machine_learning/gradients/base/base_estimator_gradient.py b/qiskit_machine_learning/gradients/base/base_estimator_gradient.py index edfe80fd0..bb85cd179 100644 --- a/qiskit_machine_learning/gradients/base/base_estimator_gradient.py +++ b/qiskit_machine_learning/gradients/base/base_estimator_gradient.py @@ -23,11 +23,13 @@ import numpy as np from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit -from qiskit.primitives import BaseEstimator +from qiskit.primitives import BaseEstimator, BaseEstimatorV1 +from qiskit.primitives.base import BaseEstimatorV2 from qiskit.primitives.utils import _circuit_key from qiskit.providers import Options from qiskit.quantum_info.operators.base_operator import BaseOperator from qiskit.transpiler.passes import TranslateParameterizedGates +from qiskit.transpiler.passmanager import BasePassManager from .estimator_gradient_result import EstimatorGradientResult from ..utils import ( @@ -37,7 +39,7 @@ _make_gradient_parameters, _make_gradient_parameter_values, ) - +from ...utils.deprecation import issue_deprecation_msg from ...algorithm_job import AlgorithmJob @@ -46,13 +48,15 @@ class BaseEstimatorGradient(ABC): def __init__( self, - estimator: BaseEstimator, + estimator: BaseEstimator | BaseEstimatorV2, options: Options | None = None, derivative_type: DerivativeType = DerivativeType.REAL, + pass_manager: BasePassManager | None = None, ): r""" Args: estimator: The estimator used to compute the gradients. + pass_manager: pass manager for isa_circuit transpilation. 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. @@ -68,7 +72,15 @@ def __init__( gradient and this type is the only supported type for function-level schemes like finite difference. """ + if isinstance(estimator, BaseEstimatorV1): + issue_deprecation_msg( + msg="V1 Primitives are deprecated", + version="0.8.0", + remedy="Use V2 primitives for continued compatibility and support.", + period="4 months", + ) self._estimator: BaseEstimator = estimator + self._pass_manager = pass_manager self._default_options = Options() if options is not None: self._default_options.update_options(**options) @@ -92,7 +104,7 @@ def run( self, circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator], - parameter_values: Sequence[Sequence[float]], + parameter_values: Sequence[Sequence[float]] | np.ndarray, parameters: Sequence[Sequence[Parameter] | None] | None = None, **options, ) -> AlgorithmJob: @@ -157,7 +169,7 @@ def _run( self, circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator], - parameter_values: Sequence[Sequence[float]], + parameter_values: Sequence[Sequence[float]] | np.ndarray, parameters: Sequence[Sequence[Parameter]], **options, ) -> EstimatorGradientResult: @@ -167,7 +179,7 @@ def _run( def _preprocess( self, circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], + parameter_values: Sequence[Sequence[float]] | np.ndarray, parameters: Sequence[Sequence[Parameter]], supported_gates: Sequence[str], ) -> tuple[Sequence[QuantumCircuit], Sequence[Sequence[float]], Sequence[Sequence[Parameter]]]: @@ -209,7 +221,7 @@ def _postprocess( self, results: EstimatorGradientResult, circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], + parameter_values: Sequence[Sequence[float]] | np.ndarray, parameters: Sequence[Sequence[Parameter]], ) -> EstimatorGradientResult: """Postprocess the gradients. This method computes the gradient of the original circuits @@ -269,7 +281,7 @@ def _postprocess( def _validate_arguments( circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator], - parameter_values: Sequence[Sequence[float]], + parameter_values: Sequence[Sequence[float]] | np.ndarray, parameters: Sequence[Sequence[Parameter]], ) -> None: """Validate the arguments of the ``run`` method. diff --git a/qiskit_machine_learning/gradients/base/base_sampler_gradient.py b/qiskit_machine_learning/gradients/base/base_sampler_gradient.py index 9e29b47ab..3db0c3e31 100644 --- a/qiskit_machine_learning/gradients/base/base_sampler_gradient.py +++ b/qiskit_machine_learning/gradients/base/base_sampler_gradient.py @@ -22,10 +22,11 @@ from copy import copy from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit -from qiskit.primitives import BaseSampler +from qiskit.primitives import BaseSampler, BaseSamplerV1 from qiskit.primitives.utils import _circuit_key from qiskit.providers import Options from qiskit.transpiler.passes import TranslateParameterizedGates +from qiskit.transpiler.passmanager import BasePassManager from .sampler_gradient_result import SamplerGradientResult from ..utils import ( @@ -34,14 +35,20 @@ _make_gradient_parameters, _make_gradient_parameter_values, ) - +from ...utils.deprecation import issue_deprecation_msg from ...algorithm_job import AlgorithmJob class BaseSamplerGradient(ABC): """Base class for a ``SamplerGradient`` to compute the gradients of the sampling probability.""" - def __init__(self, sampler: BaseSampler, options: Options | None = None): + def __init__( + self, + sampler: BaseSampler, + options: Options | None = None, + len_quasi_dist: int | None = None, + pass_manager: BasePassManager | None = None, + ): """ Args: sampler: The sampler used to compute the gradients. @@ -50,7 +57,16 @@ def __init__(self, sampler: BaseSampler, options: Options | None = None): default options > primitive's default setting. Higher priority setting overrides lower priority setting """ + if isinstance(sampler, BaseSamplerV1): + issue_deprecation_msg( + msg="V1 Primitives are deprecated", + version="0.8.0", + remedy="Use V2 primitives for continued compatibility and support.", + period="4 months", + ) self._sampler: BaseSampler = sampler + self._pass_manager = pass_manager + self._len_quasi_dist = len_quasi_dist self._default_options = Options() if options is not None: self._default_options.update_options(**options) diff --git a/qiskit_machine_learning/gradients/lin_comb/lin_comb_estimator_gradient.py b/qiskit_machine_learning/gradients/lin_comb/lin_comb_estimator_gradient.py index f7787f7e3..e70876a26 100644 --- a/qiskit_machine_learning/gradients/lin_comb/lin_comb_estimator_gradient.py +++ b/qiskit_machine_learning/gradients/lin_comb/lin_comb_estimator_gradient.py @@ -98,7 +98,7 @@ def _run( self, circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator], - parameter_values: Sequence[Sequence[float]], + parameter_values: Sequence[Sequence[float]] | np.ndarray, parameters: Sequence[Sequence[Parameter]], **options, ) -> EstimatorGradientResult: diff --git a/qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py b/qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py index cde25a0fd..8bbe5f051 100644 --- a/qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py +++ b/qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py @@ -17,14 +17,18 @@ from collections.abc import Sequence +import numpy as np + from qiskit.circuit import Parameter, QuantumCircuit from qiskit.quantum_info.operators.base_operator import BaseOperator +from qiskit.primitives.base import BaseEstimatorV2 +from qiskit.primitives import BaseEstimatorV1 +from qiskit.providers.options import Options from ..base.base_estimator_gradient import BaseEstimatorGradient from ..base.estimator_gradient_result import EstimatorGradientResult from ..utils import _make_param_shift_parameter_values - -from ...exceptions import AlgorithmError +from ...exceptions import QiskitMachineLearningError class ParamShiftEstimatorGradient(BaseEstimatorGradient): @@ -58,7 +62,7 @@ def _run( self, circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator], - parameter_values: Sequence[Sequence[float]], + parameter_values: Sequence[Sequence[float]] | np.ndarray, parameters: Sequence[Sequence[Parameter]], **options, ) -> EstimatorGradientResult: @@ -97,26 +101,59 @@ def _run_unique( job_param_values.extend(param_shift_parameter_values) all_n.append(n) - # Run the single job with all circuits. - job = self._estimator.run( - job_circuits, - job_observables, - job_param_values, - **options, - ) - try: + # Determine how to run the estimator based on its version + if isinstance(self._estimator, BaseEstimatorV1): + # Run the single job with all circuits. + job = self._estimator.run( + job_circuits, + job_observables, + job_param_values, + **options, + ) results = job.result() - except Exception as exc: - raise AlgorithmError("Estimator job failed.") from exc - - # Compute the gradients. - gradients = [] - partial_sum_n = 0 - for n in all_n: - result = results.values[partial_sum_n : partial_sum_n + n] - gradient_ = (result[: n // 2] - result[n // 2 :]) / 2 - gradients.append(gradient_) - partial_sum_n += n - - opt = self._get_local_options(options) + + # Compute the gradients. + gradients = [] + partial_sum_n = 0 + for n in all_n: + result = results.values[partial_sum_n : partial_sum_n + n] + gradient_ = (result[: n // 2] - result[n // 2 :]) / 2 + gradients.append(gradient_) + partial_sum_n += n + + opt = self._get_local_options(options) + + elif isinstance(self._estimator, BaseEstimatorV2): + isa_g_circs = self._pass_manager.run(job_circuits) + isa_g_observables = [ + op.apply_layout(isa_g_circs[i].layout) for i, op in enumerate(job_observables) + ] + # Prepare circuit-observable-parameter tuples (PUBs) + circuit_observable_params = [] + for pub in zip(isa_g_circs, isa_g_observables, job_param_values): + circuit_observable_params.append(pub) + + # For BaseEstimatorV2, run the estimator using PUBs and specified precision + job = self._estimator.run(circuit_observable_params) + results = job.result() + results = np.array([float(r.data.evs) for r in results]) + + # Compute the gradients. + gradients = [] + partial_sum_n = 0 + for n in all_n: + result = results[partial_sum_n : partial_sum_n + n] + gradient_ = (result[: n // 2] - result[n // 2 :]) / 2 + gradients.append(gradient_) + partial_sum_n += n + + opt = Options(**options) + + else: + raise QiskitMachineLearningError( + "The accepted estimators are BaseEstimatorV1 and BaseEstimatorV2; got " + + f"{type(self._estimator)} instead. Note that BaseEstimatorV1 is deprecated in" + + "Qiskit and removed in Qiskit IBM Runtime." + ) + return EstimatorGradientResult(gradients=gradients, metadata=metadata, options=opt) diff --git a/qiskit_machine_learning/gradients/param_shift/param_shift_sampler_gradient.py b/qiskit_machine_learning/gradients/param_shift/param_shift_sampler_gradient.py index 0d7f384a8..f327b6453 100644 --- a/qiskit_machine_learning/gradients/param_shift/param_shift_sampler_gradient.py +++ b/qiskit_machine_learning/gradients/param_shift/param_shift_sampler_gradient.py @@ -20,11 +20,14 @@ from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.primitives import BaseSamplerV1 +from qiskit.primitives.base import BaseSamplerV2 +from qiskit.result import QuasiDistribution + from ..base.base_sampler_gradient import BaseSamplerGradient from ..base.sampler_gradient_result import SamplerGradientResult from ..utils import _make_param_shift_parameter_values - -from ...exceptions import AlgorithmError +from ...exceptions import AlgorithmError, QiskitMachineLearningError class ParamShiftSamplerGradient(BaseSamplerGradient): @@ -91,18 +94,57 @@ def _run_unique( all_n.append(n) # Run the single job with all circuits. - job = self._sampler.run(job_circuits, job_param_values, **options) + if isinstance(self._sampler, BaseSamplerV1): + job = self._sampler.run(job_circuits, job_param_values, **options) + elif isinstance(self._sampler, BaseSamplerV2): + if self._pass_manager is None: + raise QiskitMachineLearningError( + "To use ParameterShifSamplerGradient with SamplerV2 you " + + "must pass a gradient with a pass manager" + ) + isa_g_circs = self._pass_manager.run(job_circuits) + circ_params = [ + (isa_g_circs[i], job_param_values[i]) for i in range(len(job_param_values)) + ] + job = self._sampler.run(circ_params) + else: + raise AlgorithmError( + "The accepted estimators are BaseSamplerV1 (deprecated) and BaseSamplerV2; got " + + f"{type(self._sampler)} instead." + ) + try: results = job.result() except Exception as exc: - raise AlgorithmError("Estimator job failed.") from exc + raise AlgorithmError("Sampler job failed.") from exc # Compute the gradients. gradients = [] partial_sum_n = 0 + opt = None # Required by PyLint: possibly-used-before-assignment for n in all_n: gradient = [] - result = results.quasi_dists[partial_sum_n : partial_sum_n + n] + + if isinstance(self._sampler, BaseSamplerV1): + result = results.quasi_dists[partial_sum_n : partial_sum_n + n] + opt = self._get_local_options(options) + + elif isinstance(self._sampler, BaseSamplerV2): + result = [] + for i in range(partial_sum_n, partial_sum_n + n): + bitstring_counts = results[i].data.meas.get_counts() + + # Normalize the counts to probabilities + total_shots = sum(bitstring_counts.values()) + probabilities = {k: v / total_shots for k, v in bitstring_counts.items()} + + # Convert to quasi-probabilities + counts = QuasiDistribution(probabilities) + result.append( + {k: v for k, v in counts.items() if int(k) < self._len_quasi_dist} + ) + opt = options + for dist_plus, dist_minus in zip(result[: n // 2], result[n // 2 :]): grad_dist: dict[int, float] = defaultdict(float) for key, val in dist_plus.items(): @@ -113,5 +155,4 @@ def _run_unique( gradients.append(gradient) partial_sum_n += n - opt = self._get_local_options(options) return SamplerGradientResult(gradients=gradients, metadata=metadata, options=opt) diff --git a/qiskit_machine_learning/gradients/spsa/spsa_estimator_gradient.py b/qiskit_machine_learning/gradients/spsa/spsa_estimator_gradient.py index c0387a201..8f524a0bf 100644 --- a/qiskit_machine_learning/gradients/spsa/spsa_estimator_gradient.py +++ b/qiskit_machine_learning/gradients/spsa/spsa_estimator_gradient.py @@ -75,7 +75,7 @@ def _run( self, circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator], - parameter_values: Sequence[Sequence[float]], + parameter_values: Sequence[Sequence[float]] | np.ndarray, parameters: Sequence[Sequence[Parameter]], **options, ) -> EstimatorGradientResult: diff --git a/qiskit_machine_learning/neural_networks/estimator_qnn.py b/qiskit_machine_learning/neural_networks/estimator_qnn.py index f55d82224..36b3d92ce 100644 --- a/qiskit_machine_learning/neural_networks/estimator_qnn.py +++ b/qiskit_machine_learning/neural_networks/estimator_qnn.py @@ -15,14 +15,18 @@ from __future__ import annotations import logging +import warnings from copy import copy from typing import Sequence - import numpy as np + from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.primitives import BaseEstimator, Estimator, EstimatorResult +from qiskit.primitives.base import BaseEstimatorV2 +from qiskit.primitives import BaseEstimator, BaseEstimatorV1, Estimator, EstimatorResult from qiskit.quantum_info import SparsePauliOp from qiskit.quantum_info.operators.base_operator import BaseOperator + + from ..gradients import ( BaseEstimatorGradient, EstimatorGradientResult, @@ -31,7 +35,7 @@ from ..circuit.library import QNNCircuit from ..exceptions import QiskitMachineLearningError - +from ..utils.deprecation import issue_deprecation_msg from .neural_network import NeuralNetwork logger = logging.getLogger(__name__) @@ -64,7 +68,7 @@ class EstimatorQNN(NeuralNetwork): num_qubits = 2 # Using the QNNCircuit: - # Create a parameterized 2 qubit circuit composed of the default ZZFeatureMap feature map + # Create a parametrized 2 qubit circuit composed of the default ZZFeatureMap feature map # and RealAmplitudes ansatz. qnn_qc = QNNCircuit(num_qubits) @@ -105,12 +109,14 @@ def __init__( self, *, circuit: QuantumCircuit, - estimator: BaseEstimator | None = None, + estimator: BaseEstimator | BaseEstimatorV2 | None = None, observables: Sequence[BaseOperator] | BaseOperator | None = None, input_params: Sequence[Parameter] | None = None, weight_params: Sequence[Parameter] | None = None, gradient: BaseEstimatorGradient | None = None, input_gradients: bool = False, + num_virtual_qubits: int | None = None, + default_precision: float = 0.015625, ): r""" Args: @@ -127,12 +133,12 @@ def __init__( input_params: The parameters that correspond to the input data of the network. If ``None``, the input data is not bound to any parameters. If a :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is provided the - `input_params` value here is ignored. Instead the value is taken from the + `input_params` value here is ignored. Instead, the value is taken from the :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` input_parameters. weight_params: The parameters that correspond to the trainable weights. If ``None``, the weights are not bound to any parameters. If a :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is provided the - `weight_params` value here is ignored. Instead the value is taken from the + `weight_params` value here is ignored. Instead, the value is taken from the :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` weight_parameters. gradient: The estimator gradient to be used for the backward pass. If None, a default instance of the estimator gradient, @@ -141,27 +147,64 @@ def __init__( Note that this parameter is ``False`` by default, and must be explicitly set to ``True`` for a proper gradient computation when using :class:`~qiskit_machine_learning.connectors.TorchConnector`. + num_virtual_qubits: Number of virtual qubits. + default_precision: The default precision for the estimator if not specified during run. Raises: QiskitMachineLearningError: Invalid parameter values. """ if estimator is None: estimator = Estimator() + + if isinstance(estimator, BaseEstimatorV1): + issue_deprecation_msg( + msg="V1 Primitives are deprecated", + version="0.8.0", + remedy="Use V2 primitives for continued compatibility and support.", + period="4 months", + ) self.estimator = estimator self._org_circuit = circuit + + if num_virtual_qubits is None: + self.num_virtual_qubits = circuit.num_qubits + warnings.warn( + f"No number of qubits was not specified ({num_virtual_qubits}) and was retrieved from " + + f"`circuit` ({self.num_virtual_qubits:d}). If `circuit` is transpiled, this may cause " + + "unstable behaviour.", + UserWarning, + stacklevel=2, + ) + else: + self.num_virtual_qubits = num_virtual_qubits + if observables is None: - observables = SparsePauliOp.from_list([("Z" * circuit.num_qubits, 1)]) + observables = SparsePauliOp.from_sparse_list( + [("Z" * self.num_virtual_qubits, range(self.num_virtual_qubits), 1)], + num_qubits=self.circuit.num_qubits, + ) + if isinstance(observables, BaseOperator): observables = (observables,) + self._observables = observables + if isinstance(circuit, QNNCircuit): self._input_params = list(circuit.input_parameters) self._weight_params = list(circuit.weight_parameters) else: self._input_params = list(input_params) if input_params is not None else [] self._weight_params = list(weight_params) if weight_params is not None else [] + if gradient is None: + if isinstance(self.estimator, BaseEstimatorV2): + raise QiskitMachineLearningError( + "Please provide a gradient with pass manager initialised." + ) + gradient = ParamShiftEstimatorGradient(self.estimator) + + self._default_precision = default_precision self.gradient = gradient self._input_gradients = input_gradients @@ -198,7 +241,7 @@ def weight_params(self) -> Sequence[Parameter] | None: @property def input_gradients(self) -> bool: """Returns whether gradients with respect to input data are computed by this neural network - in the ``backward`` method or not. By default such gradients are not computed.""" + in the ``backward`` method or not. By default, such gradients are not computed.""" return self._input_gradients @input_gradients.setter @@ -206,25 +249,46 @@ def input_gradients(self, input_gradients: bool) -> None: """Turn on/off computation of gradients with respect to input data.""" self._input_gradients = input_gradients + @property + def default_precision(self) -> float: + """Return the default precision""" + return self._default_precision + def _forward_postprocess(self, num_samples: int, result: EstimatorResult) -> np.ndarray: """Post-processing during forward pass of the network.""" - return np.reshape(result.values, (-1, num_samples)).T + return np.reshape(result, (-1, num_samples)).T def _forward( self, input_data: np.ndarray | None, weights: np.ndarray | None ) -> np.ndarray | None: """Forward pass of the neural network.""" parameter_values_, num_samples = self._preprocess_forward(input_data, weights) - job = self.estimator.run( - [self._circuit] * num_samples * self.output_shape[0], - [op for op in self._observables for _ in range(num_samples)], - np.tile(parameter_values_, (self.output_shape[0], 1)), - ) - try: - results = job.result() - except Exception as exc: - raise QiskitMachineLearningError("Estimator job failed.") from exc + # Determine how to run the estimator based on its version + if isinstance(self.estimator, BaseEstimatorV1): + job = self.estimator.run( + [self._circuit] * num_samples * self.output_shape[0], + [op for op in self._observables for _ in range(num_samples)], + np.tile(parameter_values_, (self.output_shape[0], 1)), + ) + results = job.result().values + + elif isinstance(self.estimator, BaseEstimatorV2): + + # Prepare circuit-observable-parameter tuples (PUBs) + circuit_observable_params = [] + for observable in self._observables: + circuit_observable_params.append((self._circuit, observable, parameter_values_)) + + # For BaseEstimatorV2, run the estimator using PUBs and specified precision + job = self.estimator.run(circuit_observable_params, precision=self._default_precision) + results = [result.data.evs for result in job.result()] + else: + raise QiskitMachineLearningError( + "The accepted estimators are BaseEstimatorV1 and BaseEstimatorV2; got " + + f"{type(self.estimator)} instead. Note that BaseEstimatorV1 is deprecated in" + + "Qiskit and removed in Qiskit IBM Runtime." + ) return self._forward_postprocess(num_samples, results) def _backward_postprocess( @@ -269,19 +333,20 @@ def _backward( param_values = np.tile(parameter_values, (num_observables, 1)) job = None + if self._input_gradients: - job = self.gradient.run(circuits, observables, param_values) # type: ignore[arg-type] + + job = self.gradient.run(circuits, observables, param_values) + elif len(parameter_values[0]) > self._num_inputs: params = [self._circuit.parameters[self._num_inputs :]] * num_circuits - job = self.gradient.run( - circuits, observables, param_values, parameters=params # type: ignore[arg-type] - ) + job = self.gradient.run(circuits, observables, param_values, parameters=params) if job is not None: try: results = job.result() except Exception as exc: - raise QiskitMachineLearningError("Estimator job failed.") from exc + raise QiskitMachineLearningError(f"Estimator job failed. {exc}") from exc input_grad, weights_grad = self._backward_postprocess(num_samples, results) diff --git a/qiskit_machine_learning/neural_networks/neural_network.py b/qiskit_machine_learning/neural_networks/neural_network.py index e75858d38..3f0e14c9c 100644 --- a/qiskit_machine_learning/neural_networks/neural_network.py +++ b/qiskit_machine_learning/neural_networks/neural_network.py @@ -293,9 +293,9 @@ def _reparameterize_circuit( if len(parameters) != (self.num_inputs + self.num_weights): raise ValueError( - f"Number of circuit parameters {len(parameters)}" - f" mismatch with sum of num inputs and weights" - f" {self.num_inputs + self.num_weights}" + f"Number of circuit parameters ({len(parameters)})" + f" does not match the sum of number of inputs and weights" + f" ({self.num_inputs + self.num_weights})." ) new_input_params = ParameterVector("inputs", self.num_inputs) diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index 6982d2e87..bb5ca4023 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -14,17 +14,19 @@ from __future__ import annotations import logging - from numbers import Integral from typing import Callable, cast, Iterable, Sequence - import numpy as np +from qiskit.primitives import BaseSamplerV1 +from qiskit.primitives.base import BaseSamplerV2 + from qiskit.circuit import Parameter, QuantumCircuit from qiskit.primitives import BaseSampler, SamplerResult, Sampler +from qiskit.result import QuasiDistribution import qiskit_machine_learning.optionals as _optionals -from .neural_network import NeuralNetwork + from ..gradients import ( BaseSamplerGradient, ParamShiftSamplerGradient, @@ -32,6 +34,8 @@ ) from ..circuit.library import QNNCircuit from ..exceptions import QiskitMachineLearningError +from ..utils.deprecation import issue_deprecation_msg +from .neural_network import NeuralNetwork if _optionals.HAS_SPARSE: @@ -128,6 +132,7 @@ def __init__( self, *, circuit: QuantumCircuit, + num_virtual_qubits: int | None = None, sampler: BaseSampler | None = None, input_params: Sequence[Parameter] | None = None, weight_params: Sequence[Parameter] | None = None, @@ -138,50 +143,53 @@ def __init__( input_gradients: bool = False, ): """ - Args: - sampler: The sampler primitive used to compute the neural network's results. - If ``None`` is given, a default instance of the reference sampler defined - by :class:`~qiskit.primitives.Sampler` will be used. - circuit: The parametrized quantum circuit that generates the samples of this network. - If a :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is passed, the - `input_params` and `weight_params` do not have to be provided, because these two - properties are taken from the - :class:`~qiskit_machine_learning.circuit.library.QNNCircuit`. - input_params: The parameters of the circuit corresponding to the input. If a - :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is provided the - `input_params` value here is ignored. Instead the value is taken from the - :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` input_parameters. - weight_params: The parameters of the circuit corresponding to the trainable weights. If - a :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is provided the - `weight_params` value here is ignored. Instead the value is taken from the - :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` weight_parameters. - sparse: Returns whether the output is sparse or not. - interpret: A callable that maps the measured integer to another unsigned integer or - tuple of unsigned integers. These are used as new indices for the (potentially - sparse) output array. If no interpret function is - passed, then an identity function will be used by this neural network. - output_shape: The output shape of the custom interpretation. It is ignored if no custom - interpret method is provided where the shape is taken to be - ``2^circuit.num_qubits``. - gradient: An optional sampler gradient to be used for the backward pass. - If ``None`` is given, a default instance of - :class:`~qiskit_machine_learning.gradients.ParamShiftSamplerGradient` will be used. - input_gradients: Determines whether to compute gradients with respect to input data. - Note that this parameter is ``False`` by default, and must be explicitly set to - ``True`` for a proper gradient computation when using - :class:`~qiskit_machine_learning.connectors.TorchConnector`. - Raises: - QiskitMachineLearningError: Invalid parameter values. + Args: sampler: The sampler primitive used to compute the neural network's results. If + ``None`` is given, a default instance of the reference sampler defined by + :class:`~qiskit.primitives.Sampler` will be used. circuit: The parametrized quantum + circuit that generates the samples of this network. If a + :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is passed, + the `input_params` and `weight_params` do not have to be provided, because these two + properties are taken from the :class:`~qiskit_machine_learning.circuit.library.QNNCircuit + `. input_params: The parameters of the circuit corresponding to the input. If a + :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is provided the + `input_params` value here is ignored. Instead, the value is taken from the + :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` input_parameters. + weight_params: The parameters of the circuit corresponding to the trainable weights. If a + :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is provided the + `weight_params` value here is ignored. Instead, the value is taken from the + :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` weight_parameters. sparse: + Returns whether the output is sparse or not. interpret: A callable that maps the measured + integer to another unsigned integer or tuple of unsigned integers. These are used as new + indices for the (potentially sparse) output array. If no interpret function is passed, + then an identity function will be used by this neural network. output_shape: The output + shape of the custom interpretation. For SamplerV1, it is ignored if no custom interpret + method is provided where the shape is taken to be ``2^circuit.num_qubits``. gradient: An + optional sampler gradient to be used for the backward pass. If ``None`` is given, + a default instance of + :class:`~qiskit_machine_learning.gradients.ParamShiftSamplerGradient` will be used. + input_gradients: Determines whether to compute gradients with respect to input data. Note + that this parameter is ``False`` by default, and must be explicitly set to ``True`` for a + proper gradient computation when using + :class:`~qiskit_machine_learning.connectors.TorchConnector`. Raises: + QiskitMachineLearningError: Invalid parameter values. """ # set primitive, provide default if sampler is None: sampler = Sampler() + + if isinstance(sampler, BaseSamplerV1): + issue_deprecation_msg( + msg="V1 Primitives are deprecated", + version="0.8.0", + remedy="Use V2 primitives for continued compatibility and support.", + period="4 months", + ) self.sampler = sampler - # set gradient - if gradient is None: - gradient = ParamShiftSamplerGradient(self.sampler) - self.gradient = gradient + if num_virtual_qubits is None: + # print statement + num_virtual_qubits = circuit.num_qubits + self.num_virtual_qubits = num_virtual_qubits self._org_circuit = circuit @@ -196,6 +204,12 @@ def __init__( _optionals.HAS_SPARSE.require_now("DOK") self.set_interpret(interpret, output_shape) + + # set gradient + if gradient is None: + gradient = ParamShiftSamplerGradient(sampler=self.sampler) + self.gradient = gradient + self._input_gradients = input_gradients super().__init__( @@ -276,10 +290,9 @@ def _compute_output_shape( # Warn user that output_shape parameter will be ignored logger.warning( "No interpret function given, output_shape will be automatically " - "determined as 2^num_qubits." + "determined as 2^num_virtual_qubits." ) - output_shape_ = (2**self.circuit.num_qubits,) - + output_shape_ = (2**self.num_virtual_qubits,) return output_shape_ def _postprocess(self, num_samples: int, result: SamplerResult) -> np.ndarray | SparseArray: @@ -296,8 +309,24 @@ def _postprocess(self, num_samples: int, result: SamplerResult) -> np.ndarray | prob = np.zeros((num_samples, *self._output_shape)) for i in range(num_samples): - counts = result.quasi_dists[i] + if isinstance(self.sampler, BaseSamplerV1): + counts = result.quasi_dists[i] + + elif isinstance(self.sampler, BaseSamplerV2): + bitstring_counts = result[i].data.meas.get_counts() + + # Normalize the counts to probabilities + total_shots = sum(bitstring_counts.values()) + probabilities = {k: v / total_shots for k, v in bitstring_counts.items()} + # Convert to quasi-probabilities + counts = QuasiDistribution(probabilities) + counts = {k: v for k, v in counts.items() if int(k) < 2**self.num_virtual_qubits} + else: + raise QiskitMachineLearningError( + "The accepted estimators are BaseSamplerV1 (deprecated) and BaseSamplerV2; " + + f"got {type(self.sampler)} instead." + ) # evaluate probabilities for b, v in counts.items(): key = self._interpret(b) @@ -387,14 +416,22 @@ def _forward( """ parameter_values, num_samples = self._preprocess_forward(input_data, weights) - # sampler allows batching - job = self.sampler.run([self._circuit] * num_samples, parameter_values) + if isinstance(self.sampler, BaseSamplerV1): + job = self.sampler.run([self._circuit] * num_samples, parameter_values) + elif isinstance(self.sampler, BaseSamplerV2): + job = self.sampler.run( + [(self._circuit, parameter_values[i]) for i in range(num_samples)] + ) + else: + raise QiskitMachineLearningError( + "The accepted estimators are BaseSamplerV1 (deprecated) and BaseSamplerV2; " + + f"got {type(self.sampler)} instead." + ) try: results = job.result() except Exception as exc: - raise QiskitMachineLearningError("Sampler job failed.") from exc + raise QiskitMachineLearningError(f"Sampler job failed: {exc}") from exc result = self._postprocess(num_samples, results) - return result def _backward( @@ -410,7 +447,6 @@ def _backward( if np.prod(parameter_values.shape) > 0: circuits = [self._circuit] * num_samples - job = None if self._input_gradients: job = self.gradient.run(circuits, parameter_values) # type: ignore[arg-type] @@ -424,7 +460,7 @@ def _backward( try: results = job.result() except Exception as exc: - raise QiskitMachineLearningError("Sampler job failed.") from exc + raise QiskitMachineLearningError(f"Sampler job failed: {exc}") from exc input_grad, weights_grad = self._postprocess_gradient(num_samples, results) diff --git a/qiskit_machine_learning/state_fidelities/compute_uncompute.py b/qiskit_machine_learning/state_fidelities/compute_uncompute.py index 3453b2081..03a9d7354 100644 --- a/qiskit_machine_learning/state_fidelities/compute_uncompute.py +++ b/qiskit_machine_learning/state_fidelities/compute_uncompute.py @@ -18,11 +18,15 @@ from copy import copy from qiskit import QuantumCircuit -from qiskit.primitives import BaseSampler +from qiskit.primitives import BaseSampler, BaseSamplerV1, SamplerResult, StatevectorSampler +from qiskit.primitives.base import BaseSamplerV2 +from qiskit.transpiler.passmanager import PassManager +from qiskit.result import QuasiDistribution from qiskit.primitives.primitive_job import PrimitiveJob from qiskit.providers import Options -from ..exceptions import AlgorithmError +from ..exceptions import AlgorithmError, QiskitMachineLearningError +from ..utils.deprecation import issue_deprecation_msg from .base_state_fidelity import BaseStateFidelity from .state_fidelity_result import StateFidelityResult from ..algorithm_job import AlgorithmJob @@ -53,7 +57,10 @@ class ComputeUncompute(BaseStateFidelity): def __init__( self, - sampler: BaseSampler, + sampler: BaseSampler | BaseSamplerV2, + *, + num_virtual_qubits: int | None = None, + pass_manager: PassManager | None = None, options: Options | None = None, local: bool = False, ) -> None: @@ -79,11 +86,31 @@ def __init__( Raises: ValueError: If the sampler is not an instance of ``BaseSampler``. """ - if not isinstance(sampler, BaseSampler): + if (not isinstance(sampler, BaseSampler)) and (not isinstance(sampler, BaseSamplerV2)): raise ValueError( - f"The sampler should be an instance of BaseSampler, " f"but got {type(sampler)}" + f"The sampler should be an instance of BaseSampler or BaseSamplerV2, " + f"but got {type(sampler)}" + ) + if ( + isinstance(sampler, BaseSamplerV2) + and (pass_manager is None) + and not isinstance(sampler, StatevectorSampler) + ): + raise ValueError(f"A pass_manager should be provided for {type(sampler)}.") + if (pass_manager is not None) and (num_virtual_qubits is None): + raise ValueError( + f"Number of virtual qubits should be provided for {type(pass_manager)}." + ) + if isinstance(sampler, BaseSamplerV1): + issue_deprecation_msg( + msg="V1 Primitives are deprecated", + version="0.8.0", + remedy="Use V2 primitives for continued compatibility and support.", + period="4 months", ) self._sampler: BaseSampler = sampler + self.num_virtual_qubits = num_virtual_qubits + self.pass_manager = pass_manager self._local = local self._default_options = Options() if options is not None: @@ -111,6 +138,8 @@ def create_fidelity_circuit( circuit = circuit_1.compose(circuit_2.inverse()) circuit.measure_all() + if self.pass_manager is not None: + circuit = self.pass_manager.run(circuit) return circuit def _run( @@ -157,28 +186,67 @@ def _run( opts = copy(self._default_options) opts.update_options(**options) - sampler_job = self._sampler.run(circuits=circuits, parameter_values=values, **opts.__dict__) - - local_opts = self._get_local_options(opts.__dict__) - return AlgorithmJob(ComputeUncompute._call, sampler_job, circuits, self._local, local_opts) + if isinstance(self._sampler, BaseSamplerV1): + sampler_job = self._sampler.run( + circuits=circuits, parameter_values=values, **opts.__dict__ + ) + local_opts = self._get_local_options(opts.__dict__) + elif isinstance(self._sampler, BaseSamplerV2): + sampler_job = self._sampler.run( + [(circuits[i], values[i]) for i in range(len(circuits))], **opts.__dict__ + ) + local_opts = opts.__dict__ + else: + raise QiskitMachineLearningError( + "The accepted estimators are BaseSamplerV1 (deprecated) and BaseSamplerV2; got" + + f" {type(self._sampler)} instead." + ) + return AlgorithmJob( + ComputeUncompute._call, + sampler_job, + circuits, + self._local, + local_opts, + self._sampler, + self._post_process_v2, + self.num_virtual_qubits, + ) @staticmethod def _call( - job: PrimitiveJob, circuits: Sequence[QuantumCircuit], local: bool, local_opts: Options + job: PrimitiveJob, + circuits: Sequence[QuantumCircuit], + local: bool, + local_opts: Options = None, + _sampler=None, + _post_process_v2=None, + num_virtual_qubits=None, ) -> StateFidelityResult: try: result = job.result() except Exception as exc: raise AlgorithmError("Sampler job failed!") from exc + if isinstance(_sampler, BaseSamplerV1): + quasi_dists = result.quasi_dists + elif isinstance(_sampler, BaseSamplerV2): + quasi_dists = _post_process_v2(result) + if local: raw_fidelities = [ - ComputeUncompute._get_local_fidelity(prob_dist, circuit.num_qubits) - for prob_dist, circuit in zip(result.quasi_dists, circuits) + ComputeUncompute._get_local_fidelity( + prob_dist, + ( + num_virtual_qubits + if isinstance(_sampler, BaseSamplerV2) + else circuit.num_qubits + ), + ) + for prob_dist, circuit in zip(quasi_dists, circuits) ] else: raw_fidelities = [ - ComputeUncompute._get_global_fidelity(prob_dist) for prob_dist in result.quasi_dists + ComputeUncompute._get_global_fidelity(prob_dist) for prob_dist in quasi_dists ] fidelities = ComputeUncompute._truncate_fidelities(raw_fidelities) @@ -225,6 +293,21 @@ def _get_local_options(self, options: Options) -> Options: opts.update_options(**options) return opts + def _post_process_v2(self, result: SamplerResult): + quasis = [] + for i in range(len(result)): + bitstring_counts = result[i].data.meas.get_counts() + + # Normalize the counts to probabilities + total_shots = sum(bitstring_counts.values()) + probabilities = {k: v / total_shots for k, v in bitstring_counts.items()} + + # Convert to quasi-probabilities + counts = QuasiDistribution(probabilities) + quasi_probs = {k: v for k, v in counts.items() if int(k) < 2**self.num_virtual_qubits} + quasis.append(quasi_probs) + return quasis + @staticmethod def _get_global_fidelity(probability_distribution: dict[int, float]) -> float: """Process the probability distribution of a measurement to determine the diff --git a/qiskit_machine_learning/utils/deprecation.py b/qiskit_machine_learning/utils/deprecation.py new file mode 100644 index 000000000..14fb89ecb --- /dev/null +++ b/qiskit_machine_learning/utils/deprecation.py @@ -0,0 +1,84 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2024, 2024. +# +# 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. +"""Deprecation utilities""" + +from typing import Callable, Any +import functools +import warnings + + +def deprecate_function(deprecated: str, version: str, remedy: str, stacklevel: int = 2) -> Callable: + """Emit a warning prior to calling decorated function. + Args: + deprecated: Function being deprecated. + version: First release the function is deprecated. + remedy: User action to take. + stacklevel: The warning stack-level to use. + + Returns: + The decorated, deprecated callable. + """ + + def decorator(func: Callable) -> Callable: + """Emit a deprecation warning.""" + + @functools.wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> Callable: + """Emit a deprecation warning.""" + issue_deprecation_msg( + f"The {deprecated} method is deprecated", + version, + remedy, + stacklevel + 1, + ) + return func(*args, **kwargs) + + return wrapper + + return decorator + + +def deprecate_arguments(deprecated: str, version: str, remedy: str, stacklevel: int = 2) -> None: + """Emit a warning about deprecated keyword arguments. + + Args: + deprecated: Keyword arguments being deprecated. + version: First release the function is deprecated. + remedy: User action to take. + stacklevel: The warning stack-level to use. + """ + issue_deprecation_msg( + f"The '{deprecated}' keyword arguments are deprecated", + version, + remedy, + stacklevel + 1, + ) + + +def issue_deprecation_msg( + msg: str, version: str, remedy: str, stacklevel: int = 2, period: str = "3 months" +) -> None: + """Emit a deprecation warning. + + Args: + msg: Deprecation message. + version: First release the function is deprecated. + remedy: User action to take. + stacklevel: The warning stack-level to use. + period: Deprecation period. + """ + warnings.warn( + f"{msg} as of qiskit-machine-learning {version} " + f"and will be removed no sooner than {period} after the release date. {remedy}", + DeprecationWarning, + stacklevel=stacklevel + 1, # Increment to account for this function. + ) diff --git a/releasenotes/notes/v2-primitive-support-2cf30f1701c31d0f.yaml b/releasenotes/notes/v2-primitive-support-2cf30f1701c31d0f.yaml new file mode 100644 index 000000000..17d00b5aa --- /dev/null +++ b/releasenotes/notes/v2-primitive-support-2cf30f1701c31d0f.yaml @@ -0,0 +1,35 @@ +--- +features: + - | + **Support for V2 Primitives**: + The `EstimatorQNN` and `SamplerQNN` classes now support `V2` primitives + (`EstimatorV2` and `SamplerV2`), allowing direct execution on IBM Quantum backends. + This enhancement ensures compatibility with Qiskit IBM Runtime’s Primitive Unified + Block (PUB) requirements and instruction set architecture (ISA) constraints for + circuits and observables. Users can switch between `V1` primitives + and `V2` primitives from version `0.8`. From version `0.9`, V1 primitives will be + removed. + +upgrade: + - | + Users working with real backends are advised to migrate to `V2` primitives + (`EstimatorV2` and `SamplerV2`) to ensure compatibility with Qiskit IBM Runtime + hardware requirements. These `V2` primitives will become the standard in + the `0.8` release going forward, while `V1` primitives are deprecated. + +deprecations: + - | + **Deprecated V1 Primitives**: + The `V1` primitives (e.g., `EstimatorV1` and `SamplerV1`) are no longer compatible + with real quantum backends via Qiskit IBM Runtime. This update provides initial + transitional support, but `V1` primitives may be fully deprecated and removed in + version `0.9`. Users should adopt `V2` primitives for both local and hardware + executions to ensure long-term compatibility. + +known_issues: + - | + **Optimizer compatibility may be unstable**: + Current implementations of `EstimatorQNN` and `SamplerQNN` using `V2` primitives + may require further testing with optimizers, especially those depending on gradient + calculations. Users are advised to use optimizers with caution and report any + issues related to optimizer compatibility in Qiskit Machine Learning’s issue tracker. diff --git a/requirements-dev.txt b/requirements-dev.txt index bdfa45cba..6a56691fc 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -17,3 +17,4 @@ mypy>=0.981 mypy-extensions>=0.4.3 nbsphinx qiskit_sphinx_theme~=1.16.0 +qiskit-ibm-runtime>=0.21 diff --git a/setup.py b/setup.py index d7caaa4be..ea5e885e4 100644 --- a/setup.py +++ b/setup.py @@ -16,12 +16,16 @@ import os import re -with open('requirements.txt') as f: +with open("requirements.txt") as f: REQUIREMENTS = f.read().splitlines() -if not hasattr(setuptools, 'find_namespace_packages') or not inspect.ismethod(setuptools.find_namespace_packages): - print("Your setuptools version:'{}' does not support PEP 420 (find_namespace_packages). " - "Upgrade it to version >='40.1.0' and repeat install.".format(setuptools.__version__)) +if not hasattr(setuptools, "find_namespace_packages") or not inspect.ismethod( + setuptools.find_namespace_packages +): + print( + "Your setuptools version:'{}' does not support PEP 420 (find_namespace_packages). " + "Upgrade it to version >='40.1.0' and repeat install.".format(setuptools.__version__) + ) sys.exit(1) VERSION_PATH = os.path.join(os.path.dirname(__file__), "qiskit_machine_learning", "VERSION.txt") @@ -39,15 +43,15 @@ ) setuptools.setup( - name='qiskit-machine-learning', + name="qiskit-machine-learning", version=VERSION, - description='Qiskit Machine Learning: A library of quantum computing machine learning experiments', + description="Qiskit Machine Learning: A library of quantum computing machine learning experiments", long_description=README, long_description_content_type="text/markdown", - url='https://github.com/qiskit-community/qiskit-machine-learning', - author='Qiskit Machine Learning Development Team', - author_email='qiskit@us.ibm.com', - license='Apache-2.0', + url="https://github.com/qiskit-community/qiskit-machine-learning", + author="Qiskit Machine Learning Development Team", + author_email="qiskit@us.ibm.com", + license="Apache-2.0", classifiers=[ "Environment :: Console", "License :: OSI Approved :: Apache Software License", @@ -61,21 +65,23 @@ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", - "Topic :: Scientific/Engineering" + "Topic :: Scientific/Engineering", ], - keywords='qiskit sdk quantum machine learning ml', - packages=setuptools.find_packages(include=['qiskit_machine_learning','qiskit_machine_learning.*']), + keywords="qiskit sdk quantum machine learning ml", + packages=setuptools.find_packages( + include=["qiskit_machine_learning", "qiskit_machine_learning.*"] + ), install_requires=REQUIREMENTS, include_package_data=True, python_requires=">=3.9", extras_require={ - 'torch': ["torch"], - 'sparse': ["sparse"], + "torch": ["torch"], + "sparse": ["sparse"], }, project_urls={ "Bug Tracker": "https://github.com/qiskit-community/qiskit-machine-learning/issues", "Documentation": "https://qiskit-community.github.io/qiskit-machine-learning/", "Source Code": "https://github.com/qiskit-community/qiskit-machine-learning", }, - zip_safe=False + zip_safe=False, ) diff --git a/test/neural_networks/test_estimator_qnn.py b/test/neural_networks/test_estimator_qnn_v1.py similarity index 99% rename from test/neural_networks/test_estimator_qnn.py rename to test/neural_networks/test_estimator_qnn_v1.py index 566329f27..483eaf0c1 100644 --- a/test/neural_networks/test_estimator_qnn.py +++ b/test/neural_networks/test_estimator_qnn_v1.py @@ -20,9 +20,10 @@ from qiskit.circuit import Parameter, QuantumCircuit from qiskit.circuit.library import ZZFeatureMap, RealAmplitudes, ZFeatureMap from qiskit.quantum_info import SparsePauliOp -from qiskit_machine_learning.circuit.library import QNNCircuit +from qiskit_machine_learning.circuit.library import QNNCircuit from qiskit_machine_learning.neural_networks.estimator_qnn import EstimatorQNN +from qiskit_machine_learning.utils import algorithm_globals CASE_DATA = { "shape_1_1": { @@ -178,6 +179,7 @@ def _test_network_passes( estimator_qnn, case_data, ): + algorithm_globals.random_seed = 52 test_data = case_data["test_data"] weights = case_data["weights"] correct_forwards = case_data["correct_forwards"] @@ -407,7 +409,7 @@ def test_setters_getters(self): estimator_qnn.input_gradients = True self.assertTrue(estimator_qnn.input_gradients) - def test_qnn_qc_circui_construction(self): + def test_qnn_qc_circuit_construction(self): """Test Estimator QNN properties and forward/backward pass for QNNCircuit construction""" num_qubits = 2 feature_map = ZZFeatureMap(feature_dimension=num_qubits) diff --git a/test/neural_networks/test_estimator_qnn_v2.py b/test/neural_networks/test_estimator_qnn_v2.py new file mode 100644 index 000000000..b8fad6557 --- /dev/null +++ b/test/neural_networks/test_estimator_qnn_v2.py @@ -0,0 +1,569 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2024. +# +# 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. + +""" Test EstimatorQNN """ + +import unittest + +from test import QiskitMachineLearningTestCase + +import numpy as np + +from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.circuit.library import ZZFeatureMap, RealAmplitudes, ZFeatureMap +from qiskit.quantum_info import SparsePauliOp +from qiskit.providers.fake_provider import GenericBackendV2 +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager +from qiskit_ibm_runtime import Session, EstimatorV2 + +from qiskit_machine_learning.circuit.library import QNNCircuit +from qiskit_machine_learning.neural_networks.estimator_qnn import EstimatorQNN +from qiskit_machine_learning.utils import algorithm_globals + +from qiskit_machine_learning.gradients import ParamShiftEstimatorGradient + +algorithm_globals.random_seed = 52 + +CASE_DATA = { + "shape_1_1": { + "test_data": [1, [1], [[1], [2]], [[[1], [2]], [[3], [4]]]], + "weights": [1], + "correct_forwards": [ + [[0.08565359]], + [[0.08565359]], + [[0.08565359], [-0.90744233]], + [[[0.08565359], [-0.90744233]], [[-1.06623996], [-0.24474149]]], + ], + "correct_weight_backwards": [ + [[[0.70807342]]], + [[[0.70807342]]], + [[[0.70807342]], [[0.7651474]]], + [[[[0.70807342]], [[0.7651474]]], [[[0.11874839]], [[-0.63682734]]]], + ], + "correct_input_backwards": [ + [[[-1.13339757]]], + [[[-1.13339757]]], + [[[-1.13339757]], [[-0.68445233]]], + [[[[-1.13339757]], [[-0.68445233]]], [[[0.39377522]], [[1.10996765]]]], + ], + }, + "shape_2_1": { + "test_data": [[1, 2], [[1, 2]], [[1, 2], [3, 4]]], + "weights": [1, 2], + "correct_forwards": [ + [[0.41256026]], + [[0.41256026]], + [[0.41256026], [0.72848859]], + ], + "correct_weight_backwards": [ + [[[0.12262287, -0.17203964]]], + [[[0.12262287, -0.17203964]]], + [[[0.12262287, -0.17203964]], [[0.03230095, -0.04531817]]], + ], + "correct_input_backwards": [ + [[[-0.81570272, -0.39688474]]], + [[[-0.81570272, -0.39688474]]], + [[[-0.81570272, -0.39688474]], [[0.25229775, 0.67111573]]], + ], + }, + "shape_1_2": { + "test_data": [ + [1], + [[1], [2]], + [[[1], [2]], [[3], [4]]], + ], + "weights": [1], + "correct_forwards": [ + [[0.08565359, 0.17130718]], + [[0.08565359, 0.17130718], [-0.90744233, -1.81488467]], + [ + [[0.08565359, 0.17130718], [-0.90744233, -1.81488467]], + [[-1.06623996, -2.13247992], [-0.24474149, -0.48948298]], + ], + ], + "correct_weight_backwards": [ + [[[0.70807342], [1.41614684]]], + [[[0.70807342], [1.41614684]], [[0.7651474], [1.5302948]]], + [ + [[[0.70807342], [1.41614684]], [[0.7651474], [1.5302948]]], + [[[0.11874839], [0.23749678]], [[-0.63682734], [-1.27365468]]], + ], + ], + "correct_input_backwards": [ + [[[-1.13339757], [-2.26679513]]], + [[[-1.13339757], [-2.26679513]], [[-0.68445233], [-1.36890466]]], + [ + [[[-1.13339757], [-2.26679513]], [[-0.68445233], [-1.36890466]]], + [[[0.39377522], [0.78755044]], [[1.10996765], [2.2199353]]], + ], + ], + }, + "shape_2_2": { + "test_data": [[1, 2], [[1, 2], [3, 4]]], + "weights": [1, 2], + "correct_forwards": [ + [[-0.07873524, 0.4912955]], + [[-0.07873524, 0.4912955], [-0.0207402, 0.74922879]], + ], + "correct_weight_backwards": [ + [[[0.12262287, -0.17203964], [0, 0]]], + [[[0.12262287, -0.17203964], [0, 0]], [[0.03230095, -0.04531817], [0, 0]]], + ], + "correct_input_backwards": [ + [[[-0.05055532, -0.17203964], [-0.7651474, -0.2248451]]], + [ + [[-0.05055532, -0.17203964], [-0.7651474, -0.2248451]], + [[0.14549777, 0.02401345], [0.10679997, 0.64710228]], + ], + ], + }, + "no_input_parameters": { + "test_data": [None], + "weights": [1, 1], + "correct_forwards": [[[0.08565359]]], + "correct_weight_backwards": [[[[-1.13339757, 0.70807342]]]], + "correct_input_backwards": [None], + }, + "no_weight_parameters": { + "test_data": [[1, 1]], + "weights": None, + "correct_forwards": [[[0.08565359]]], + "correct_weight_backwards": [None], + "correct_input_backwards": [[[[-1.13339757, 0.70807342]]]], + }, + "no_parameters": { + "test_data": [None], + "weights": None, + "correct_forwards": [[[1]]], + "correct_weight_backwards": [None], + "correct_input_backwards": [None], + }, + "default_observables": { + "test_data": [[[1], [2]]], + "weights": [1], + "correct_forwards": [[[-0.45464871], [-0.4912955]]], + "correct_weight_backwards": [[[[0.70807342]], [[0.7651474]]]], + "correct_input_backwards": [[[[-0.29192658]], [[0.2248451]]]], + }, + "single_observable": { + "test_data": [1, [1], [[1], [2]], [[[1], [2]], [[3], [4]]]], + "weights": [1], + "correct_forwards": [ + [[0.08565359]], + [[0.08565359]], + [[0.08565359], [-0.90744233]], + [[[0.08565359], [-0.90744233]], [[-1.06623996], [-0.24474149]]], + ], + "correct_weight_backwards": [ + [[[0.70807342]]], + [[[0.70807342]]], + [[[0.70807342]], [[0.7651474]]], + [[[[0.70807342]], [[0.7651474]]], [[[0.11874839]], [[-0.63682734]]]], + ], + "correct_input_backwards": [ + [[[-1.13339757]]], + [[[-1.13339757]]], + [[[-1.13339757]], [[-0.68445233]]], + [[[[-1.13339757]], [[-0.68445233]]], [[[0.39377522]], [[1.10996765]]]], + ], + }, +} + + +class TestEstimatorQNNV2(QiskitMachineLearningTestCase): + """EstimatorQNN Tests for estimator_v2. The correct references is obtained from EstimatorQNN""" + + tolerance: dict[str, float] = dict(atol=3 * 1.0e-1, rtol=3 * 1.0e-1) + backend = GenericBackendV2(num_qubits=2, seed=123) + session = Session(backend=backend) + + def __init__( + self, + TestCase, + ): + self.estimator = EstimatorV2(mode=self.session, options={"default_shots": 1e3}) + self.pm = generate_preset_pass_manager(backend=self.backend, optimization_level=0) + self.gradient = ParamShiftEstimatorGradient(estimator=self.estimator, pass_manager=self.pm) + super().__init__(TestCase) + + def _test_network_passes( + self, + estimator_qnn, + case_data, + ): + test_data = case_data["test_data"] + weights = case_data["weights"] + correct_forwards = case_data["correct_forwards"] + correct_weight_backwards = case_data["correct_weight_backwards"] + correct_input_backwards = case_data["correct_input_backwards"] + + # test forward pass + with self.subTest("forward pass"): + for i, inputs in enumerate(test_data): + forward = estimator_qnn.forward(inputs, weights) + np.testing.assert_allclose(forward, correct_forwards[i], **self.tolerance) + # test backward pass without input_gradients + with self.subTest("backward pass without input gradients"): + for i, inputs in enumerate(test_data): + input_backward, weight_backward = estimator_qnn.backward(inputs, weights) + if correct_weight_backwards[i] is None: + self.assertIsNone(weight_backward) + else: + np.testing.assert_allclose( + weight_backward, correct_weight_backwards[i], **self.tolerance + ) + self.assertIsNone(input_backward) + # test backward pass with input_gradients + with self.subTest("backward pass with input gradients"): + estimator_qnn.input_gradients = True + for i, inputs in enumerate(test_data): + input_backward, weight_backward = estimator_qnn.backward(inputs, weights) + if correct_weight_backwards[i] is None: + self.assertIsNone(weight_backward) + else: + np.testing.assert_allclose( + weight_backward, correct_weight_backwards[i], **self.tolerance + ) + if correct_input_backwards[i] is None: + self.assertIsNone(input_backward) + else: + np.testing.assert_allclose( + input_backward, correct_input_backwards[i], **self.tolerance + ) + + def test_estimator_qnn_1_1(self): + """Test Estimator QNN with input/output dimension 1/1.""" + params = [Parameter("input1"), Parameter("weight1")] + qc = QuantumCircuit(1) + qc.h(0) + qc.ry(params[0], 0) + qc.rx(params[1], 0) + isa_qc = self.pm.run(qc) + op = SparsePauliOp.from_list([("Z", 1), ("X", 1)]) + isa_ob = op.apply_layout(isa_qc.layout) + estimator_qnn = EstimatorQNN( + circuit=isa_qc, + observables=[isa_ob], + input_params=[params[0]], + weight_params=[params[1]], + estimator=self.estimator, + gradient=self.gradient, + num_virtual_qubits=qc.num_qubits, + ) + + self._test_network_passes(estimator_qnn, CASE_DATA["shape_1_1"]) + + def test_estimator_qnn_2_1(self): + """Test Estimator QNN with input/output dimension 2/1.""" + params = [ + Parameter("input1"), + Parameter("input2"), + Parameter("weight1"), + Parameter("weight2"), + ] + qc = QuantumCircuit(2) + qc.h(0) + qc.ry(params[0], 0) + qc.ry(params[1], 1) + qc.rx(params[2], 0) + qc.rx(params[3], 1) + isa_qc = self.pm.run(qc) + op = SparsePauliOp.from_list([("ZZ", 1), ("XX", 1)]) + op = op.apply_layout(isa_qc.layout) + estimator_qnn = EstimatorQNN( + circuit=isa_qc, + observables=[op], + input_params=params[:2], + weight_params=params[2:], + estimator=self.estimator, + gradient=self.gradient, + num_virtual_qubits=qc.num_qubits, + ) + + self._test_network_passes(estimator_qnn, CASE_DATA["shape_2_1"]) + + def test_estimator_qnn_1_2(self): + """Test Estimator QNN with input/output dimension 1/2.""" + params = [Parameter("input1"), Parameter("weight1")] + qc = QuantumCircuit(1) + qc.h(0) + qc.ry(params[0], 0) + qc.rx(params[1], 0) + + isa_qc = self.pm.run(qc) + op1 = SparsePauliOp.from_list([("Z", 1), ("X", 1)]) + op1 = op1.apply_layout(isa_qc.layout) + op2 = SparsePauliOp.from_list([("Z", 2), ("X", 2)]) + op2 = op2.apply_layout(isa_qc.layout) + + # construct QNN + estimator_qnn = EstimatorQNN( + circuit=isa_qc, + observables=[op1, op2], + input_params=[params[0]], + weight_params=[params[1]], + estimator=self.estimator, + gradient=self.gradient, + num_virtual_qubits=qc.num_qubits, + ) + + self._test_network_passes(estimator_qnn, CASE_DATA["shape_1_2"]) + + def test_estimator_qnn_2_2(self): + """Test Estimator QNN with input/output dimension 2/2.""" + params = [ + Parameter("input1"), + Parameter("input2"), + Parameter("weight1"), + Parameter("weight2"), + ] + qc = QuantumCircuit(2) + qc.h(0) + qc.ry(params[0], 0) + qc.ry(params[1], 1) + qc.rx(params[2], 0) + qc.rx(params[3], 1) + isa_qc = self.pm.run(qc) + op1 = SparsePauliOp.from_list([("ZZ", 1)]) + op1 = op1.apply_layout(isa_qc.layout) + op2 = SparsePauliOp.from_list([("XX", 1)]) + op2 = op2.apply_layout(isa_qc.layout) + + estimator_qnn = EstimatorQNN( + circuit=isa_qc, + observables=[op1, op2], + input_params=params[:2], + weight_params=params[2:], + estimator=self.estimator, + gradient=self.gradient, + num_virtual_qubits=qc.num_qubits, + ) + + self._test_network_passes(estimator_qnn, CASE_DATA["shape_2_2"]) + + def test_no_input_parameters(self): + """Test Estimator QNN with no input parameters.""" + params = [Parameter("weight0"), Parameter("weight1")] + qc = QuantumCircuit(1) + qc.h(0) + qc.ry(params[0], 0) + qc.rx(params[1], 0) + isa_qc = self.pm.run(qc) + op = SparsePauliOp.from_list([("Z", 1), ("X", 1)]) + op = op.apply_layout(isa_qc.layout) + estimator_qnn = EstimatorQNN( + circuit=isa_qc, + observables=[op], + input_params=None, + weight_params=params, + estimator=self.estimator, + gradient=self.gradient, + num_virtual_qubits=qc.num_qubits, + ) + self._test_network_passes(estimator_qnn, CASE_DATA["no_input_parameters"]) + + def test_no_weight_parameters(self): + """Test Estimator QNN with no weight parameters.""" + params = [Parameter("input0"), Parameter("input1")] + qc = QuantumCircuit(1) + qc.h(0) + qc.ry(params[0], 0) + qc.rx(params[1], 0) + isa_qc = self.pm.run(qc) + op = SparsePauliOp.from_list([("Z", 1), ("X", 1)]) + op = op.apply_layout(isa_qc.layout) + estimator_qnn = EstimatorQNN( + circuit=isa_qc, + observables=[op], + input_params=params, + weight_params=None, + estimator=self.estimator, + gradient=self.gradient, + num_virtual_qubits=qc.num_qubits, + ) + self._test_network_passes(estimator_qnn, CASE_DATA["no_weight_parameters"]) + + def test_no_parameters(self): + """Test Estimator QNN with no parameters.""" + qc = QuantumCircuit(1) + qc.h(0) + isa_qc = self.pm.run(qc) + op = SparsePauliOp.from_list([("Z", 1), ("X", 1)]) + op = op.apply_layout(isa_qc.layout) + estimator_qnn = EstimatorQNN( + circuit=isa_qc, + observables=[op], + input_params=None, + weight_params=None, + estimator=self.estimator, + gradient=self.gradient, + num_virtual_qubits=qc.num_qubits, + ) + self._test_network_passes(estimator_qnn, CASE_DATA["no_parameters"]) + + def test_default_observables(self): + """Test Estimator QNN with default observables.""" + params = [Parameter("input1"), Parameter("weight1")] + qc = QuantumCircuit(1) + qc.h(0) + qc.ry(params[0], 0) + qc.rx(params[1], 0) + isa_qc = self.pm.run(qc) + estimator_qnn = EstimatorQNN( + circuit=isa_qc, + input_params=[params[0]], + weight_params=[params[1]], + estimator=self.estimator, + gradient=self.gradient, + num_virtual_qubits=qc.num_qubits, + ) + self._test_network_passes(estimator_qnn, CASE_DATA["default_observables"]) + + def test_single_observable(self): + """Test Estimator QNN with single observable.""" + params = [Parameter("input1"), Parameter("weight1")] + qc = QuantumCircuit(1) + qc.h(0) + qc.ry(params[0], 0) + qc.rx(params[1], 0) + isa_qc = self.pm.run(qc) + op = SparsePauliOp.from_list([("Z", 1), ("X", 1)]) + op = op.apply_layout(isa_qc.layout) + estimator_qnn = EstimatorQNN( + circuit=isa_qc, + observables=op, + input_params=[params[0]], + weight_params=[params[1]], + estimator=self.estimator, + gradient=self.gradient, + num_virtual_qubits=isa_qc.num_qubits, + ) + self._test_network_passes(estimator_qnn, CASE_DATA["single_observable"]) + + def test_setters_getters(self): + """Test Estimator QNN properties.""" + params = [Parameter("input1"), Parameter("weight1")] + qc = QuantumCircuit(1) + qc.h(0) + qc.ry(params[0], 0) + qc.rx(params[1], 0) + isa_qc = self.pm.run(qc) + op = SparsePauliOp.from_list([("Z", 1), ("X", 1)]) + op = op.apply_layout(isa_qc.layout) + estimator_qnn = EstimatorQNN( + circuit=isa_qc, + observables=[op], + input_params=[params[0]], + weight_params=[params[1]], + estimator=self.estimator, + gradient=self.gradient, + num_virtual_qubits=qc.num_qubits, + ) + with self.subTest("Test circuit getter."): + self.assertEqual(estimator_qnn.circuit, isa_qc) + with self.subTest("Test observables getter."): + self.assertEqual(estimator_qnn.observables, [op]) + with self.subTest("Test input_params getter."): + self.assertEqual(estimator_qnn.input_params, [params[0]]) + with self.subTest("Test weight_params getter."): + self.assertEqual(estimator_qnn.weight_params, [params[1]]) + with self.subTest("Test input_gradients setter and getter."): + self.assertFalse(estimator_qnn.input_gradients) + estimator_qnn.input_gradients = True + self.assertTrue(estimator_qnn.input_gradients) + + @unittest.skip("Test unstable, to be checked.") + def test_qnn_qc_circuit_construction(self): + """Test Estimator QNN properties and forward/backward pass for QNNCircuit construction""" + num_qubits = 2 + feature_map = ZZFeatureMap(feature_dimension=num_qubits) + ansatz = RealAmplitudes(num_qubits=num_qubits, reps=1) + + qc = QuantumCircuit(num_qubits) + qc.compose(feature_map, inplace=True) + qc.compose(ansatz, inplace=True) + isa_qc = self.pm.run(qc) + + estimator_qc = EstimatorQNN( + circuit=isa_qc, + input_params=feature_map.parameters, + weight_params=ansatz.parameters, + input_gradients=True, + estimator=self.estimator, + gradient=self.gradient, + num_virtual_qubits=qc.num_qubits, + ) + + qnn_qc = QNNCircuit(num_qubits=num_qubits, feature_map=feature_map, ansatz=ansatz) + isa_qnn_qc = self.pm.run(qnn_qc) + estimator_qnn_qc = EstimatorQNN( + circuit=isa_qnn_qc, + input_params=qnn_qc.feature_map.parameters, + weight_params=qnn_qc.ansatz.parameters, + input_gradients=True, + estimator=self.estimator, + gradient=self.gradient, + num_virtual_qubits=qc.num_qubits, + ) + + input_data = [1, 2] + weights = [1, 2, 3, 4] + + with self.subTest("Test if Estimator QNN properties are equal."): + self.assertEqual(estimator_qnn_qc.input_params, estimator_qc.input_params) + self.assertEqual(estimator_qnn_qc.weight_params, estimator_qc.weight_params) + self.assertEqual(estimator_qnn_qc.observables, estimator_qc.observables) + + with self.subTest("Test if forward pass yields equal results."): + forward_qc = estimator_qc.forward(input_data=input_data, weights=weights) + forward_qnn_qc = estimator_qnn_qc.forward(input_data=input_data, weights=weights) + np.testing.assert_allclose(forward_qc, forward_qnn_qc, **self.tolerance) + + with self.subTest("Test if backward pass yields equal results."): + backward_qc = estimator_qc.backward(input_data=input_data, weights=weights) + backward_qnn_qc = estimator_qnn_qc.backward(input_data=input_data, weights=weights) + + # Test if input grad is close (difference due to shots) + np.testing.assert_allclose(backward_qc[0], backward_qnn_qc[0], **self.tolerance) + # Test if weights grad is close (difference due to shots) + np.testing.assert_allclose(backward_qc[1], backward_qnn_qc[1], **self.tolerance) + + def test_binding_order(self): + """Test parameter binding order gives result as expected""" + qc = ZFeatureMap(feature_dimension=2, reps=1) + input_params = qc.parameters + weight = Parameter("weight") + for i in range(qc.num_qubits): + qc.rx(weight, i) + isa_qc = self.pm.run(qc) + op = SparsePauliOp.from_list([("Z" * isa_qc.num_qubits, 1)]) + op = op.apply_layout(isa_qc.layout) + estimator_qnn = EstimatorQNN( + circuit=isa_qc, + observables=op, + input_params=input_params, + weight_params=[weight], + estimator=self.estimator, + gradient=self.gradient, + num_virtual_qubits=qc.num_qubits, + ) + + estimator_qnn_weights = [3] + estimator_qnn_input = [2, 33] + res = estimator_qnn.forward(estimator_qnn_input, estimator_qnn_weights) + # When parameters were used in circuit order, before being assigned correctly, so inputs + # went to input params, weights to weight params, this gave 0.00613403 + self.assertAlmostEqual(res[0][0], 0.00040017, delta=0.05) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/neural_networks/test_sampler_qnn.py b/test/neural_networks/test_sampler_qnn.py index 07283b59c..9651a93d4 100644 --- a/test/neural_networks/test_sampler_qnn.py +++ b/test/neural_networks/test_sampler_qnn.py @@ -23,11 +23,18 @@ from qiskit.circuit import Parameter, QuantumCircuit from qiskit.primitives import Sampler +from qiskit.providers.fake_provider import GenericBackendV2 +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap -from qiskit_machine_learning.utils import algorithm_globals +from qiskit_ibm_runtime import Session, SamplerV2 + +from qiskit_machine_learning.utils import algorithm_globals from qiskit_machine_learning.circuit.library import QNNCircuit from qiskit_machine_learning.neural_networks.sampler_qnn import SamplerQNN +from qiskit_machine_learning.gradients.param_shift.param_shift_sampler_gradient import ( + ParamShiftSamplerGradient, +) import qiskit_machine_learning.optionals as _optionals if _optionals.HAS_SPARSE: @@ -45,8 +52,9 @@ class SparseArray: # type: ignore DEFAULT = "default" SHOTS = "shots" +V2 = "v2" SPARSE = [True, False] -SAMPLERS = [DEFAULT, SHOTS] +SAMPLERS = [DEFAULT, SHOTS, V2] INTERPRET_TYPES = [0, 1, 2] BATCH_SIZES = [2] INPUT_GRADS = [True, False] @@ -69,6 +77,8 @@ def setUp(self): self.qc = QuantumCircuit(num_qubits) self.qc.append(feature_map, range(2)) self.qc.append(var_form, range(2)) + self.qc.measure_all() + self.num_virtual_qubits = num_qubits # store params self.input_params = list(feature_map.parameters) @@ -93,7 +103,10 @@ def interpret_2d(x): # define sampler primitives self.sampler = Sampler() self.sampler_shots = Sampler(options={"shots": 100, "seed": 42}) - + self.backend = GenericBackendV2(num_qubits=8) + self.session = Session(backend=self.backend) + self.sampler_v2 = SamplerV2(mode=self.session) + self.pm = None self.array_type = {True: SparseArray, False: np.ndarray} # pylint: disable=too-many-positional-arguments @@ -101,12 +114,33 @@ def _get_qnn( self, sparse, sampler_type, interpret_id, input_params, weight_params, input_grads ): """Construct QNN from configuration.""" + # get interpret setting + interpret = None + output_shape = None + if interpret_id == 1: + interpret = self.interpret_1d + output_shape = self.output_shape_1d + elif interpret_id == 2: + interpret = self.interpret_2d + output_shape = self.output_shape_2d # get quantum instance + gradient = None if sampler_type == SHOTS: sampler = self.sampler_shots elif sampler_type == DEFAULT: sampler = self.sampler + elif sampler_type == V2: + sampler = self.sampler_v2 + + if self.qc.layout is None: + self.pm = generate_preset_pass_manager(optimization_level=1, backend=self.backend) + self.qc = self.pm.run(self.qc) + gradient = ParamShiftSamplerGradient( + sampler=self.sampler, + len_quasi_dist=2**self.num_virtual_qubits, + pass_manager=self.pm, + ) else: sampler = None @@ -124,11 +158,13 @@ def _get_qnn( qnn = SamplerQNN( sampler=sampler, circuit=self.qc, + num_virtual_qubits=self.num_virtual_qubits, input_params=input_params, weight_params=weight_params, sparse=sparse, interpret=interpret, output_shape=output_shape, + gradient=gradient, input_gradients=input_grads, ) return qnn @@ -345,7 +381,7 @@ def test_no_parameters(self): sampler_qnn.input_gradients = True self._verify_qnn(sampler_qnn, 1, input_data=None, weights=None) - def test_qnn_qc_circui_construction(self): + def test_qnn_qc_circuit_construction(self): """Test Sampler QNN properties and forward/backward pass for QNNCircuit construction""" num_qubits = 2 feature_map = ZZFeatureMap(feature_dimension=num_qubits) diff --git a/test/state_fidelities/test_compute_uncompute_v2.py b/test/state_fidelities/test_compute_uncompute_v2.py new file mode 100644 index 000000000..819b206fc --- /dev/null +++ b/test/state_fidelities/test_compute_uncompute_v2.py @@ -0,0 +1,343 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2024. +# +# 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. + +"""Tests for Fidelity.""" + +import unittest +from test import QiskitMachineLearningTestCase + +import numpy as np + +from qiskit.circuit import QuantumCircuit, ParameterVector +from qiskit.circuit.library import RealAmplitudes +from qiskit.primitives import Sampler +from qiskit.providers.fake_provider import GenericBackendV2 +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager + +from qiskit_ibm_runtime import Session, SamplerV2 + +from qiskit_machine_learning.state_fidelities import ComputeUncompute + + +class TestComputeUncompute(QiskitMachineLearningTestCase): + """Test Compute-Uncompute Fidelity class""" + + def setUp(self): + super().setUp() + parameters = ParameterVector("x", 2) + + rx_rotations = QuantumCircuit(2) + rx_rotations.rx(parameters[0], 0) + rx_rotations.rx(parameters[1], 1) + + ry_rotations = QuantumCircuit(2) + ry_rotations.ry(parameters[0], 0) + ry_rotations.ry(parameters[1], 1) + + plus = QuantumCircuit(2) + plus.h([0, 1]) + + zero = QuantumCircuit(2) + + rx_rotation = QuantumCircuit(2) + rx_rotation.rx(parameters[0], 0) + rx_rotation.h(1) + + self._circuit = [rx_rotations, ry_rotations, plus, zero, rx_rotation] + + self.backend = GenericBackendV2( + num_qubits=4, + calibrate_instructions=None, + pulse_channels=False, + noise_info=False, + seed=123, + ) + self.session = Session(backend=self.backend) + self._sampler = SamplerV2(mode=self.session) + self.pm = generate_preset_pass_manager(optimization_level=0, backend=self.backend) + + self._left_params = np.array([[0, 0], [np.pi / 2, 0], [0, np.pi / 2], [np.pi, np.pi]]) + self._right_params = np.array([[0, 0], [0, 0], [np.pi / 2, 0], [0, 0]]) + + def test_1param_pair(self): + """test for fidelity with one pair of parameters""" + fidelity = ComputeUncompute( + self._sampler, pass_manager=self.pm, num_virtual_qubits=self._circuit[0].num_qubits + ) + job = fidelity.run( + self._circuit[0], self._circuit[1], self._left_params[0], self._right_params[0] + ) + result = job.result() + np.testing.assert_allclose(result.fidelities, np.array([1.0])) + + def test_1param_pair_local(self): + """test for fidelity with one pair of parameters""" + fidelity = ComputeUncompute( + self._sampler, + local=True, + pass_manager=self.pm, + num_virtual_qubits=self._circuit[0].num_qubits, + ) + job = fidelity.run( + self._circuit[0], self._circuit[1], self._left_params[0], self._right_params[0] + ) + result = job.result() + np.testing.assert_allclose(result.fidelities, np.array([1.0])) + + def test_local(self): + """test difference between local and global fidelity""" + fidelity_global = ComputeUncompute( + self._sampler, + local=False, + pass_manager=self.pm, + num_virtual_qubits=self._circuit[2].num_qubits, + ) + fidelity_local = ComputeUncompute( + self._sampler, + local=True, + pass_manager=self.pm, + num_virtual_qubits=self._circuit[2].num_qubits, + ) + fidelities = [] + for fidelity in [fidelity_global, fidelity_local]: + job = fidelity.run(self._circuit[2], self._circuit[3]) + result = job.result() + fidelities.append(result.fidelities[0]) + np.testing.assert_allclose(fidelities, np.array([0.25, 0.5]), atol=1e-1, rtol=1e-1) + + def test_4param_pairs(self): + """test for fidelity with four pairs of parameters""" + fidelity = ComputeUncompute( + self._sampler, pass_manager=self.pm, num_virtual_qubits=self._circuit[0].num_qubits + ) + n = len(self._left_params) + job = fidelity.run( + [self._circuit[0]] * n, [self._circuit[1]] * n, self._left_params, self._right_params + ) + results = job.result() + np.testing.assert_allclose( + results.fidelities, np.array([1.0, 0.5, 0.25, 0.0]), atol=1e-1, rtol=1e-1 + ) + + def test_symmetry(self): + """test for fidelity with the same circuit""" + fidelity = ComputeUncompute( + self._sampler, pass_manager=self.pm, num_virtual_qubits=self._circuit[0].num_qubits + ) + n = len(self._left_params) + job_1 = fidelity.run( + [self._circuit[0]] * n, [self._circuit[0]] * n, self._left_params, self._right_params + ) + job_2 = fidelity.run( + [self._circuit[0]] * n, [self._circuit[0]] * n, self._right_params, self._left_params + ) + print(job_1) + results_1 = job_1.result() + results_2 = job_2.result() + np.testing.assert_allclose(results_1.fidelities, results_2.fidelities, atol=1e-1, rtol=1e-1) + + def test_no_params(self): + """test for fidelity without parameters""" + fidelity = ComputeUncompute( + self._sampler, pass_manager=self.pm, num_virtual_qubits=self._circuit[2].num_qubits + ) + job = fidelity.run([self._circuit[2]], [self._circuit[3]]) + results = job.result() + np.testing.assert_allclose(results.fidelities, np.array([0.25]), atol=1e-1, rtol=1e-1) + + job = fidelity.run([self._circuit[2]], [self._circuit[3]], [], []) + results = job.result() + np.testing.assert_allclose(results.fidelities, np.array([0.25]), atol=1e-1, rtol=1e-1) + + def test_left_param(self): + """test for fidelity with only left parameters""" + fidelity = ComputeUncompute( + self._sampler, pass_manager=self.pm, num_virtual_qubits=self._circuit[1].num_qubits + ) + n = len(self._left_params) + job = fidelity.run( + [self._circuit[1]] * n, [self._circuit[3]] * n, values_1=self._left_params + ) + results = job.result() + np.testing.assert_allclose( + results.fidelities, np.array([1.0, 0.5, 0.5, 0.0]), atol=1e-1, rtol=1e-1 + ) + + def test_right_param(self): + """test for fidelity with only right parameters""" + fidelity = ComputeUncompute( + self._sampler, pass_manager=self.pm, num_virtual_qubits=self._circuit[1].num_qubits + ) + n = len(self._left_params) + job = fidelity.run( + [self._circuit[3]] * n, [self._circuit[1]] * n, values_2=self._left_params + ) + results = job.result() + np.testing.assert_allclose( + results.fidelities, np.array([1.0, 0.5, 0.5, 0.0]), atol=1e-1, rtol=1e-1 + ) + + def test_not_set_circuits(self): + """test for fidelity with no circuits.""" + fidelity = ComputeUncompute( + self._sampler, pass_manager=self.pm, num_virtual_qubits=self._circuit[0].num_qubits + ) + with self.assertRaises(TypeError): + job = fidelity.run( + circuits_1=None, + circuits_2=None, + values_1=self._left_params, + values_2=self._right_params, + ) + job.result() + + def test_circuit_mismatch(self): + """test for fidelity with different number of left/right circuits.""" + fidelity = ComputeUncompute( + self._sampler, pass_manager=self.pm, num_virtual_qubits=self._circuit[0].num_qubits + ) + n = len(self._left_params) + with self.assertRaises(ValueError): + job = fidelity.run( + [self._circuit[0]] * n, + [self._circuit[1]] * (n + 1), + self._left_params, + self._right_params, + ) + job.result() + + def test_asymmetric_params(self): + """test for fidelity when the 2 circuits have different number of + left/right parameters.""" + + fidelity = ComputeUncompute( + self._sampler, pass_manager=self.pm, num_virtual_qubits=self._circuit[0].num_qubits + ) + n = len(self._left_params) + right_params = [[p] for p in self._right_params[:, 0]] + job = fidelity.run( + [self._circuit[0]] * n, [self._circuit[4]] * n, self._left_params, right_params + ) + result = job.result() + np.testing.assert_allclose( + result.fidelities, np.array([0.5, 0.25, 0.25, 0.0]), atol=1e-1, rtol=1e-1 + ) + + def test_input_format(self): + """test for different input format variations""" + + circuit = RealAmplitudes(2) + fidelity = ComputeUncompute( + self._sampler, pass_manager=self.pm, num_virtual_qubits=circuit.num_qubits + ) + values = np.random.random(circuit.num_parameters) + shift = np.ones_like(values) * 0.01 + + # lists of circuits, lists of numpy arrays + job = fidelity.run([circuit], [circuit], [values], [values + shift]) + result_1 = job.result() + + # lists of circuits, lists of lists + shift_val = values + shift + job = fidelity.run([circuit], [circuit], [values.tolist()], [shift_val.tolist()]) + result_2 = job.result() + + # circuits, lists + shift_val = values + shift + job = fidelity.run(circuit, circuit, values.tolist(), shift_val.tolist()) + result_3 = job.result() + + # circuits, np.arrays + job = fidelity.run(circuit, circuit, values, values + shift) + result_4 = job.result() + + np.testing.assert_allclose(result_1.fidelities, result_2.fidelities, atol=1e-1, rtol=1e-1) + np.testing.assert_allclose(result_1.fidelities, result_3.fidelities, atol=1e-1, rtol=1e-1) + np.testing.assert_allclose(result_1.fidelities, result_4.fidelities, atol=1e-1, rtol=1e-1) + + def test_input_measurements(self): + """test for fidelity with measurements on input circuits""" + fidelity = ComputeUncompute( + self._sampler, pass_manager=self.pm, num_virtual_qubits=self._circuit[0].num_qubits + ) + 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, pass_manager=self.pm, num_virtual_qubits=self._circuit[2].num_qubits + ) + 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}, + pass_manager=self.pm, + num_virtual_qubits=self._circuit[2].num_qubits, + ) + 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}, + pass_manager=self.pm, + num_virtual_qubits=self._circuit[2].num_qubits, + ) + 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}, + pass_manager=self.pm, + num_virtual_qubits=self._circuit[2].num_qubits, + ) + 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()