diff --git a/qiskit_machine_learning/utils/adjust_num_qubits.py b/qiskit_machine_learning/utils/adjust_num_qubits.py index dd74176f6..ca60e8f61 100644 --- a/qiskit_machine_learning/utils/adjust_num_qubits.py +++ b/qiskit_machine_learning/utils/adjust_num_qubits.py @@ -10,16 +10,14 @@ # 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. -"""Helper functions to adjust number of qubits.""" +"""Helper functions to derive the number of qubits, feature map, and ansatz.""" from __future__ import annotations -import warnings from qiskit import QuantumCircuit from qiskit.circuit.library import real_amplitudes, z_feature_map, zz_feature_map from ..exceptions import QiskitMachineLearningError -from ..utils.deprecation import issue_deprecation_msg # pylint: disable=invalid-name @@ -29,48 +27,16 @@ def derive_num_qubits_feature_map_ansatz( ansatz: QuantumCircuit | None = None, ) -> tuple[int, QuantumCircuit, QuantumCircuit]: """ - Derives a correct number of qubits, feature map, and ansatz from the parameters. - - If the number of qubits is not ``None``, then the feature map and ansatz are adjusted to this - number of qubits if required. If such an adjustment fails, an error is raised. Also, if the - feature map or ansatz or both are ``None``, then :func:`~qiskit.circuit.library.zz_feature_map` - and :func:`~qiskit.circuit.library.real_amplitudes` are created respectively. If there's just - one qubit, :func:`~qiskit.circuit.library.z_feature_map` is created instead. - - If the number of qubits is ``None``, then the number of qubits is derived from the feature map - or ansatz. Both the feature map and ansatz in this case must have the same number of qubits. - If the number of qubits of the feature map is not the same as the number of qubits of - the ansatz, an error is raised. If only one of the feature map and ansatz are ``None``, then - :func:`~qiskit.circuit.library.zz_feature_map` or :func:`~qiskit.circuit.library.real_amplitudes` - are created respectively. - - If the number of qubits is not ``None``, then the feature map and ansatz are adjusted to this - number of qubits if required. If such an adjustment fails, an error is raised. Also, if the - feature map or ansatz or both are ``None``, then :meth:`~qiskit.circuit.library.zz_feature_map` - and :meth:`~qiskit.circuit.library.real_amplitudes` are created respectively. If there's just - one qubit, :meth:`~qiskit.circuit.library.z_feature_map` is created instead. - - If all the parameters are ``None`` an error is raised. - - .. warning:: - - The ``num_qubits`` argument and automatic qubit alignment (padding or resizing of - ``feature_map`` and ``ansatz`` circuits) are **deprecated** as of version ``0.9.1`` and - will be removed after a 6-month deprecation period. - - In a future release, this function will require that ``feature_map`` and ``ansatz`` are - explicitly provided with the **same number of qubits**, and passing ``num_qubits`` or - relying on automatic circuit alignment will raise an error. - - To ensure forward compatibility, remove the ``num_qubits`` argument (or set it to - ``None``) and construct ``feature_map`` and ``ansatz`` with matching numbers of qubits - before calling this function. - - See https://github.com/qiskit-community/qiskit-machine-learning/issues/1010 for details. + Derive the number of qubits, feature map, and ansatz from the parameters. + All provided arguments must agree on the number of qubits. If only some are + provided, the missing ones are constructed at the agreed qubit count using + :func:`~qiskit.circuit.library.zz_feature_map` (or + :func:`~qiskit.circuit.library.z_feature_map` for a single qubit) and + :func:`~qiskit.circuit.library.real_amplitudes`. Args: - num_qubits: Number of qubits (deprecated). + num_qubits: Number of qubits. feature_map: A feature map. ansatz: An ansatz. @@ -78,145 +44,36 @@ def derive_num_qubits_feature_map_ansatz( A tuple of number of qubits, feature map, and ansatz. Raises: - QiskitMachineLearningError: If correct values can not be derived from the parameters. + QiskitMachineLearningError: If no arguments are provided, or if the + provided arguments disagree on the number of qubits. """ - candidates = {} - + counts: dict[str, int] = {} + if num_qubits is not None: + counts["num_qubits"] = num_qubits if feature_map is not None: - candidates["feature_map"] = feature_map.num_qubits + counts["feature_map"] = feature_map.num_qubits if ansatz is not None: - candidates["ansatz"] = ansatz.num_qubits - if num_qubits is not None: - candidates["num_qubits"] = num_qubits - - issue_deprecation_msg( - msg=( - "The num_qubits argument and the qubit auto-alignment of circuits are " - "deprecated and will be removed. " - "See https://github.com/qiskit-community/qiskit-machine-learning/issues/1010 " - "for more details." - ), - version="0.10.0", - remedy=( - "Remove the num_qubits argument (or set to None), and make sure that the " - "feature_map and ansatz have the same number of qubits before passing " - "them as arguments." - ), - period="6 months", - ) + counts["ansatz"] = ansatz.num_qubits - if not candidates: + if not counts: raise QiskitMachineLearningError( - "Unable to determine number of qubits: " - "provide `num_qubits` (int), `feature_map` (QuantumCircuit), " - "or `ansatz` (QuantumCircuit)." + "Unable to determine number of qubits: provide `num_qubits` (int), " + "`feature_map` (QuantumCircuit), or `ansatz` (QuantumCircuit)." ) - # Check consensus on num_qubits - unique_vals = set(candidates.values()) - if len(unique_vals) > 1: - conflicts = ", ".join(f"{k}={v}" for k, v in candidates.items()) - warnings.warn( - ( - f"Inconsistent qubit numbers detected: {conflicts}. " - "Ensure all inputs agree on the number of qubits." - ), - UserWarning, - ) - issue_deprecation_msg( - msg=( - "Qubit auto-alignment of circuits is deprecated and will be removed. " - "In the future, an error will be raised if the number of qubits in " - "feature_map and ansatz are note the same. " - "See https://github.com/qiskit-community/qiskit-machine-learning/issues/1010 " - "for more details." - ), - version="0.10.0", - remedy=( - "Ensure that the feature_map and ansatz have the same number of qubits " - "before passing them as arguments." - ), - period="6 months", + unique_counts = set(counts.values()) + if len(unique_counts) > 1: + details = ", ".join(f"{k}={v}" for k, v in counts.items()) + raise QiskitMachineLearningError( + f"Inconsistent qubit counts: {details}. " + "Adjust the inputs to match before passing them as arguments." ) - # Final resolved number of qubits - resolved_num_qubits = max(unique_vals) - - def default_feature_map(n: int) -> QuantumCircuit: - return z_feature_map(n) if n == 1 else zz_feature_map(n) - - def default_ansatz(n: int) -> QuantumCircuit: - return real_amplitudes(n) + resolved = next(iter(unique_counts)) if feature_map is None: - feature_map = default_feature_map(resolved_num_qubits) - candidates["feature_map"] = feature_map.num_qubits - else: - feature_map = _pad_if_needed(feature_map, resolved_num_qubits) - + feature_map = z_feature_map(resolved) if resolved == 1 else zz_feature_map(resolved) if ansatz is None: - ansatz = default_ansatz(resolved_num_qubits) - candidates["ansatz"] = ansatz.num_qubits - else: - ansatz = _pad_if_needed(ansatz, resolved_num_qubits) + ansatz = real_amplitudes(resolved) - # Mismatch in the circuits' num_qubits is unacceptable - if candidates["feature_map"] != candidates["ansatz"]: - raise QiskitMachineLearningError( - f"Inconsistent qubit numbers detected between the feature map ({candidates['feature_map']}) " - f"and the ansatz ({candidates['ansatz']}). These must match at all times." - ) - - return resolved_num_qubits, feature_map, ansatz - - -def _pad_if_needed(circ: QuantumCircuit, requested_num_qubits: int) -> QuantumCircuit | None: - """ - .. warning:: - - This function is **deprecated** as of version ``0.9.1``. - See https://github.com/qiskit-community/qiskit-machine-learning/issues/1010 for details. - """ - circ_nq = circ.num_qubits - - if requested_num_qubits == circ_nq: - return circ - - if requested_num_qubits < circ_nq: - raise QiskitMachineLearningError( - f"Requesting num_qubits={requested_num_qubits} to a circuit with {circ_nq} qubits. " - f"Circuit cutting is not supported by default. Please, remove qubit registers manually." - ) - - warnings.warn( - ( - f"Requesting num_qubits={requested_num_qubits} to a circuit with {circ_nq} qubits. " - f"Padding with {requested_num_qubits - circ_nq} idle qubits." - ), - UserWarning, - ) - padded = QuantumCircuit(requested_num_qubits, circ.num_clbits, name=circ.name) - padded.compose(circ, inplace=True) - return padded - - -# pylint: disable=unused-argument -def _adjust_num_qubits(circuit: QuantumCircuit, circuit_name: str, num_qubits: int) -> None: - """ - Tries to adjust the number of qubits of the circuit by trying to set ``num_qubits`` properties. - - Args: - circuit: A circuit to adjust. - circuit_name: A circuit name, used in the error description. - num_qubits: A number of qubits to set. - - Raises: - QiskitMachineLearningError: if number of qubits can't be adjusted. - - """ - issue_deprecation_msg( - msg="No longer in use", - version="0.9.0", - remedy="Check ", - period="0 months", - ) + return resolved, feature_map, ansatz diff --git a/releasenotes/notes/remove-qubit-auto-alignment-1024.yaml b/releasenotes/notes/remove-qubit-auto-alignment-1024.yaml new file mode 100644 index 000000000..b1dd1f9c3 --- /dev/null +++ b/releasenotes/notes/remove-qubit-auto-alignment-1024.yaml @@ -0,0 +1,22 @@ +--- +upgrade: + - | + The qubit auto-alignment behavior of + :func:`~qiskit_machine_learning.utils.derive_num_qubits_feature_map_ansatz` + has been removed, as previously announced in version ``0.9.1``. The + function no longer accepts a ``num_qubits`` argument; the number of qubits + is now derived directly from ``feature_map`` and/or ``ansatz``. If both + are provided, they must have the same number of qubits, otherwise a + ``QiskitMachineLearningError`` is raised. + - | + The ``num_qubits`` argument is no longer supported by the constructors of + :class:`~qiskit_machine_learning.algorithms.classifiers.VQC` and + :class:`~qiskit_machine_learning.algorithms.regressors.VQR`, nor by + :func:`~qiskit_machine_learning.circuit.library.qnn_circuit`. Passing it + raises a ``QiskitMachineLearningError``. Construct ``feature_map`` and + ``ansatz`` with the desired number of qubits before passing them to these + APIs. +fixes: + - | + Removes the deprecated qubit auto-alignment feature. See + `#1024 `__. \ No newline at end of file diff --git a/test/utils/test_adjust_num_qubits.py b/test/utils/test_adjust_num_qubits.py index 1cf84a4ac..be360c146 100644 --- a/test/utils/test_adjust_num_qubits.py +++ b/test/utils/test_adjust_num_qubits.py @@ -10,7 +10,7 @@ # 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 adjusting number of qubits in a feature map / ansatz.""" +"""Tests for derive_num_qubits_feature_map_ansatz.""" from test import QiskitMachineLearningTestCase import itertools @@ -22,7 +22,7 @@ @ddt -class TestAdjustNumQubits(QiskitMachineLearningTestCase): +class TestDeriveNumQubits(QiskitMachineLearningTestCase): """Tests for the derive_num_qubits_feature_map_ansatz function.""" def setUp(self) -> None: @@ -35,13 +35,13 @@ def setUp(self) -> None: } def test_all_none(self): - """Test when all parameters are ``None``.""" + """All parameters None must raise.""" self.assertRaises( QiskitMachineLearningError, derive_num_qubits_feature_map_ansatz, None, None, None ) - def test_incompatible_feature_map_ansatz(self): - """Test when feature map and ansatz are incompatible.""" + def test_mismatch_feature_map_ansatz(self): + """feature_map and ansatz with different qubit counts must raise.""" self.assertRaises( QiskitMachineLearningError, derive_num_qubits_feature_map_ansatz, @@ -50,6 +50,26 @@ def test_incompatible_feature_map_ansatz(self): self.properties["ra2"], ) + def test_mismatch_num_qubits_feature_map(self): + """num_qubits != feature_map.num_qubits must raise.""" + self.assertRaises( + QiskitMachineLearningError, + derive_num_qubits_feature_map_ansatz, + 2, + self.properties["z1"], + None, + ) + + def test_mismatch_num_qubits_ansatz(self): + """num_qubits != ansatz.num_qubits must raise.""" + self.assertRaises( + QiskitMachineLearningError, + derive_num_qubits_feature_map_ansatz, + 2, + None, + self.properties["ra1"], + ) + @idata( itertools.chain( itertools.product([1], [None, "z1"], [None, "ra1"]), @@ -58,45 +78,28 @@ def test_incompatible_feature_map_ansatz(self): ) @unpack def test_num_qubits_is_set(self, num_qubits, feature_map, ansatz): - """Test when the number of qubits is set.""" + """When num_qubits is set and inputs agree.""" feature_map = self.properties.get(feature_map) ansatz = self.properties.get(ansatz) - # derived objects - num_qubits_der, feature_map_der, ansatz_der = derive_num_qubits_feature_map_ansatz( - num_qubits, feature_map, ansatz - ) - self.assertEqual(num_qubits_der, num_qubits) - - self._test_feature_map(feature_map_der, feature_map, num_qubits) - self._test_ansatz(ansatz_der, num_qubits) + nq, fm, anz = derive_num_qubits_feature_map_ansatz(num_qubits, feature_map, ansatz) + self.assertEqual(nq, num_qubits) + self.assertEqual(fm.num_qubits, num_qubits) + self.assertEqual(anz.num_qubits, num_qubits) + self.assertIsInstance(fm, QuantumCircuit) + self.assertIsInstance(anz, QuantumCircuit) @idata([(None, "ra1"), (None, "ra2"), ("z1", None), ("z1", "ra1"), ("z2", None), ("z2", "ra2")]) @unpack def test_num_qubits_isnot_set(self, feature_map, ansatz): - """Test when the number of qubits is not set.""" - ansatz = self.properties.get(ansatz) + """When num_qubits is None and inputs determine the count.""" feature_map = self.properties.get(feature_map) + ansatz = self.properties.get(ansatz) + expected = feature_map.num_qubits if feature_map is not None else ansatz.num_qubits - num_qubits_der, feature_map_der, ansatz_der = derive_num_qubits_feature_map_ansatz( - None, feature_map, ansatz - ) - num_qubits = feature_map.num_qubits if feature_map is not None else ansatz.num_qubits - - self.assertEqual(num_qubits_der, num_qubits) - self._test_feature_map(feature_map_der, feature_map, num_qubits) - self._test_ansatz(ansatz_der, num_qubits) - - def _test_feature_map(self, feature_map_der, feature_map_org, num_qubits_expected): - self.assertIsNotNone(feature_map_der) - self.assertEqual(feature_map_der.num_qubits, num_qubits_expected) - - if feature_map_org is None and num_qubits_expected == 1: - self.assertIsInstance(feature_map_der, QuantumCircuit) - if feature_map_org is None and num_qubits_expected == 2: - self.assertIsInstance(feature_map_der, QuantumCircuit) - - def _test_ansatz(self, ansatz_der, num_qubits_expected): - self.assertIsNotNone(ansatz_der) - self.assertEqual(ansatz_der.num_qubits, num_qubits_expected) - self.assertIsInstance(ansatz_der, QuantumCircuit) + nq, fm, anz = derive_num_qubits_feature_map_ansatz(None, feature_map, ansatz) + self.assertEqual(nq, expected) + self.assertEqual(fm.num_qubits, expected) + self.assertEqual(anz.num_qubits, expected) + self.assertIsInstance(fm, QuantumCircuit) + self.assertIsInstance(anz, QuantumCircuit)