From 38d1ef8293410c426466ec5b7e5f5480c3bc7bc8 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Fri, 15 Jul 2022 14:42:27 +0200 Subject: [PATCH 01/82] Fix pairwise entanglement (#8346) --- qiskit/circuit/library/n_local/n_local.py | 4 ++-- .../fix-zzmap-pairwise-5653395849fec454.yaml | 24 +++++++++++++++++++ .../circuit/library/test_pauli_feature_map.py | 8 +++++++ 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/fix-zzmap-pairwise-5653395849fec454.yaml diff --git a/qiskit/circuit/library/n_local/n_local.py b/qiskit/circuit/library/n_local/n_local.py index 64217fa0221c..9c5cdfddd463 100644 --- a/qiskit/circuit/library/n_local/n_local.py +++ b/qiskit/circuit/library/n_local/n_local.py @@ -1006,8 +1006,8 @@ def get_entangler_map( "qubits in the circuit." ) - if entanglement == "pairwise" and num_block_qubits != 2: - raise ValueError("Pairwise entanglement is only defined for blocks of 2 qubits.") + if entanglement == "pairwise" and num_block_qubits > 2: + raise ValueError("Pairwise entanglement is not defined for blocks with more than 2 qubits.") if entanglement == "full": return list(combinations(list(range(n)), m)) diff --git a/releasenotes/notes/fix-zzmap-pairwise-5653395849fec454.yaml b/releasenotes/notes/fix-zzmap-pairwise-5653395849fec454.yaml new file mode 100644 index 000000000000..e5c600dd70e0 --- /dev/null +++ b/releasenotes/notes/fix-zzmap-pairwise-5653395849fec454.yaml @@ -0,0 +1,24 @@ +--- +fixes: + - | + Fix the pairwise entanglement structure for :class:`~.NLocal` circuits. + This led to a bug in the :class:`~.ZZFeatureMap`, where using `entanglement="pairwise"` + raised an error. Now it correctly produces the desired feature map:: + + from qiskit.circuit.library import ZZFeatureMap + encoding = ZZFeatureMap(4, entanglement="pairwise", reps=1) + print(encoding.decompose().draw()) + + The above prints:: + + ┌───┐┌─────────────┐ + q_0: ┤ H ├┤ P(2.0*x[0]) ├──■────────────────────────────────────■──────────────────────────────────────────── + ├───┤├─────────────┤┌─┴─┐┌──────────────────────────────┐┌─┴─┐ + q_1: ┤ H ├┤ P(2.0*x[1]) ├┤ X ├┤ P(2.0*(π - x[0])*(π - x[1])) ├┤ X ├──■────────────────────────────────────■── + ├───┤├─────────────┤└───┘└──────────────────────────────┘└───┘┌─┴─┐┌──────────────────────────────┐┌─┴─┐ + q_2: ┤ H ├┤ P(2.0*x[2]) ├──■────────────────────────────────────■──┤ X ├┤ P(2.0*(π - x[1])*(π - x[2])) ├┤ X ├ + ├───┤├─────────────┤┌─┴─┐┌──────────────────────────────┐┌─┴─┐└───┘└──────────────────────────────┘└───┘ + q_3: ┤ H ├┤ P(2.0*x[3]) ├┤ X ├┤ P(2.0*(π - x[2])*(π - x[3])) ├┤ X ├────────────────────────────────────────── + └───┘└─────────────┘└───┘└──────────────────────────────┘└───┘ + + diff --git a/test/python/circuit/library/test_pauli_feature_map.py b/test/python/circuit/library/test_pauli_feature_map.py index ec006fadf2c8..bc2754c1a075 100644 --- a/test/python/circuit/library/test_pauli_feature_map.py +++ b/test/python/circuit/library/test_pauli_feature_map.py @@ -152,6 +152,14 @@ def zz_evolution(circuit, qubit1, qubit2): self.assertTrue(Operator(encoding).equiv(ref)) + def test_zz_pairwise_entanglement(self): + """Test the ZZ feature map works with pairwise entanglement.""" + num_qubits = 5 + encoding = ZZFeatureMap(num_qubits, entanglement="pairwise", reps=1) + ops = encoding.decompose().count_ops() + expected_ops = {"h": num_qubits, "p": 2 * num_qubits - 1, "cx": 2 * (num_qubits - 1)} + self.assertEqual(ops, expected_ops) + def test_pauli_alpha(self): """Test Pauli rotation factor (getter, setter).""" encoding = PauliFeatureMap() From 40668f04b5f9777d787b1a9c6598aea68a0fa276 Mon Sep 17 00:00:00 2001 From: "Daniel J. Egger" <38065505+eggerdj@users.noreply.github.com> Date: Fri, 15 Jul 2022 21:21:56 +0200 Subject: [PATCH 02/82] Tensored fitter (#8345) * This PR adds a method subset_fitter to the TensoredMeasFitter in Terra. The subset_fitter provides a new fitter by reordering the calibration matrices in the internal list of the fitter. --- qiskit/utils/mitigation/fitters.py | 51 ++++++++++- qiskit/utils/quantum_instance.py | 4 + ...nsored-subset-fitter-bd28e6e6ec5bdaae.yaml | 9 ++ .../test_measure_error_mitigation.py | 87 +++++++++++++++++-- 4 files changed, 143 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/tensored-subset-fitter-bd28e6e6ec5bdaae.yaml diff --git a/qiskit/utils/mitigation/fitters.py b/qiskit/utils/mitigation/fitters.py index 2839f9771bcc..cc9e604126f4 100644 --- a/qiskit/utils/mitigation/fitters.py +++ b/qiskit/utils/mitigation/fitters.py @@ -116,7 +116,7 @@ def add_data(self, new_results, rebuild_cal_matrix=True): self._tens_fitt.add_data(new_results, rebuild_cal_matrix) - def subset_fitter(self, qubit_sublist=None): + def subset_fitter(self, qubit_sublist): """ Return a fitter object that is a subset of the qubits in the original list. @@ -431,3 +431,52 @@ def _build_calibration_matrices(self): out=np.zeros_like(self._cal_matrices[mat_index]), where=sums_of_columns != 0, ) + + def subset_fitter(self, qubit_sublist): + """Return a fitter object that is a subset of the qubits in the original list. + + This is only a partial implementation of the ``subset_fitter`` method since only + mitigation patterns of length 1 are supported. This corresponds to patterns of the + form ``[[0], [1], [2], ...]``. Note however, that such patterns are a good first + approximation to mitigate readout errors on large quantum circuits. + + Args: + qubit_sublist (list): must be a subset of qubit_list + + Returns: + TensoredMeasFitter: A new fitter that has the calibration for a + subset of qubits + + Raises: + QiskitError: If the calibration matrix is not initialized + QiskitError: If the mit pattern is not a tensor of single-qubit + measurement error mitigation. + QiskitError: If a qubit in the given ``qubit_sublist`` is not in the list of + qubits in the mit. pattern. + """ + if self._cal_matrices is None: + raise QiskitError("Calibration matrices are not initialized.") + + if qubit_sublist is None: + raise QiskitError("Qubit sublist must be specified.") + + if not all(len(tensor) == 1 for tensor in self._mit_pattern): + raise QiskitError( + f"Each element in the mit pattern should have length 1. Found {self._mit_pattern}." + ) + + supported_qubits = set(tensor[0] for tensor in self._mit_pattern) + for qubit in qubit_sublist: + if qubit not in supported_qubits: + raise QiskitError(f"Qubit {qubit} is not in the mit pattern {self._mit_pattern}.") + + new_mit_pattern = [[idx] for idx in qubit_sublist] + new_substate_labels_list = [self._substate_labels_list[idx] for idx in qubit_sublist] + + new_fitter = TensoredMeasFitter( + results=None, mit_pattern=new_mit_pattern, substate_labels_list=new_substate_labels_list + ) + + new_fitter.cal_matrices = [self._cal_matrices[idx] for idx in qubit_sublist] + + return new_fitter diff --git a/qiskit/utils/quantum_instance.py b/qiskit/utils/quantum_instance.py index 5982ac0ec5c8..3e77285620c9 100644 --- a/qiskit/utils/quantum_instance.py +++ b/qiskit/utils/quantum_instance.py @@ -682,6 +682,10 @@ def _find_save_state(data): tmp_result.results = [result.results[i] for i in c_idx] if curr_qubit_index == qubit_index: tmp_fitter = meas_error_mitigation_fitter + elif isinstance(meas_error_mitigation_fitter, TensoredMeasFitter): + # Different from the complete meas. fitter as only the Terra fitter + # implements the ``subset_fitter`` method. + tmp_fitter = meas_error_mitigation_fitter.subset_fitter(curr_qubit_index) elif _MeasFitterType.COMPLETE_MEAS_FITTER == _MeasFitterType.type_from_instance( meas_error_mitigation_fitter ): diff --git a/releasenotes/notes/tensored-subset-fitter-bd28e6e6ec5bdaae.yaml b/releasenotes/notes/tensored-subset-fitter-bd28e6e6ec5bdaae.yaml new file mode 100644 index 000000000000..6061ff04b3d0 --- /dev/null +++ b/releasenotes/notes/tensored-subset-fitter-bd28e6e6ec5bdaae.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + The ``subset_fitter`` method is added to the :class:`.TensoredMeasFitter` + class. The implementation is restricted to mitigation patterns in which each + qubit is mitigated individually, e.g. ``[[0], [1], [2]]``. This is, however, + the most widely used case. It allows the :class:`.TensoredMeasFitter` to + be used in cases where the numberical order of the physical qubits does not + match the index of the classical bit. diff --git a/test/python/algorithms/test_measure_error_mitigation.py b/test/python/algorithms/test_measure_error_mitigation.py index 33aad9f8f2ff..ba9a0d0168c3 100644 --- a/test/python/algorithms/test_measure_error_mitigation.py +++ b/test/python/algorithms/test_measure_error_mitigation.py @@ -15,10 +15,10 @@ import unittest from test.python.algorithms import QiskitAlgorithmsTestCase -from ddt import ddt, data +from ddt import ddt, data, unpack import numpy as np import retworkx as rx -from qiskit import QuantumCircuit +from qiskit import QuantumCircuit, execute from qiskit.quantum_info import Pauli from qiskit.exceptions import QiskitError from qiskit.utils import QuantumInstance, algorithm_globals @@ -27,6 +27,7 @@ from qiskit.algorithms.optimizers import SPSA, COBYLA from qiskit.circuit.library import EfficientSU2 from qiskit.utils.mitigation import CompleteMeasFitter, TensoredMeasFitter +from qiskit.utils.measurement_error_mitigation import build_measurement_error_mitigation_circuits from qiskit.utils import optionals if optionals.HAS_AER: @@ -46,8 +47,19 @@ class TestMeasurementErrorMitigation(QiskitAlgorithmsTestCase): """Test measurement error mitigation.""" @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required for this test") - @data("CompleteMeasFitter", "TensoredMeasFitter") - def test_measurement_error_mitigation_with_diff_qubit_order(self, fitter_str): + @data( + ("CompleteMeasFitter", None, False), + ("TensoredMeasFitter", None, False), + ("TensoredMeasFitter", [[0, 1]], True), + ("TensoredMeasFitter", [[1], [0]], False), + ) + @unpack + def test_measurement_error_mitigation_with_diff_qubit_order( + self, + fitter_str, + mit_pattern, + fails, + ): """measurement error mitigation with different qubit order""" algorithm_globals.random_seed = 0 @@ -68,6 +80,7 @@ def test_measurement_error_mitigation_with_diff_qubit_order(self, fitter_str): noise_model=noise_model, measurement_error_mitigation_cls=fitter_cls, cals_matrix_refresh_period=0, + mit_pattern=mit_pattern, ) # circuit qc1 = QuantumCircuit(2, 2) @@ -81,15 +94,14 @@ def test_measurement_error_mitigation_with_diff_qubit_order(self, fitter_str): qc2.measure(1, 0) qc2.measure(0, 1) - if fitter_cls == TensoredMeasFitter: + if fails: self.assertRaisesRegex( QiskitError, - "TensoredMeasFitter doesn't support subset_fitter.", + "Each element in the mit pattern should have length 1.", quantum_instance.execute, [qc1, qc2], ) else: - # this should run smoothly quantum_instance.execute([qc1, qc2]) self.assertGreater(quantum_instance.time_taken, 0.0) @@ -387,6 +399,67 @@ def test_circuit_modified(self): _ = qi.execute(circuits_input, had_transpiled=True) self.assertEqual(circuits_ref, circuits_input, msg="Transpiled circuit array modified.") + @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required for this test") + def test_tensor_subset_fitter(self): + """Test the subset fitter method of the tensor fitter.""" + + # Construct a noise model where readout has errors of different strengths. + noise_model = noise.NoiseModel() + # big error + read_err0 = noise.errors.readout_error.ReadoutError([[0.90, 0.10], [0.25, 0.75]]) + # ideal + read_err1 = noise.errors.readout_error.ReadoutError([[1.00, 0.00], [0.00, 1.00]]) + # small error + read_err2 = noise.errors.readout_error.ReadoutError([[0.98, 0.02], [0.03, 0.97]]) + noise_model.add_readout_error(read_err0, (0,)) + noise_model.add_readout_error(read_err1, (1,)) + noise_model.add_readout_error(read_err2, (2,)) + + mit_pattern = [[idx] for idx in range(3)] + backend = Aer.get_backend("aer_simulator") + backend.set_options(seed_simulator=123) + mit_circuits = build_measurement_error_mitigation_circuits( + [0, 1, 2], + TensoredMeasFitter, + backend, + backend_config={}, + compile_config={}, + mit_pattern=mit_pattern, + ) + result = execute(mit_circuits[0], backend, noise_model=noise_model).result() + fitter = TensoredMeasFitter(result, mit_pattern=mit_pattern) + cal_matrices = fitter.cal_matrices + + # Check that permutations and permuted subsets match. + for subset in [[1, 0], [1, 2], [0, 2], [2, 0, 1]]: + with self.subTest(subset=subset): + new_fitter = fitter.subset_fitter(subset) + for idx, qubit in enumerate(subset): + self.assertTrue(np.allclose(new_fitter.cal_matrices[idx], cal_matrices[qubit])) + + self.assertRaisesRegex( + QiskitError, + "Qubit 3 is not in the mit pattern", + fitter.subset_fitter, + [0, 2, 3], + ) + + # Test that we properly correct a circuit with permuted measurements. + circuit = QuantumCircuit(3, 3) + circuit.x(range(3)) + circuit.measure(1, 0) + circuit.measure(2, 1) + circuit.measure(0, 2) + + result = execute( + circuit, backend, noise_model=noise_model, shots=1000, seed_simulator=0 + ).result() + new_result = fitter.subset_fitter([1, 2, 0]).filter.apply(result) + + # The noisy result should have a poor 111 state, the mit. result should be good. + self.assertTrue(result.get_counts()["111"] < 800) + self.assertTrue(new_result.get_counts()["111"] > 990) + if __name__ == "__main__": unittest.main() From e4e4646f18d6e7d5dae0bc5308bcb5f5c9aa206d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Jul 2022 13:31:33 +0000 Subject: [PATCH 03/82] Bump hashbrown from 0.12.2 to 0.12.3 (#8361) Bumps [hashbrown](https://github.com/rust-lang/hashbrown) from 0.12.2 to 0.12.3. - [Release notes](https://github.com/rust-lang/hashbrown/releases) - [Changelog](https://github.com/rust-lang/hashbrown/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/hashbrown/compare/v0.12.2...v0.12.3) --- updated-dependencies: - dependency-name: hashbrown dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 532823a6f9f3..2e99a07e2dea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -95,9 +95,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "607c8a29735385251a339424dd462993c0fed8fa09d378f259377df08c126022" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ "ahash", "rayon", diff --git a/Cargo.toml b/Cargo.toml index d716af3014a4..d54b91f9d511 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ version = "^0.15.0" features = ["rayon"] [dependencies.hashbrown] -version = "0.12.2" +version = "0.12.3" features = ["rayon"] [profile.release] From 007a74610d9e19c47b1e701479fdd86fa2fb8f1f Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 19 Jul 2022 03:15:54 -0400 Subject: [PATCH 04/82] Fix lint failure in qiskit/extensions/unitary.py (#8366) Recently the pylint CI runs have all started failing because of an unused OrderedDict import failure in qiskit/extensions/unitary.py. The OrderedDict usage was removed in #8234 but the import was left in accidently. This should have been caught by the lint job on that PR but for some reason it was not caught and has sat there until relatively recently when pylint started erroring because of the error. Normally for failures like this they can be attributed to environment differences, typically a release of pylint or astroid. However, we pin those package versions because of their tendancy to change behavior, and also a diff between the installed python packages in CI doesn't show any differences. Regardless of the underlying cause to unblock CI this commit is necessary to fix the unused import error. --- qiskit/extensions/unitary.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit/extensions/unitary.py b/qiskit/extensions/unitary.py index 5d9364805ded..a1bd2d2551aa 100644 --- a/qiskit/extensions/unitary.py +++ b/qiskit/extensions/unitary.py @@ -14,7 +14,6 @@ Arbitrary unitary circuit instruction. """ -from collections import OrderedDict import numpy from qiskit.circuit import Gate, ControlledGate From 6dd0d69ec9bea85c694d31c2b10f16da23fcffbf Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 19 Jul 2022 11:34:38 -0400 Subject: [PATCH 05/82] Reimplement SabreSwap heuristic scoring in Rust (#7977) * Reimplement SabreSwap heuristic scoring in multithreaded Rust This commit re-implements the core heuristic scoring of swap candidates in the SabreSwap pass as a multithread Rust routine. The heuristic scoring in sabre previously looped over all potential swap candidates serially in Python and applied a computed a heuristic score on which to candidate to pick. This can easily be done in parallel as there is no data dependency between scoring the different candidates. By performing this in Rust not only is the scoring operation done more quickly for each candidate but we can also leverage multithreading to do this efficiently in parallel. * Make sabre_swap a separate Rust module This commit moves the sabre specific code into a separate rust module. We already were using a separate Python module for the sabre code this just mirrors that in the rust code for better organization. * Fix lint * Remove unnecessary parallel iteration This commit removes an unecessary parallel iterator over the swap scores to find the minimum and just does it serially. The threading overhead for the parallel iterator is unecessary as it is fairly quick. * Revert change to DECAY_RESET_INTERVAL behavior * Avoid Bit._index * Add __str__ definition for DEBUG logs * Cleanup greedy swap path * Preserve insertion order in SwapScores The use of an inner hashmap meant the swap candidates were being evaluated in a different order based on the hash seeding instead of the order generated from the python side. This commit fixes by switching the internal type to an IndexMap which for a little overhead preserves the insertion order on iteration. * Work with virtual indices win obtain swap * Simplify decay reset() method * Fix lint * Fix typo * Rename nlayout methods * Update docstrings for SwapScores type * Use correct swap method for _undo_operations() * Fix rebase error * Revert test change * Reverse if condition in lookahead cost * Fix missing len division on lookahead cost * Remove unused EXTENDED_SET_WEIGHT python global * Switch to serial iterator for heuristic scoring While the heuristic scoring can be done in parallel as there is no data dependency between computing the score for candidates the overhead of dealing with multithreading eliminates and benefits from parallel execution. This is because the relative computation is fairly quick and the number of candidates is never very large (since coupling maps are typically sparsely connected). This commit switches to a serial iterator which will speed up execution in practice over running the iteration in parallel. * Return a 2d numpy array for best swaps and avoid conversion cost * Migrate obtain_swaps to rust This commit simplifies the rust loop by avoiding the need to have a mutable shared swap scores between rust and python. Instead the obtain swaps function to get the swap candidates for each layer is migrated to rust using a new neighbors table which is computed once per sabre class. This moves the iteration from obtain swaps to rust and eliminates it as a bottleneck. * Remove unused SwapScores class * Fix module metadata path * Add release note * Add rust docstrings * Pre-allocate candidate_swaps * Double swap instead of clone * Remove unnecessary list comprehensions * Move random choice into rust After rewriting the heuristic scoring in rust the biggest bottleneck in the function (outside of computing the extended set and applying gates to the dag) was performing the random choice between the best candidates via numpy. This wasn't necessary since we can just do the random choice in rust and have it return the best candidate. This commit adds a new class to represent a shared rng that is reused on each scoring call and changes sabre_score_heuristic to return the best swap. The tradeoff with this PR is that it changes the seeding so when compared to previous versions of SabreSwap different results will be returned with the same seed value. * Use int32 for max default rng seed for windows compat * Fix bounds check on custom sequence type's __getitem__ Co-authored-by: Kevin Hartman * Only run parallel sort if not in a parallel context This commit updates the sort step in the sabre algorithm to only run a parallel sort if we're not already in a parallel context. This is to prevent a potential over dispatch of work if we're trying to use multiple threads from multiple processes. At the same time the sort algorithm used is switched to the unstable variant because a stable sort isn't necessary for this application and an unstable sort has less overhead. Co-authored-by: Kevin Hartman --- Cargo.lock | 21 +- Cargo.toml | 5 +- qiskit/__init__.py | 1 + .../transpiler/passes/routing/sabre_swap.py | 206 +++++++++--------- .../notes/rabre-rwap-ae51631bec7450df.yaml | 17 ++ src/lib.rs | 2 + src/nlayout.rs | 26 +++ src/sabre_swap/edge_list.rs | 101 +++++++++ src/sabre_swap/mod.rs | 206 ++++++++++++++++++ src/sabre_swap/neighbor_table.rs | 73 +++++++ src/sabre_swap/qubits_decay.rs | 85 ++++++++ src/sabre_swap/sabre_rng.rs | 35 +++ test/python/transpiler/test_mappers.py | 2 +- .../transpiler/test_preset_passmanagers.py | 30 +-- test/python/transpiler/test_sabre_layout.py | 18 +- 15 files changed, 683 insertions(+), 145 deletions(-) create mode 100644 releasenotes/notes/rabre-rwap-ae51631bec7450df.yaml create mode 100644 src/sabre_swap/edge_list.rs create mode 100644 src/sabre_swap/mod.rs create mode 100644 src/sabre_swap/neighbor_table.rs create mode 100644 src/sabre_swap/qubits_decay.rs create mode 100644 src/sabre_swap/sabre_rng.rs diff --git a/Cargo.lock b/Cargo.lock index 2e99a07e2dea..617fe9878121 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,9 +68,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ff1f980957787286a554052d03c7aee98d99cc32e09f6d45f0a814133c87978" +checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83" dependencies = [ "cfg-if", "once_cell", @@ -78,9 +78,9 @@ dependencies = [ [[package]] name = "either" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" [[package]] name = "getrandom" @@ -120,6 +120,7 @@ checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", "hashbrown", + "rayon", ] [[package]] @@ -247,9 +248,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" +checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" [[package]] name = "parking_lot" @@ -474,9 +475,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "smallvec" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" [[package]] name = "syn" @@ -497,9 +498,9 @@ checksum = "c02424087780c9b71cc96799eaeddff35af2bc513278cda5c99fc1f5d026d3c1" [[package]] name = "unicode-ident" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" +checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" [[package]] name = "unindent" diff --git a/Cargo.toml b/Cargo.toml index d54b91f9d511..ab3fff6df1c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,6 @@ numpy = "0.16.2" rand = "0.8" rand_pcg = "0.3" rand_distr = "0.4.3" -indexmap = "1.9.1" ahash = "0.7.6" num-complex = "0.4" num-bigint = "0.4" @@ -32,6 +31,10 @@ features = ["rayon"] version = "0.12.3" features = ["rayon"] +[dependencies.indexmap] +version = "1.9.1" +features = ["rayon"] + [profile.release] lto = 'fat' codegen-units = 1 diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 88be51b78213..aed1be6694c9 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -25,6 +25,7 @@ # manually define them on import so people can directly import # qiskit._accelerate.* submodules and not have to rely on attribute access sys.modules["qiskit._accelerate.stochastic_swap"] = qiskit._accelerate.stochastic_swap +sys.modules["qiskit._accelerate.sabre_swap"] = qiskit._accelerate.sabre_swap sys.modules["qiskit._accelerate.pauli_expval"] = qiskit._accelerate.pauli_expval sys.modules["qiskit._accelerate.dense_layout"] = qiskit._accelerate.dense_layout sys.modules["qiskit._accelerate.sparse_pauli_op"] = qiskit._accelerate.sparse_pauli_op diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py index c3929431c59b..3f83d83b04dd 100644 --- a/qiskit/transpiler/passes/routing/sabre_swap.py +++ b/qiskit/transpiler/passes/routing/sabre_swap.py @@ -25,10 +25,20 @@ from qiskit.transpiler.layout import Layout from qiskit.dagcircuit import DAGOpNode +# pylint: disable=import-error +from qiskit._accelerate.sabre_swap import ( + sabre_score_heuristic, + Heuristic, + EdgeList, + QubitsDecay, + NeighborTable, + SabreRng, +) +from qiskit._accelerate.stochastic_swap import NLayout # pylint: disable=import-error + logger = logging.getLogger(__name__) EXTENDED_SET_SIZE = 20 # Size of lookahead window. TODO: set dynamically to len(current_layout) -EXTENDED_SET_WEIGHT = 0.5 # Weight of lookahead window compared to front_layer. DECAY_RATE = 0.001 # Decay coefficient for penalizing serial swaps. DECAY_RESET_INTERVAL = 5 # How often to reset all decay rates to 1. @@ -83,6 +93,9 @@ def __init__( fake_run (bool): if true, it only pretend to do routing, i.e., no swap is effectively added. + Raises: + TranspilerError: If the specified heuristic is not valid. + Additional Information: The search space of possible SWAPs on physical qubits is explored @@ -135,9 +148,24 @@ def __init__( else: self.coupling_map = deepcopy(coupling_map) self.coupling_map.make_symmetric() + self._neighbor_table = None + if coupling_map is not None: + self._neighbor_table = NeighborTable(retworkx.adjacency_matrix(self.coupling_map.graph)) - self.heuristic = heuristic - self.seed = seed + if heuristic == "basic": + self.heuristic = Heuristic.Basic + elif heuristic == "lookahead": + self.heuristic = Heuristic.Lookahead + elif heuristic == "decay": + self.heuristic = Heuristic.Decay + else: + raise TranspilerError("Heuristic %s not recognized." % heuristic) + + if seed is None: + ii32 = np.iinfo(np.int32) + self.seed = np.random.default_rng(None).integers(0, ii32.max, dtype=int) + else: + self.seed = seed self.fake_run = fake_run self.required_predecessors = None self.qubits_decay = None @@ -171,7 +199,7 @@ def run(self, dag): self.dist_matrix = self.coupling_map.distance_matrix - rng = np.random.default_rng(self.seed) + rng = SabreRng(self.seed) # Preserve input DAG's name, regs, wire_map, etc. but replace the graph. mapped_dag = None @@ -180,12 +208,15 @@ def run(self, dag): canonical_register = dag.qregs["q"] current_layout = Layout.generate_trivial_layout(canonical_register) - self._bit_indices = {bit: idx for idx, bit in enumerate(canonical_register)} + layout_mapping = { + self._bit_indices[k]: v for k, v in current_layout.get_virtual_bits().items() + } + layout = NLayout(layout_mapping, len(dag.qubits), self.coupling_map.size()) # A decay factor for each qubit used to heuristically penalize recently # used qubits (to encourage parallelism). - self.qubits_decay = dict.fromkeys(dag.qubits, 1) + self.qubits_decay = QubitsDecay(len(dag.qubits)) # Start algorithm from the front layer and iterate until all gates done. self.required_predecessors = self._build_required_predecessors(dag) @@ -199,11 +230,10 @@ def run(self, dag): new_front_layer = [] for node in front_layer: if len(node.qargs) == 2: - v0, v1 = node.qargs - # Accessing layout._v2p directly to avoid overhead from __getitem__ and a - # single access isn't feasible because the layout is updated on each iteration + v0 = self._bit_indices[node.qargs[0]] + v1 = self._bit_indices[node.qargs[1]] if self.coupling_map.graph.has_edge( - current_layout._v2p[v0], current_layout._v2p[v1] + layout.logical_to_physical(v0), layout.logical_to_physical(v1) ): execute_gate_list.append(node) else: @@ -217,20 +247,20 @@ def run(self, dag): # the gate with the smallest distance between its arguments. This is a release # valve for the algorithm to avoid infinite loops only, and should generally not # come into play for most circuits. - self._undo_operations(ops_since_progress, mapped_dag, current_layout) - self._add_greedy_swaps(front_layer, mapped_dag, current_layout, canonical_register) + self._undo_operations(ops_since_progress, mapped_dag, layout) + self._add_greedy_swaps(front_layer, mapped_dag, layout, canonical_register) continue if execute_gate_list: for node in execute_gate_list: - self._apply_gate(mapped_dag, node, current_layout, canonical_register) + self._apply_gate(mapped_dag, node, layout, canonical_register) for successor in self._successors(node, dag): self.required_predecessors[successor] -= 1 if self._is_resolved(successor): front_layer.append(successor) if node.qargs: - self._reset_qubits_decay() + self.qubits_decay.reset() # Diagnostics if do_expensive_logging: @@ -256,32 +286,43 @@ def run(self, dag): # After all free gates are exhausted, heuristically find # the best swap and insert it. When two or more swaps tie # for best score, pick one randomly. + if extended_set is None: extended_set = self._obtain_extended_set(dag, front_layer) - swap_scores = {} - for swap_qubits in self._obtain_swaps(front_layer, current_layout): - trial_layout = current_layout.copy() - trial_layout.swap(*swap_qubits) - score = self._score_heuristic( - self.heuristic, front_layer, extended_set, trial_layout, swap_qubits + extended_set_list = EdgeList(len(extended_set)) + for x in extended_set: + extended_set_list.append( + self._bit_indices[x.qargs[0]], self._bit_indices[x.qargs[1]] + ) + + front_layer_list = EdgeList(len(front_layer)) + for x in front_layer: + front_layer_list.append( + self._bit_indices[x.qargs[0]], self._bit_indices[x.qargs[1]] ) - swap_scores[swap_qubits] = score - min_score = min(swap_scores.values()) - best_swaps = [k for k, v in swap_scores.items() if v == min_score] - best_swaps.sort(key=lambda x: (self._bit_indices[x[0]], self._bit_indices[x[1]])) - best_swap = rng.choice(best_swaps) + best_swap = sabre_score_heuristic( + front_layer_list, + layout, + self._neighbor_table, + extended_set_list, + self.dist_matrix, + self.qubits_decay, + self.heuristic, + rng, + ) + best_swap_qargs = [canonical_register[best_swap[0]], canonical_register[best_swap[1]]] swap_node = self._apply_gate( mapped_dag, - DAGOpNode(op=SwapGate(), qargs=best_swap), - current_layout, + DAGOpNode(op=SwapGate(), qargs=best_swap_qargs), + layout, canonical_register, ) - current_layout.swap(*best_swap) + layout.swap_logical(*best_swap) ops_since_progress.append(swap_node) num_search_steps += 1 if num_search_steps % DECAY_RESET_INTERVAL == 0: - self._reset_qubits_decay() + self.qubits_decay.reset() else: self.qubits_decay[best_swap[0]] += DECAY_RATE self.qubits_decay[best_swap[1]] += DECAY_RATE @@ -290,27 +331,21 @@ def run(self, dag): if do_expensive_logging: logger.debug("SWAP Selection...") logger.debug("extended_set: %s", [(n.name, n.qargs) for n in extended_set]) - logger.debug("swap scores: %s", swap_scores) logger.debug("best swap: %s", best_swap) logger.debug("qubits decay: %s", self.qubits_decay) - - self.property_set["final_layout"] = current_layout + layout_mapping = layout.layout_mapping() + output_layout = Layout({dag.qubits[k]: v for (k, v) in layout_mapping}) + self.property_set["final_layout"] = output_layout if not self.fake_run: return mapped_dag return dag def _apply_gate(self, mapped_dag, node, current_layout, canonical_register): - new_node = _transform_gate_for_layout(node, current_layout, canonical_register) + new_node = self._transform_gate_for_layout(node, current_layout, canonical_register) if self.fake_run: return new_node return mapped_dag.apply_operation_back(new_node.op, new_node.qargs, new_node.cargs) - def _reset_qubits_decay(self): - """Reset all qubit decay factors to 1 upon request (to forget about - past penalizations). - """ - self.qubits_decay = {k: 1 for k in self.qubits_decay.keys()} - def _build_required_predecessors(self, dag): out = defaultdict(int) # We don't need to count in- or out-wires: outs can never be predecessors, and all input @@ -360,97 +395,50 @@ def _obtain_extended_set(self, dag, front_layer): self.required_predecessors[node] += 1 return extended_set - def _obtain_swaps(self, front_layer, current_layout): - """Return a set of candidate swaps that affect qubits in front_layer. - - For each virtual qubit in front_layer, find its current location - on hardware and the physical qubits in that neighborhood. Every SWAP - on virtual qubits that corresponds to one of those physical couplings - is a candidate SWAP. - - Candidate swaps are sorted so SWAP(i,j) and SWAP(j,i) are not duplicated. - """ - candidate_swaps = set() - for node in front_layer: - for virtual in node.qargs: - physical = current_layout[virtual] - for neighbor in self.coupling_map.neighbors(physical): - virtual_neighbor = current_layout[neighbor] - swap = sorted([virtual, virtual_neighbor], key=lambda q: self._bit_indices[q]) - candidate_swaps.add(tuple(swap)) - return candidate_swaps - def _add_greedy_swaps(self, front_layer, dag, layout, qubits): """Mutate ``dag`` and ``layout`` by applying greedy swaps to ensure that at least one gate can be routed.""" - layout_map = layout._v2p target_node = min( front_layer, - key=lambda node: self.dist_matrix[layout_map[node.qargs[0]], layout_map[node.qargs[1]]], + key=lambda node: self.dist_matrix[ + layout.logical_to_physical(self._bit_indices[node.qargs[0]]), + layout.logical_to_physical(self._bit_indices[node.qargs[1]]), + ], ) - for pair in _shortest_swap_path(tuple(target_node.qargs), self.coupling_map, layout): + for pair in _shortest_swap_path( + tuple(target_node.qargs), self.coupling_map, layout, qubits + ): self._apply_gate(dag, DAGOpNode(op=SwapGate(), qargs=pair), layout, qubits) - layout.swap(*pair) - - def _compute_cost(self, layer, layout): - cost = 0 - layout_map = layout._v2p - for node in layer: - cost += self.dist_matrix[layout_map[node.qargs[0]], layout_map[node.qargs[1]]] - return cost - - def _score_heuristic(self, heuristic, front_layer, extended_set, layout, swap_qubits=None): - """Return a heuristic score for a trial layout. - - Assuming a trial layout has resulted from a SWAP, we now assign a cost - to it. The goodness of a layout is evaluated based on how viable it makes - the remaining virtual gates that must be applied. - """ - first_cost = self._compute_cost(front_layer, layout) - if heuristic == "basic": - return first_cost - - first_cost /= len(front_layer) - second_cost = 0 - if extended_set: - second_cost = self._compute_cost(extended_set, layout) / len(extended_set) - total_cost = first_cost + EXTENDED_SET_WEIGHT * second_cost - if heuristic == "lookahead": - return total_cost - - if heuristic == "decay": - return ( - max(self.qubits_decay[swap_qubits[0]], self.qubits_decay[swap_qubits[1]]) - * total_cost - ) - - raise TranspilerError("Heuristic %s not recognized." % heuristic) + layout.swap_logical(*[self._bit_indices[x] for x in pair]) def _undo_operations(self, operations, dag, layout): """Mutate ``dag`` and ``layout`` by undoing the swap gates listed in ``operations``.""" if dag is None: for operation in reversed(operations): - layout.swap(*operation.qargs) + layout.swap_logical(*[self._bit_indices[x] for x in operation.qargs]) else: for operation in reversed(operations): dag.remove_op_node(operation) p0 = self._bit_indices[operation.qargs[0]] p1 = self._bit_indices[operation.qargs[1]] - layout.swap(p0, p1) - + layout.swap_logical(p0, p1) -def _transform_gate_for_layout(op_node, layout, device_qreg): - """Return node implementing a virtual op on given layout.""" - mapped_op_node = copy(op_node) - mapped_op_node.qargs = tuple(device_qreg[layout._v2p[x]] for x in op_node.qargs) - return mapped_op_node + def _transform_gate_for_layout(self, op_node, layout, device_qreg): + """Return node implementing a virtual op on given layout.""" + mapped_op_node = copy(op_node) + mapped_op_node.qargs = tuple( + device_qreg[layout.logical_to_physical(self._bit_indices[x])] for x in op_node.qargs + ) + return mapped_op_node -def _shortest_swap_path(target_qubits, coupling_map, layout): +def _shortest_swap_path(target_qubits, coupling_map, layout, qreg): """Return an iterator that yields the swaps between virtual qubits needed to bring the two virtual qubits in ``target_qubits`` together in the coupling map.""" v_start, v_goal = target_qubits - start, goal = layout._v2p[v_start], layout._v2p[v_goal] + start, goal = layout.logical_to_physical(qreg.index(v_start)), layout.logical_to_physical( + qreg.index(v_goal) + ) # TODO: remove the list call once using retworkx 0.12, as the return value can be sliced. path = list(retworkx.dijkstra_shortest_paths(coupling_map.graph, start, target=goal)[goal]) # Swap both qubits towards the "centre" (as opposed to applying the same swaps to one) to @@ -458,6 +446,6 @@ def _shortest_swap_path(target_qubits, coupling_map, layout): split = len(path) // 2 forwards, backwards = path[1:split], reversed(path[split:-1]) for swap in forwards: - yield v_start, layout._p2v[swap] + yield v_start, qreg[layout.physical_to_logical(swap)] for swap in backwards: - yield v_goal, layout._p2v[swap] + yield v_goal, qreg[layout.physical_to_logical(swap)] diff --git a/releasenotes/notes/rabre-rwap-ae51631bec7450df.yaml b/releasenotes/notes/rabre-rwap-ae51631bec7450df.yaml new file mode 100644 index 000000000000..6c846f06cf2b --- /dev/null +++ b/releasenotes/notes/rabre-rwap-ae51631bec7450df.yaml @@ -0,0 +1,17 @@ +--- +features: + - | + The :class:`~.SabreSwap` transpiler pass has significantly improved + performance because of a rewrite of the internal scoring heuristic in + Rust. +upgrade: + - | + The output from the :class:`~.SabreSwap` transpiler pass (including when + ``optimization_level=3`` or ``routing_method`` or ``layout_method`` are + set to ``'sabre'`` when calling :func:`~.transpile`) with a fixed + seed value may change from previous releases. This is caused by a new + random number generator being used as part of the rewrite of the + :class:`~.SabreSwap` pass in Rust which significantly improved the + performance. If you rely on having consistent output you can run + the pass in an earlier version of Qiskit and leverage :mod:`qiskit.qpy` + to save the circuit and then load it using the current version. diff --git a/src/lib.rs b/src/lib.rs index 528b599f1f19..0451cefe9ec6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,7 @@ mod nlayout; mod optimize_1q_gates; mod pauli_exp_val; mod results; +mod sabre_swap; mod sparse_pauli_op; mod stochastic_swap; @@ -41,6 +42,7 @@ pub fn getenv_use_multiple_threads() -> bool { #[pymodule] fn _accelerate(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_wrapped(wrap_pymodule!(stochastic_swap::stochastic_swap))?; + m.add_wrapped(wrap_pymodule!(sabre_swap::sabre_swap))?; m.add_wrapped(wrap_pymodule!(pauli_exp_val::pauli_expval))?; m.add_wrapped(wrap_pymodule!(dense_layout::dense_layout))?; m.add_wrapped(wrap_pymodule!(sparse_pauli_op::sparse_pauli_op))?; diff --git a/src/nlayout.rs b/src/nlayout.rs index 53675d07f521..e4ca1223b33d 100644 --- a/src/nlayout.rs +++ b/src/nlayout.rs @@ -86,4 +86,30 @@ impl NLayout { .map(|i| [i, self.logic_to_phys[i]]) .collect() } + + /// Get physical bit from logical bit + #[pyo3(text_signature = "(self, logical_bit, /)")] + fn logical_to_physical(&self, logical_bit: usize) -> usize { + self.logic_to_phys[logical_bit] + } + + /// Get logical bit from physical bit + #[pyo3(text_signature = "(self, physical_bit, /)")] + pub fn physical_to_logical(&self, physical_bit: usize) -> usize { + self.phys_to_logic[physical_bit] + } + + /// Swap the specified virtual qubits + #[pyo3(text_signature = "(self, bit_a, bit_b, /)")] + pub fn swap_logical(&mut self, bit_a: usize, bit_b: usize) { + self.logic_to_phys.swap(bit_a, bit_b); + self.phys_to_logic[self.logic_to_phys[bit_a]] = bit_a; + self.phys_to_logic[self.logic_to_phys[bit_b]] = bit_b; + } + + /// Swap the specified physical qubits + #[pyo3(text_signature = "(self, bit_a, bit_b, /)")] + pub fn swap_physical(&mut self, bit_a: usize, bit_b: usize) { + self.swap(bit_a, bit_b) + } } diff --git a/src/sabre_swap/edge_list.rs b/src/sabre_swap/edge_list.rs new file mode 100644 index 000000000000..a1dbf0fb55e7 --- /dev/null +++ b/src/sabre_swap/edge_list.rs @@ -0,0 +1,101 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2022 +// +// 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. + +use pyo3::exceptions::PyIndexError; +use pyo3::prelude::*; + +/// A simple container that contains a vector representing edges in the +/// coupling map that are found to be optimal by the swap mapper. +#[pyclass(module = "qiskit._accelerate.sabre_swap")] +#[pyo3(text_signature = "(/)")] +#[derive(Clone, Debug)] +pub struct EdgeList { + pub edges: Vec<[usize; 2]>, +} + +impl Default for EdgeList { + fn default() -> Self { + Self::new(None) + } +} + +#[pymethods] +impl EdgeList { + #[new] + pub fn new(capacity: Option) -> Self { + match capacity { + Some(size) => EdgeList { + edges: Vec::with_capacity(size), + }, + None => EdgeList { edges: Vec::new() }, + } + } + + /// Append an edge to the list. + /// + /// Args: + /// edge_start (int): The start qubit of the edge. + /// edge_end (int): The end qubit of the edge. + #[pyo3(text_signature = "(self, edge_start, edge_end, /)")] + pub fn append(&mut self, edge_start: usize, edge_end: usize) { + self.edges.push([edge_start, edge_end]); + } + + pub fn __iter__(slf: PyRef) -> PyResult> { + let iter = EdgeListIter { + inner: slf.edges.clone().into_iter(), + }; + Py::new(slf.py(), iter) + } + + pub fn __len__(&self) -> usize { + self.edges.len() + } + + pub fn __contains__(&self, object: [usize; 2]) -> bool { + self.edges.contains(&object) + } + + pub fn __getitem__(&self, object: usize) -> PyResult<[usize; 2]> { + if object >= self.edges.len() { + return Err(PyIndexError::new_err(format!( + "Index {} out of range for this EdgeList", + object + ))); + } + Ok(self.edges[object]) + } + + fn __getstate__(&self) -> Vec<[usize; 2]> { + self.edges.clone() + } + + fn __setstate__(&mut self, state: Vec<[usize; 2]>) { + self.edges = state + } +} + +#[pyclass] +pub struct EdgeListIter { + inner: std::vec::IntoIter<[usize; 2]>, +} + +#[pymethods] +impl EdgeListIter { + fn __iter__(slf: PyRef) -> PyRef { + slf + } + + fn __next__(mut slf: PyRefMut) -> Option<[usize; 2]> { + slf.inner.next() + } +} diff --git a/src/sabre_swap/mod.rs b/src/sabre_swap/mod.rs new file mode 100644 index 000000000000..73323cd446d4 --- /dev/null +++ b/src/sabre_swap/mod.rs @@ -0,0 +1,206 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2022 +// +// 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. + +#![allow(clippy::too_many_arguments)] + +pub mod edge_list; +pub mod neighbor_table; +pub mod qubits_decay; +pub mod sabre_rng; + +use ndarray::prelude::*; +use numpy::PyReadonlyArray2; +use pyo3::prelude::*; +use pyo3::wrap_pyfunction; +use pyo3::Python; + +use hashbrown::HashSet; +use rand::prelude::SliceRandom; +use rayon::prelude::*; + +use crate::getenv_use_multiple_threads; +use crate::nlayout::NLayout; + +use edge_list::EdgeList; +use neighbor_table::NeighborTable; +use qubits_decay::QubitsDecay; +use sabre_rng::SabreRng; + +const EXTENDED_SET_WEIGHT: f64 = 0.5; // Weight of lookahead window compared to front_layer. + +#[pyclass] +pub enum Heuristic { + Basic, + Lookahead, + Decay, +} + +/// Return a set of candidate swaps that affect qubits in front_layer. +/// +/// For each virtual qubit in front_layer, find its current location +/// on hardware and the physical qubits in that neighborhood. Every SWAP +/// on virtual qubits that corresponds to one of those physical couplings +/// is a candidate SWAP. +/// +/// Candidate swaps are sorted so SWAP(i,j) and SWAP(j,i) are not duplicated. +fn obtain_swaps( + front_layer: &EdgeList, + neighbors: &NeighborTable, + layout: &NLayout, +) -> HashSet<[usize; 2]> { + // This will likely under allocate as it's a function of the number of + // neighbors for the qubits in the layer too, but this is basically a + // minimum allocation assuming each qubit has only 1 unique neighbor + let mut candidate_swaps: HashSet<[usize; 2]> = + HashSet::with_capacity(2 * front_layer.edges.len()); + for node in &front_layer.edges { + for v in node { + let physical = layout.logic_to_phys[*v]; + for neighbor in &neighbors.neighbors[physical] { + let virtual_neighbor = layout.phys_to_logic[*neighbor]; + let swap: [usize; 2] = if &virtual_neighbor > v { + [*v, virtual_neighbor] + } else { + [virtual_neighbor, *v] + }; + candidate_swaps.insert(swap); + } + } + } + candidate_swaps +} + +/// Run the sabre heuristic scoring +/// +/// Args: +/// layers (EdgeList): The input layer edge list to score and find the +/// best swaps +/// layout (NLayout): The current layout +/// neighbor_table (NeighborTable): The table of neighbors for each node +/// in the coupling graph +/// extended_set (EdgeList): The extended set +/// distance_matrix (ndarray): The 2D array distance matrix for the coupling +/// graph +/// qubits_decay (QubitsDecay): The current qubit decay factors for +/// heuristic (Heuristic): The chosen heuristic method to use +/// Returns: +/// ndarray: A 2d array of the best swap candidates all with the minimum score +#[pyfunction] +pub fn sabre_score_heuristic( + layer: EdgeList, + layout: &mut NLayout, + neighbor_table: &NeighborTable, + extended_set: EdgeList, + distance_matrix: PyReadonlyArray2, + qubits_decay: QubitsDecay, + heuristic: &Heuristic, + rng: &mut SabreRng, +) -> [usize; 2] { + // Run in parallel only if we're not already in a multiprocessing context + // unless force threads is set. + let run_in_parallel = getenv_use_multiple_threads(); + let dist = distance_matrix.as_array(); + let candidate_swaps = obtain_swaps(&layer, neighbor_table, layout); + let mut min_score = f64::MAX; + let mut best_swaps: Vec<[usize; 2]> = Vec::new(); + for swap_qubits in candidate_swaps { + layout.swap_logical(swap_qubits[0], swap_qubits[1]); + let score = score_heuristic( + heuristic, + &layer.edges, + &extended_set.edges, + layout, + &swap_qubits, + &dist, + &qubits_decay.decay, + ); + if score < min_score { + min_score = score; + best_swaps.clear(); + best_swaps.push(swap_qubits); + } else if score == min_score { + best_swaps.push(swap_qubits); + } + layout.swap_logical(swap_qubits[0], swap_qubits[1]); + } + if run_in_parallel { + best_swaps.par_sort_unstable(); + } else { + best_swaps.sort_unstable(); + } + *best_swaps.choose(&mut rng.rng).unwrap() +} + +#[inline] +fn compute_cost(layer: &[[usize; 2]], layout: &NLayout, dist: &ArrayView2) -> f64 { + layer + .iter() + .map(|gate| dist[[layout.logic_to_phys[gate[0]], layout.logic_to_phys[gate[1]]]]) + .sum() +} + +fn score_lookahead( + layer: &[[usize; 2]], + extended_set: &[[usize; 2]], + layout: &NLayout, + dist: &ArrayView2, +) -> f64 { + let mut first_cost = compute_cost(layer, layout, dist); + first_cost /= layer.len() as f64; + let second_cost = if extended_set.is_empty() { + 0. + } else { + compute_cost(extended_set, layout, dist) / extended_set.len() as f64 + }; + first_cost + EXTENDED_SET_WEIGHT * second_cost +} + +fn score_decay( + layer: &[[usize; 2]], + extended_set: &[[usize; 2]], + layout: &NLayout, + dist: &ArrayView2, + swap_qubits: &[usize; 2], + qubits_decay: &[f64], +) -> f64 { + let total_cost = score_lookahead(layer, extended_set, layout, dist); + qubits_decay[swap_qubits[0]].max(qubits_decay[swap_qubits[1]]) * total_cost +} + +fn score_heuristic( + heuristic: &Heuristic, + layer: &[[usize; 2]], + extended_set: &[[usize; 2]], + layout: &NLayout, + swap_qubits: &[usize; 2], + dist: &ArrayView2, + qubits_decay: &[f64], +) -> f64 { + match heuristic { + Heuristic::Basic => compute_cost(layer, layout, dist), + Heuristic::Lookahead => score_lookahead(layer, extended_set, layout, dist), + Heuristic::Decay => { + score_decay(layer, extended_set, layout, dist, swap_qubits, qubits_decay) + } + } +} + +#[pymodule] +pub fn sabre_swap(_py: Python, m: &PyModule) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(sabre_score_heuristic))?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/src/sabre_swap/neighbor_table.rs b/src/sabre_swap/neighbor_table.rs new file mode 100644 index 000000000000..d59700ed6352 --- /dev/null +++ b/src/sabre_swap/neighbor_table.rs @@ -0,0 +1,73 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2022 +// +// 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. + +use crate::getenv_use_multiple_threads; +use ndarray::prelude::*; +use numpy::PyReadonlyArray2; +use pyo3::prelude::*; +use rayon::prelude::*; + +/// A simple container that contains a vector of vectors representing +/// neighbors of each node in the coupling map +/// +/// This object is typically created once from the adjacency matrix of +/// a coupling map, for example:: +/// +/// neigh_table = NeighborTable(retworkx.adjacency_matrix(coupling_map.graph)) +/// +/// and used solely to represent neighbors of each node in qiskit-terra's rust +/// module. +#[pyclass(module = "qiskit._accelerate.sabre_swap")] +#[pyo3(text_signature = "(/)")] +#[derive(Clone, Debug)] +pub struct NeighborTable { + pub neighbors: Vec>, +} + +#[pymethods] +impl NeighborTable { + #[new] + pub fn new(adjacency_matrix: PyReadonlyArray2) -> Self { + let adj_mat = adjacency_matrix.as_array(); + let run_in_parallel = getenv_use_multiple_threads(); + let build_neighbors = |row: ArrayView1| -> Vec { + row.iter() + .enumerate() + .filter_map( + |(row_index, value)| { + if *value == 0. { + None + } else { + Some(row_index) + } + }, + ) + .collect() + }; + if run_in_parallel { + NeighborTable { + neighbors: adj_mat + .axis_iter(Axis(0)) + .into_par_iter() + .map(|row| build_neighbors(row)) + .collect(), + } + } else { + NeighborTable { + neighbors: adj_mat + .axis_iter(Axis(0)) + .map(|row| build_neighbors(row)) + .collect(), + } + } + } +} diff --git a/src/sabre_swap/qubits_decay.rs b/src/sabre_swap/qubits_decay.rs new file mode 100644 index 000000000000..0a5899af1bc5 --- /dev/null +++ b/src/sabre_swap/qubits_decay.rs @@ -0,0 +1,85 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2022 +// +// 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. + +use numpy::IntoPyArray; +use pyo3::exceptions::PyIndexError; +use pyo3::prelude::*; +use pyo3::Python; + +/// A container for qubit decay values for each qubit +/// +/// This class tracks the qubit decay for the sabre heuristic. When initialized +/// all qubits are set to a value of ``1.``. This class implements the sequence +/// protocol and can be modified in place like any python sequence. +/// +/// Args: +/// qubit_count (int): The number of qubits +#[pyclass(module = "qiskit._accelerate.sabre_swap")] +#[pyo3(text_signature = "(qubit_indices, logical_qubits, physical_qubits, /)")] +#[derive(Clone, Debug)] +pub struct QubitsDecay { + pub decay: Vec, +} + +#[pymethods] +impl QubitsDecay { + #[new] + pub fn new(qubit_count: usize) -> Self { + QubitsDecay { + decay: vec![1.; qubit_count], + } + } + + // Mapping Protocol + pub fn __len__(&self) -> usize { + self.decay.len() + } + + pub fn __contains__(&self, object: f64) -> bool { + self.decay.contains(&object) + } + + pub fn __getitem__(&self, object: usize) -> PyResult { + match self.decay.get(object) { + Some(val) => Ok(*val), + None => Err(PyIndexError::new_err(format!( + "Index {} out of range for this EdgeList", + object + ))), + } + } + + pub fn __setitem__(mut slf: PyRefMut, object: usize, value: f64) -> PyResult<()> { + if object >= slf.decay.len() { + return Err(PyIndexError::new_err(format!( + "Index {} out of range for this EdgeList", + object + ))); + } + slf.decay[object] = value; + Ok(()) + } + + pub fn __array__(&self, py: Python) -> PyObject { + self.decay.clone().into_pyarray(py).into() + } + + pub fn __str__(&self) -> PyResult { + Ok(format!("{:?}", self.decay)) + } + + /// Reset decay for all qubits back to default ``1.`` + #[pyo3(text_signature = "(self, /)")] + pub fn reset(mut slf: PyRefMut) { + slf.decay.fill_with(|| 1.); + } +} diff --git a/src/sabre_swap/sabre_rng.rs b/src/sabre_swap/sabre_rng.rs new file mode 100644 index 000000000000..79a4a70acb13 --- /dev/null +++ b/src/sabre_swap/sabre_rng.rs @@ -0,0 +1,35 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2022 +// +// 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. + +use pyo3::prelude::*; +use rand::prelude::*; +use rand_pcg::Pcg64Mcg; + +/// A rng container that shares an rng state between python and sabre's rust +/// code. It should be initialized once and passed to +/// ``sabre_score_heuristic`` to avoid recreating a rng on the inner loop +#[pyclass(module = "qiskit._accelerate.sabre_swap")] +#[pyo3(text_signature = "(/)")] +#[derive(Clone, Debug)] +pub struct SabreRng { + pub rng: Pcg64Mcg, +} + +#[pymethods] +impl SabreRng { + #[new] + pub fn new(seed: u64) -> Self { + SabreRng { + rng: Pcg64Mcg::seed_from_u64(seed), + } + } +} diff --git a/test/python/transpiler/test_mappers.py b/test/python/transpiler/test_mappers.py index 5bedf2dc6dd8..ef061f7c7d28 100644 --- a/test/python/transpiler/test_mappers.py +++ b/test/python/transpiler/test_mappers.py @@ -294,7 +294,7 @@ class TestsSabreSwap(SwapperCommonTestCases, QiskitTestCase): """Test SwapperCommonTestCases using SabreSwap.""" pass_class = SabreSwap - additional_args = {"seed": 0} + additional_args = {"seed": 4242} if __name__ == "__main__": diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index bfe84c65ba43..2ce8ef1d0343 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -698,26 +698,26 @@ def test_layout_tokyo_fully_connected_cx(self, level): } sabre_layout = { - 18: qr[0], - 13: qr[1], - 14: qr[2], - 17: qr[3], - 19: qr[4], + 6: qr[0], + 11: qr[1], + 10: qr[2], + 5: qr[3], + 16: qr[4], 0: ancilla[0], 1: ancilla[1], 2: ancilla[2], 3: ancilla[3], 4: ancilla[4], - 5: ancilla[5], - 6: ancilla[6], - 7: ancilla[7], - 8: ancilla[8], - 9: ancilla[9], - 10: ancilla[10], - 11: ancilla[11], - 12: ancilla[12], - 15: ancilla[13], - 16: ancilla[14], + 7: ancilla[5], + 8: ancilla[6], + 9: ancilla[7], + 12: ancilla[8], + 13: ancilla[9], + 14: ancilla[10], + 15: ancilla[11], + 17: ancilla[12], + 18: ancilla[13], + 19: ancilla[14], } expected_layout_level0 = trivial_layout diff --git a/test/python/transpiler/test_sabre_layout.py b/test/python/transpiler/test_sabre_layout.py index 85561f102e47..85a975dd6a48 100644 --- a/test/python/transpiler/test_sabre_layout.py +++ b/test/python/transpiler/test_sabre_layout.py @@ -56,10 +56,10 @@ def test_5q_circuit_20q_coupling(self): pass_.run(dag) layout = pass_.property_set["layout"] - self.assertEqual(layout[qr[0]], 10) - self.assertEqual(layout[qr[1]], 12) - self.assertEqual(layout[qr[2]], 7) - self.assertEqual(layout[qr[3]], 11) + self.assertEqual(layout[qr[0]], 11) + self.assertEqual(layout[qr[1]], 6) + self.assertEqual(layout[qr[2]], 12) + self.assertEqual(layout[qr[3]], 5) self.assertEqual(layout[qr[4]], 13) def test_6q_circuit_20q_coupling(self): @@ -92,12 +92,12 @@ def test_6q_circuit_20q_coupling(self): pass_.run(dag) layout = pass_.property_set["layout"] - self.assertEqual(layout[qr0[0]], 2) - self.assertEqual(layout[qr0[1]], 3) + self.assertEqual(layout[qr0[0]], 8) + self.assertEqual(layout[qr0[1]], 2) self.assertEqual(layout[qr0[2]], 10) - self.assertEqual(layout[qr1[0]], 1) - self.assertEqual(layout[qr1[1]], 7) - self.assertEqual(layout[qr1[2]], 5) + self.assertEqual(layout[qr1[0]], 3) + self.assertEqual(layout[qr1[1]], 12) + self.assertEqual(layout[qr1[2]], 11) if __name__ == "__main__": From 77535607f916f36bb734e63fa453704360191f2c Mon Sep 17 00:00:00 2001 From: "Kevin J. Sung" Date: Tue, 19 Jul 2022 13:36:36 -0400 Subject: [PATCH 06/82] Fix bug in circuit decompose (#8364) * fix bug in circuit decompose * add test * Update test/python/circuit/test_circuit_operations.py Co-authored-by: Matthew Treinish Co-authored-by: Matthew Treinish Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/transpiler/passes/basis/decompose.py | 4 +--- releasenotes/notes/decompose-fix-993f7242eaa69407.yaml | 4 ++++ test/python/circuit/test_circuit_operations.py | 7 +++++++ 3 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/decompose-fix-993f7242eaa69407.yaml diff --git a/qiskit/transpiler/passes/basis/decompose.py b/qiskit/transpiler/passes/basis/decompose.py index 10eff7252b0e..fb5a929e0f37 100644 --- a/qiskit/transpiler/passes/basis/decompose.py +++ b/qiskit/transpiler/passes/basis/decompose.py @@ -128,9 +128,7 @@ def _should_decompose(self, node) -> bool: node.name in gates or any(fnmatch(node.name, p) for p in strings_list) ): return True - elif not has_label and ( # check if Gate type given - any(isinstance(node.op, op) for op in gate_type_list) - ): + elif any(isinstance(node.op, op) for op in gate_type_list): # check if Gate type given return True else: return False diff --git a/releasenotes/notes/decompose-fix-993f7242eaa69407.yaml b/releasenotes/notes/decompose-fix-993f7242eaa69407.yaml new file mode 100644 index 000000000000..93f259266ef5 --- /dev/null +++ b/releasenotes/notes/decompose-fix-993f7242eaa69407.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - Fixed a bug in :meth:`.QuantumCircuit.decompose` that caused the + `gates_to_decompose` argument to be handled incorrectly. \ No newline at end of file diff --git a/test/python/circuit/test_circuit_operations.py b/test/python/circuit/test_circuit_operations.py index 336fcfd397a6..426683e62a1a 100644 --- a/test/python/circuit/test_circuit_operations.py +++ b/test/python/circuit/test_circuit_operations.py @@ -1265,3 +1265,10 @@ def test_pop_previous_instruction_removes_parameters(self): instruction = test._pop_previous_instruction_in_scope() self.assertEqual(list(last_instructions), [instruction]) self.assertEqual({y}, set(test.parameters)) + + def test_decompose_gate_type(self): + """Test decompose specifying gate type.""" + circuit = QuantumCircuit(1) + circuit.append(SGate(label="s_gate"), [0]) + decomposed = circuit.decompose(gates_to_decompose=SGate) + self.assertNotIn("s", decomposed.count_ops()) From f2c2fe263eedc0dd4cc2f954921bc57e2e89d076 Mon Sep 17 00:00:00 2001 From: Adenilton Silva <7927558+adjs@users.noreply.github.com> Date: Tue, 19 Jul 2022 15:41:06 -0300 Subject: [PATCH 07/82] Fix Uc gate global phase (#8231) * global phase UCGate * Create global-phase-ucgate-cd61355e314a3e64.yaml * Update qiskit/extensions/quantum_initializer/isometry.py * Update qiskit/extensions/quantum_initializer/isometry.py Co-authored-by: ewinston Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .../quantum_initializer/isometry.py | 37 +++++++++++++------ qiskit/extensions/quantum_initializer/uc.py | 6 +-- .../global-phase-ucgate-cd61355e314a3e64.yaml | 4 ++ test/python/circuit/test_uc.py | 21 +++++++++-- 4 files changed, 50 insertions(+), 18 deletions(-) create mode 100644 releasenotes/notes/global-phase-ucgate-cd61355e314a3e64.yaml diff --git a/qiskit/extensions/quantum_initializer/isometry.py b/qiskit/extensions/quantum_initializer/isometry.py index cb3bb1f12311..5d17398ab3a3 100644 --- a/qiskit/extensions/quantum_initializer/isometry.py +++ b/qiskit/extensions/quantum_initializer/isometry.py @@ -22,7 +22,6 @@ import itertools import numpy as np - from qiskit.circuit.exceptions import CircuitError from qiskit.circuit.instruction import Instruction from qiskit.circuit.quantumcircuit import QuantumCircuit @@ -73,6 +72,8 @@ def __init__(self, isometry, num_ancillas_zero, num_ancillas_dirty, epsilon=_EPS if len(isometry.shape) == 1: isometry = isometry.reshape(isometry.shape[0], 1) + self.iso_data = isometry + self.num_ancillas_zero = num_ancillas_zero self.num_ancillas_dirty = num_ancillas_dirty self._inverse = None @@ -103,15 +104,23 @@ def __init__(self, isometry, num_ancillas_zero, num_ancillas_dirty, epsilon=_EPS super().__init__("isometry", num_qubits, 0, [isometry]) def _define(self): + # TODO The inverse().inverse() is because there is code to uncompute (_gates_to_uncompute) # an isometry, but not for generating its decomposition. It would be cheaper to do the # later here instead. - gate = self.inverse().inverse() + gate = self.inv_gate() + gate = gate.inverse() q = QuantumRegister(self.num_qubits) iso_circuit = QuantumCircuit(q) iso_circuit.append(gate, q[:]) self.definition = iso_circuit + def inverse(self): + self.params = [] + inv = super().inverse() + self.params = [self.iso_data] + return inv + def _gates_to_uncompute(self): """ Call to create a circuit with gates that take the desired isometry to the first 2^m columns @@ -127,9 +136,9 @@ def _gates_to_uncompute(self): ) = self._define_qubit_role(q) # Copy the isometry (this is computationally expensive for large isometries but guarantees # to keep a copyof the input isometry) - remaining_isometry = self.params[0].astype(complex) # note: "astype" does copy the isometry + remaining_isometry = self.iso_data.astype(complex) # note: "astype" does copy the isometry diag = [] - m = int(np.log2((self.params[0]).shape[1])) + m = int(np.log2((self.iso_data).shape[1])) # Decompose the column with index column_index and attache the gate to the circuit object. # Return the isometry that is left to decompose, where the columns up to index column_index # correspond to the firstfew columns of the identity matrix up to diag, and hence we only @@ -148,7 +157,7 @@ def _decompose_column(self, circuit, q, diag, remaining_isometry, column_index): """ Decomposes the column with index column_index. """ - n = int(np.log2(self.params[0].shape[0])) + n = int(np.log2(self.iso_data.shape[0])) for s in range(n): self._disentangle(circuit, q, diag, remaining_isometry, column_index, s) @@ -163,7 +172,7 @@ def _disentangle(self, circuit, q, diag, remaining_isometry, column_index, s): # (note that we remove columns of the isometry during the procedure for efficiency) k_prime = 0 v = remaining_isometry - n = int(np.log2(self.params[0].shape[0])) + n = int(np.log2(self.iso_data.shape[0])) # MCG to set one entry to zero (preparation for disentangling with UCGate): index1 = 2 * _a(k, s + 1) * 2**s + _b(k, s + 1) @@ -215,7 +224,7 @@ def _disentangle(self, circuit, q, diag, remaining_isometry, column_index, s): # The qubit with label n-s-1 is disentangled into the basis state k_s(k,s). def _find_squs_for_disentangling(self, v, k, s): k_prime = 0 - n = int(np.log2(self.params[0].shape[0])) + n = int(np.log2(self.iso_data.shape[0])) if _b(k, s + 1) == 0: i_start = _a(k, s + 1) else: @@ -242,7 +251,7 @@ def _append_ucg_up_to_diagonal(self, circ, q, single_qubit_gates, control_labels q_ancillas_zero, q_ancillas_dirty, ) = self._define_qubit_role(q) - n = int(np.log2(self.params[0].shape[0])) + n = int(np.log2(self.iso_data.shape[0])) qubits = q_input + q_ancillas_for_output # Note that we have to reverse the control labels, since controls are provided by # increasing qubit number toa UCGate by convention @@ -264,7 +273,7 @@ def _append_mcg_up_to_diagonal(self, circ, q, gate, control_labels, target_label q_ancillas_zero, q_ancillas_dirty, ) = self._define_qubit_role(q) - n = int(np.log2(self.params[0].shape[0])) + n = int(np.log2(self.iso_data.shape[0])) qubits = q_input + q_ancillas_for_output control_qubits = _reverse_qubit_oder(_get_qubits_by_label(control_labels, qubits, n)) target_qubit = _get_qubits_by_label([target_label], qubits, n)[0] @@ -284,8 +293,10 @@ def _append_mcg_up_to_diagonal(self, circ, q, gate, control_labels, target_label return mcg_up_to_diag._get_diagonal() def _define_qubit_role(self, q): - n = int(np.log2((self.params[0]).shape[0])) - m = int(np.log2((self.params[0]).shape[1])) + + n = int(np.log2(self.iso_data.shape[0])) + m = int(np.log2(self.iso_data.shape[1])) + # Define the role of the qubits q_input = q[:m] q_ancillas_for_output = q[m:n] @@ -297,10 +308,12 @@ def validate_parameter(self, parameter): """Isometry parameter has to be an ndarray.""" if isinstance(parameter, np.ndarray): return parameter + if isinstance(parameter, (list, int)): + return parameter else: raise CircuitError(f"invalid param type {type(parameter)} for gate {self.name}") - def inverse(self): + def inv_gate(self): """Return the adjoint of the unitary.""" if self._inverse is None: # call to generate the circuit that takes the isometry to the first 2^m columns diff --git a/qiskit/extensions/quantum_initializer/uc.py b/qiskit/extensions/quantum_initializer/uc.py index f432a7ebd082..61618efb3563 100644 --- a/qiskit/extensions/quantum_initializer/uc.py +++ b/qiskit/extensions/quantum_initializer/uc.py @@ -138,8 +138,7 @@ def _dec_ucg(self): circuit = QuantumCircuit(q) # If there is no control, we use the ZYZ decomposition if not q_controls: - theta, phi, lamb = _DECOMPOSER1Q.angles(self.params[0]) - circuit.u(theta, phi, lamb, q) + circuit.unitary(self.params[0], [q]) return circuit, diag # If there is at least one control, first, # we find the single qubit gates of the decomposition. @@ -160,7 +159,7 @@ def _dec_ucg(self): .dot(HGate().to_matrix()) ) # Add single-qubit gate - circuit.squ(squ, q_target) + circuit.unitary(squ, [q_target]) # The number of the control qubit is given by the number of zeros at the end # of the binary representation of (i+1) binary_rep = np.binary_repr(i + 1) @@ -169,6 +168,7 @@ def _dec_ucg(self): # Add C-NOT gate if not i == len(single_qubit_gates) - 1: circuit.cx(q_controls[q_contr_index], q_target) + circuit.global_phase -= 0.25 * np.pi if not self.up_to_diagonal: # Important: the diagonal gate is given in the computational basis of the qubits # q[k-1],...,q[0],q_target (ordered with decreasing significance), diff --git a/releasenotes/notes/global-phase-ucgate-cd61355e314a3e64.yaml b/releasenotes/notes/global-phase-ucgate-cd61355e314a3e64.yaml new file mode 100644 index 000000000000..536b66acfa6a --- /dev/null +++ b/releasenotes/notes/global-phase-ucgate-cd61355e314a3e64.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - | + Fixed a global phase bug in :class:`~.UCGate`. diff --git a/test/python/circuit/test_uc.py b/test/python/circuit/test_uc.py index a53367132833..cedff9f4ac41 100644 --- a/test/python/circuit/test_uc.py +++ b/test/python/circuit/test_uc.py @@ -46,9 +46,9 @@ class TestUCGate(QiskitTestCase): [_id, _id], [_id, 1j * _id], [_id, _not, _id, _not], - [random_unitary(2, seed=541234).data for i in range(2**2)], - [random_unitary(2, seed=975163).data for i in range(2**3)], - [random_unitary(2, seed=629462).data for i in range(2**4)], + [random_unitary(2, seed=541234).data for _ in range(2**2)], + [random_unitary(2, seed=975163).data for _ in range(2**3)], + [random_unitary(2, seed=629462).data for _ in range(2**4)], ], up_to_diagonal=[True, False], ) @@ -70,6 +70,21 @@ def test_ucg(self, squs, up_to_diagonal): unitary_desired = _get_ucg_matrix(squs) self.assertTrue(matrix_equal(unitary_desired, unitary, ignore_phase=True)) + def test_global_phase_ucg(self): + """ "Test global phase of uniformly controlled gates""" + gates = [random_unitary(2).data for _ in range(2**2)] + num_con = int(np.log2(len(gates))) + q = QuantumRegister(num_con + 1) + qc = QuantumCircuit(q) + + qc.uc(gates, q[1:], q[0], up_to_diagonal=False) + simulator = BasicAer.get_backend("unitary_simulator") + result = execute(qc, simulator).result() + unitary = result.get_unitary(qc) + unitary_desired = _get_ucg_matrix(gates) + + self.assertTrue(np.allclose(unitary_desired, unitary)) + def _get_ucg_matrix(squs): return block_diag(*squs) From d9af1d1f7a64a9bbd054f4e6d91752e2d203ad70 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Wed, 20 Jul 2022 21:26:39 +0200 Subject: [PATCH 08/82] Fix `Optimizer` reconstruction from `settings` (#8381) * fix settings in optimizers * add reno * don't duplicate args Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .../algorithms/optimizers/scipy_optimizer.py | 22 ++++++++++++---- ...x-optimizer-settings-881585bfa8130cb7.yaml | 15 +++++++++++ .../algorithms/optimizers/test_optimizers.py | 25 +++++++++++++++++++ 3 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/fix-optimizer-settings-881585bfa8130cb7.yaml diff --git a/qiskit/algorithms/optimizers/scipy_optimizer.py b/qiskit/algorithms/optimizers/scipy_optimizer.py index d1a41111e378..aa8349d03f33 100644 --- a/qiskit/algorithms/optimizers/scipy_optimizer.py +++ b/qiskit/algorithms/optimizers/scipy_optimizer.py @@ -86,11 +86,23 @@ def get_support_level(self): @property def settings(self) -> Dict[str, Any]: - settings = { - "max_evals_grouped": self._max_evals_grouped, - "options": self._options, - **self._kwargs, - } + options = self._options.copy() + if hasattr(self, "_OPTIONS"): + # all _OPTIONS should be keys in self._options, but add a failsafe here + attributes = [ + option + for option in self._OPTIONS # pylint: disable=no-member + if option in options.keys() + ] + + settings = {attr: options.pop(attr) for attr in attributes} + else: + settings = {} + + settings["max_evals_grouped"] = self._max_evals_grouped + settings["options"] = options + settings.update(self._kwargs) + # the subclasses don't need the "method" key as the class type specifies the method if self.__class__ == SciPyOptimizer: settings["method"] = self._method diff --git a/releasenotes/notes/fix-optimizer-settings-881585bfa8130cb7.yaml b/releasenotes/notes/fix-optimizer-settings-881585bfa8130cb7.yaml new file mode 100644 index 000000000000..ca0b42f965a6 --- /dev/null +++ b/releasenotes/notes/fix-optimizer-settings-881585bfa8130cb7.yaml @@ -0,0 +1,15 @@ +--- +fixes: + - | + Fix a bug in the :class:`~.Optimizer` classes where re-constructing a new optimizer instance + from a previously exisiting :attr:`~.Optimizer.settings` reset both the new and previous + optimizer settings to the defaults. This notably led to a bug if :class:`~.Optimizer` objects + were send as input to Qiskit Runtime programs. + + Now optimizer objects are correctly reconstructed:: + + >>> from qiskit.algorithms.optimizers import COBYLA + >>> original = COBYLA(maxiter=1) + >>> reconstructed = COBYLA(**original.settings) + >>> reconstructed._options["maxiter"] + 1 # used to be 1000! diff --git a/test/python/algorithms/optimizers/test_optimizers.py b/test/python/algorithms/optimizers/test_optimizers.py index 0bfe1d14cdde..5ce8e87cb1b6 100644 --- a/test/python/algorithms/optimizers/test_optimizers.py +++ b/test/python/algorithms/optimizers/test_optimizers.py @@ -226,6 +226,31 @@ def test_scipy(self, method, options): self.assertEqual(from_dict._method, method.lower()) self.assertEqual(from_dict._options, options) + def test_independent_reconstruction(self): + """Test the SciPyOptimizers don't reset all settings upon creating a new instance. + + COBYLA is used as representative example here.""" + + kwargs = {"coffee": "without sugar"} + options = {"tea": "with milk"} + optimizer = COBYLA(maxiter=1, options=options, **kwargs) + serialized = optimizer.settings + from_dict = COBYLA(**serialized) + + with self.subTest(msg="test attributes"): + self.assertEqual(from_dict.settings["maxiter"], 1) + + with self.subTest(msg="test options"): + # options should only contain values that are *not* already in the initializer + # (e.g. should not contain maxiter) + self.assertEqual(from_dict.settings["options"], {"tea": "with milk"}) + + with self.subTest(msg="test kwargs"): + self.assertEqual(from_dict.settings["coffee"], "without sugar") + + with self.subTest(msg="option ids differ"): + self.assertNotEqual(id(serialized["options"]), id(from_dict.settings["options"])) + def test_adam(self): """Test ADAM is serializable.""" From 1312624309526812eb62b97e0d47699d46649a25 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Thu, 21 Jul 2022 10:38:55 +0200 Subject: [PATCH 09/82] Fix `EvolvedOp.to_instruction` (#8384) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/opflow/evolutions/evolved_op.py | 2 +- ...lvedop-to-instruction-c90c4f1aa6b4232a.yaml | 18 ++++++++++++++++++ test/python/opflow/test_evolution.py | 14 ++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/fix-evolvedop-to-instruction-c90c4f1aa6b4232a.yaml diff --git a/qiskit/opflow/evolutions/evolved_op.py b/qiskit/opflow/evolutions/evolved_op.py index 251f68fa4f0f..e4427e858ec4 100644 --- a/qiskit/opflow/evolutions/evolved_op.py +++ b/qiskit/opflow/evolutions/evolved_op.py @@ -169,7 +169,7 @@ def log_i(self, massive: bool = False) -> OperatorBase: # pylint: disable=arguments-differ def to_instruction(self, massive: bool = False) -> Instruction: - mat_op = self.primitive.to_matrix_op(massive=massive) + mat_op = self.to_matrix_op(massive=massive) if not isinstance(mat_op, MatrixOp): raise OpflowError("to_instruction is not allowed for ListOp.") return mat_op.to_instruction() diff --git a/releasenotes/notes/fix-evolvedop-to-instruction-c90c4f1aa6b4232a.yaml b/releasenotes/notes/fix-evolvedop-to-instruction-c90c4f1aa6b4232a.yaml new file mode 100644 index 000000000000..f0e17a4be4ea --- /dev/null +++ b/releasenotes/notes/fix-evolvedop-to-instruction-c90c4f1aa6b4232a.yaml @@ -0,0 +1,18 @@ +--- +fixes: + - | + Fix :meth:`~.EvolvedOp.to_instruction` which previously tried to create a + :class:`~.UnitaryGate` without exponentiating the operator to evolve. + Since this operator is generally not unitary, this raised an error (and if + the operator would have been unitary by chance, it would not have been the expected result). + + Now calling :meth:`~.EvolvedOp.to_instruction` correctly produces a gate + that implements the time evolution of the operator it holds:: + + >>> from qiskit.opflow import EvolvedOp, X + >>> op = EvolvedOp(0.5 * X) + >>> op.to_instruction() + Instruction( + name='unitary', num_qubits=1, num_clbits=0, + params=[array([[0.87758256+0.j, 0.-0.47942554j], [0.-0.47942554j, 0.87758256+0.j]])] + ) diff --git a/test/python/opflow/test_evolution.py b/test/python/opflow/test_evolution.py index fe27179d1dd8..e06a9c35d746 100644 --- a/test/python/opflow/test_evolution.py +++ b/test/python/opflow/test_evolution.py @@ -19,6 +19,7 @@ import qiskit from qiskit.circuit import Parameter, ParameterVector +from qiskit.extensions import UnitaryGate from qiskit.opflow import ( CX, CircuitOp, @@ -364,6 +365,19 @@ def test_suzuki_directly(self): ) np.testing.assert_array_almost_equal(evolution.to_matrix(), matrix) + def test_evolved_op_to_instruction(self): + """Test calling `to_instruction` on a plain EvolvedOp. + + Regression test of Qiskit/qiskit-terra#8025. + """ + op = EvolvedOp(0.5 * X) + circuit = op.to_instruction() + + unitary = scipy.linalg.expm(-0.5j * X.to_matrix()) + expected = UnitaryGate(unitary) + + self.assertEqual(circuit, expected) + if __name__ == "__main__": unittest.main() From 5b166c05182ca3c062106d08a9610ac246aa3e83 Mon Sep 17 00:00:00 2001 From: Guillermo-Mijares-Vilarino <106545082+Guillermo-Mijares-Vilarino@users.noreply.github.com> Date: Fri, 22 Jul 2022 08:49:40 +0200 Subject: [PATCH 10/82] Added links from gate classes' circuit library pages to their corresponding QuantumCircuit methods (#8370) * Added link from IGate to QuantumCircuit.id method in Circuit Library * Added link from UGate to QuantumCircuit.u method in Circuit Library * Added link from PhaseGate to QuantumCircuit.p method in API reference * Added link from XGate to QuantumCircuit.x method in Circuit Library * Added link from YGate to QuantumCircuit.y method in Circuit Library * Added link from ZGate to QuantumCircuit.z in Circuit Library * Added link from HGate to QuantumCircuit.h method in Circuit Library * Added link from SGate to QuantumCircuit.s and from SdgGate to QuantumCircuit.sdg in Circuit Library * Added link from TGate to QuantumCircuit.t and from TdgGate to QuantumCircuit.tdg in Circuit Library * Added link from RXGate to QuantumCircuit.rx in Circuit Library * Added link from RYGate to QuantumCircuit.ry in Circuit Library * Added link from RZGate to QuantumCircuit.rz in Circuit Library * Added link from CUGate to QuantumCircuit.cu method in Circuit Library * Added link from CPhaseGate to QuantumCircuit.cp method in Circuit Library * Added link from CXGate to QuantumCircuit.cx method in Circuit Library * Added link from CCXGate to QuantumCircuit.ccx method in Circuit Library * Added link from CYGate to QuantumCircuit.cy method in Circuit Library * Added link from CZGate to QuantumCircuit.cz method in Circuit Library * Added link from CHGate to QuantumCircuit.ch method in Circuit Library * Added link from CRXGate to QuantumCircuit.crx method in Circuit Library * Added link from CRYGate to QuantumCircuit.cry method in Circuit Library * Added link from CRZGate to QuantumCircuit.crz method in Circuit Library * Added link from SwapGate to QuantumCircuit.swap method in Circuit Library * Added link from CSwapGate to QuantumCircuit.cswap method in Circuit Library * corrected wrong phases in Sdg and Tdg gates * Added links corresponding to RCCX, RC3X and MCX gates * Added MCPhaseGate link to mcp method * Added links corresponding to RXX, RYY, RZZ and RZX gates * Added DCXGate link * Added ECRGate link * Added iSWAPGate link * Added SX, SXdg and CSX links * Added RGate link * Added mention to i and cnot methods * Added RVGate, Barrier and PauliGate links * Added mention to toffoli and fredkin methods --- qiskit/circuit/barrier.py | 6 ++++- .../library/generalized_gates/pauli.py | 5 +++- .../circuit/library/generalized_gates/rv.py | 3 +++ qiskit/circuit/library/standard_gates/dcx.py | 3 +++ qiskit/circuit/library/standard_gates/ecr.py | 3 +++ qiskit/circuit/library/standard_gates/h.py | 6 +++++ qiskit/circuit/library/standard_gates/i.py | 4 ++++ .../circuit/library/standard_gates/iswap.py | 3 +++ qiskit/circuit/library/standard_gates/p.py | 9 ++++++++ qiskit/circuit/library/standard_gates/r.py | 3 +++ qiskit/circuit/library/standard_gates/rx.py | 6 +++++ qiskit/circuit/library/standard_gates/rxx.py | 3 +++ qiskit/circuit/library/standard_gates/ry.py | 6 +++++ qiskit/circuit/library/standard_gates/ryy.py | 3 +++ qiskit/circuit/library/standard_gates/rz.py | 6 +++++ qiskit/circuit/library/standard_gates/rzx.py | 3 +++ qiskit/circuit/library/standard_gates/rzz.py | 3 +++ qiskit/circuit/library/standard_gates/s.py | 8 ++++++- qiskit/circuit/library/standard_gates/swap.py | 7 ++++++ qiskit/circuit/library/standard_gates/sx.py | 9 ++++++++ qiskit/circuit/library/standard_gates/t.py | 8 ++++++- qiskit/circuit/library/standard_gates/u.py | 6 +++++ qiskit/circuit/library/standard_gates/x.py | 23 ++++++++++++++++++- qiskit/circuit/library/standard_gates/y.py | 6 +++++ qiskit/circuit/library/standard_gates/z.py | 6 +++++ 25 files changed, 143 insertions(+), 5 deletions(-) diff --git a/qiskit/circuit/barrier.py b/qiskit/circuit/barrier.py index c10b32069595..62ecc04512e4 100644 --- a/qiskit/circuit/barrier.py +++ b/qiskit/circuit/barrier.py @@ -10,7 +10,11 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Barrier instruction.""" +"""Barrier instruction. + +Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` +with the :meth:`~qiskit.circuit.QuantumCircuit.barrier` method. +""" from qiskit.exceptions import QiskitError from .instruction import Instruction diff --git a/qiskit/circuit/library/generalized_gates/pauli.py b/qiskit/circuit/library/generalized_gates/pauli.py index 217b1d0b6fa4..5989ad26e192 100644 --- a/qiskit/circuit/library/generalized_gates/pauli.py +++ b/qiskit/circuit/library/generalized_gates/pauli.py @@ -32,7 +32,10 @@ class PauliGate(Gate): a single pass on the statevector. The functionality is equivalent to applying - the pauli gates sequentially using standard Qiskit gates + the pauli gates sequentially using standard Qiskit gates. + + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.pauli` method. """ def __init__(self, label): diff --git a/qiskit/circuit/library/generalized_gates/rv.py b/qiskit/circuit/library/generalized_gates/rv.py index 35cf34742bd8..04eb70b23e36 100644 --- a/qiskit/circuit/library/generalized_gates/rv.py +++ b/qiskit/circuit/library/generalized_gates/rv.py @@ -21,6 +21,9 @@ class RVGate(Gate): r"""Rotation around arbitrary rotation axis :math:`v` where :math:`|v|` is angle of rotation in radians. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.rv` method. + **Circuit symbol:** .. parsed-literal:: diff --git a/qiskit/circuit/library/standard_gates/dcx.py b/qiskit/circuit/library/standard_gates/dcx.py index a2804cb1ca27..32d8868cebc1 100644 --- a/qiskit/circuit/library/standard_gates/dcx.py +++ b/qiskit/circuit/library/standard_gates/dcx.py @@ -23,6 +23,9 @@ class DCXGate(Gate): A 2-qubit Clifford gate consisting of two back-to-back CNOTs with alternate controls. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.dcx` method. + .. parsed-literal:: ┌───┐ q_0: ──■──┤ X ├ diff --git a/qiskit/circuit/library/standard_gates/ecr.py b/qiskit/circuit/library/standard_gates/ecr.py index e2501ca79c79..f6e4c650783a 100644 --- a/qiskit/circuit/library/standard_gates/ecr.py +++ b/qiskit/circuit/library/standard_gates/ecr.py @@ -27,6 +27,9 @@ class ECRGate(Gate): single-qubit pre-rotations. The echoing procedure mitigates some unwanted terms (terms other than ZX) to cancel in an experiment. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.ecr` method. + **Circuit Symbol:** .. parsed-literal:: diff --git a/qiskit/circuit/library/standard_gates/h.py b/qiskit/circuit/library/standard_gates/h.py index 893f05d35299..114b9008834e 100644 --- a/qiskit/circuit/library/standard_gates/h.py +++ b/qiskit/circuit/library/standard_gates/h.py @@ -29,6 +29,9 @@ class HGate(Gate): changing computation basis from :math:`|0\rangle,|1\rangle` to :math:`|+\rangle,|-\rangle` and vice-versa. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.h` method. + **Circuit symbol:** .. parsed-literal:: @@ -108,6 +111,9 @@ class CHGate(ControlledGate): Applies a Hadamard on the target qubit if the control is in the :math:`|1\rangle` state. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.ch` method. + **Circuit symbol:** .. parsed-literal:: diff --git a/qiskit/circuit/library/standard_gates/i.py b/qiskit/circuit/library/standard_gates/i.py index 67015fa05574..a30cfe7643fb 100644 --- a/qiskit/circuit/library/standard_gates/i.py +++ b/qiskit/circuit/library/standard_gates/i.py @@ -23,6 +23,10 @@ class IGate(Gate): Identity gate corresponds to a single-qubit gate wait cycle, and should not be optimized or unrolled (it is an opaque gate). + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.i` and + :meth:`~qiskit.circuit.QuantumCircuit.id` methods. + **Matrix Representation:** .. math:: diff --git a/qiskit/circuit/library/standard_gates/iswap.py b/qiskit/circuit/library/standard_gates/iswap.py index 7897e6ebb998..1fe50d74e87a 100644 --- a/qiskit/circuit/library/standard_gates/iswap.py +++ b/qiskit/circuit/library/standard_gates/iswap.py @@ -26,6 +26,9 @@ class iSwapGate(Gate): states and phase the :math:`|01\rangle` and :math:`|10\rangle` amplitudes by i. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.iswap` method. + **Circuit Symbol:** .. parsed-literal:: diff --git a/qiskit/circuit/library/standard_gates/p.py b/qiskit/circuit/library/standard_gates/p.py index 55014ce37a9d..2d3ebd09a00e 100644 --- a/qiskit/circuit/library/standard_gates/p.py +++ b/qiskit/circuit/library/standard_gates/p.py @@ -26,6 +26,9 @@ class PhaseGate(Gate): This is a diagonal gate. It can be implemented virtually in hardware via framechanges (i.e. at zero error and duration). + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.p` method. + **Circuit symbol:** .. parsed-literal:: @@ -129,6 +132,9 @@ class CPhaseGate(ControlledGate): This is a diagonal and symmetric gate that induces a phase on the state of the target qubit, depending on the control state. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.cp` method. + **Circuit symbol:** .. parsed-literal:: @@ -245,6 +251,9 @@ class MCPhaseGate(ControlledGate): This is a diagonal and symmetric gate that induces a phase on the state of the target qubit, depending on the state of the control qubits. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.mcp` method. + **Circuit symbol:** .. parsed-literal:: diff --git a/qiskit/circuit/library/standard_gates/r.py b/qiskit/circuit/library/standard_gates/r.py index 956887351a56..1452ae0b9bcb 100644 --- a/qiskit/circuit/library/standard_gates/r.py +++ b/qiskit/circuit/library/standard_gates/r.py @@ -25,6 +25,9 @@ class RGate(Gate): r"""Rotation θ around the cos(φ)x + sin(φ)y axis. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.r` method. + **Circuit symbol:** .. parsed-literal:: diff --git a/qiskit/circuit/library/standard_gates/rx.py b/qiskit/circuit/library/standard_gates/rx.py index c78414223a93..09139674886c 100644 --- a/qiskit/circuit/library/standard_gates/rx.py +++ b/qiskit/circuit/library/standard_gates/rx.py @@ -26,6 +26,9 @@ class RXGate(Gate): r"""Single-qubit rotation about the X axis. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.rx` method. + **Circuit symbol:** .. parsed-literal:: @@ -107,6 +110,9 @@ def __array__(self, dtype=None): class CRXGate(ControlledGate): r"""Controlled-RX gate. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.crx` method. + **Circuit symbol:** .. parsed-literal:: diff --git a/qiskit/circuit/library/standard_gates/rxx.py b/qiskit/circuit/library/standard_gates/rxx.py index a20708211a5e..e71e09ca9351 100644 --- a/qiskit/circuit/library/standard_gates/rxx.py +++ b/qiskit/circuit/library/standard_gates/rxx.py @@ -24,6 +24,9 @@ class RXXGate(Gate): This gate is symmetric, and is maximally entangling at :math:`\theta = \pi/2`. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.rxx` method. + **Circuit Symbol:** .. parsed-literal:: diff --git a/qiskit/circuit/library/standard_gates/ry.py b/qiskit/circuit/library/standard_gates/ry.py index cacb7174a388..f96d75958eb7 100644 --- a/qiskit/circuit/library/standard_gates/ry.py +++ b/qiskit/circuit/library/standard_gates/ry.py @@ -25,6 +25,9 @@ class RYGate(Gate): r"""Single-qubit rotation about the Y axis. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.ry` method. + **Circuit symbol:** .. parsed-literal:: @@ -106,6 +109,9 @@ def __array__(self, dtype=None): class CRYGate(ControlledGate): r"""Controlled-RY gate. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.cry` method. + **Circuit symbol:** .. parsed-literal:: diff --git a/qiskit/circuit/library/standard_gates/ryy.py b/qiskit/circuit/library/standard_gates/ryy.py index 1299b7381266..343e32947635 100644 --- a/qiskit/circuit/library/standard_gates/ryy.py +++ b/qiskit/circuit/library/standard_gates/ryy.py @@ -24,6 +24,9 @@ class RYYGate(Gate): This gate is symmetric, and is maximally entangling at :math:`\theta = \pi/2`. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.ryy` method. + **Circuit Symbol:** .. parsed-literal:: diff --git a/qiskit/circuit/library/standard_gates/rz.py b/qiskit/circuit/library/standard_gates/rz.py index f45f30606d41..32fdc5be157c 100644 --- a/qiskit/circuit/library/standard_gates/rz.py +++ b/qiskit/circuit/library/standard_gates/rz.py @@ -25,6 +25,9 @@ class RZGate(Gate): This is a diagonal gate. It can be implemented virtually in hardware via framechanges (i.e. at zero error and duration). + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.rz` method. + **Circuit symbol:** .. parsed-literal:: @@ -121,6 +124,9 @@ class CRZGate(ControlledGate): This is a diagonal but non-symmetric gate that induces a phase on the state of the target qubit, depending on the control state. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.crz` method. + **Circuit symbol:** .. parsed-literal:: diff --git a/qiskit/circuit/library/standard_gates/rzx.py b/qiskit/circuit/library/standard_gates/rzx.py index 4729f3b5b340..f1e25dc115b3 100644 --- a/qiskit/circuit/library/standard_gates/rzx.py +++ b/qiskit/circuit/library/standard_gates/rzx.py @@ -26,6 +26,9 @@ class RZXGate(Gate): The cross-resonance gate (CR) for superconducting qubits implements a ZX interaction (however other terms are also present in an experiment). + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.rzx` method. + **Circuit Symbol:** .. parsed-literal:: diff --git a/qiskit/circuit/library/standard_gates/rzz.py b/qiskit/circuit/library/standard_gates/rzz.py index c367f6b902c3..cd7bf27092e1 100644 --- a/qiskit/circuit/library/standard_gates/rzz.py +++ b/qiskit/circuit/library/standard_gates/rzz.py @@ -23,6 +23,9 @@ class RZZGate(Gate): This gate is symmetric, and is maximally entangling at :math:`\theta = \pi/2`. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.rzz` method. + **Circuit Symbol:** .. parsed-literal:: diff --git a/qiskit/circuit/library/standard_gates/s.py b/qiskit/circuit/library/standard_gates/s.py index a5e8d5ce7348..a9c2d96555a4 100644 --- a/qiskit/circuit/library/standard_gates/s.py +++ b/qiskit/circuit/library/standard_gates/s.py @@ -26,6 +26,9 @@ class SGate(Gate): This is a Clifford gate and a square-root of Pauli-Z. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.s` method. + **Matrix Representation:** .. math:: @@ -82,6 +85,9 @@ class SdgGate(Gate): This is a Clifford gate and a square-root of Pauli-Z. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.sdg` method. + **Matrix Representation:** .. math:: @@ -99,7 +105,7 @@ class SdgGate(Gate): q_0: ┤ Sdg ├ └─────┘ - Equivalent to a :math:`\pi/2` radian rotation about the Z axis. + Equivalent to a :math:`-\pi/2` radian rotation about the Z axis. """ def __init__(self, label: Optional[str] = None): diff --git a/qiskit/circuit/library/standard_gates/swap.py b/qiskit/circuit/library/standard_gates/swap.py index db2710a37cbd..9e317c8eb65f 100644 --- a/qiskit/circuit/library/standard_gates/swap.py +++ b/qiskit/circuit/library/standard_gates/swap.py @@ -24,6 +24,9 @@ class SwapGate(Gate): This is a symmetric and Clifford gate. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.swap` method. + **Circuit symbol:** .. parsed-literal:: @@ -112,6 +115,10 @@ def __array__(self, dtype=None): class CSwapGate(ControlledGate): r"""Controlled-SWAP gate, also known as the Fredkin gate. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.cswap` and + :meth:`~qiskit.circuit.QuantumCircuit.fredkin` methods. + **Circuit symbol:** .. parsed-literal:: diff --git a/qiskit/circuit/library/standard_gates/sx.py b/qiskit/circuit/library/standard_gates/sx.py index 5c27b15e8faf..84a8fc52272c 100644 --- a/qiskit/circuit/library/standard_gates/sx.py +++ b/qiskit/circuit/library/standard_gates/sx.py @@ -23,6 +23,9 @@ class SXGate(Gate): r"""The single-qubit Sqrt(X) gate (:math:`\sqrt{X}`). + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.sx` method. + **Matrix Representation:** .. math:: @@ -112,6 +115,9 @@ def __array__(self, dtype=None): class SXdgGate(Gate): r"""The inverse single-qubit Sqrt(X) gate. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.sxdg` method. + .. math:: \sqrt{X}^{\dagger} = \frac{1}{2} \begin{pmatrix} @@ -167,6 +173,9 @@ def __array__(self, dtype=None): class CSXGate(ControlledGate): r"""Controlled-√X gate. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.csx` method. + **Circuit symbol:** .. parsed-literal:: diff --git a/qiskit/circuit/library/standard_gates/t.py b/qiskit/circuit/library/standard_gates/t.py index b433aeff43f5..9fddfa008e4c 100644 --- a/qiskit/circuit/library/standard_gates/t.py +++ b/qiskit/circuit/library/standard_gates/t.py @@ -27,6 +27,9 @@ class TGate(Gate): This is a non-Clifford gate and a fourth-root of Pauli-Z. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.t` method. + **Matrix Representation:** .. math:: @@ -83,6 +86,9 @@ class TdgGate(Gate): This is a non-Clifford gate and a fourth-root of Pauli-Z. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.tdg` method. + **Matrix Representation:** .. math:: @@ -100,7 +106,7 @@ class TdgGate(Gate): q_0: ┤ Tdg ├ └─────┘ - Equivalent to a :math:`\pi/2` radian rotation about the Z axis. + Equivalent to a :math:`-\pi/4` radian rotation about the Z axis. """ def __init__(self, label: Optional[str] = None): diff --git a/qiskit/circuit/library/standard_gates/u.py b/qiskit/circuit/library/standard_gates/u.py index 93139df849d3..bb3e26495678 100644 --- a/qiskit/circuit/library/standard_gates/u.py +++ b/qiskit/circuit/library/standard_gates/u.py @@ -25,6 +25,9 @@ class UGate(Gate): r"""Generic single-qubit rotation gate with 3 Euler angles. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.u` method. + **Circuit symbol:** .. parsed-literal:: @@ -131,6 +134,9 @@ class CUGate(ControlledGate): This is a controlled version of the U gate (generic single qubit rotation), including a possible global phase :math:`e^{i\gamma}` of the U gate. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.cu` method. + **Circuit symbol:** .. parsed-literal:: diff --git a/qiskit/circuit/library/standard_gates/x.py b/qiskit/circuit/library/standard_gates/x.py index c3d050bee5e2..cc6eebc2f96e 100644 --- a/qiskit/circuit/library/standard_gates/x.py +++ b/qiskit/circuit/library/standard_gates/x.py @@ -30,6 +30,9 @@ class XGate(Gate): r"""The single-qubit Pauli-X gate (:math:`\sigma_x`). + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.x` method. + **Matrix Representation:** .. math:: @@ -125,6 +128,10 @@ def __array__(self, dtype=None): class CXGate(ControlledGate): r"""Controlled-X gate. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.cx` and + :meth:`~qiskit.circuit.QuantumCircuit.cnot` methods. + **Circuit symbol:** .. parsed-literal:: @@ -254,6 +261,10 @@ def __array__(self, dtype=None): class CCXGate(ControlledGate): r"""CCX gate, also known as Toffoli gate. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.ccx` and + :meth:`~qiskit.circuit.QuantumCircuit.toffoli` methods. + **Circuit symbol:** .. parsed-literal:: @@ -412,6 +423,9 @@ class RCCXGate(Gate): This concrete implementation is from https://arxiv.org/abs/1508.03273, the dashed box of Fig. 3. + + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.rccx` method. """ def __init__(self, label: Optional[str] = None): @@ -691,6 +705,9 @@ class RC3XGate(Gate): This concrete implementation is from https://arxiv.org/abs/1508.03273, the complete circuit of Fig. 4. + + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.rcccx` method. """ def __init__(self, label: Optional[str] = None): @@ -880,7 +897,11 @@ def __array__(self, dtype=None): class MCXGate(ControlledGate): - """The general, multi-controlled X gate.""" + """The general, multi-controlled X gate. + + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.mcx` method. + """ def __new__( cls, diff --git a/qiskit/circuit/library/standard_gates/y.py b/qiskit/circuit/library/standard_gates/y.py index a1006d7dc781..57c48ee6aad9 100644 --- a/qiskit/circuit/library/standard_gates/y.py +++ b/qiskit/circuit/library/standard_gates/y.py @@ -25,6 +25,9 @@ class YGate(Gate): r"""The single-qubit Pauli-Y gate (:math:`\sigma_y`). + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.y` method. + **Matrix Representation:** .. math:: @@ -119,6 +122,9 @@ def __array__(self, dtype=complex): class CYGate(ControlledGate): r"""Controlled-Y gate. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.cy` method. + **Circuit symbol:** .. parsed-literal:: diff --git a/qiskit/circuit/library/standard_gates/z.py b/qiskit/circuit/library/standard_gates/z.py index 491f173984bf..df57352e869a 100644 --- a/qiskit/circuit/library/standard_gates/z.py +++ b/qiskit/circuit/library/standard_gates/z.py @@ -23,6 +23,9 @@ class ZGate(Gate): r"""The single-qubit Pauli-Z gate (:math:`\sigma_z`). + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.z` method. + **Matrix Representation:** .. math:: @@ -119,6 +122,9 @@ class CZGate(ControlledGate): This is a Clifford and symmetric gate. + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.cz` method. + **Circuit symbol:** .. parsed-literal:: From aa6d359612fe58e000368bf0ad3a64f7c40e360b Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Mon, 25 Jul 2022 14:45:51 +0200 Subject: [PATCH 11/82] Sphinx 5.1 breaks doc building (#8392) --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index eef50c6b96e1..abd450fce01e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -19,7 +19,7 @@ pylatexenc>=1.4 ddt>=1.2.0,!=1.4.0,!=1.4.3 seaborn>=0.9.0 reno>=3.4.0 -Sphinx>=3.0.0 +Sphinx>=3.0.0,<5.1 qiskit-sphinx-theme>=1.6 qiskit-toqm>=0.0.4;platform_machine != 'aarch64' or platform_system != 'Linux' sphinx-autodoc-typehints<1.14.0 From f8681295ddaf732b4ce620b38897504de557f012 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 25 Jul 2022 11:12:46 -0400 Subject: [PATCH 12/82] Fix handling of numpy arrays for indices in marginal_distribution (#8288) * Fix handling of numpy arrays for indices in marginal_distribution This commit fixes an issue with the marginal_distribution() function where it would previously error if a numpy arrray of ints was passed in for the indices parameter. This was caused by the input validation looking for empty input by checking `not indices` which isn't valid for numpy arrays. This commit fixes this by adjusting the check for empty lists to work with any sequence input. Fixes #8283 * Update releasenotes/notes/fix-numpy-indices-marginal-dist-45889e49ba337d84.yaml Co-authored-by: Jim Garrison Co-authored-by: Jim Garrison --- qiskit/result/utils.py | 8 +++++--- ...x-numpy-indices-marginal-dist-45889e49ba337d84.yaml | 7 +++++++ test/python/result/test_counts.py | 10 ++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/fix-numpy-indices-marginal-dist-45889e49ba337d84.yaml diff --git a/qiskit/result/utils.py b/qiskit/result/utils.py index 498798bbe94b..d0fb018e8a58 100644 --- a/qiskit/result/utils.py +++ b/qiskit/result/utils.py @@ -14,7 +14,7 @@ """Utility functions for working with Results.""" -from typing import List, Union, Optional, Dict +from typing import Sequence, Union, Optional, Dict, List from collections import Counter from copy import deepcopy @@ -199,7 +199,9 @@ def marginal_memory( def marginal_distribution( - counts: dict, indices: Optional[List[int]] = None, format_marginal: bool = False + counts: dict, + indices: Optional[Sequence[int]] = None, + format_marginal: bool = False, ) -> Dict[str, int]: """Marginalize counts from an experiment over some indices of interest. @@ -222,7 +224,7 @@ def marginal_distribution( is invalid. """ num_clbits = len(max(counts.keys()).replace(" ", "")) - if indices is not None and (not indices or not set(indices).issubset(range(num_clbits))): + if indices is not None and (len(indices) == 0 or not set(indices).issubset(range(num_clbits))): raise QiskitError(f"indices must be in range [0, {num_clbits - 1}].") if isinstance(counts, Counts): diff --git a/releasenotes/notes/fix-numpy-indices-marginal-dist-45889e49ba337d84.yaml b/releasenotes/notes/fix-numpy-indices-marginal-dist-45889e49ba337d84.yaml new file mode 100644 index 000000000000..713a22b1f9ff --- /dev/null +++ b/releasenotes/notes/fix-numpy-indices-marginal-dist-45889e49ba337d84.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixed an issue with the :func:`~.marginal_distribution` function: when + a numpy array was passed in for the ``indices`` argument the function would + raise an error. + Fixed `#8283 `__ diff --git a/test/python/result/test_counts.py b/test/python/result/test_counts.py index 33d93c25e81d..6fa52b92917a 100644 --- a/test/python/result/test_counts.py +++ b/test/python/result/test_counts.py @@ -16,6 +16,8 @@ import unittest +import numpy as np + from qiskit.result import counts from qiskit import exceptions from qiskit.result import utils @@ -50,6 +52,14 @@ def test_marginal_distribution(self): result = utils.marginal_distribution(counts_obj, [0, 1]) self.assertEqual(expected, result) + def test_marginal_distribution_numpy_indices(self): + raw_counts = {"0x0": 4, "0x1": 7, "0x2": 10, "0x6": 5, "0x9": 11, "0xD": 9, "0xE": 8} + expected = {"00": 4, "01": 27, "10": 23} + indices = np.asarray([0, 1]) + counts_obj = counts.Counts(raw_counts, creg_sizes=[["c0", 4]], memory_slots=4) + result = utils.marginal_distribution(counts_obj, indices) + self.assertEqual(expected, result) + def test_int_outcomes(self): raw_counts = {"0x0": 21, "0x2": 12, "0x3": 5, "0x2E": 265} expected = {0: 21, 2: 12, 3: 5, 46: 265} From 35bb826938f10fcffea0b6f54c92e9d6ceb02a16 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Tue, 26 Jul 2022 13:09:48 +0200 Subject: [PATCH 13/82] Add explanatory reference to ``PiecewiseChebychev`` (#8393) * Add explanatort reference to ``PiecewiseChebychev`` and update other locations with published version * fix name * Fix links Co-authored-by: Matthew Treinish Co-authored-by: Matthew Treinish Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/algorithms/linear_solvers/hhl.py | 10 +++++----- .../circuit/library/arithmetic/piecewise_chebyshev.py | 6 +++++- .../arithmetic/piecewise_polynomial_pauli_rotations.py | 4 ++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/qiskit/algorithms/linear_solvers/hhl.py b/qiskit/algorithms/linear_solvers/hhl.py index 71b954148de2..6e59de4babcb 100644 --- a/qiskit/algorithms/linear_solvers/hhl.py +++ b/qiskit/algorithms/linear_solvers/hhl.py @@ -87,12 +87,12 @@ class HHL(LinearSolver): References: [1]: Harrow, A. W., Hassidim, A., Lloyd, S. (2009). - Quantum algorithm for linear systems of equations. - `Phys. Rev. Lett. 103, 15 (2009), 1–15. `_ + Quantum algorithm for linear systems of equations. + `Phys. Rev. Lett. 103, 15 (2009), 1–15. `_ - [2]: Carrera Vazquez, A., Hiptmair, R., & Woerner, S. (2020). - Enhancing the Quantum Linear Systems Algorithm using Richardson Extrapolation. - `arXiv:2009.04484 `_ + [2]: Carrera Vazquez, A., Hiptmair, R., & Woerner, S. (2022). + Enhancing the Quantum Linear Systems Algorithm Using Richardson Extrapolation. + `ACM Transactions on Quantum Computing 3, 1, Article 2 `_ """ diff --git a/qiskit/circuit/library/arithmetic/piecewise_chebyshev.py b/qiskit/circuit/library/arithmetic/piecewise_chebyshev.py index 27bd25ec6cef..9c3e57b19a32 100644 --- a/qiskit/circuit/library/arithmetic/piecewise_chebyshev.py +++ b/qiskit/circuit/library/arithmetic/piecewise_chebyshev.py @@ -29,7 +29,8 @@ class PiecewiseChebyshev(BlueprintCircuit): polynomial Chebyshev approximation on :math:`n` qubits to :math:`f(x)` on the given intervals. All the polynomials in the approximation are of degree :math:`d`. - The values of the parameters are calculated according to [1]. + The values of the parameters are calculated according to [1] and see [2] for a more + detailed explanation of the circuit construction and how it acts on the qubits. Examples: @@ -51,6 +52,9 @@ class PiecewiseChebyshev(BlueprintCircuit): [1]: Haener, T., Roetteler, M., & Svore, K. M. (2018). Optimizing Quantum Circuits for Arithmetic. `arXiv:1805.12445 `_ + [2]: Carrera Vazquez, A., Hiptmair, H., & Woerner, S. (2022). + Enhancing the Quantum Linear Systems Algorithm Using Richardson Extrapolation. + `ACM Transactions on Quantum Computing 3, 1, Article 2 `_ """ def __init__( diff --git a/qiskit/circuit/library/arithmetic/piecewise_polynomial_pauli_rotations.py b/qiskit/circuit/library/arithmetic/piecewise_polynomial_pauli_rotations.py index 831fcef8424e..736a2cf76118 100644 --- a/qiskit/circuit/library/arithmetic/piecewise_polynomial_pauli_rotations.py +++ b/qiskit/circuit/library/arithmetic/piecewise_polynomial_pauli_rotations.py @@ -81,9 +81,9 @@ class PiecewisePolynomialPauliRotations(FunctionalPauliRotations): Optimizing Quantum Circuits for Arithmetic. `arXiv:1805.12445 `_ - [2]: Carrera Vazquez, A., Hiptmair, R., & Woerner, S. (2020). + [2]: Carrera Vazquez, A., Hiptmair, R., & Woerner, S. (2022). Enhancing the Quantum Linear Systems Algorithm using Richardson Extrapolation. - `arXiv:2009.04484 `_ + `ACM Transactions on Quantum Computing 3, 1, Article 2 `_ """ def __init__( From 28802cf0575c0284bbbe7d2eea093501d21904d3 Mon Sep 17 00:00:00 2001 From: Polly Shaw Date: Wed, 27 Jul 2022 02:57:56 +0100 Subject: [PATCH 14/82] Fixes #7078: Change prefix of MemorySlot channel in Qasm Instructions (#8166) * Fixes #7078: Add is_schedulable property to qiskit.pulse.Channel and don't apply a delay to it if True RegisterSlot and MemorySlot have this set to False. The default implementation in Channel sets it to True. * Fixes #7078: Introduce ClassicalIOChannel as a subclass of pulse.Channel RegisterSlot, MemorySlot and SnapshotChannel now derive from this class. There are some tests to fix. * Fixes #7078: Make Acquire.channels only return the channel * Fixes #7078: Execute black * Fixes #7078: Put slots in new tuple in acquire * Fixes #7078: Add is_schedulable property to qiskit.pulse.Channel and don't apply a delay to it if True RegisterSlot and MemorySlot have this set to False. The default implementation in Channel sets it to True. * Fixes #7078: Remove is_schedulable property from qiskit.pulse.channel * Fixes #7078: Put checks in SetPhase, ShiftPhase and SetPhase. Remove check from Delay. * Fixes #7078: Add unit tests for restrictions on Classical IO channels * Fixes #7078: Add unit tests for abstract nature of ClassicalIOChannel * Fixes #7078: Tidying after self-review. * Fixes #7078: Add release note. * Fixes #7078: remove typo Co-authored-by: Naoki Kanazawa --- qiskit/pulse/channels.py | 12 +++- qiskit/pulse/instructions/frequency.py | 14 ++++- qiskit/pulse/instructions/phase.py | 15 ++++- qiskit/pulse/instructions/play.py | 2 +- qiskit/pulse/transforms/canonicalization.py | 3 + ...classical-io-channel-0a616e6ca75b7687.yaml | 13 ++++ test/python/pulse/test_channels.py | 17 ++++- test/python/pulse/test_instructions.py | 63 +++++++++++++++++++ test/python/pulse/test_transforms.py | 25 +++++++- 9 files changed, 155 insertions(+), 9 deletions(-) create mode 100644 releasenotes/notes/introduce-classical-io-channel-0a616e6ca75b7687.yaml diff --git a/qiskit/pulse/channels.py b/qiskit/pulse/channels.py index 175d93ef8e87..c88fddadd77b 100644 --- a/qiskit/pulse/channels.py +++ b/qiskit/pulse/channels.py @@ -165,6 +165,12 @@ class PulseChannel(Channel, metaclass=ABCMeta): pass +class ClassicalIOChannel(Channel, metaclass=ABCMeta): + """Base class of classical IO channels. These cannot have instructions scheduled on them.""" + + pass + + class DriveChannel(PulseChannel): """Drive channels transmit signals to qubits which enact gate operations.""" @@ -192,7 +198,7 @@ class AcquireChannel(Channel): prefix = "a" -class SnapshotChannel(Channel): +class SnapshotChannel(ClassicalIOChannel): """Snapshot channels are used to specify instructions for simulators.""" prefix = "s" @@ -202,13 +208,13 @@ def __init__(self): super().__init__(0) -class MemorySlot(Channel): +class MemorySlot(ClassicalIOChannel): """Memory slot channels represent classical memory storage.""" prefix = "m" -class RegisterSlot(Channel): +class RegisterSlot(ClassicalIOChannel): """Classical resister slot channels represent classical registers (low-latency classical memory). """ diff --git a/qiskit/pulse/instructions/frequency.py b/qiskit/pulse/instructions/frequency.py index 09233080b593..15866e0ae79a 100644 --- a/qiskit/pulse/instructions/frequency.py +++ b/qiskit/pulse/instructions/frequency.py @@ -16,7 +16,7 @@ from typing import Optional, Union, Tuple from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.pulse.channels import PulseChannel +from qiskit.pulse.channels import PulseChannel, PulseError from qiskit.pulse.instructions.instruction import Instruction @@ -46,7 +46,13 @@ def __init__( frequency: New frequency of the channel in Hz. channel: The channel this instruction operates on. name: Name of this set channel frequency instruction. + Raises: + PulseError: If channel is not a PulseChannel. """ + if not isinstance(channel, PulseChannel): + raise PulseError( + "The `channel` argument to `SetFrequency` must be of type `channels.PulseChannel`." + ) if not isinstance(frequency, ParameterExpression): frequency = float(frequency) super().__init__(operands=(frequency, channel), name=name) @@ -93,9 +99,15 @@ def __init__( frequency: Frequency shift of the channel in Hz. channel: The channel this instruction operates on. name: Name of this set channel frequency instruction. + Raises: + PulseError: If channel is not a PulseChannel. """ if not isinstance(frequency, ParameterExpression): frequency = float(frequency) + if not isinstance(channel, PulseChannel): + raise PulseError( + "The `channel` argument to `ShiftFrequency` must be of type `channels.PulseChannel`." + ) super().__init__(operands=(frequency, channel), name=name) @property diff --git a/qiskit/pulse/instructions/phase.py b/qiskit/pulse/instructions/phase.py index b9067486d923..d8db037a5aa1 100644 --- a/qiskit/pulse/instructions/phase.py +++ b/qiskit/pulse/instructions/phase.py @@ -18,7 +18,7 @@ from typing import Optional, Union, Tuple from qiskit.circuit import ParameterExpression -from qiskit.pulse.channels import PulseChannel +from qiskit.pulse.channels import PulseChannel, PulseError from qiskit.pulse.instructions.instruction import Instruction @@ -52,7 +52,14 @@ def __init__( phase: The rotation angle in radians. channel: The channel this instruction operates on. name: Display name for this instruction. + + Raises: + PulseError: If channel is not a PulseChannel. """ + if not isinstance(channel, PulseChannel): + raise PulseError( + "The `channel` argument to `ShiftPhase` must be of type `channels.PulseChannel`." + ) super().__init__(operands=(phase, channel), name=name) @property @@ -108,7 +115,13 @@ def __init__( phase: The rotation angle in radians. channel: The channel this instruction operates on. name: Display name for this instruction. + Raises: + PulseError: If channel is not a PulseChannel. """ + if not isinstance(channel, PulseChannel): + raise PulseError( + "The `channel` argument to `SetPhase` must be of type `channels.PulseChannel`." + ) super().__init__(operands=(phase, channel), name=name) @property diff --git a/qiskit/pulse/instructions/play.py b/qiskit/pulse/instructions/play.py index 2a8536643e76..41effc205d83 100644 --- a/qiskit/pulse/instructions/play.py +++ b/qiskit/pulse/instructions/play.py @@ -41,7 +41,7 @@ def __init__(self, pulse: Pulse, channel: PulseChannel, name: Optional[str] = No name: Name of the instruction for display purposes. Defaults to ``pulse.name``. Raises: - PulseError: If pulse is not a Pulse type. + PulseError: If pulse is not a Pulse type, or channel is not a PulseChannel. """ if not isinstance(pulse, Pulse): raise PulseError("The `pulse` argument to `Play` must be of type `library.Pulse`.") diff --git a/qiskit/pulse/transforms/canonicalization.py b/qiskit/pulse/transforms/canonicalization.py index a019de2eda27..4e2409ffa354 100644 --- a/qiskit/pulse/transforms/canonicalization.py +++ b/qiskit/pulse/transforms/canonicalization.py @@ -18,6 +18,7 @@ import numpy as np from qiskit.pulse import channels as chans, exceptions, instructions +from qiskit.pulse.channels import ClassicalIOChannel from qiskit.pulse.exceptions import PulseError from qiskit.pulse.exceptions import UnassignedDurationError from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap @@ -475,6 +476,8 @@ def pad( channels = channels or schedule.channels for channel in channels: + if isinstance(channel, ClassicalIOChannel): + continue if channel not in schedule.channels: schedule |= instructions.Delay(until, channel) continue diff --git a/releasenotes/notes/introduce-classical-io-channel-0a616e6ca75b7687.yaml b/releasenotes/notes/introduce-classical-io-channel-0a616e6ca75b7687.yaml new file mode 100644 index 000000000000..71a79556a4c8 --- /dev/null +++ b/releasenotes/notes/introduce-classical-io-channel-0a616e6ca75b7687.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + In the qiskit.pulse.channels package, ClassicalIOChannel class has been added + as an abstract base class of MemorySlot, RegisterSlot, and SnapshotChannel. + + The qiskit.pulse.transforms.canonicalization.pad method does not introduce + delays to any channels which are instances of ClassicalIOChannel. + + In qiskit.pulse.instructions, the constructors to SetPhase, ShiftPhase, + SetFrequency and ShiftFrequency now throw a PulseError if the channel parameter + is not of type PulseChannel. + diff --git a/test/python/pulse/test_channels.py b/test/python/pulse/test_channels.py index 3637a1cea121..604202fb5754 100644 --- a/test/python/pulse/test_channels.py +++ b/test/python/pulse/test_channels.py @@ -17,8 +17,9 @@ from qiskit.pulse.channels import ( AcquireChannel, Channel, - DriveChannel, + ClassicalIOChannel, ControlChannel, + DriveChannel, MeasureChannel, MemorySlot, PulseChannel, @@ -67,8 +68,17 @@ def test_channel_hash(self): self.assertEqual(hash_1, hash_2) +class TestClassicalIOChannel(QiskitTestCase): + """Test base classical IO channel.""" + + def test_cannot_be_instantiated(self): + """Test base classical IO channel cannot be instantiated.""" + with self.assertRaises(NotImplementedError): + ClassicalIOChannel(0) + + class TestMemorySlot(QiskitTestCase): - """AcquireChannel tests.""" + """MemorySlot tests.""" def test_default(self): """Test default memory slot.""" @@ -76,6 +86,7 @@ def test_default(self): self.assertEqual(memory_slot.index, 123) self.assertEqual(memory_slot.name, "m123") + self.assertTrue(isinstance(memory_slot, ClassicalIOChannel)) class TestRegisterSlot(QiskitTestCase): @@ -87,6 +98,7 @@ def test_default(self): self.assertEqual(register_slot.index, 123) self.assertEqual(register_slot.name, "c123") + self.assertTrue(isinstance(register_slot, ClassicalIOChannel)) class TestSnapshotChannel(QiskitTestCase): @@ -98,6 +110,7 @@ def test_default(self): self.assertEqual(snapshot_channel.index, 0) self.assertEqual(snapshot_channel.name, "s0") + self.assertTrue(isinstance(snapshot_channel, ClassicalIOChannel)) class TestDriveChannel(QiskitTestCase): diff --git a/test/python/pulse/test_instructions.py b/test/python/pulse/test_instructions.py index a5284c27ed2f..111ebd35f4ae 100644 --- a/test/python/pulse/test_instructions.py +++ b/test/python/pulse/test_instructions.py @@ -156,6 +156,64 @@ def test_freq(self): ) self.assertEqual(repr(set_freq), "SetFrequency(4500000000.0, DriveChannel(1), name='test')") + def test_freq_non_pulse_channel(self): + """Test set frequency constructor with illegal channel""" + with self.assertRaises(exceptions.PulseError): + instructions.SetFrequency(4.5e9, channels.RegisterSlot(1), name="test") + + +class TestShiftFrequency(QiskitTestCase): + """Shift frequency tests.""" + + def test_shift_freq(self): + """Test shift frequency basic functionality.""" + shift_freq = instructions.ShiftFrequency(4.5e9, channels.DriveChannel(1), name="test") + + self.assertIsInstance(shift_freq.id, int) + self.assertEqual(shift_freq.duration, 0) + self.assertEqual(shift_freq.frequency, 4.5e9) + self.assertEqual(shift_freq.operands, (4.5e9, channels.DriveChannel(1))) + self.assertEqual( + shift_freq, instructions.ShiftFrequency(4.5e9, channels.DriveChannel(1), name="test") + ) + self.assertNotEqual( + shift_freq, instructions.ShiftFrequency(4.5e8, channels.DriveChannel(1), name="test") + ) + self.assertEqual( + repr(shift_freq), "ShiftFrequency(4500000000.0, DriveChannel(1), name='test')" + ) + + def test_freq_non_pulse_channel(self): + """Test shift frequency constructor with illegal channel""" + with self.assertRaises(exceptions.PulseError): + instructions.ShiftFrequency(4.5e9, channels.RegisterSlot(1), name="test") + + +class TestSetPhase(QiskitTestCase): + """Test the instruction construction.""" + + def test_default(self): + """Test basic SetPhase.""" + set_phase = instructions.SetPhase(1.57, channels.DriveChannel(0)) + + self.assertIsInstance(set_phase.id, int) + self.assertEqual(set_phase.name, None) + self.assertEqual(set_phase.duration, 0) + self.assertEqual(set_phase.phase, 1.57) + self.assertEqual(set_phase.operands, (1.57, channels.DriveChannel(0))) + self.assertEqual( + set_phase, instructions.SetPhase(1.57, channels.DriveChannel(0), name="test") + ) + self.assertNotEqual( + set_phase, instructions.SetPhase(1.57j, channels.DriveChannel(0), name="test") + ) + self.assertEqual(repr(set_phase), "SetPhase(1.57, DriveChannel(0))") + + def test_set_phase_non_pulse_channel(self): + """Test shift phase constructor with illegal channel""" + with self.assertRaises(exceptions.PulseError): + instructions.SetPhase(1.57, channels.RegisterSlot(1), name="test") + class TestShiftPhase(QiskitTestCase): """Test the instruction construction.""" @@ -177,6 +235,11 @@ def test_default(self): ) self.assertEqual(repr(shift_phase), "ShiftPhase(1.57, DriveChannel(0))") + def test_shift_phase_non_pulse_channel(self): + """Test shift phase constructor with illegal channel""" + with self.assertRaises(exceptions.PulseError): + instructions.ShiftPhase(1.57, channels.RegisterSlot(1), name="test") + class TestSnapshot(QiskitTestCase): """Snapshot tests.""" diff --git a/test/python/pulse/test_transforms.py b/test/python/pulse/test_transforms.py index 49038afdb096..e19d024c2b4f 100644 --- a/test/python/pulse/test_transforms.py +++ b/test/python/pulse/test_transforms.py @@ -29,7 +29,13 @@ Constant, ) from qiskit.pulse import transforms, instructions -from qiskit.pulse.channels import MemorySlot, DriveChannel, AcquireChannel +from qiskit.pulse.channels import ( + MemorySlot, + DriveChannel, + AcquireChannel, + RegisterSlot, + SnapshotChannel, +) from qiskit.pulse.instructions import directives from qiskit.test import QiskitTestCase from qiskit.providers.fake_provider import FakeOpenPulse2Q @@ -349,6 +355,23 @@ def test_padding_prepended_delay(self): self.assertEqual(transforms.pad(sched, until=30, inplace=True), ref_sched) + def test_pad_no_delay_on_classical_io_channels(self): + """Test padding does not apply to classical IO channels.""" + delay = 10 + sched = ( + Delay(delay, MemorySlot(0)).shift(20) + + Delay(delay, RegisterSlot(0)).shift(10) + + Delay(delay, SnapshotChannel()) + ) + + ref_sched = ( + Delay(delay, MemorySlot(0)).shift(20) + + Delay(delay, RegisterSlot(0)).shift(10) + + Delay(delay, SnapshotChannel()) + ) + + self.assertEqual(transforms.pad(sched, until=15), ref_sched) + def get_pulse_ids(schedules: List[Schedule]) -> Set[int]: """Returns ids of pulses used in Schedules.""" From f8f638a686c3c617b3d8345b2f3e70fd9f928980 Mon Sep 17 00:00:00 2001 From: Guillermo-Mijares-Vilarino <106545082+Guillermo-Mijares-Vilarino@users.noreply.github.com> Date: Wed, 27 Jul 2022 12:23:28 +0200 Subject: [PATCH 15/82] Added U1, U2 and U3 deprecation warnings into Circuit Library (#8391) * Added U1, U2 and U3 deprecation warnings in Circuit Library * corrected parentheses and removed cdots * Remove cdots Co-authored-by: Julien Gacon * moved formulas to examples * Add linebreak Co-authored-by: Julien Gacon * Fix sphinx identifier * corrected global phases Co-authored-by: Julien Gacon Co-authored-by: Luciano Bello Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/circuit/library/standard_gates/u1.py | 16 +++++++++++ qiskit/circuit/library/standard_gates/u2.py | 30 +++++++++++++++++++-- qiskit/circuit/library/standard_gates/u3.py | 18 +++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/qiskit/circuit/library/standard_gates/u1.py b/qiskit/circuit/library/standard_gates/u1.py index 0f105aeea1e4..e39c9cc5f46b 100644 --- a/qiskit/circuit/library/standard_gates/u1.py +++ b/qiskit/circuit/library/standard_gates/u1.py @@ -27,6 +27,22 @@ class U1Gate(Gate): This is a diagonal gate. It can be implemented virtually in hardware via framechanges (i.e. at zero error and duration). + .. warning:: + + This gate is deprecated. Instead, the following replacements should be used + + .. math:: + + U1(\lambda) = P(\lambda)= U(0,0,\lambda) + + .. code-block:: python + + circuit = QuantumCircuit(1) + circuit.p(lambda, 0) # or circuit.u(0, 0, lambda) + + + + **Circuit symbol:** .. parsed-literal:: diff --git a/qiskit/circuit/library/standard_gates/u2.py b/qiskit/circuit/library/standard_gates/u2.py index 864594a4c107..fa0f00f790c5 100644 --- a/qiskit/circuit/library/standard_gates/u2.py +++ b/qiskit/circuit/library/standard_gates/u2.py @@ -26,8 +26,20 @@ class U2Gate(Gate): Implemented using one X90 pulse on IBM Quantum systems: - .. math:: - U2(\phi, \lambda) = RZ(\phi).RY(\frac{\pi}{2}).RZ(\lambda) + .. warning:: + + This gate is deprecated. Instead, the following replacements should be used + + .. math:: + + U2(\phi, \lambda) = U\left(\frac{\pi}{2}, \phi, \lambda\right) + + .. code-block:: python + + circuit = QuantumCircuit(1) + circuit.u(pi/2, phi, lambda) + + **Circuit symbol:** @@ -49,11 +61,25 @@ class U2Gate(Gate): **Examples:** + .. math:: + + U2(\phi,\lambda) = e^{i \frac{\phi + \lambda}{2}}RZ(\phi) + RY\left(\frac{\pi}{2}\right) RZ(\lambda) + = e^{- i\frac{\pi}{4}} P\left(\frac{\pi}{2} + \phi\right) + \sqrt{X} P\left(\lambda- \frac{\pi}{2}\right) + .. math:: U2(0, \pi) = H + + .. math:: + U2(0, 0) = RY(\pi/2) + + .. math:: + U2(-\pi/2, \pi/2) = RX(\pi/2) + .. seealso:: :class:`~qiskit.circuit.library.standard_gates.U3Gate`: diff --git a/qiskit/circuit/library/standard_gates/u3.py b/qiskit/circuit/library/standard_gates/u3.py index 647d641dd00e..ba9d8cf42555 100644 --- a/qiskit/circuit/library/standard_gates/u3.py +++ b/qiskit/circuit/library/standard_gates/u3.py @@ -24,6 +24,19 @@ class U3Gate(Gate): r"""Generic single-qubit rotation gate with 3 Euler angles. + .. warning:: + + This gate is deprecated. Instead, the following replacements should be used + + .. math:: + + U3(\theta, \phi, \lambda) = U(\theta, \phi, \lambda) + + .. code-block:: python + + circuit = QuantumCircuit(1) + circuit.u(theta, phi, lambda) + **Circuit symbol:** .. parsed-literal:: @@ -52,6 +65,11 @@ class U3Gate(Gate): **Examples:** + .. math:: + + U3(\theta, \phi, \lambda) = e^{-i \frac{\pi + \theta}{2}} P(\phi + \pi) \sqrt{X} + P(\theta + \pi) \sqrt{X} P(\lambda) + .. math:: U3\left(\theta, -\frac{\pi}{2}, \frac{\pi}{2}\right) = RX(\theta) From 625ff98f68ea1903862c41e9d714f55ce7965a15 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Wed, 27 Jul 2022 19:19:44 +0200 Subject: [PATCH 16/82] Fix converting VectorStateFn to a CircuitStateFn (#8405) --- qiskit/opflow/state_fns/circuit_state_fn.py | 5 ++--- ...vector-to-circuit-fn-02cb3424269fa733.yaml | 21 +++++++++++++++++++ .../python/opflow/test_state_op_meas_evals.py | 13 ++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/fix-opflow-vector-to-circuit-fn-02cb3424269fa733.yaml diff --git a/qiskit/opflow/state_fns/circuit_state_fn.py b/qiskit/opflow/state_fns/circuit_state_fn.py index 9f959e8718c8..7d5b3a2957de 100644 --- a/qiskit/opflow/state_fns/circuit_state_fn.py +++ b/qiskit/opflow/state_fns/circuit_state_fn.py @@ -20,8 +20,7 @@ from qiskit import BasicAer, ClassicalRegister, QuantumCircuit, transpile from qiskit.circuit import Instruction, ParameterExpression from qiskit.circuit.exceptions import CircuitError -from qiskit.circuit.library import IGate -from qiskit.extensions import Initialize +from qiskit.circuit.library import IGate, StatePreparation from qiskit.opflow.exceptions import OpflowError from qiskit.opflow.list_ops.composed_op import ComposedOp from qiskit.opflow.list_ops.list_op import ListOp @@ -123,7 +122,7 @@ def from_vector(statevector: np.ndarray) -> "CircuitStateFn": """ normalization_coeff = np.linalg.norm(statevector) normalized_sv = statevector / normalization_coeff - return CircuitStateFn(Initialize(normalized_sv), coeff=normalization_coeff) + return CircuitStateFn(StatePreparation(normalized_sv), coeff=normalization_coeff) def primitive_strings(self) -> Set[str]: return {"QuantumCircuit"} diff --git a/releasenotes/notes/fix-opflow-vector-to-circuit-fn-02cb3424269fa733.yaml b/releasenotes/notes/fix-opflow-vector-to-circuit-fn-02cb3424269fa733.yaml new file mode 100644 index 000000000000..d101486d0fa9 --- /dev/null +++ b/releasenotes/notes/fix-opflow-vector-to-circuit-fn-02cb3424269fa733.yaml @@ -0,0 +1,21 @@ +--- +fixes: + - | + Previously it was not possible to adjoint a :class:`.CircuitStateFn` that has been + constructed from a :class:`.VectorStateFn`. That's because the statevector has been + converted to a circuit with the :class:`~qiskit.extensions.Initialize` instruction, which + is not unitary. This problem is now fixed by instead using the :class:`.StatePreparation` + instruction, which can be used since the state is assumed to start out in the all 0 state. + + For example we can now do:: + + from qiskit import QuantumCircuit + from qiskit.opflow import StateFn + + left = StateFn([0, 1]) + left_circuit = left.to_circuit_op().primitive + + right_circuit = QuantumCircuit(1) + right_circuit.x(0) + + overlap = left_circuit.inverse().compose(right_circuit) # this line raised an error before! diff --git a/test/python/opflow/test_state_op_meas_evals.py b/test/python/opflow/test_state_op_meas_evals.py index 937065ca691d..d5b36f7d89f4 100644 --- a/test/python/opflow/test_state_op_meas_evals.py +++ b/test/python/opflow/test_state_op_meas_evals.py @@ -24,6 +24,7 @@ from qiskit.utils import QuantumInstance from qiskit.opflow import StateFn, Zero, One, H, X, I, Z, Plus, Minus, CircuitSampler, ListOp from qiskit.opflow.exceptions import OpflowError +from qiskit.quantum_info import Statevector @ddt @@ -217,6 +218,18 @@ def test_quantum_instance_with_backend_shots(self): res = sampler.convert(~Plus @ Plus).eval() self.assertAlmostEqual(res, 1 + 0j, places=2) + def test_adjoint_vector_to_circuit_fn(self): + """Test it is possible to adjoint a VectorStateFn that was converted to a CircuitStateFn.""" + left = StateFn([0, 1]) + left_circuit = left.to_circuit_op().primitive + + right_circuit = QuantumCircuit(1) + right_circuit.x(0) + + circuit = left_circuit.inverse().compose(right_circuit) + + self.assertTrue(Statevector(circuit).equiv([1, 0])) + if __name__ == "__main__": unittest.main() From 4fceb8476c99f7f29fbc5519947f13d9e5bf4da3 Mon Sep 17 00:00:00 2001 From: Ikko Hamamura Date: Thu, 28 Jul 2022 23:29:48 +0900 Subject: [PATCH 17/82] Improve the performance of assemble when parameter_binds is a list of empty dicts (#8407) * Fix the condition * Update qiskit/compiler/assembler.py * fix lint Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/compiler/assembler.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/qiskit/compiler/assembler.py b/qiskit/compiler/assembler.py index 9dc53b30e21c..d7905ad02620 100644 --- a/qiskit/compiler/assembler.py +++ b/qiskit/compiler/assembler.py @@ -16,19 +16,18 @@ import uuid import warnings from time import time -from typing import Union, List, Dict, Optional +from typing import Dict, List, Optional, Union import numpy as np from qiskit.assembler import assemble_circuits, assemble_schedules from qiskit.assembler.run_config import RunConfig -from qiskit.circuit import QuantumCircuit, Qubit, Parameter +from qiskit.circuit import Parameter, QuantumCircuit, Qubit from qiskit.exceptions import QiskitError from qiskit.providers.backend import Backend -from qiskit.pulse import LoConfig, Instruction -from qiskit.pulse import Schedule, ScheduleBlock +from qiskit.pulse import Instruction, LoConfig, Schedule, ScheduleBlock from qiskit.pulse.channels import PulseChannel -from qiskit.qobj import QobjHeader, Qobj +from qiskit.qobj import Qobj, QobjHeader from qiskit.qobj.utils import MeasLevel, MeasReturnType logger = logging.getLogger(__name__) @@ -569,7 +568,7 @@ def _expand_parameters(circuits, run_config): """ parameter_binds = run_config.parameter_binds - if parameter_binds or any(circuit.parameters for circuit in circuits): + if parameter_binds and any(parameter_binds) or any(circuit.parameters for circuit in circuits): # Unroll params here in order to handle ParamVects all_bind_parameters = [ From e9f2a2b1975bce588a33a4675e8dcce3920b3493 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Thu, 28 Jul 2022 18:27:07 +0200 Subject: [PATCH 18/82] Remove upper cap on Sphinx version (undoes #8392) (#8416) https://github.com/sphinx-doc/sphinx/issues/10701 is fixed --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index abd450fce01e..eef50c6b96e1 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -19,7 +19,7 @@ pylatexenc>=1.4 ddt>=1.2.0,!=1.4.0,!=1.4.3 seaborn>=0.9.0 reno>=3.4.0 -Sphinx>=3.0.0,<5.1 +Sphinx>=3.0.0 qiskit-sphinx-theme>=1.6 qiskit-toqm>=0.0.4;platform_machine != 'aarch64' or platform_system != 'Linux' sphinx-autodoc-typehints<1.14.0 From 244fd840a85ba2c8c13d29bd72c20c51c6722481 Mon Sep 17 00:00:00 2001 From: Edwin Navarro Date: Thu, 28 Jul 2022 14:37:43 -0700 Subject: [PATCH 19/82] Fix MPL and LaTeX circuit drawer when `idle_wires=False` and there are barriers (#8321) * Fix idle_wires False with barrier Co-authored-by: Luciano Bello --- qiskit/visualization/latex.py | 8 +++++--- qiskit/visualization/matplotlib.py | 14 ++++++++------ .../fix-idle-wires-display-de0ecc60d4000ca0.yaml | 8 ++++++++ .../circuit/references/idle_wires_barrier.png | Bin 0 -> 3549 bytes .../circuit/test_circuit_matplotlib_drawer.py | 7 +++++++ .../references/test_latex_idle_wires_barrier.tex | 11 +++++++++++ test/python/visualization/test_circuit_latex.py | 9 +++++++++ 7 files changed, 48 insertions(+), 9 deletions(-) create mode 100644 releasenotes/notes/fix-idle-wires-display-de0ecc60d4000ca0.yaml create mode 100644 test/ipynb/mpl/circuit/references/idle_wires_barrier.png create mode 100644 test/python/visualization/references/test_latex_idle_wires_barrier.tex diff --git a/qiskit/visualization/latex.py b/qiskit/visualization/latex.py index 46873687d6b0..1c2471081983 100644 --- a/qiskit/visualization/latex.py +++ b/qiskit/visualization/latex.py @@ -420,7 +420,7 @@ def _build_latex_array(self): for node in layer: op = node.op num_cols_op = 1 - wire_list = [self._wire_map[qarg] for qarg in node.qargs] + wire_list = [self._wire_map[qarg] for qarg in node.qargs if qarg in self._qubits] if op.condition: self._add_condition(op, wire_list, column) @@ -435,7 +435,9 @@ def _build_latex_array(self): gate_text += get_param_str(op, "latex", ndigits=4) gate_text = generate_latex_label(gate_text) if node.cargs: - cwire_list = [self._wire_map[carg] for carg in node.cargs] + cwire_list = [ + self._wire_map[carg] for carg in node.cargs if carg in self._clbits + ] else: cwire_list = [] @@ -582,7 +584,7 @@ def _build_measure(self, node, col): def _build_barrier(self, node, col): """Build a partial or full barrier if plot_barriers set""" if self._plot_barriers: - indexes = [self._wire_map[qarg] for qarg in node.qargs] + indexes = [self._wire_map[qarg] for qarg in node.qargs if qarg in self._qubits] indexes.sort() first = last = indexes[0] for index in indexes[1:]: diff --git a/qiskit/visualization/matplotlib.py b/qiskit/visualization/matplotlib.py index 7138e436c7c9..fd648c780f63 100644 --- a/qiskit/visualization/matplotlib.py +++ b/qiskit/visualization/matplotlib.py @@ -574,15 +574,17 @@ def _get_coords(self, n_lines): # get qubit index q_indxs = [] for qarg in node.qargs: - q_indxs.append(self._wire_map[qarg]) + if qarg in self._qubits: + q_indxs.append(self._wire_map[qarg]) c_indxs = [] for carg in node.cargs: - register = get_bit_register(self._circuit, carg) - if register is not None and self._cregbundle: - c_indxs.append(self._wire_map[register]) - else: - c_indxs.append(self._wire_map[carg]) + if carg in self._clbits: + register = get_bit_register(self._circuit, carg) + if register is not None and self._cregbundle: + c_indxs.append(self._wire_map[register]) + else: + c_indxs.append(self._wire_map[carg]) # qubit coordinate self._data[node]["q_xy"] = [ diff --git a/releasenotes/notes/fix-idle-wires-display-de0ecc60d4000ca0.yaml b/releasenotes/notes/fix-idle-wires-display-de0ecc60d4000ca0.yaml new file mode 100644 index 000000000000..d5c1777b3d48 --- /dev/null +++ b/releasenotes/notes/fix-idle-wires-display-de0ecc60d4000ca0.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + A bug in the ``mpl`` and ``latex`` circuit drawers, when + setting the ``idle_wires`` option to False when there was + a ``barrier`` in the circuit would cause the drawers to + fail, has been fixed. + (see `#8313 `__ diff --git a/test/ipynb/mpl/circuit/references/idle_wires_barrier.png b/test/ipynb/mpl/circuit/references/idle_wires_barrier.png new file mode 100644 index 0000000000000000000000000000000000000000..2bb7ab8f2fe1140d585d425bbd8cc23d3a69233c GIT binary patch literal 3549 zcmd56$d7$6`e3Mfc# z!cfFO0-_*96Qv_kls2>w1_+Um@UFAw&AhkPdw<`0KkmI}ox9iGXSZ*EC+V{7Pr^b{ zLLd-G7-nf^4+4R`fpy_N0pOhpt}+8YXR+qaSckx0usE-qNRW*eHpnj!>xa4~6Nneiu6U|0tC96>#C59UOiHzMGZxz2^o=9GZ`3~`AvC`|G2KHqmangPe^j~z>t_BzjZi9Dsi3jzO?{&p?EAr*CXczF2P9~4()=YWHB zVUFkNV_x~ohu+mB5QBNGpLQei2R^IL3On=`@MIv+Y7SRkq>=6>lcz6>^noA;tq_Pk zV$PaD;XgZU#0AgO$_ooGB9Ta`bJ()JzCLWfjUMV#1FE8?CUol-^>2L>!3?m-f zJI^rUihA1y!T!7Q%xdE>NKxf2>UH(?;#c)4-qY;;qcMo_*?E@7uw^&inhLwRtez#6= zacN1%hjtt$ogG`_+H6bBg7(LP_q<#_A{4*DN5JM*@{rLHZ6Wyde!woR2~wcqN?iXik)ud`{ry}urSijFn2Pf9k8|%IDZpAvGL8B#EHmt8=HIsMCZL9;FM@?=EH}@AB%oMkJck^1O%{HHXE`-Pi)say7A>v z6BidoG-1-~y;oQ=yZ86h#EOMsTv~1xk3}z*muBYXh7TH{@`gI_$IsSNds-6`1SG`8 zu?UTmLh)GMPWa%!Kv_wNMIeRS6w3JAvtgmfhb*o){&w$i8|PUJ*P3u$VzRP5N-VPa zzzfk=`iJn-zTJ3f2J2m}miOZDx7!c01#Uz{kWQg`GnI*k5lsxrZgl)b+4xQ`|FI^U z14q;bdNP!BkiFuAgM-T7QEJO;tcge@el?;M)3}^Awk)%qd!I2rA0Sx3t5y$*+~scF zu!iLu{~Dx!9KJg=bj5J(wHV80+dgX*(0GaJ1tCK>Y%*PfW(JJgaL zop4chJbXS+a}(|B8#I%5!$uefDL=5Vm6JN3QZq^etM z4s$Z1IzNgv`eM7ev2kH6kn(V#ZjQX(Oh!A|*@yl?EVi$n*m}tHqq!E;>-=&^-OoI!q@MKdkGQ=@2v0 zT@eeBPniz-eFoMXH6AkjW}uWzo)EU9)?R1PbI)lrKK7x z!jENf9iGjc)o~JhwROFq!n#vW zrdGzF(UlXomY}e{=~QDz&;HgU?qRYF@38Xq$+m35_aZ1PlxvP+EhGHLUphII+O{o| zH#awl=R$5|E1(U9rxzD*I9<77MQ{(Fm7v?o?d7}eepWX9)50Uuz+cfz*8@Mv!RmXv zAR0B#sPsE@p6yETy!Dso0JfcW{+3X+*FYnfIn^OG(*dh8OTsxl@E;2x#q9FjvM}zI z6Kp?XBI>@r_>Vt^B8W|%9egX36bjYoqp#!AP<8)Eoj1^8&oXU`p5@F zZf(q0vpl0H7XZCJCI}y6KfztAML?ttRbWE20wwf^*+4#VcCGSFBcSI^8|&e74)yH#Ca<<({F@!f^KJZdEq7LlH&@S#{=TK$b#1 zAcrppt;v;p7%Pj~HGGW;*FKzLA{pV?Ls0lc!qt}8JSy?(OiAFCEQ#A)f9J$;);kEF zO6oqpeUfd(!soTQDIOLpQ8uu*w=X0R2oOf_R6Dpis!@X+wpMr|d`1MwqQ|gDOj|`o zMP_z4e34c);8bj!_woF`J_M=!^S^*T0e*OA&Y&l2$!51t!v~;NP5w*C>f|7RXvzO)PW_z77hC$r1e8@4=>i#Kq^Cb65~V0q zORJw9K>xzU(k;yZ_B8mpC0HWKYzLT^f9MB-6Tw=>{8PWY_XZK*no>#zQ@EpKp<<>| z!Uz*)Qd_5LeaGH`tG35d=wG#h1Uv!8=yT@G8An&wCEU?}CyfSTNoEbcALXDf4T;pJ z5s4ZCQ@Os{6>g+Z)Nqa4)kEW^|K(<&+Ko9*Ne0DwoU{0!-592J!AEN|i)-z)r+}*# N2xe|;R&&ua?jJ)#hw1 Date: Sun, 31 Jul 2022 12:21:45 -0400 Subject: [PATCH 20/82] Fix compatibility with Scipy 1.9.0 (#8426) Scipy 1.9.0 was recently released and introduced a few new deprecation warnings. Due to the terra test suite being configured to make DeprecationWarnings raised during the execution of tests fatal this is causing failures in CI and local test runs when scipy 1.9.0 is used. The first deprecation warning is that the minimize function has deprecated the maxiter kwarg and replaced it with the maxfun argument (which was introduced in scipy 1.5.0) for the TNC optimizer. To address this the maxiter argument for the algorithms scipy optimizer is changed to translate maxiter arguments (which is part of qiskit's api) when the tnc optimizer is being used to send maxfun to scipy instead to avoid the warning. The second warning being emitted is caused by scikit-learn's internal usage of scipy. To avoid a failure in CI an exclude is added to the test suite to ignore that deprecation warning. Fixes #8424 --- qiskit/algorithms/optimizers/scipy_optimizer.py | 10 ++++++++++ qiskit/test/base.py | 2 ++ 2 files changed, 12 insertions(+) diff --git a/qiskit/algorithms/optimizers/scipy_optimizer.py b/qiskit/algorithms/optimizers/scipy_optimizer.py index aa8349d03f33..3d6bad2f5ee3 100644 --- a/qiskit/algorithms/optimizers/scipy_optimizer.py +++ b/qiskit/algorithms/optimizers/scipy_optimizer.py @@ -138,6 +138,13 @@ def minimize( if jac is not None and self._method == "l-bfgs-b": jac = self._wrap_gradient(jac) + # Starting in scipy 1.9.0 maxiter is deprecated and maxfun (added in 1.5.0) + # should be used instead + swapped_deprecated_args = False + if self._method == "tnc" and "maxiter" in self._options: + swapped_deprecated_args = True + self._options["maxfun"] = self._options.pop("maxiter") + raw_result = minimize( fun=fun, x0=x0, @@ -147,6 +154,9 @@ def minimize( options=self._options, **self._kwargs, ) + if swapped_deprecated_args: + self._options["maxiter"] = self._options.pop("maxfun") + result = OptimizerResult() result.x = raw_result.x result.fun = raw_result.fun diff --git a/qiskit/test/base.py b/qiskit/test/base.py index 1d017661c255..2fc5dbc21850 100644 --- a/qiskit/test/base.py +++ b/qiskit/test/base.py @@ -228,6 +228,8 @@ def setUpClass(cls): # Internal deprecation warning emitted by jupyter client when # calling nbconvert in python 3.10 r"There is no current event loop", + # Caused by internal scikit-learn scipy usage + r"The 'sym_pos' keyword is deprecated and should be replaced by using", ] for msg in allow_DeprecationWarning_message: warnings.filterwarnings("default", category=DeprecationWarning, message=msg) From cf951202c77b49d28e7d23f43088b191638f282a Mon Sep 17 00:00:00 2001 From: Pedro Rivero Date: Mon, 1 Aug 2022 12:31:01 -0400 Subject: [PATCH 21/82] Handle repeated stage names in StagedPassManager (#8421) * Handle repeated stages * Add reno * Fix lint * Revert back to original code base * Add test and docs for repeated stages * Update reno * Avoid redundant stage setting from kwargs on repeated stages * Remove release note --- qiskit/transpiler/passmanager.py | 14 +++++++---- .../transpiler/test_staged_passmanager.py | 24 ++++++++++++++++++- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/qiskit/transpiler/passmanager.py b/qiskit/transpiler/passmanager.py index 489e1a6df651..86a124e32b40 100644 --- a/qiskit/transpiler/passmanager.py +++ b/qiskit/transpiler/passmanager.py @@ -362,10 +362,12 @@ class StagedPassManager(PassManager): users as the relative positions of the stage are preserved so the behavior will not change between releases. - These stages will be executed in order and any stage set to ``None`` will be skipped. If - a :class:`~qiskit.transpiler.PassManager` input is being used for more than 1 stage here - (for example in the case of a :class:`~.Pass` that covers both Layout and Routing) you will want - to set that to the earliest stage in sequence that it covers. + These stages will be executed in order and any stage set to ``None`` will be skipped. + If a stage is provided multiple times (i.e. at diferent relative positions), the + associated passes, including pre and post, will run once per declaration. + If a :class:`~qiskit.transpiler.PassManager` input is being used for more than 1 stage here + (for example in the case of a :class:`~.Pass` that covers both Layout and Routing) you will + want to set that to the earliest stage in sequence that it covers. """ invalid_stage_regex = re.compile( @@ -380,6 +382,8 @@ def __init__(self, stages: Optional[Iterable[str]] = None, **kwargs) -> None: instance. If this is not specified the default stages list ``['init', 'layout', 'routing', 'translation', 'optimization', 'scheduling']`` is used. After instantiation, the final list will be immutable and stored as tuple. + If a stage is provided multiple times (i.e. at diferent relative positions), the + associated passes, including pre and post, will run once per declaration. kwargs: The initial :class:`~.PassManager` values for any stages defined in ``stages``. If a argument is not defined the stages will default to ``None`` indicating an empty/undefined @@ -403,7 +407,7 @@ def __init__(self, stages: Optional[Iterable[str]] = None, **kwargs) -> None: super().__setattr__("_expanded_stages", tuple(self._generate_expanded_stages())) super().__init__() self._validate_init_kwargs(kwargs) - for stage in self.expanded_stages: + for stage in set(self.expanded_stages): pm = kwargs.get(stage, None) setattr(self, stage, pm) diff --git a/test/python/transpiler/test_staged_passmanager.py b/test/python/transpiler/test_staged_passmanager.py index 04a036b9f000..d25e96365789 100644 --- a/test/python/transpiler/test_staged_passmanager.py +++ b/test/python/transpiler/test_staged_passmanager.py @@ -19,7 +19,7 @@ from qiskit.transpiler import PassManager, StagedPassManager from qiskit.transpiler.exceptions import TranspilerError -from qiskit.transpiler.passes import Optimize1qGates, Unroller, Depth +from qiskit.transpiler.passes import Optimize1qGates, Unroller, Depth, BasicSwap from qiskit.test import QiskitTestCase @@ -98,6 +98,26 @@ def test_invalid_stages(self): for stage in invalid_stages: self.assertIn(stage, message) + def test_repeated_stages(self): + stages = ["alpha", "omega", "alpha"] + pre_alpha = PassManager(Unroller(["u", "cx"])) + alpha = PassManager(Depth()) + post_alpha = PassManager(BasicSwap([[0, 1], [1, 2]])) + omega = PassManager(Optimize1qGates()) + spm = StagedPassManager( + stages, pre_alpha=pre_alpha, alpha=alpha, post_alpha=post_alpha, omega=omega + ) + passes = [ + *pre_alpha.passes(), + *alpha.passes(), + *post_alpha.passes(), + *omega.passes(), + *pre_alpha.passes(), + *alpha.passes(), + *post_alpha.passes(), + ] + self.assertEqual(spm.passes(), passes) + def test_edit_stages(self): spm = StagedPassManager() with self.assertRaises(AttributeError): @@ -120,6 +140,8 @@ def test_setattr(self): spm.init = spm mock_target = "qiskit.transpiler.passmanager.StagedPassManager._update_passmanager" with patch(mock_target, spec=True) as mock: + spm.max_iteration = spm.max_iteration + mock.assert_not_called() spm.init = None mock.assert_called_once() From 9a78f31b091c733bbc2b84f665dd586d4793dc4f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Aug 2022 11:14:42 +0000 Subject: [PATCH 22/82] Bump ndarray from 0.15.4 to 0.15.6 (#8430) Bumps [ndarray](https://github.com/rust-ndarray/ndarray) from 0.15.4 to 0.15.6. - [Release notes](https://github.com/rust-ndarray/ndarray/releases) - [Changelog](https://github.com/rust-ndarray/ndarray/blob/master/RELEASES.md) - [Commits](https://github.com/rust-ndarray/ndarray/compare/0.15.4...0.15.6) --- updated-dependencies: - dependency-name: ndarray dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 617fe9878121..4dd238e56295 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,9 +171,9 @@ dependencies = [ [[package]] name = "ndarray" -version = "0.15.4" +version = "0.15.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec23e6762830658d2b3d385a75aa212af2f67a4586d4442907144f3bb6a1ca8" +checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" dependencies = [ "matrixmultiply", "num-complex", diff --git a/Cargo.toml b/Cargo.toml index ab3fff6df1c5..a31dc0d82b94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ version = "0.16.5" features = ["extension-module", "hashbrown", "num-complex", "num-bigint"] [dependencies.ndarray] -version = "^0.15.0" +version = "^0.15.6" features = ["rayon"] [dependencies.hashbrown] From 82245c0141ffa56f2eee8c1bec7823c471a31871 Mon Sep 17 00:00:00 2001 From: Shelly Garion <46566946+ShellyGarion@users.noreply.github.com> Date: Tue, 2 Aug 2022 19:32:40 +0300 Subject: [PATCH 23/82] Add optimization for TwoLocal synthesis when entanglement='full' (#8335) * add optimization for CX subcircuits with full entanglement * preserve the option of full_explicit * add a test that full and full_explicit are the same * add docstring to the test * minor lint fix * add comments following review * minor fix: add default value None to entanglement_gates * minor lint fix * add the option reverse_linear, and remove full_entanglement * add optimization for CX subcircuits with full entanglement * preserve the option of full_explicit * minor lint fix * add comments following review * add the option reverse_linear, and remove full_entanglement * return extra space lines * update comment * add documentation in TwoLocal * update documentation in TwoLocal * add documentation in RealAmplitudes. Change default eantanglement to reverse_linear * fix block size in reverse_linear * enhance tests for reverse_local * add documentation in EfficientSU2. Change default entanglement to reverse_linear * reduce the nuber of tests of reverse_linear vs. full * add a test that ZZ feature map works with reverse_linear entanglement * style update * add release notes * add entanglement='pairwise' to the docs of TwoLocal Co-authored-by: Luciano Bello Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: Julien Gacon --- .../circuit/library/n_local/efficient_su2.py | 23 ++++---- qiskit/circuit/library/n_local/n_local.py | 7 ++- .../library/n_local/real_amplitudes.py | 36 ++++++++---- qiskit/circuit/library/n_local/two_local.py | 13 ++++- ...-entanglement-nlocal-38581e4ffb7a7c68.yaml | 9 +++ test/python/circuit/library/test_nlocal.py | 57 +++++++++++++++++-- .../circuit/library/test_pauli_feature_map.py | 8 ++- 7 files changed, 120 insertions(+), 33 deletions(-) create mode 100644 releasenotes/notes/add-reverse-linear-entanglement-nlocal-38581e4ffb7a7c68.yaml diff --git a/qiskit/circuit/library/n_local/efficient_su2.py b/qiskit/circuit/library/n_local/efficient_su2.py index b22fef5d0fc6..91be34ec66a1 100644 --- a/qiskit/circuit/library/n_local/efficient_su2.py +++ b/qiskit/circuit/library/n_local/efficient_su2.py @@ -35,13 +35,13 @@ class EfficientSU2(TwoLocal): .. parsed-literal:: - ┌──────────┐┌──────────┐ ░ ░ ░ ┌───────────┐┌───────────┐ - ┤ RY(θ[0]) ├┤ RZ(θ[3]) ├─░───■────■────────░─ ... ─░─┤ RY(θ[12]) ├┤ RZ(θ[15]) ├ - ├──────────┤├──────────┤ ░ ┌─┴─┐ │ ░ ░ ├───────────┤├───────────┤ - ┤ RY(θ[1]) ├┤ RZ(θ[4]) ├─░─┤ X ├──┼────■───░─ ... ─░─┤ RY(θ[13]) ├┤ RZ(θ[16]) ├ - ├──────────┤├──────────┤ ░ └───┘┌─┴─┐┌─┴─┐ ░ ░ ├───────────┤├───────────┤ - ┤ RY(θ[2]) ├┤ RZ(θ[5]) ├─░──────┤ X ├┤ X ├─░─ ... ─░─┤ RY(θ[14]) ├┤ RZ(θ[17]) ├ - └──────────┘└──────────┘ ░ └───┘└───┘ ░ ░ └───────────┘└───────────┘ + ┌──────────┐┌──────────┐ ░ ░ ░ ┌───────────┐┌───────────┐ + ┤ RY(θ[0]) ├┤ RZ(θ[3]) ├─░────────■───░─ ... ─░─┤ RY(θ[12]) ├┤ RZ(θ[15]) ├ + ├──────────┤├──────────┤ ░ ┌─┴─┐ ░ ░ ├───────────┤├───────────┤ + ┤ RY(θ[1]) ├┤ RZ(θ[4]) ├─░───■──┤ X ├─░─ ... ─░─┤ RY(θ[13]) ├┤ RZ(θ[16]) ├ + ├──────────┤├──────────┤ ░ ┌─┴─┐└───┘ ░ ░ ├───────────┤├───────────┤ + ┤ RY(θ[2]) ├┤ RZ(θ[5]) ├─░─┤ X ├──────░─ ... ─░─┤ RY(θ[14]) ├┤ RZ(θ[17]) ├ + └──────────┘└──────────┘ ░ └───┘ ░ ░ └───────────┘└───────────┘ See :class:`~qiskit.circuit.library.RealAmplitudes` for more detail on the possible arguments and options such as skipping unentanglement qubits, which apply here too. @@ -86,7 +86,7 @@ def __init__( List[Union[str, type, Instruction, QuantumCircuit]], ] ] = None, - entanglement: Union[str, List[List[int]], Callable[[int], List[int]]] = "full", + entanglement: Union[str, List[List[int]], Callable[[int], List[int]]] = "reverse_linear", reps: int = 3, skip_unentangled_qubits: bool = False, skip_final_rotation_layer: bool = False, @@ -106,9 +106,12 @@ def __init__( If a list of gates is provided, all gates are applied to each qubit in the provided order. entanglement: Specifies the entanglement structure. Can be a string ('full', 'linear' - , 'circular' or 'sca'), a list of integer-pairs specifying the indices of qubits - entangled with one another, or a callable returning such a list provided with + , 'reverse_linear', 'circular' or 'sca'), a list of integer-pairs specifying the indices + of qubits entangled with one another, or a callable returning such a list provided with the index of the entanglement layer. + Default to 'reverse_linear' entanglement. + Note that 'reverse_linear' entanglement provides the same unitary as 'full' + with fewer entangling gates. See the Examples section of :class:`~qiskit.circuit.library.TwoLocal` for more detail. initial_state: A `QuantumCircuit` object to prepend to the circuit. diff --git a/qiskit/circuit/library/n_local/n_local.py b/qiskit/circuit/library/n_local/n_local.py index 9c5cdfddd463..902374f98958 100644 --- a/qiskit/circuit/library/n_local/n_local.py +++ b/qiskit/circuit/library/n_local/n_local.py @@ -1011,7 +1011,12 @@ def get_entangler_map( if entanglement == "full": return list(combinations(list(range(n)), m)) - if entanglement in ["linear", "circular", "sca", "pairwise"]: + elif entanglement == "reverse_linear": + # reverse linear connectivity. In the case of m=2 and the entanglement_block='cx' + # then it's equivalent to 'full' entanglement + reverse = [tuple(range(n - i - m, n - i)) for i in range(n - m + 1)] + return reverse + elif entanglement in ["linear", "circular", "sca", "pairwise"]: linear = [tuple(range(i, i + m)) for i in range(n - m + 1)] # if the number of block qubits is 1, we don't have to add the 'circular' part if entanglement == "linear" or m == 1: diff --git a/qiskit/circuit/library/n_local/real_amplitudes.py b/qiskit/circuit/library/n_local/real_amplitudes.py index ba66293e3646..2838c898f2da 100644 --- a/qiskit/circuit/library/n_local/real_amplitudes.py +++ b/qiskit/circuit/library/n_local/real_amplitudes.py @@ -29,18 +29,17 @@ class RealAmplitudes(TwoLocal): It is called ``RealAmplitudes`` since the prepared quantum states will only have real amplitudes, the complex part is always 0. - For example a ``RealAmplitudes`` circuit with 2 repetitions on 3 qubits with ``'full'`` + For example a ``RealAmplitudes`` circuit with 2 repetitions on 3 qubits with ``'reverse_linear'`` entanglement is .. parsed-literal:: - - ┌──────────┐ ░ ░ ┌──────────┐ ░ ░ ┌──────────┐ - ┤ RY(θ[0]) ├─░───■────■────────░─┤ RY(θ[3]) ├─░───■────■────────░─┤ RY(θ[6]) ├ - ├──────────┤ ░ ┌─┴─┐ │ ░ ├──────────┤ ░ ┌─┴─┐ │ ░ ├──────────┤ - ┤ RY(θ[1]) ├─░─┤ X ├──┼────■───░─┤ RY(θ[4]) ├─░─┤ X ├──┼────■───░─┤ RY(θ[7]) ├ - ├──────────┤ ░ └───┘┌─┴─┐┌─┴─┐ ░ ├──────────┤ ░ └───┘┌─┴─┐┌─┴─┐ ░ ├──────────┤ - ┤ RY(θ[2]) ├─░──────┤ X ├┤ X ├─░─┤ RY(θ[5]) ├─░──────┤ X ├┤ X ├─░─┤ RY(θ[8]) ├ - └──────────┘ ░ └───┘└───┘ ░ └──────────┘ ░ └───┘└───┘ ░ └──────────┘ + ┌──────────┐ ░ ░ ┌──────────┐ ░ ░ ┌──────────┐ + ┤ Ry(θ[0]) ├─░────────■───░─┤ Ry(θ[3]) ├─░────────■───░─┤ Ry(θ[6]) ├ + ├──────────┤ ░ ┌─┴─┐ ░ ├──────────┤ ░ ┌─┴─┐ ░ ├──────────┤ + ┤ Ry(θ[1]) ├─░───■──┤ X ├─░─┤ Ry(θ[4]) ├─░───■──┤ X ├─░─┤ Ry(θ[7]) ├ + ├──────────┤ ░ ┌─┴─┐└───┘ ░ ├──────────┤ ░ ┌─┴─┐└───┘ ░ ├──────────┤ + ┤ Ry(θ[2]) ├─░─┤ X ├──────░─┤ Ry(θ[5]) ├─░─┤ X ├──────░─┤ Ry(θ[8]) ├ + └──────────┘ ░ └───┘ ░ └──────────┘ ░ └───┘ ░ └──────────┘ The entanglement can be set using the ``entanglement`` keyword as string or a list of index-pairs. See the documentation of :class:`~qiskit.circuit.library.TwoLocal` and @@ -56,6 +55,16 @@ class RealAmplitudes(TwoLocal): Examples: >>> ansatz = RealAmplitudes(3, reps=2) # create the circuit on 3 qubits + >>> print(ansatz) + ┌──────────┐ ┌──────────┐ ┌──────────┐ + q_0: ┤ Ry(θ[0]) ├──────────■──────┤ Ry(θ[3]) ├──────────■──────┤ Ry(θ[6]) ├ + ├──────────┤ ┌─┴─┐ ├──────────┤ ┌─┴─┐ ├──────────┤ + q_1: ┤ Ry(θ[1]) ├──■─────┤ X ├────┤ Ry(θ[4]) ├──■─────┤ X ├────┤ Ry(θ[7]) ├ + ├──────────┤┌─┴─┐┌──┴───┴───┐└──────────┘┌─┴─┐┌──┴───┴───┐└──────────┘ + q_2: ┤ Ry(θ[2]) ├┤ X ├┤ Ry(θ[5]) ├────────────┤ X ├┤ Ry(θ[8]) ├──────────── + └──────────┘└───┘└──────────┘ └───┘└──────────┘ + + >>> ansatz = RealAmplitudes(3, entanglement='full', reps=2) # it is the same unitary as above >>> print(ansatz) ┌──────────┐ ┌──────────┐ ┌──────────┐ q_0: ┤ RY(θ[0]) ├──■────■──┤ RY(θ[3]) ├──────────────■────■──┤ RY(θ[6]) ├──────────── @@ -107,7 +116,7 @@ class RealAmplitudes(TwoLocal): def __init__( self, num_qubits: Optional[int] = None, - entanglement: Union[str, List[List[int]], Callable[[int], List[int]]] = "full", + entanglement: Union[str, List[List[int]], Callable[[int], List[int]]] = "reverse_linear", reps: int = 3, skip_unentangled_qubits: bool = False, skip_final_rotation_layer: bool = False, @@ -123,9 +132,12 @@ def __init__( reps: Specifies how often the structure of a rotation layer followed by an entanglement layer is repeated. entanglement: Specifies the entanglement structure. Can be a string ('full', 'linear' - or 'sca'), a list of integer-pairs specifying the indices of qubits - entangled with one another, or a callable returning such a list provided with + 'reverse_linear, 'circular' or 'sca'), a list of integer-pairs specifying the indices + of qubits entangled with one another, or a callable returning such a list provided with the index of the entanglement layer. + Default to 'reverse_linear' entanglement. + Note that 'reverse_linear' entanglement provides the same unitary as 'full' + with fewer entangling gates. See the Examples section of :class:`~qiskit.circuit.library.TwoLocal` for more detail. initial_state: A `QuantumCircuit` object to prepend to the circuit. diff --git a/qiskit/circuit/library/n_local/two_local.py b/qiskit/circuit/library/n_local/two_local.py index c4586f2603fb..14edd8955bb5 100644 --- a/qiskit/circuit/library/n_local/two_local.py +++ b/qiskit/circuit/library/n_local/two_local.py @@ -61,6 +61,13 @@ class TwoLocal(NLocal): * ``'full'`` entanglement is each qubit is entangled with all the others. * ``'linear'`` entanglement is qubit :math:`i` entangled with qubit :math:`i + 1`, for all :math:`i \in \{0, 1, ... , n - 2\}`, where :math:`n` is the total number of qubits. + * ``'reverse_linear'`` entanglement is qubit :math:`i` entangled with qubit :math:`i + 1`, + for all :math:`i \in \{n-2, n-3, ... , 1, 0\}`, where :math:`n` is the total number of qubits. + Note that if ``entanglement_blocks = 'cx'`` then this option provides the same unitary as + ``'full'`` with fewer entangling gates. + * ``'pairwise'`` entanglement is one layer where qubit :math:`i` is entangled with qubit + :math:`i + 1`, for all even values of :math:`i`, and then a second layer where qubit :math:`i` + is entangled with qubit :math:`i + 1`, for all odd values of :math:`i`. * ``'circular'`` entanglement is linear entanglement but with an additional entanglement of the first and last qubit before the linear part. * ``'sca'`` (shifted-circular-alternating) entanglement is a generalized and modified version @@ -179,10 +186,12 @@ def __init__( entanglement_blocks: The gates used in the entanglement layer. Can be specified in the same format as `rotation_blocks`. entanglement: Specifies the entanglement structure. Can be a string ('full', 'linear' - , 'circular' or 'sca'), a list of integer-pairs specifying the indices of qubits - entangled with one another, or a callable returning such a list provided with + , 'reverse_linear, 'circular' or 'sca'), a list of integer-pairs specifying the indices + of qubits entangled with one another, or a callable returning such a list provided with the index of the entanglement layer. Default to 'full' entanglement. + Note that if ``entanglement_blocks = 'cx'``, then ``'full'`` entanglement provides the + same unitary as ``'reverse_linear'`` but the latter option has fewer entangling gates. See the Examples section for more detail. reps: Specifies how often a block consisting of a rotation layer and entanglement layer is repeated. diff --git a/releasenotes/notes/add-reverse-linear-entanglement-nlocal-38581e4ffb7a7c68.yaml b/releasenotes/notes/add-reverse-linear-entanglement-nlocal-38581e4ffb7a7c68.yaml new file mode 100644 index 000000000000..1f51ab260d56 --- /dev/null +++ b/releasenotes/notes/add-reverse-linear-entanglement-nlocal-38581e4ffb7a7c68.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Add a new entanglement method `entanglement="reverse_linear"` to :class:`~.NLocal` circuits. + In the case of :class:`~.TwoLocal` circuits, if `entanglement_blocks="cx"` then + `entanglement="reverse_linear"` actually provides the same n-qubit circuit as + `entanglement="full"` but with only n-1 CX gates, instead of n(n-1)/2. + For :class:`~.RealAmplitudes` and :class:`~.EfficientSU2` circuits, the default value + has therefore been changed from `entanglement="full"` to `entanglement="reverse_linear"`. diff --git a/test/python/circuit/library/test_nlocal.py b/test/python/circuit/library/test_nlocal.py index 3204269ed067..5c77bac358aa 100644 --- a/test/python/circuit/library/test_nlocal.py +++ b/test/python/circuit/library/test_nlocal.py @@ -13,6 +13,7 @@ """Test library of n-local circuits.""" import unittest +from test import combine import numpy as np @@ -40,6 +41,7 @@ ) from qiskit.circuit.random.utils import random_circuit from qiskit.converters.circuit_to_dag import circuit_to_dag +from qiskit.quantum_info import Operator @ddt @@ -296,7 +298,16 @@ def test_skip_unentangled_qubits(self): idle = set(dag.idle_wires()) self.assertEqual(skipped_set, idle) - @data("linear", "full", "circular", "sca", ["linear", "full"], ["circular", "linear", "sca"]) + @data( + "linear", + "full", + "circular", + "sca", + "reverse_linear", + ["linear", "full"], + ["reverse_linear", "full"], + ["circular", "linear", "sca"], + ) def test_entanglement_by_str(self, entanglement): """Test setting the entanglement of the layers by str.""" reps = 3 @@ -311,6 +322,8 @@ def test_entanglement_by_str(self, entanglement): def get_expected_entangler_map(rep_num, mode): if mode == "linear": return [(0, 1, 2), (1, 2, 3), (2, 3, 4)] + elif mode == "reverse_linear": + return [(2, 3, 4), (1, 2, 3), (0, 1, 2)] elif mode == "full": return [ (0, 1, 2), @@ -343,7 +356,7 @@ def get_expected_entangler_map(rep_num, mode): with self.subTest(rep_num=rep_num): # using a set here since the order does not matter - self.assertEqual(set(entangler_map), set(expected)) + self.assertEqual(entangler_map, expected) def test_pairwise_entanglement(self): """Test pairwise entanglement.""" @@ -620,8 +633,30 @@ def test_ry_blocks(self): expected = [(-np.pi, np.pi)] * two.num_parameters np.testing.assert_almost_equal(two.parameter_bounds, expected) - def test_ry_circuit(self): - """Test an RealAmplitudes circuit.""" + def test_ry_circuit_reverse_linear(self): + """Test a RealAmplitudes circuit with entanglement = "reverse_linear".""" + num_qubits = 3 + reps = 2 + entanglement = "reverse_linear" + parameters = ParameterVector("theta", num_qubits * (reps + 1)) + param_iter = iter(parameters) + + expected = QuantumCircuit(3) + for _ in range(reps): + for i in range(num_qubits): + expected.ry(next(param_iter), i) + expected.cx(1, 2) + expected.cx(0, 1) + for i in range(num_qubits): + expected.ry(next(param_iter), i) + + library = RealAmplitudes( + num_qubits, reps=reps, entanglement=entanglement + ).assign_parameters(parameters) + self.assertCircuitEqual(library, expected) + + def test_ry_circuit_full(self): + """Test a RealAmplitudes circuit with entanglement = "full".""" num_qubits = 3 reps = 2 entanglement = "full" @@ -648,7 +683,6 @@ def test_ry_circuit(self): library = RealAmplitudes( num_qubits, reps=reps, entanglement=entanglement ).assign_parameters(parameters) - self.assertCircuitEqual(library, expected) def test_ryrz_blocks(self): @@ -868,6 +902,19 @@ def test_circuit_with_numpy_integers(self): self.assertEqual(two_np32.decompose().count_ops()["cx"], expected_cx) self.assertEqual(two_np64.decompose().count_ops()["cx"], expected_cx) + @combine(num_qubits=[4, 5]) + def test_full_vs_reverse_linear(self, num_qubits): + """Test that 'full' and 'reverse_linear' provide the same unitary element.""" + reps = 2 + full = RealAmplitudes(num_qubits=num_qubits, entanglement="full", reps=reps) + num_params = (reps + 1) * num_qubits + np.random.seed(num_qubits) + params = np.random.rand(num_params) + reverse = RealAmplitudes(num_qubits=num_qubits, entanglement="reverse_linear", reps=reps) + full.assign_parameters(params, inplace=True) + reverse.assign_parameters(params, inplace=True) + assert Operator(full) == Operator(reverse) + if __name__ == "__main__": unittest.main() diff --git a/test/python/circuit/library/test_pauli_feature_map.py b/test/python/circuit/library/test_pauli_feature_map.py index bc2754c1a075..4cdc199b899f 100644 --- a/test/python/circuit/library/test_pauli_feature_map.py +++ b/test/python/circuit/library/test_pauli_feature_map.py @@ -13,6 +13,7 @@ """Test library of Pauli feature map circuits.""" import unittest +from test import combine import numpy as np @@ -152,10 +153,11 @@ def zz_evolution(circuit, qubit1, qubit2): self.assertTrue(Operator(encoding).equiv(ref)) - def test_zz_pairwise_entanglement(self): - """Test the ZZ feature map works with pairwise entanglement.""" + @combine(entanglement=["linear", "reverse_linear", "pairwise"]) + def test_zz_entanglement(self, entanglement): + """Test the ZZ feature map works with pairwise, linear and reverse_linear entanglement.""" num_qubits = 5 - encoding = ZZFeatureMap(num_qubits, entanglement="pairwise", reps=1) + encoding = ZZFeatureMap(num_qubits, entanglement=entanglement, reps=1) ops = encoding.decompose().count_ops() expected_ops = {"h": num_qubits, "p": 2 * num_qubits - 1, "cx": 2 * (num_qubits - 1)} self.assertEqual(ops, expected_ops) From fcb5ed46bba7ca4ad2bcba0306cbec8916ade119 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Wed, 3 Aug 2022 00:17:29 +0200 Subject: [PATCH 24/82] Fix code-block:: python (#8434) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/algorithms/optimizers/spsa.py | 64 +++++++++---------- qiskit/extensions/unitary.py | 4 +- .../passes/scheduling/padding/pad_delay.py | 2 +- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/qiskit/algorithms/optimizers/spsa.py b/qiskit/algorithms/optimizers/spsa.py index 6634c98dfdcb..707409efe4cf 100644 --- a/qiskit/algorithms/optimizers/spsa.py +++ b/qiskit/algorithms/optimizers/spsa.py @@ -110,6 +110,38 @@ def loss(x): two_spsa = SPSA(maxiter=300, second_order=True) result = two_spsa.optimize(ansatz.num_parameters, loss, initial_point=initial_point) + The `termination_checker` can be used to implement a custom termination criterion. + + .. code-block:: python + + import numpy as np + from qiskit.algorithms.optimizers import SPSA + + def objective(x): + return np.linalg.norm(x) + .04*np.random.rand(1) + + class TerminationChecker: + + def __init__(self, N : int): + self.N = N + self.values = [] + + def __call__(self, nfev, parameters, value, stepsize, accepted) -> bool: + self.values.append(value) + + if len(self.values) > self.N: + last_values = self.values[-self.N:] + pp = np.polyfit(range(self.N), last_values, 1) + slope = pp[0] / self.N + + if slope > 0: + return True + return False + + spsa = SPSA(maxiter=200, termination_checker=TerminationChecker(10)) + parameters, value, niter = spsa.optimize(2, objective, initial_point=[0.5, 0.5]) + print(f'SPSA completed after {niter} iterations') + References: @@ -206,38 +238,6 @@ def __init__( ValueError: If ``learning_rate`` or ``perturbation`` is an array with less elements than the number of iterations. - Example: - .. code-block::python - - import numpy as np - from qiskit.algorithms.optimizers import SPSA - - def objective(x): - return np.linalg.norm(x) + .04*np.random.rand(1) - - class TerminationChecker: - - def __init__(self, N : int): - self.N = N - self.values = [] - - def __call__(self, nfev, parameters, value, stepsize, accepted) -> bool: - self.values.append(value) - - if len(self.values) > self.N: - last_values = self.values[-self.N:] - pp = np.polyfit(range(self.N), last_values, 1) - slope = pp[0] / self.N - - if slope > 0: - return True - return False - - spsa = SPSA(maxiter=200, termination_checker=TerminationChecker(10)) - parameters, value, niter = spsa.optimize(2, objective, initial_point=[0.5, 0.5]) - print(f'SPSA completed after {niter} iterations') - - """ super().__init__() diff --git a/qiskit/extensions/unitary.py b/qiskit/extensions/unitary.py index a1bd2d2551aa..ff166044e82a 100644 --- a/qiskit/extensions/unitary.py +++ b/qiskit/extensions/unitary.py @@ -43,7 +43,7 @@ class UnitaryGate(Gate): to a quantum circuit. The matrix can also be directly applied to the quantum circuit, see :meth:`~qiskit.QuantumCircuit.unitary`. - .. code-block::python + .. code-block:: python from qiskit import QuantumCircuit from qiskit.extensions import UnitaryGate @@ -230,7 +230,7 @@ def unitary(self, obj, qubits, label=None): Apply a gate specified by a unitary matrix to a quantum circuit - .. code-block::python + .. code-block:: python from qiskit import QuantumCircuit matrix = [[0, 0, 0, 1], diff --git a/qiskit/transpiler/passes/scheduling/padding/pad_delay.py b/qiskit/transpiler/passes/scheduling/padding/pad_delay.py index c188247e2b26..c0a12267211a 100644 --- a/qiskit/transpiler/passes/scheduling/padding/pad_delay.py +++ b/qiskit/transpiler/passes/scheduling/padding/pad_delay.py @@ -24,7 +24,7 @@ class PadDelay(BasePadding): Consecutive delays will be merged in the output of this pass. - .. code-block::python + .. code-block:: python durations = InstructionDurations([("x", None, 160), ("cx", None, 800)]) From f206accc270ff2144387c8f25833b31ad8e87572 Mon Sep 17 00:00:00 2001 From: Guillermo-Mijares-Vilarino <106545082+Guillermo-Mijares-Vilarino@users.noreply.github.com> Date: Thu, 4 Aug 2022 10:55:45 +0200 Subject: [PATCH 25/82] Added plot state qsphere code example to API reference in order to better showcase the different arguments (#8355) * changed plot_state_qsphere code examples in API reference to better showcase the different arguments * run tests * removed reference to Aer and made changes to circuit * simplified state commands * Update qiskit/visualization/state_visualization.py Co-authored-by: Matthew Treinish * removed repeated imports and matplotlib inline * added comment and removed figsize Co-authored-by: Junye Huang Co-authored-by: Matthew Treinish Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/visualization/state_visualization.py | 24 ++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/qiskit/visualization/state_visualization.py b/qiskit/visualization/state_visualization.py index 00273d10302d..5b08ecba947a 100644 --- a/qiskit/visualization/state_visualization.py +++ b/qiskit/visualization/state_visualization.py @@ -698,20 +698,38 @@ def plot_state_qsphere( QiskitError: Input statevector does not have valid dimensions. - Example: + Examples: .. jupyter-execute:: from qiskit import QuantumCircuit from qiskit.quantum_info import Statevector from qiskit.visualization import plot_state_qsphere - %matplotlib inline qc = QuantumCircuit(2) qc.h(0) qc.cx(0, 1) - state = Statevector.from_instruction(qc) + state = Statevector(qc) plot_state_qsphere(state) + + .. jupyter-execute:: + + # You can show the phase of each state and use + # degrees instead of radians + + from qiskit.quantum_info import DensityMatrix + import numpy as np + + qc = QuantumCircuit(2) + qc.h([0, 1]) + qc.cz(0,1) + qc.ry(np.pi/3, 0) + qc.rx(np.pi/5, 1) + qc.z(1) + + matrix = DensityMatrix(qc) + plot_state_qsphere(matrix, + show_state_phases = True, use_degrees = True) """ from matplotlib import gridspec from matplotlib import pyplot as plt From 316eaf5da1c3ff1c34d018c5aaca72e88a43483b Mon Sep 17 00:00:00 2001 From: Guillermo-Mijares-Vilarino <106545082+Guillermo-Mijares-Vilarino@users.noreply.github.com> Date: Thu, 4 Aug 2022 16:28:58 +0200 Subject: [PATCH 26/82] Added plot bloch vector API reference code examples (#8350) * changed plot_bloch_vector in API reference code examples to better showcase the different arguments * Removed extra imports and matplotlib inline * Added comments and removed figsize * remove spaces around = in function arguments Co-authored-by: Junye Huang Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/visualization/state_visualization.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/qiskit/visualization/state_visualization.py b/qiskit/visualization/state_visualization.py index 5b08ecba947a..2599e29ad0e8 100644 --- a/qiskit/visualization/state_visualization.py +++ b/qiskit/visualization/state_visualization.py @@ -198,13 +198,21 @@ def plot_bloch_vector(bloch, title="", ax=None, figsize=None, coord_type="cartes Raises: MissingOptionalLibraryError: Requires matplotlib. - Example: + Examples: .. jupyter-execute:: from qiskit.visualization import plot_bloch_vector - %matplotlib inline plot_bloch_vector([0,1,0], title="New Bloch Sphere") + + .. jupyter-execute:: + + # You can use spherical coordinates instead of cartesian. + + import numpy as np + + plot_bloch_vector([1, np.pi/2, np.pi/3], coord_type='spherical') + """ from qiskit.visualization.bloch import Bloch From 3b74f5e37d8d3fbf37bba6febe58d18b83e978dd Mon Sep 17 00:00:00 2001 From: Guillermo-Mijares-Vilarino <106545082+Guillermo-Mijares-Vilarino@users.noreply.github.com> Date: Thu, 4 Aug 2022 18:39:33 +0200 Subject: [PATCH 27/82] Changed plot_state_hinton code example in API reference to better showcase what the Hinton does (#8353) * changed plot_state_hinton code examples in API reference to better showcase the different arguments changed plot_state_hinton examples to better showcase the different options * run tests * Put the circuit in the extra example into the first and removed the extra * simplified density matrix definition command * Removed matplotlib inline * Removed figsize * changed order of imports Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/visualization/state_visualization.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/qiskit/visualization/state_visualization.py b/qiskit/visualization/state_visualization.py index 2599e29ad0e8..28ca5f637219 100644 --- a/qiskit/visualization/state_visualization.py +++ b/qiskit/visualization/state_visualization.py @@ -70,20 +70,23 @@ def plot_state_hinton( MissingOptionalLibraryError: Requires matplotlib. VisualizationError: if input is not a valid N-qubit state. - Example: + Examples: .. jupyter-execute:: + import numpy as np from qiskit import QuantumCircuit from qiskit.quantum_info import DensityMatrix from qiskit.visualization import plot_state_hinton - %matplotlib inline qc = QuantumCircuit(2) - qc.h(0) - qc.cx(0, 1) + qc.h([0, 1]) + qc.cz(0,1) + qc.ry(np.pi/3 , 0) + qc.rx(np.pi/5, 1) - state = DensityMatrix.from_instruction(qc) + state = DensityMatrix(qc) plot_state_hinton(state, title="New Hinton Plot") + """ from matplotlib import pyplot as plt From beea6f0f2de0aa8f0484fb14c5024f22c0b5b715 Mon Sep 17 00:00:00 2001 From: Guillermo-Mijares-Vilarino <106545082+Guillermo-Mijares-Vilarino@users.noreply.github.com> Date: Thu, 4 Aug 2022 20:30:24 +0200 Subject: [PATCH 28/82] Added plot state paulivec examples to the code examples in the API reference to better showcase the different arguments (#8354) * changed plot_state_paulivec code examples in API reference to better showcase the different arguments * run tests * Removed reference to Aer and transpile * simplified state commands * Removed extra imports and matplotlib inline * Add comments and remove figsize * unwrap comments and codes, remove spaces around = in function arguments * import numpy before importing qiskit * reduce comment length to 100 Co-authored-by: Junye Huang Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/visualization/state_visualization.py | 27 +++++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/qiskit/visualization/state_visualization.py b/qiskit/visualization/state_visualization.py index 28ca5f637219..b47b1fa2f416 100644 --- a/qiskit/visualization/state_visualization.py +++ b/qiskit/visualization/state_visualization.py @@ -554,21 +554,38 @@ def plot_state_paulivec( MissingOptionalLibraryError: Requires matplotlib. VisualizationError: if input is not a valid N-qubit state. - Example: + Examples: .. jupyter-execute:: + # You can set a color for all the bars. + from qiskit import QuantumCircuit from qiskit.quantum_info import Statevector from qiskit.visualization import plot_state_paulivec - %matplotlib inline qc = QuantumCircuit(2) qc.h(0) qc.cx(0, 1) - state = Statevector.from_instruction(qc) - plot_state_paulivec(state, color='midnightblue', - title="New PauliVec plot") + state = Statevector(qc) + plot_state_paulivec(state, color='midnightblue', title="New PauliVec plot") + + .. jupyter-execute:: + + # If you introduce a list with less colors than bars, the color of the bars will + # alternate following the sequence from the list. + + import numpy as np + from qiskit.quantum_info import DensityMatrix + + qc = QuantumCircuit(2) + qc.h([0, 1]) + qc.cz(0, 1) + qc.ry(np.pi/3, 0) + qc.rx(np.pi/5, 1) + + matrix = DensityMatrix(qc) + plot_state_paulivec(matrix, color=['crimson', 'midnightblue', 'seagreen']) """ from matplotlib import pyplot as plt From 6a1a10287ebde0b445b66a1de5de99df5bf13ad9 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Thu, 4 Aug 2022 21:57:01 +0200 Subject: [PATCH 29/82] Follow-up on #8434 (Fix ``code-blocks::python``) (#8442) * Fix code-block:: python * Follow-up on code-blocks:: python Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/algorithms/minimum_eigen_solvers/vqe.py | 4 ++-- qiskit/algorithms/optimizers/gradient_descent.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit/algorithms/minimum_eigen_solvers/vqe.py b/qiskit/algorithms/minimum_eigen_solvers/vqe.py index 29d10b7be31e..87ab8707b0f1 100755 --- a/qiskit/algorithms/minimum_eigen_solvers/vqe.py +++ b/qiskit/algorithms/minimum_eigen_solvers/vqe.py @@ -92,7 +92,7 @@ class VQE(VariationalAlgorithm, MinimumEigensolver): The callable _must_ have the argument names ``fun, x0, jac, bounds`` as indicated in the following code block. - .. code-block::python + .. code-block:: python from qiskit.algorithms.optimizers import OptimizerResult @@ -111,7 +111,7 @@ def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: The above signature also allows to directly pass any SciPy minimizer, for instance as - .. code-block::python + .. code-block:: python from functools import partial from scipy.optimize import minimize diff --git a/qiskit/algorithms/optimizers/gradient_descent.py b/qiskit/algorithms/optimizers/gradient_descent.py index 5ca2e0f8cad8..38ed55048623 100644 --- a/qiskit/algorithms/optimizers/gradient_descent.py +++ b/qiskit/algorithms/optimizers/gradient_descent.py @@ -50,7 +50,7 @@ class GradientDescent(Optimizer): A minimum example that will use finite difference gradients with a default perturbation of 0.01 and a default learning rate of 0.01. - .. code-block::python + .. code-block:: python from qiskit.algorithms.optimizers import GradientDescent @@ -70,7 +70,7 @@ def f(x): Note how much faster this convergences (i.e. less ``nfevs``) compared to the previous example. - .. code-block::python + .. code-block:: python from qiskit.algorithms.optimizers import GradientDescent From 37057987f4999dbdcb9916c091172577514351fb Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Thu, 4 Aug 2022 23:43:51 +0200 Subject: [PATCH 30/82] Projected Variational Quantum Dynamics (#8304) * pvqd dradt * update as theta + difference * black * refactor test * update to new time evo framework * add gradients * use gradient only if supported * polishing! - reno - remove old pvqd file - allow attributes to be None - more tests * fix algorithms import * changes from code review * add more tests for different ops * refactor PVQD to multiple files * remove todo * comments from review * rm OrderedDict from unitary no idea why this unused import existed, that should be caught before this PR? * changes from code review * remove function to attach intial states * include comments from review - support MatrixOp - default for timestep - update reno with refs - test step and get_loss * make only quantum instance and optimizer optional, and use num_timesteps * fix docs * add comment why Optimizer is optional * use class attributes to document mutable attrs * rm duplicate quantum_instance doc * fix attributes docs Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/algorithms/__init__.py | 5 + .../algorithms/evolvers/evolution_problem.py | 6 +- qiskit/algorithms/evolvers/pvqd/__init__.py | 18 + qiskit/algorithms/evolvers/pvqd/pvqd.py | 414 ++++++++++++++++++ .../algorithms/evolvers/pvqd/pvqd_result.py | 55 +++ qiskit/algorithms/evolvers/pvqd/utils.py | 100 +++++ .../project-dynamics-2f848a5f89655429.yaml | 45 ++ test/python/algorithms/evolvers/test_pvqd.py | 325 ++++++++++++++ 8 files changed, 966 insertions(+), 2 deletions(-) create mode 100644 qiskit/algorithms/evolvers/pvqd/__init__.py create mode 100644 qiskit/algorithms/evolvers/pvqd/pvqd.py create mode 100644 qiskit/algorithms/evolvers/pvqd/pvqd_result.py create mode 100644 qiskit/algorithms/evolvers/pvqd/utils.py create mode 100644 releasenotes/notes/project-dynamics-2f848a5f89655429.yaml create mode 100644 test/python/algorithms/evolvers/test_pvqd.py diff --git a/qiskit/algorithms/__init__.py b/qiskit/algorithms/__init__.py index 452f9a4f1220..63e44798a710 100644 --- a/qiskit/algorithms/__init__.py +++ b/qiskit/algorithms/__init__.py @@ -108,6 +108,8 @@ RealEvolver ImaginaryEvolver TrotterQRTE + PVQD + PVQDResult EvolutionResult EvolutionProblem @@ -246,6 +248,7 @@ from .exceptions import AlgorithmError from .aux_ops_evaluator import eval_observables from .evolvers.trotterization import TrotterQRTE +from .evolvers.pvqd import PVQD, PVQDResult __all__ = [ "AlgorithmResult", @@ -292,6 +295,8 @@ "PhaseEstimationScale", "PhaseEstimation", "PhaseEstimationResult", + "PVQD", + "PVQDResult", "IterativePhaseEstimation", "AlgorithmError", "eval_observables", diff --git a/qiskit/algorithms/evolvers/evolution_problem.py b/qiskit/algorithms/evolvers/evolution_problem.py index b069effe1747..e0f9fe3063c6 100644 --- a/qiskit/algorithms/evolvers/evolution_problem.py +++ b/qiskit/algorithms/evolvers/evolution_problem.py @@ -31,7 +31,7 @@ def __init__( self, hamiltonian: OperatorBase, time: float, - initial_state: Union[StateFn, QuantumCircuit], + initial_state: Optional[Union[StateFn, QuantumCircuit]] = None, aux_operators: Optional[ListOrDict[OperatorBase]] = None, truncation_threshold: float = 1e-12, t_param: Optional[Parameter] = None, @@ -41,7 +41,9 @@ def __init__( Args: hamiltonian: The Hamiltonian under which to evolve the system. time: Total time of evolution. - initial_state: Quantum state to be evolved. + initial_state: The quantum state to be evolved for methods like Trotterization. + For variational time evolutions, where the evolution happens in an ansatz, + this argument is not required. aux_operators: Optional list of auxiliary operators to be evaluated with the evolved ``initial_state`` and their expectation values returned. truncation_threshold: Defines a threshold under which values can be assumed to be 0. diff --git a/qiskit/algorithms/evolvers/pvqd/__init__.py b/qiskit/algorithms/evolvers/pvqd/__init__.py new file mode 100644 index 000000000000..9377ce631b4e --- /dev/null +++ b/qiskit/algorithms/evolvers/pvqd/__init__.py @@ -0,0 +1,18 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""The projected Variational Quantum Dynamic (p-VQD) module.""" + +from .pvqd_result import PVQDResult +from .pvqd import PVQD + +__all__ = ["PVQD", "PVQDResult"] diff --git a/qiskit/algorithms/evolvers/pvqd/pvqd.py b/qiskit/algorithms/evolvers/pvqd/pvqd.py new file mode 100644 index 000000000000..e4a1d5893bb5 --- /dev/null +++ b/qiskit/algorithms/evolvers/pvqd/pvqd.py @@ -0,0 +1,414 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019, 2022. +# +# 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. + +"""The projected Variational Quantum Dynamics Algorithm.""" + +from typing import Optional, Union, List, Tuple, Callable + +import logging +import numpy as np + +from qiskit import QiskitError +from qiskit.algorithms.optimizers import Optimizer, Minimizer +from qiskit.circuit import QuantumCircuit, ParameterVector +from qiskit.circuit.library import PauliEvolutionGate +from qiskit.extensions import HamiltonianGate +from qiskit.providers import Backend +from qiskit.opflow import OperatorBase, CircuitSampler, ExpectationBase, StateFn, MatrixOp +from qiskit.synthesis import EvolutionSynthesis, LieTrotter +from qiskit.utils import QuantumInstance + +from .pvqd_result import PVQDResult +from .utils import _get_observable_evaluator, _is_gradient_supported + +from ..evolution_problem import EvolutionProblem +from ..evolution_result import EvolutionResult +from ..real_evolver import RealEvolver + +logger = logging.getLogger(__name__) + + +class PVQD(RealEvolver): + """The projected Variational Quantum Dynamics (p-VQD) Algorithm. + + In each timestep, this algorithm computes the next state with a Trotter formula + (specified by the ``evolution`` argument) and projects the timestep onto a variational form + (``ansatz``). The projection is determined by maximizing the fidelity of the Trotter-evolved + state and the ansatz, using a classical optimization routine. See Ref. [1] for details. + + The following attributes can be set via the initializer but can also be read and + updated once the PVQD object has been constructed. + + Attributes: + + ansatz (QuantumCircuit): The parameterized circuit representing the time-evolved state. + initial_parameters (np.ndarray): The parameters of the ansatz at time 0. + expectation (ExpectationBase): The method to compute expectation values. + optimizer (Optional[Union[Optimizer, Minimizer]]): The classical optimization routine + used to maximize the fidelity of the Trotter step and ansatz. + num_timesteps (Optional[int]): The number of timesteps to take. If None, it is automatically + selected to achieve a timestep of approximately 0.01. + evolution (Optional[EvolutionSynthesis]): The method to perform the Trotter step. + Defaults to first-order Lie-Trotter evolution. + use_parameter_shift (bool): If True, use the parameter shift rule for loss function + gradients (if the ansatz supports). + initial_guess (Optional[np.ndarray]): The starting point for the first classical optimization + run, at time 0. Defaults to random values in :math:`[-0.01, 0.01]`. + + Example: + + This snippet computes the real time evolution of a quantum Ising model on two + neighboring sites and keeps track of the magnetization. + + .. code-block:: python + + import numpy as np + + from qiskit import BasicAer + from qiskit.circuit.library import EfficientSU2 + from qiskit.opflow import X, Z, I, MatrixExpectation + + backend = BasicAer.get_backend("statevector_simulator") + expectation = MatrixExpectation() + hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I) + observable = Z ^ Z + ansatz = EfficientSU2(2, reps=1) + initial_parameters = np.zeros(ansatz.num_parameters) + + time = 1 + optimizer = L_BFGS_B() + + # setup the algorithm + pvqd = PVQD( + ansatz, + initial_parameters, + num_timesteps=100, + optimizer=optimizer, + quantum_instance=backend, + expectation=expectation + ) + + # specify the evolution problem + problem = EvolutionProblem( + hamiltonian, time, aux_operators=[hamiltonian, observable] + ) + + # and evolve! + result = pvqd.evolve(problem) + + References: + + [1] Stefano Barison, Filippo Vicentini, and Giuseppe Carleo (2021), An efficient + quantum algorithm for the time evolution of parameterized circuits, + `Quantum 5, 512 `_. + """ + + def __init__( + self, + ansatz: QuantumCircuit, + initial_parameters: np.ndarray, + expectation: ExpectationBase, + optimizer: Optional[Union[Optimizer, Minimizer]] = None, + num_timesteps: Optional[int] = None, + evolution: Optional[EvolutionSynthesis] = None, + use_parameter_shift: bool = True, + initial_guess: Optional[np.ndarray] = None, + quantum_instance: Optional[Union[Backend, QuantumInstance]] = None, + ) -> None: + """ + Args: + ansatz: A parameterized circuit preparing the variational ansatz to model the + time evolved quantum state. + initial_parameters: The initial parameters for the ansatz. Together with the ansatz, + these define the initial state of the time evolution. + expectation: The expectation converter to evaluate expectation values. + optimizer: The classical optimizers used to minimize the overlap between + Trotterization and ansatz. Can be either a :class:`.Optimizer` or a callable + using the :class:`.Minimizer` protocol. This argument is optional since it is + not required for :meth:`get_loss`, but it has to be set before :meth:`evolve` + is called. + num_timestep: The number of time steps. If ``None`` it will be set such that the timestep + is close to 0.01. + evolution: The evolution synthesis to use for the construction of the Trotter step. + Defaults to first-order Lie-Trotter decomposition, see also + :mod:`~qiskit.synthesis.evolution` for different options. + use_parameter_shift: If True, use the parameter shift rule to compute gradients. + If False, the optimizer will not be passed a gradient callable. In that case, + Qiskit optimizers will use a finite difference rule to approximate the gradients. + initial_guess: The initial guess for the first VQE optimization. Afterwards the + previous iteration result is used as initial guess. If None, this is set to + a random vector with elements in the interval :math:`[-0.01, 0.01]`. + quantum_instance: The backend or quantum instance used to evaluate the circuits. + """ + if evolution is None: + evolution = LieTrotter() + + self.ansatz = ansatz + self.initial_parameters = initial_parameters + self.num_timesteps = num_timesteps + self.optimizer = optimizer + self.initial_guess = initial_guess + self.expectation = expectation + self.evolution = evolution + self.use_parameter_shift = use_parameter_shift + + self._sampler = None + self.quantum_instance = quantum_instance + + @property + def quantum_instance(self) -> Optional[QuantumInstance]: + """Return the current quantum instance.""" + return self._quantum_instance + + @quantum_instance.setter + def quantum_instance(self, quantum_instance: Optional[Union[Backend, QuantumInstance]]) -> None: + """Set the quantum instance and circuit sampler.""" + if quantum_instance is not None: + if not isinstance(quantum_instance, QuantumInstance): + quantum_instance = QuantumInstance(quantum_instance) + self._sampler = CircuitSampler(quantum_instance) + + self._quantum_instance = quantum_instance + + def step( + self, + hamiltonian: OperatorBase, + ansatz: QuantumCircuit, + theta: np.ndarray, + dt: float, + initial_guess: np.ndarray, + ) -> Tuple[np.ndarray, float]: + """Perform a single time step. + + Args: + hamiltonian: The Hamiltonian under which to evolve. + ansatz: The parameterized quantum circuit which attempts to approximate the + time-evolved state. + theta: The current parameters. + dt: The time step. + initial_guess: The initial guess for the classical optimization of the + fidelity between the next variational state and the Trotter-evolved last state. + If None, this is set to a random vector with elements in the interval + :math:`[-0.01, 0.01]`. + + Returns: + A tuple consisting of the next parameters and the fidelity of the optimization. + """ + self._validate_setup() + + loss, gradient = self.get_loss(hamiltonian, ansatz, dt, theta) + + if initial_guess is None: + initial_guess = np.random.random(self.initial_parameters.size) * 0.01 + + if isinstance(self.optimizer, Optimizer): + optimizer_result = self.optimizer.minimize(loss, initial_guess, gradient) + else: + optimizer_result = self.optimizer(loss, initial_guess, gradient) + + # clip the fidelity to [0, 1] + fidelity = np.clip(1 - optimizer_result.fun, 0, 1) + + return theta + optimizer_result.x, fidelity + + def get_loss( + self, + hamiltonian: OperatorBase, + ansatz: QuantumCircuit, + dt: float, + current_parameters: np.ndarray, + ) -> Tuple[Callable[[np.ndarray], float], Optional[Callable[[np.ndarray], np.ndarray]]]: + + """Get a function to evaluate the infidelity between Trotter step and ansatz. + + Args: + hamiltonian: The Hamiltonian under which to evolve. + ansatz: The parameterized quantum circuit which attempts to approximate the + time-evolved state. + dt: The time step. + current_parameters: The current parameters. + + Returns: + A callable to evaluate the infidelity and, if gradients are supported and required, + a second callable to evaluate the gradient of the infidelity. + """ + self._validate_setup(skip={"optimizer"}) + + # use Trotterization to evolve the current state + trotterized = ansatz.bind_parameters(current_parameters) + + if isinstance(hamiltonian, MatrixOp): + evolution_gate = HamiltonianGate(hamiltonian.primitive, time=dt) + else: + evolution_gate = PauliEvolutionGate(hamiltonian, time=dt, synthesis=self.evolution) + + trotterized.append(evolution_gate, ansatz.qubits) + + # define the overlap of the Trotterized state and the ansatz + x = ParameterVector("w", ansatz.num_parameters) + shifted = ansatz.assign_parameters(current_parameters + x) + overlap = StateFn(trotterized).adjoint() @ StateFn(shifted) + + converted = self.expectation.convert(overlap) + + def evaluate_loss( + displacement: Union[np.ndarray, List[np.ndarray]] + ) -> Union[float, List[float]]: + """Evaluate the overlap of the ansatz with the Trotterized evolution. + + Args: + displacement: The parameters for the ansatz. + + Returns: + The fidelity of the ansatz with parameters ``theta`` and the Trotterized evolution. + """ + if isinstance(displacement, list): + displacement = np.asarray(displacement) + value_dict = {x_i: displacement[:, i].tolist() for i, x_i in enumerate(x)} + else: + value_dict = dict(zip(x, displacement)) + + sampled = self._sampler.convert(converted, params=value_dict) + + # in principle we could add different loss functions here, but we're currently + # not aware of a use-case for a different one than in the paper + return 1 - np.abs(sampled.eval()) ** 2 + + if _is_gradient_supported(ansatz) and self.use_parameter_shift: + + def evaluate_gradient(displacement: np.ndarray) -> np.ndarray: + """Evaluate the gradient with the parameter-shift rule. + + This is hardcoded here since the gradient framework does not support computing + gradients for overlaps. + + Args: + displacement: The parameters for the ansatz. + + Returns: + The gradient. + """ + # construct lists where each element is shifted by plus (or minus) pi/2 + dim = displacement.size + plus_shifts = (displacement + np.pi / 2 * np.identity(dim)).tolist() + minus_shifts = (displacement - np.pi / 2 * np.identity(dim)).tolist() + + evaluated = evaluate_loss(plus_shifts + minus_shifts) + + gradient = (evaluated[:dim] - evaluated[dim:]) / 2 + + return gradient + + else: + evaluate_gradient = None + + return evaluate_loss, evaluate_gradient + + def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: + """ + Args: + evolution_problem: The evolution problem containing the hamiltonian, total evolution + time and observables to evaluate. + + Returns: + A result object containing the evolution information and evaluated observables. + + Raises: + ValueError: If the evolution time is not positive or the timestep is too small. + NotImplementedError: If the evolution problem contains an initial state. + """ + self._validate_setup() + + time = evolution_problem.time + observables = evolution_problem.aux_operators + hamiltonian = evolution_problem.hamiltonian + + # determine the number of timesteps and set the timestep + num_timesteps = ( + int(np.ceil(time / 0.01)) if self.num_timesteps is None else self.num_timesteps + ) + timestep = time / num_timesteps + + if evolution_problem.initial_state is not None: + raise NotImplementedError( + "Setting an initial state for the evolution is not yet supported for PVQD." + ) + + # get the function to evaluate the observables for a given set of ansatz parameters + if observables is not None: + evaluate_observables = _get_observable_evaluator( + self.ansatz, observables, self.expectation, self._sampler + ) + observable_values = [evaluate_observables(self.initial_parameters)] + + fidelities = [1] + parameters = [self.initial_parameters] + times = np.linspace(0, time, num_timesteps + 1).tolist() # +1 to include initial time 0 + + initial_guess = self.initial_guess + + for _ in range(num_timesteps): + # perform VQE to find the next parameters + next_parameters, fidelity = self.step( + hamiltonian, self.ansatz, parameters[-1], timestep, initial_guess + ) + + # set initial guess to last parameter update + initial_guess = next_parameters - parameters[-1] + + parameters.append(next_parameters) + fidelities.append(fidelity) + if observables is not None: + observable_values.append(evaluate_observables(next_parameters)) + + evolved_state = self.ansatz.bind_parameters(parameters[-1]) + + result = PVQDResult( + evolved_state=evolved_state, + times=times, + parameters=parameters, + fidelities=fidelities, + estimated_error=1 - np.prod(fidelities), + ) + if observables is not None: + result.observables = observable_values + result.aux_ops_evaluated = observable_values[-1] + + return result + + def _validate_setup(self, skip=None): + """Validate the current setup and raise an error if something misses to run.""" + + if skip is None: + skip = {} + + required_attributes = {"quantum_instance", "optimizer"}.difference(skip) + + for attr in required_attributes: + if getattr(self, attr, None) is None: + raise ValueError(f"The {attr} cannot be None.") + + if self.num_timesteps is not None and self.num_timesteps <= 0: + raise ValueError( + f"The number of timesteps must be positive but is {self.num_timesteps}." + ) + + if self.ansatz.num_parameters == 0: + raise QiskitError( + "The ansatz cannot have 0 parameters, otherwise it cannot be trained." + ) + + if len(self.initial_parameters) != self.ansatz.num_parameters: + raise QiskitError( + f"Mismatching number of parameters in the ansatz ({self.ansatz.num_parameters}) " + f"and the initial parameters ({len(self.initial_parameters)})." + ) diff --git a/qiskit/algorithms/evolvers/pvqd/pvqd_result.py b/qiskit/algorithms/evolvers/pvqd/pvqd_result.py new file mode 100644 index 000000000000..e14e7a0c9db5 --- /dev/null +++ b/qiskit/algorithms/evolvers/pvqd/pvqd_result.py @@ -0,0 +1,55 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""Result object for p-VQD.""" + +from typing import Union, Optional, List, Tuple +import numpy as np + +from qiskit.circuit import QuantumCircuit +from qiskit.opflow import StateFn, OperatorBase + +from ..evolution_result import EvolutionResult + + +class PVQDResult(EvolutionResult): + """The result object for the p-VQD algorithm.""" + + def __init__( + self, + evolved_state: Union[StateFn, QuantumCircuit, OperatorBase], + aux_ops_evaluated: Optional[List[Tuple[complex, complex]]] = None, + times: Optional[List[float]] = None, + parameters: Optional[List[np.ndarray]] = None, + fidelities: Optional[List[float]] = None, + estimated_error: Optional[float] = None, + observables: Optional[List[List[float]]] = None, + ): + """ + Args: + evolved_state: An evolved quantum state. + aux_ops_evaluated: Optional list of observables for which expected values on an evolved + state are calculated. These values are in fact tuples formatted as (mean, standard + deviation). + times: The times evaluated during the time integration. + parameters: The parameter values at each evaluation time. + fidelities: The fidelity of the Trotter step and variational update at each iteration. + estimated_error: The overall estimated error evaluated as one minus the + product of all fidelities. + observables: The value of the observables evaluated at each iteration. + """ + super().__init__(evolved_state, aux_ops_evaluated) + self.times = times + self.parameters = parameters + self.fidelities = fidelities + self.estimated_error = estimated_error + self.observables = observables diff --git a/qiskit/algorithms/evolvers/pvqd/utils.py b/qiskit/algorithms/evolvers/pvqd/utils.py new file mode 100644 index 000000000000..589f12005de8 --- /dev/null +++ b/qiskit/algorithms/evolvers/pvqd/utils.py @@ -0,0 +1,100 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + + +"""Utilities for p-VQD.""" + +from typing import Union, List, Callable +import logging + +import numpy as np + +from qiskit.circuit import QuantumCircuit, Parameter, ParameterExpression +from qiskit.compiler import transpile +from qiskit.exceptions import QiskitError +from qiskit.opflow import ListOp, CircuitSampler, ExpectationBase, StateFn, OperatorBase +from qiskit.opflow.gradients.circuit_gradients import ParamShift + +logger = logging.getLogger(__name__) + + +def _is_gradient_supported(ansatz: QuantumCircuit) -> bool: + """Check whether we can apply a simple parameter shift rule to obtain gradients.""" + + # check whether the circuit can be unrolled to supported gates + try: + unrolled = transpile(ansatz, basis_gates=ParamShift.SUPPORTED_GATES, optimization_level=0) + except QiskitError: + # failed to map to supported basis + logger.log( + logging.INFO, + "No gradient support: Failed to unroll to gates supported by parameter-shift.", + ) + return False + + # check whether all parameters are unique and we do not need to apply the chain rule + # (since it's not implemented yet) + total_num_parameters = 0 + for circuit_instruction in unrolled.data: + for param in circuit_instruction.operation.params: + if isinstance(param, ParameterExpression): + if isinstance(param, Parameter): + total_num_parameters += 1 + else: + logger.log( + logging.INFO, + "No gradient support: Circuit is only allowed to have plain parameters, " + "as the chain rule is not yet implemented.", + ) + return False + + if total_num_parameters != ansatz.num_parameters: + logger.log( + logging.INFO, + "No gradient support: Circuit is only allowed to have unique parameters, " + "as the product rule is not yet implemented.", + ) + return False + + return True + + +def _get_observable_evaluator( + ansatz: QuantumCircuit, + observables: Union[OperatorBase, List[OperatorBase]], + expectation: ExpectationBase, + sampler: CircuitSampler, +) -> Callable[[np.ndarray], Union[float, List[float]]]: + """Get a callable to evaluate a (list of) observable(s) for given circuit parameters.""" + + if isinstance(observables, list): + observables = ListOp(observables) + + expectation_value = StateFn(observables, is_measurement=True) @ StateFn(ansatz) + converted = expectation.convert(expectation_value) + + ansatz_parameters = ansatz.parameters + + def evaluate_observables(theta: np.ndarray) -> Union[float, List[float]]: + """Evaluate the observables for the ansatz parameters ``theta``. + + Args: + theta: The ansatz parameters. + + Returns: + The observables evaluated at the ansatz parameters. + """ + value_dict = dict(zip(ansatz_parameters, theta)) + sampled = sampler.convert(converted, params=value_dict) + return sampled.eval() + + return evaluate_observables diff --git a/releasenotes/notes/project-dynamics-2f848a5f89655429.yaml b/releasenotes/notes/project-dynamics-2f848a5f89655429.yaml new file mode 100644 index 000000000000..a33fdfefed28 --- /dev/null +++ b/releasenotes/notes/project-dynamics-2f848a5f89655429.yaml @@ -0,0 +1,45 @@ +features: + - | + Added the :class:`PVQD` class to the time evolution framework. This class implements the + projected Variational Quantum Dynamics (p-VQD) algorithm as :class:`.PVQD` of + `Barison et al. `_. + + In each timestep this algorithm computes the next state with a Trotter formula and projects it + onto a variational form. The projection is determined by maximizing the fidelity of the + Trotter-evolved state and the ansatz, using a classical optimization routine. + + .. code-block:: python + + import numpy as np + + from qiskit import BasicAer + from qiskit.circuit.library import EfficientSU2 + from qiskit.opflow import X, Z, I, MatrixExpectation + + backend = BasicAer.get_backend("statevector_simulator") + expectation = MatrixExpectation() + hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I) + observable = Z ^ Z + ansatz = EfficientSU2(2, reps=1) + initial_parameters = np.zeros(ansatz.num_parameters) + + time = 1 + optimizer = L_BFGS_B() + + # setup the algorithm + pvqd = PVQD( + ansatz, + initial_parameters, + num_timesteps=100, + optimizer=optimizer, + quantum_instance=backend, + expectation=expectation + ) + + # specify the evolution problem + problem = EvolutionProblem( + hamiltonian, time, aux_operators=[hamiltonian, observable] + ) + + # and evolve! + result = pvqd.evolve(problem) diff --git a/test/python/algorithms/evolvers/test_pvqd.py b/test/python/algorithms/evolvers/test_pvqd.py new file mode 100644 index 000000000000..5c450fa24822 --- /dev/null +++ b/test/python/algorithms/evolvers/test_pvqd.py @@ -0,0 +1,325 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2018, 2022. +# +# 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 PVQD.""" + +from functools import partial +from ddt import ddt, data, unpack +import numpy as np + +from qiskit.test import QiskitTestCase + +from qiskit import BasicAer, QiskitError +from qiskit.circuit import QuantumCircuit, Parameter, Gate +from qiskit.algorithms.evolvers import EvolutionProblem +from qiskit.algorithms.evolvers.pvqd import PVQD +from qiskit.algorithms.optimizers import L_BFGS_B, GradientDescent, SPSA, OptimizerResult +from qiskit.circuit.library import EfficientSU2 +from qiskit.opflow import X, Z, I, MatrixExpectation, PauliExpectation + + +# pylint: disable=unused-argument, invalid-name +def gradient_supplied(fun, x0, jac, info): + """A mock optimizer that checks whether the gradient is supported or not.""" + result = OptimizerResult() + result.x = x0 + result.fun = 0 + info["has_gradient"] = jac is not None + + return result + + +class WhatAmI(Gate): + """An custom opaque gate that can be inverted but not decomposed.""" + + def __init__(self, angle): + super().__init__(name="whatami", num_qubits=2, params=[angle]) + + def inverse(self): + return WhatAmI(-self.params[0]) + + +@ddt +class TestPVQD(QiskitTestCase): + """Tests for the pVQD algorithm.""" + + def setUp(self): + super().setUp() + self.sv_backend = BasicAer.get_backend("statevector_simulator") + self.qasm_backend = BasicAer.get_backend("qasm_simulator") + self.expectation = MatrixExpectation() + self.hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I) + self.observable = Z ^ Z + self.ansatz = EfficientSU2(2, reps=1) + self.initial_parameters = np.zeros(self.ansatz.num_parameters) + + @data( + ("ising", MatrixExpectation, True, "sv", 2), + ("ising_matrix", MatrixExpectation, True, "sv", None), + ("ising", PauliExpectation, True, "qasm", 2), + ("pauli", PauliExpectation, False, "qasm", None), + ) + @unpack + def test_pvqd(self, hamiltonian_type, expectation_cls, gradient, backend_type, num_timesteps): + """Test a simple evolution.""" + time = 0.02 + + if hamiltonian_type == "ising": + hamiltonian = self.hamiltonian + elif hamiltonian_type == "ising_matrix": + hamiltonian = self.hamiltonian.to_matrix_op() + else: # hamiltonian_type == "pauli": + hamiltonian = X ^ X + + # parse input arguments + if gradient: + optimizer = GradientDescent(maxiter=1) + else: + optimizer = L_BFGS_B(maxiter=1) + + backend = self.sv_backend if backend_type == "sv" else self.qasm_backend + expectation = expectation_cls() + + # run pVQD keeping track of the energy and the magnetization + pvqd = PVQD( + self.ansatz, + self.initial_parameters, + num_timesteps=num_timesteps, + optimizer=optimizer, + quantum_instance=backend, + expectation=expectation, + ) + problem = EvolutionProblem(hamiltonian, time, aux_operators=[hamiltonian, self.observable]) + result = pvqd.evolve(problem) + + self.assertTrue(len(result.fidelities) == 3) + self.assertTrue(np.all(result.times == [0.0, 0.01, 0.02])) + self.assertTrue(np.asarray(result.observables).shape == (3, 2)) + num_parameters = self.ansatz.num_parameters + self.assertTrue( + len(result.parameters) == 3 + and np.all([len(params) == num_parameters for params in result.parameters]) + ) + + def test_step(self): + """Test calling the step method directly.""" + + pvqd = PVQD( + self.ansatz, + self.initial_parameters, + optimizer=L_BFGS_B(maxiter=100), + quantum_instance=self.sv_backend, + expectation=MatrixExpectation(), + ) + + # perform optimization for a timestep of 0, then the optimal parameters are the current + # ones and the fidelity is 1 + theta_next, fidelity = pvqd.step( + self.hamiltonian.to_matrix_op(), + self.ansatz, + self.initial_parameters, + dt=0.0, + initial_guess=np.zeros_like(self.initial_parameters), + ) + + self.assertTrue(np.allclose(theta_next, self.initial_parameters)) + self.assertAlmostEqual(fidelity, 1) + + def test_get_loss(self): + """Test getting the loss function directly.""" + + pvqd = PVQD( + self.ansatz, + self.initial_parameters, + quantum_instance=self.sv_backend, + expectation=MatrixExpectation(), + use_parameter_shift=False, + ) + + theta = np.ones(self.ansatz.num_parameters) + loss, gradient = pvqd.get_loss( + self.hamiltonian, self.ansatz, dt=0.0, current_parameters=theta + ) + + displacement = np.arange(self.ansatz.num_parameters) + + with self.subTest(msg="check gradient is None"): + self.assertIsNone(gradient) + + with self.subTest(msg="check loss works"): + self.assertGreater(loss(displacement), 0) + self.assertAlmostEqual(loss(np.zeros_like(theta)), 0) + + def test_invalid_num_timestep(self): + """Test raises if the num_timestep is not positive.""" + pvqd = PVQD( + self.ansatz, + self.initial_parameters, + num_timesteps=0, + optimizer=L_BFGS_B(), + quantum_instance=self.sv_backend, + expectation=self.expectation, + ) + problem = EvolutionProblem( + self.hamiltonian, time=0.01, aux_operators=[self.hamiltonian, self.observable] + ) + + with self.assertRaises(ValueError): + _ = pvqd.evolve(problem) + + def test_initial_guess_and_observables(self): + """Test doing no optimizations stays at initial guess.""" + initial_guess = np.zeros(self.ansatz.num_parameters) + + pvqd = PVQD( + self.ansatz, + self.initial_parameters, + num_timesteps=10, + optimizer=SPSA(maxiter=0, learning_rate=0.1, perturbation=0.01), + initial_guess=initial_guess, + quantum_instance=self.sv_backend, + expectation=self.expectation, + ) + problem = EvolutionProblem( + self.hamiltonian, time=0.1, aux_operators=[self.hamiltonian, self.observable] + ) + + result = pvqd.evolve(problem) + + observables = result.aux_ops_evaluated + self.assertEqual(observables[0], 0.1) # expected energy + self.assertEqual(observables[1], 1) # expected magnetization + + def test_missing_attributesquantum_instance(self): + """Test appropriate error is raised if the quantum instance is missing.""" + pvqd = PVQD( + self.ansatz, + self.initial_parameters, + optimizer=L_BFGS_B(maxiter=1), + expectation=self.expectation, + ) + problem = EvolutionProblem(self.hamiltonian, time=0.01) + + attrs_to_test = [ + ("optimizer", L_BFGS_B(maxiter=1)), + ("quantum_instance", self.qasm_backend), + ] + + for attr, value in attrs_to_test: + with self.subTest(msg=f"missing: {attr}"): + # set attribute to None to invalidate the setup + setattr(pvqd, attr, None) + + with self.assertRaises(ValueError): + _ = pvqd.evolve(problem) + + # set the correct value again + setattr(pvqd, attr, value) + + with self.subTest(msg="all set again"): + result = pvqd.evolve(problem) + self.assertIsNotNone(result.evolved_state) + + def test_zero_parameters(self): + """Test passing an ansatz with zero parameters raises an error.""" + problem = EvolutionProblem(self.hamiltonian, time=0.02) + + pvqd = PVQD( + QuantumCircuit(2), + np.array([]), + optimizer=SPSA(maxiter=10, learning_rate=0.1, perturbation=0.01), + quantum_instance=self.sv_backend, + expectation=self.expectation, + ) + + with self.assertRaises(QiskitError): + _ = pvqd.evolve(problem) + + def test_initial_state_raises(self): + """Test passing an initial state raises an error for now.""" + initial_state = QuantumCircuit(2) + initial_state.x(0) + + problem = EvolutionProblem( + self.hamiltonian, + time=0.02, + initial_state=initial_state, + ) + + pvqd = PVQD( + self.ansatz, + self.initial_parameters, + optimizer=SPSA(maxiter=0, learning_rate=0.1, perturbation=0.01), + quantum_instance=self.sv_backend, + expectation=self.expectation, + ) + + with self.assertRaises(NotImplementedError): + _ = pvqd.evolve(problem) + + +class TestPVQDUtils(QiskitTestCase): + """Test some utility functions for PVQD.""" + + def setUp(self): + super().setUp() + self.sv_backend = BasicAer.get_backend("statevector_simulator") + self.expectation = MatrixExpectation() + self.hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I) + self.ansatz = EfficientSU2(2, reps=1) + + def test_gradient_supported(self): + """Test the gradient support is correctly determined.""" + # gradient supported here + wrapped = EfficientSU2(2) # a circuit wrapped into a big instruction + plain = wrapped.decompose() # a plain circuit with already supported instructions + + # gradients not supported on the following circuits + x = Parameter("x") + duplicated = QuantumCircuit(2) + duplicated.rx(x, 0) + duplicated.rx(x, 1) + + needs_chainrule = QuantumCircuit(2) + needs_chainrule.rx(2 * x, 0) + + custom_gate = WhatAmI(x) + unsupported = QuantumCircuit(2) + unsupported.append(custom_gate, [0, 1]) + + tests = [ + (wrapped, True), # tuple: (circuit, gradient support) + (plain, True), + (duplicated, False), + (needs_chainrule, False), + (unsupported, False), + ] + + # used to store the info if a gradient callable is passed into the + # optimizer of not + info = {"has_gradient": None} + optimizer = partial(gradient_supplied, info=info) + + pvqd = PVQD( + ansatz=None, + initial_parameters=np.array([]), + optimizer=optimizer, + quantum_instance=self.sv_backend, + expectation=self.expectation, + ) + problem = EvolutionProblem(self.hamiltonian, time=0.01) + for circuit, expected_support in tests: + with self.subTest(circuit=circuit, expected_support=expected_support): + pvqd.ansatz = circuit + pvqd.initial_parameters = np.zeros(circuit.num_parameters) + _ = pvqd.evolve(problem) + self.assertEqual(info["has_gradient"], expected_support) From 2bab09c1aae84e5bf38ba52fa3a272667c1887cc Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Fri, 5 Aug 2022 00:50:36 +0200 Subject: [PATCH 31/82] Fix ``ParameterExpression.is_real`` if ``symengine`` is installed (#8456) * Fix is_real on ParameterExpression * typo in reno Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/circuit/parameterexpression.py | 11 +++++++++-- .../fix-paramexpr-isreal-8d20348b4ce6cbe7.yaml | 14 ++++++++++++++ test/python/circuit/test_parameters.py | 8 ++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/fix-paramexpr-isreal-8d20348b4ce6cbe7.yaml diff --git a/qiskit/circuit/parameterexpression.py b/qiskit/circuit/parameterexpression.py index 641a7404c553..30bc46922197 100644 --- a/qiskit/circuit/parameterexpression.py +++ b/qiskit/circuit/parameterexpression.py @@ -508,14 +508,21 @@ def __eq__(self, other): def is_real(self): """Return whether the expression is real""" - if not self._symbol_expr.is_real and self._symbol_expr.is_real is not None: + # workaround for symengine behavior that const * (0 + 1 * I) is not real + # see https://github.com/symengine/symengine.py/issues/414 + if _optionals.HAS_SYMENGINE and self._symbol_expr.is_real is None: + symbol_expr = self._symbol_expr.evalf() + else: + symbol_expr = self._symbol_expr + + if not symbol_expr.is_real and symbol_expr.is_real is not None: # Symengine returns false for is_real on the expression if # there is a imaginary component (even if that component is 0), # but the parameter will evaluate as real. Check that if the # expression's is_real attribute returns false that we have a # non-zero imaginary if _optionals.HAS_SYMENGINE: - if self._symbol_expr.imag != 0.0: + if symbol_expr.imag != 0.0: return False else: return False diff --git a/releasenotes/notes/fix-paramexpr-isreal-8d20348b4ce6cbe7.yaml b/releasenotes/notes/fix-paramexpr-isreal-8d20348b4ce6cbe7.yaml new file mode 100644 index 000000000000..f9219b813169 --- /dev/null +++ b/releasenotes/notes/fix-paramexpr-isreal-8d20348b4ce6cbe7.yaml @@ -0,0 +1,14 @@ +--- +fixes: + - | + Fix a bug where a bound :class:`.ParameterExpression` was not identified as real + if ``symengine`` was installed and the bound expression was not a plain ``1j``. + For example:: + + from qiskit.circuit import Parameter + + x = Parameter("x") + expr = 1j * x + bound = expr.bind({x: 2}) + print(bound.is_real()) # used to be True, but is now False + diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index ed25350bf227..3a96963b7f86 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -1718,6 +1718,14 @@ def test_parameter_expression_grad(self): self.assertEqual(expr.gradient(x), 2 * x) self.assertEqual(expr.gradient(x).gradient(x), 2) + def test_bound_expression_is_real(self): + """Test is_real on bound parameters.""" + x = Parameter("x") + expr = 1j * x + bound = expr.bind({x: 2}) + + self.assertFalse(bound.is_real()) + class TestParameterEquality(QiskitTestCase): """Test equality of Parameters and ParameterExpressions.""" From 6a5adf20e86c74ce507dfb82a4d67ab05522048d Mon Sep 17 00:00:00 2001 From: Jun Doi Date: Tue, 9 Aug 2022 04:26:25 +0900 Subject: [PATCH 32/82] Changing aer_simulator_statevector checking method (#8411) * checking backend name by checking string is included * added release note --- qiskit/utils/backend_utils.py | 2 +- qiskit/utils/quantum_instance.py | 2 +- releasenotes/notes/backend_name_fix-175e12b5cf902f99.yaml | 7 +++++++ 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/backend_name_fix-175e12b5cf902f99.yaml diff --git a/qiskit/utils/backend_utils.py b/qiskit/utils/backend_utils.py index bfee6417cce9..39f889b6b96d 100644 --- a/qiskit/utils/backend_utils.py +++ b/qiskit/utils/backend_utils.py @@ -157,7 +157,7 @@ def is_statevector_backend(backend): if isinstance(backend, StatevectorSimulator): return True - if isinstance(backend, AerSimulator) and backend.name() == "aer_simulator_statevector": + if isinstance(backend, AerSimulator) and "aer_simulator_statevector" in backend.name(): return True if backend is None: return False diff --git a/qiskit/utils/quantum_instance.py b/qiskit/utils/quantum_instance.py index 3e77285620c9..4268678f3677 100644 --- a/qiskit/utils/quantum_instance.py +++ b/qiskit/utils/quantum_instance.py @@ -507,7 +507,7 @@ def execute(self, circuits, had_transpiled: bool = False): # transpile here, the method always returns a copied list circuits = self.transpile(circuits) - if self.is_statevector and self.backend_name == "aer_simulator_statevector": + if self.is_statevector and "aer_simulator_statevector" in self.backend_name: try: from qiskit.providers.aer.library import SaveStatevector diff --git a/releasenotes/notes/backend_name_fix-175e12b5cf902f99.yaml b/releasenotes/notes/backend_name_fix-175e12b5cf902f99.yaml new file mode 100644 index 000000000000..e9bb9be5acf4 --- /dev/null +++ b/releasenotes/notes/backend_name_fix-175e12b5cf902f99.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + 'aer_simulator_statevector_gpu' was not recognized correctly as statevector + method in some function when using GPU on Qiskit Aer. + This fix recognizes both 'aer_simulator_statevector' and + 'aer_simulator_statevector_gpu' the backend is statevector. From db612922aa0e0d96d07f40991343da44a31baf1e Mon Sep 17 00:00:00 2001 From: Ikko Hamamura Date: Tue, 9 Aug 2022 17:33:31 +0900 Subject: [PATCH 33/82] Add run method to primitives (#8382) * add submit method * add method docstrings * deprecate context manager * [new feature] append * deprecate __call__ * optional in constructor * Introduce PrimitiveFuture * refactoring * ping Sphinx<5.1 * Extend Job * JobV1 * rename submit to run * rm PrimitiveFuture * fix lint * add tests * add releasenote * rm _submit and add _run * use dict for ids * run cannot take integer indices * deprecate args in constructor * refactoring * Update qiskit/providers/job.py Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> * Update qiskit/primitives/base_sampler.py Co-authored-by: Jessie Yu * refactoring * support parameters in run * fix docs (args order) Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> Co-authored-by: Jessie Yu --- qiskit/primitives/base_estimator.py | 292 ++++++--- qiskit/primitives/base_sampler.py | 198 ++++-- qiskit/primitives/estimator.py | 50 +- qiskit/primitives/estimator_result.py | 2 +- qiskit/primitives/primitive_job.py | 68 +++ qiskit/primitives/sampler.py | 84 ++- qiskit/primitives/sampler_result.py | 2 +- qiskit/primitives/utils.py | 11 - qiskit/providers/job.py | 11 +- .../notes/primitive-run-5d1afab3655330a6.yaml | 37 ++ test/python/primitives/test_estimator.py | 441 ++++++++++---- test/python/primitives/test_sampler.py | 566 ++++++++++++------ 12 files changed, 1310 insertions(+), 452 deletions(-) create mode 100644 qiskit/primitives/primitive_job.py create mode 100644 releasenotes/notes/primitive-run-5d1afab3655330a6.yaml diff --git a/qiskit/primitives/base_estimator.py b/qiskit/primitives/base_estimator.py index 2c71da55835a..830cb54c12fc 100644 --- a/qiskit/primitives/base_estimator.py +++ b/qiskit/primitives/base_estimator.py @@ -32,9 +32,9 @@ The estimator is called with the following inputs. -* circuit indexes: a list of indexes of the quantum circuits. +* circuits: a list of the quantum circuits. -* observable indexes: a list of indexes of the observables. +* observables: a list of the observables. * parameters: a list of parameters of the quantum circuits. (:class:`~qiskit.circuit.parametertable.ParameterView` or @@ -74,56 +74,50 @@ H2 = SparsePauliOp.from_list([("IZ", 1)]) H3 = SparsePauliOp.from_list([("ZI", 1), ("ZZ", 1)]) - with Estimator([psi1, psi2], [H1, H2, H3], [params1, params2]) as e: - theta1 = [0, 1, 1, 2, 3, 5] - theta2 = [0, 1, 1, 2, 3, 5, 8, 13] - theta3 = [1, 2, 3, 4, 5, 6] - - # calculate [ ] - result = e([0], [0], [theta1]) - print(result) - - # calculate [ , ] - result2 = e([0, 0], [1, 2], [theta1]*2) - print(result2) - - # calculate [ ] - result3 = e([1], [1], [theta2]) - print(result3) - - # calculate [ , ] - result4 = e([0, 0], [0, 0], [theta1, theta3]) - print(result4) - - # calculate [ , - # , - # ] - result5 = e([0, 1, 0], [0, 1, 2], [theta1, theta2, theta3]) - print(result5) - - # Objects can be passed instead of indices. - # calculate [ ] - # Note that passing objects has an overhead - # since the corresponding indices need to be searched. - result6 = e([psi2], [H2], [theta2]) - print(result6) + theta1 = [0, 1, 1, 2, 3, 5] + theta2 = [0, 1, 1, 2, 3, 5, 8, 13] + theta3 = [1, 2, 3, 4, 5, 6] + + estimator = Estimator() + + # calculate [ ] + result = estimator.run([psi1], [H1], [theta1]).result() + print(result) + + # calculate [ ] + result2 = estimator.run([psi2], [H1], [theta2]).result() + print(result2) + + # calculate [ , ] + result3 = estimator.run([psi1, psi1], [H2, H3], [theta1]*2).result() + print(result3) + + # calculate [ , + # , + # ] + result4 = estimator.run([psi1, psi2, psi1], [H1, H2, H3], [theta1, theta2, theta3]).result() + print(result4) """ from __future__ import annotations from abc import ABC, abstractmethod from collections.abc import Iterable, Sequence from copy import copy +from typing import cast +from warnings import warn import numpy as np from qiskit.circuit import Parameter, QuantumCircuit from qiskit.circuit.parametertable import ParameterView from qiskit.exceptions import QiskitError +from qiskit.opflow import PauliSumOp +from qiskit.providers import JobV1 as Job from qiskit.quantum_info.operators import SparsePauliOp -from qiskit.utils.deprecation import deprecate_arguments +from qiskit.quantum_info.operators.base_operator import BaseOperator +from qiskit.utils.deprecation import deprecate_arguments, deprecate_function from .estimator_result import EstimatorResult -from .utils import _finditer class BaseEstimator(ABC): @@ -132,10 +126,12 @@ class BaseEstimator(ABC): Base class for Estimator that estimates expectation values of quantum circuits and observables. """ + __hash__ = None # type: ignore + def __init__( self, - circuits: Iterable[QuantumCircuit] | QuantumCircuit, - observables: Iterable[SparsePauliOp] | SparsePauliOp, + circuits: Iterable[QuantumCircuit] | QuantumCircuit | None = None, + observables: Iterable[SparsePauliOp] | SparsePauliOp | None = None, parameters: Iterable[Iterable[Parameter]] | None = None, ): """ @@ -153,23 +149,31 @@ def __init__( Raises: QiskitError: For mismatch of circuits and parameters list. """ + if circuits is not None or observables is not None or parameters is not None: + warn( + "The BaseEstimator 'circuits', `observables`, `parameters` kwarg are deprecated " + "as of 0.22.0 and will be removed no earlier than 3 months after the " + "release date. You can use 'run' method to append objects.", + DeprecationWarning, + 2, + ) if isinstance(circuits, QuantumCircuit): circuits = (circuits,) - self._circuits = tuple(circuits) + self._circuits = [] if circuits is None else list(circuits) if isinstance(observables, SparsePauliOp): observables = (observables,) - self._observables = tuple(observables) + self._observables = [] if observables is None else list(observables) # To guarantee that they exist as instance variable. # With only dynamic set, the python will not know if the attribute exists or not. - self._circuit_ids = self._circuit_ids - self._observable_ids = self._observable_ids + self._circuit_ids: dict[int, int] = self._circuit_ids + self._observable_ids: dict[int, int] = self._observable_ids if parameters is None: - self._parameters = tuple(circ.parameters for circ in self._circuits) + self._parameters = [circ.parameters for circ in self._circuits] else: - self._parameters = tuple(ParameterView(par) for par in parameters) + self._parameters = [ParameterView(par) for par in parameters] if len(self._parameters) != len(self._circuits): raise QiskitError( f"Different number of parameters ({len(self._parameters)}) and " @@ -184,33 +188,45 @@ def __init__( def __new__( cls, - circuits: Iterable[QuantumCircuit] | QuantumCircuit, - observables: Iterable[SparsePauliOp] | SparsePauliOp, - *args, # pylint: disable=unused-argument + circuits: Iterable[QuantumCircuit] | QuantumCircuit | None = None, + observables: Iterable[SparsePauliOp] | SparsePauliOp | None = None, parameters: Iterable[Iterable[Parameter]] | None = None, # pylint: disable=unused-argument **kwargs, # pylint: disable=unused-argument ): self = super().__new__(cls) - if isinstance(circuits, Iterable): + if circuits is None: + self._circuit_ids = {} + elif isinstance(circuits, Iterable): circuits = copy(circuits) - self._circuit_ids = [id(circuit) for circuit in circuits] + self._circuit_ids = {id(circuit): i for i, circuit in enumerate(circuits)} else: - self._circuit_ids = [id(circuits)] - if isinstance(observables, Iterable): + self._circuit_ids = {id(circuits): 0} + if observables is None: + self._observable_ids = {} + elif isinstance(observables, Iterable): observables = copy(observables) - self._observable_ids = [id(observable) for observable in observables] + self._observable_ids = {id(observable): i for i, observable in enumerate(observables)} else: - self._observable_ids = [id(observables)] + self._observable_ids = {id(observables): 0} return self + @deprecate_function( + "The BaseEstimator.__enter__ method is deprecated as of Qiskit Terra 0.21.0 " + "and will be removed no sooner than 3 months after the releasedate. " + "BaseEstimator should be initialized directly.", + ) def __enter__(self): return self + @deprecate_function( + "The BaseEstimator.__exit__ method is deprecated as of Qiskit Terra 0.21.0 " + "and will be removed no sooner than 3 months after the releasedate. " + "BaseEstimator should be initialized directly.", + ) def __exit__(self, *exc_info): self.close() - @abstractmethod def close(self): """Close the session and free resources""" ... @@ -222,7 +238,7 @@ def circuits(self) -> tuple[QuantumCircuit, ...]: Returns: The quantum circuits. """ - return self._circuits + return tuple(self._circuits) @property def observables(self) -> tuple[SparsePauliOp, ...]: @@ -231,7 +247,7 @@ def observables(self) -> tuple[SparsePauliOp, ...]: Returns: The observables. """ - return self._observables + return tuple(self._observables) @property def parameters(self) -> tuple[ParameterView, ...]: @@ -240,8 +256,13 @@ def parameters(self) -> tuple[ParameterView, ...]: Returns: Parameters, where ``parameters[i][j]`` is the j-th parameter of the i-th circuit. """ - return self._parameters + return tuple(self._parameters) + @deprecate_function( + "The BaseSampler.__call__ method is deprecated as of Qiskit Terra 0.21.0 " + "and will be removed no sooner than 3 months after the releasedate. " + "Use run method instead.", + ) @deprecate_arguments({"circuit_indices": "circuits", "observable_indices": "observables"}) def __call__( self, @@ -290,30 +311,31 @@ def __call__( parameter_values = parameter_values.tolist() # Allow objects - try: - circuits = [ - next(_finditer(id(circuit), self._circuit_ids)) - if not isinstance(circuit, (int, np.integer)) - else circuit - for circuit in circuits - ] - except StopIteration as err: + circuits = [ + self._circuit_ids.get(id(circuit)) # type: ignore + if not isinstance(circuit, (int, np.integer)) + else circuit + for circuit in circuits + ] + if any(circuit is None for circuit in circuits): raise QiskitError( "The circuits passed when calling estimator is not one of the circuits used to " "initialize the session." - ) from err - try: - observables = [ - next(_finditer(id(observable), self._observable_ids)) - if not isinstance(observable, (int, np.integer)) - else observable - for observable in observables - ] - except StopIteration as err: + ) + observables = [ + self._observable_ids.get(id(observable)) # type: ignore + if not isinstance(observable, (int, np.integer)) + else observable + for observable in observables + ] + if any(observable is None for observable in observables): raise QiskitError( "The observables passed when calling estimator is not one of the observables used to " "initialize the session." - ) from err + ) + + circuits = cast("list[int]", circuits) + observables = cast("list[int]", observables) # Allow optional if parameter_values is None: @@ -372,6 +394,110 @@ def __call__( **run_options, ) + def run( + self, + circuits: Sequence[QuantumCircuit], + observables: Sequence[BaseOperator | PauliSumOp], + parameter_values: Sequence[Sequence[float]] | None = None, + parameters: Sequence[Sequence[Parameter]] | None = None, + **run_options, + ) -> Job: + """Run the job of the estimation of expectation value(s). + + ``circuits``, ``observables``, and ``parameter_values`` should have the same + length. The i-th element of the result is the expectation of observable + + .. code-block:: python + + obs = observables[i] + + for the state prepared by + + .. code-block:: python + + circ = circuits[i] + + with bound parameters + + .. code-block:: python + + values = parameter_values[i]. + + Args: + circuits: the list of circuit objects. + observables: the list of observable objects. + parameter_values: concrete parameters to be bound. + parameters: Parameters of quantum circuits, specifying the order in which values + will be bound. Defaults to ``[circ.parameters for circ in circuits]`` + The indexing is such that ``parameters[i, j]`` is the j-th formal parameter of + ``circuits[i]``. + run_options: runtime options used for circuit execution. + + Returns: + The job object of EstimatorResult. + + Raises: + QiskitError: Invalid arguments are given. + """ + # Support ndarray + if isinstance(parameter_values, np.ndarray): + parameter_values = parameter_values.tolist() + + # Allow optional + if parameter_values is None: + for i, circuit in enumerate(circuits): + if circuit.num_parameters != 0: + raise QiskitError( + f"The {i}-th circuit is parameterised," + "but parameter values are not given." + ) + parameter_values = [[]] * len(circuits) + + if parameters is None: + parameter_views = [circ.parameters for circ in circuits] + else: + parameter_views = [ParameterView(par) for par in parameters] + if len(self._parameters) != len(self._circuits): + raise QiskitError( + f"Different number of parameters ({len(self._parameters)}) and " + f"circuits ({len(self._circuits)})" + ) + for i, (circ, params) in enumerate(zip(self._circuits, self._parameters)): + if circ.num_parameters != len(params): + raise QiskitError( + f"Different numbers of parameters of {i}-th circuit: " + f"expected {circ.num_parameters}, actual {len(params)}." + ) + + # Validation + if len(circuits) != len(observables): + raise QiskitError( + f"The number of circuits ({len(circuits)}) does not match " + f"the number of observables ({len(observables)})." + ) + if len(circuits) != len(parameter_values): + raise QiskitError( + f"The number of circuits ({len(circuits)}) does not match " + f"the number of parameter value sets ({len(parameter_values)})." + ) + + for i, (circuit, parameter_value) in enumerate(zip(circuits, parameter_values)): + if len(parameter_value) != circuit.num_parameters: + raise QiskitError( + f"The number of values ({len(parameter_value)}) does not match " + f"the number of parameters ({circuit.num_parameters}) for the {i}-th circuit." + ) + + for i, (circuit, observable) in enumerate(zip(circuits, observables)): + if circuit.num_qubits != observable.num_qubits: + raise QiskitError( + f"The number of qubits of the {i}-th circuit ({circuit.num_qubits}) does " + f"not match the number of qubits of the {i}-th observable " + f"({observable.num_qubits})." + ) + + return self._run(circuits, observables, parameter_values, parameter_views, **run_options) + @abstractmethod def _call( self, @@ -381,3 +507,17 @@ def _call( **run_options, ) -> EstimatorResult: ... + + # This will be comment out after 0.22. (This is necessary for the compatibility.) + # @abstractmethod + def _run( + self, + circuits: Sequence[QuantumCircuit], + observables: Sequence[BaseOperator | PauliSumOp], + parameter_values: Sequence[Sequence[float]], + parameters: list[ParameterView], + **run_options, + ) -> Job: + raise NotImplementedError( + "_run method is not implemented. This method will be @abstractmethod after 0.22." + ) diff --git a/qiskit/primitives/base_sampler.py b/qiskit/primitives/base_sampler.py index 12c2cba371ce..42591e582045 100644 --- a/qiskit/primitives/base_sampler.py +++ b/qiskit/primitives/base_sampler.py @@ -27,7 +27,7 @@ The sampler is run with the following inputs. -* circuit indexes: a list of indices of the circuits to evaluate. +* circuits: a list of QuantumCircuit objects to evaluate. * parameter values (:math:`\theta_k`): list of sets of parameter values to be bound to the parameters of the quantum circuits. @@ -55,22 +55,15 @@ bell.measure_all() # executes a Bell circuit - with Sampler(circuits=[bell], parameters=[[]]) as sampler: - result = sampler(parameters=[[]], circuits=[0]) - print([q.binary_probabilities() for q in result.quasi_dists]) + sampler = Sampler() + result = sampler.run(circuits=[bell]).result() + print([q.binary_probabilities() for q in result.quasi_dists]) # executes three Bell circuits - with Sampler([bell]*3, [[]] * 3) as sampler: - result = sampler([0, 1, 2], [[]]*3) - print([q.binary_probabilities() for q in result.quasi_dists]) - - # executes three Bell circuits with objects. - # Objects can be passed instead of indices. - # Note that passing objects has an overhead - # since the corresponding indices need to be searched. - with Sampler([bell]) as sampler: - result = sampler([bell, bell, bell]) - print([q.binary_probabilities() for q in result.quasi_dists]) + # Argument `parameters` is optional. + sampler = Sampler() + result = sampler.run([bell, bell, bell]).result() + print([q.binary_probabilities() for q in result.quasi_dists]) # parameterized circuit pqc = RealAmplitudes(num_qubits=2, reps=2) @@ -82,34 +75,35 @@ theta2 = [1, 2, 3, 4, 5, 6] theta3 = [0, 1, 2, 3, 4, 5, 6, 7] - with Sampler(circuits=[pqc, pqc2], parameters=[pqc.parameters, pqc2.parameters]) as sampler: - result = sampler([0, 0, 1], [theta1, theta2, theta3]) + sampler = Sampler() + result = sampler.run([pqc, pqc, pqc2], [theta1, theta2, theta3]).result() - # result of pqc(theta1) - print(result.quasi_dists[0].binary_probabilities()) + # result of pqc(theta1) + print(result.quasi_dists[0].binary_probabilities()) - # result of pqc(theta2) - print(result.quasi_dists[1].binary_probabilities()) - - # result of pqc2(theta3) - print(result.quasi_dists[2].binary_probabilities()) + # result of pqc(theta2) + print(result.quasi_dists[1].binary_probabilities()) + # result of pqc2(theta3) + print(result.quasi_dists[2].binary_probabilities()) """ from __future__ import annotations from abc import ABC, abstractmethod from collections.abc import Iterable, Sequence from copy import copy +from typing import cast +from warnings import warn import numpy as np from qiskit.circuit import Parameter, QuantumCircuit from qiskit.circuit.parametertable import ParameterView from qiskit.exceptions import QiskitError -from qiskit.utils.deprecation import deprecate_arguments +from qiskit.providers import JobV1 as Job +from qiskit.utils.deprecation import deprecate_arguments, deprecate_function from .sampler_result import SamplerResult -from .utils import _finditer class BaseSampler(ABC): @@ -118,9 +112,11 @@ class BaseSampler(ABC): Base class of Sampler that calculates quasi-probabilities of bitstrings from quantum circuits. """ + __hash__ = None # type: ignore + def __init__( self, - circuits: Iterable[QuantumCircuit] | QuantumCircuit, + circuits: Iterable[QuantumCircuit] | QuantumCircuit | None = None, parameters: Iterable[Iterable[Parameter]] | None = None, ): """ @@ -132,18 +128,26 @@ def __init__( Raises: QiskitError: For mismatch of circuits and parameters list. """ + if circuits is not None or parameters is not None: + warn( + "The BaseSampler 'circuits', and `parameters` kwarg are deprecated " + "as of 0.22.0 and will be removed no earlier than 3 months after the " + "release date. You can use 'run' method to append objects.", + DeprecationWarning, + 2, + ) if isinstance(circuits, QuantumCircuit): circuits = (circuits,) - self._circuits = tuple(circuits) + self._circuits = [] if circuits is None else list(circuits) # To guarantee that they exist as instance variable. # With only dynamic set, the python will not know if the attribute exists or not. - self._circuit_ids = self._circuit_ids + self._circuit_ids: dict[int, int] = self._circuit_ids if parameters is None: - self._parameters = tuple(circ.parameters for circ in self._circuits) + self._parameters = [circ.parameters for circ in self._circuits] else: - self._parameters = tuple(ParameterView(par) for par in parameters) + self._parameters = [ParameterView(par) for par in parameters] if len(self._parameters) != len(self._circuits): raise QiskitError( f"Different number of parameters ({len(self._parameters)}) " @@ -152,27 +156,37 @@ def __init__( def __new__( cls, - circuits: Iterable[QuantumCircuit] | QuantumCircuit, - *args, # pylint: disable=unused-argument + circuits: Iterable[QuantumCircuit] | QuantumCircuit | None = None, parameters: Iterable[Iterable[Parameter]] | None = None, # pylint: disable=unused-argument **kwargs, # pylint: disable=unused-argument ): self = super().__new__(cls) - if isinstance(circuits, Iterable): + if circuits is None: + self._circuit_ids = {} + elif isinstance(circuits, Iterable): circuits = copy(circuits) - self._circuit_ids = [id(circuit) for circuit in circuits] + self._circuit_ids = {id(circuit): i for i, circuit in enumerate(circuits)} else: - self._circuit_ids = [id(circuits)] + self._circuit_ids = {id(circuits): 0} return self + @deprecate_function( + "The BaseSampler.__enter__ method is deprecated as of Qiskit Terra 0.21.0 " + "and will be removed no sooner than 3 months after the releasedate. " + "BaseSampler should be initialized directly.", + ) def __enter__(self): return self + @deprecate_function( + "The BaseSampler.__exit__ method is deprecated as of Qiskit Terra 0.21.0 " + "and will be removed no sooner than 3 months after the releasedate. " + "BaseSampler should be initialized directly.", + ) def __exit__(self, *exc_info): self.close() - @abstractmethod def close(self): """Close the session and free resources""" ... @@ -184,7 +198,7 @@ def circuits(self) -> tuple[QuantumCircuit, ...]: Returns: The quantum circuits to be sampled. """ - return self._circuits + return tuple(self._circuits) @property def parameters(self) -> tuple[ParameterView, ...]: @@ -193,8 +207,13 @@ def parameters(self) -> tuple[ParameterView, ...]: Returns: List of the parameters in each quantum circuit. """ - return self._parameters + return tuple(self._parameters) + @deprecate_function( + "The BaseSampler.__call__ method is deprecated as of Qiskit Terra 0.21.0 " + "and will be removed no sooner than 3 months after the releasedate. " + "Use run method instead.", + ) @deprecate_arguments({"circuit_indices": "circuits"}) def __call__( self, @@ -223,18 +242,19 @@ def __call__( parameter_values = parameter_values.tolist() # Allow objects - try: - circuits = [ - next(_finditer(id(circuit), self._circuit_ids)) - if not isinstance(circuit, (int, np.integer)) - else circuit - for circuit in circuits - ] - except StopIteration as err: + circuits = [ + self._circuit_ids.get(id(circuit)) # type: ignore + if not isinstance(circuit, (int, np.integer)) + else circuit + for circuit in circuits + ] + if any(circuit is None for circuit in circuits): raise QiskitError( "The circuits passed when calling sampler is not one of the circuits used to " "initialize the session." - ) from err + ) + + circuits = cast("list[int]", circuits) # Allow optional if parameter_values is None: @@ -272,6 +292,75 @@ def __call__( **run_options, ) + def run( + self, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]] | None = None, + parameters: Sequence[Sequence[Parameter]] | None = None, + **run_options, + ) -> Job: + """Run the job of the sampling of bitstrings. + + Args: + circuits: the list of circuit objects. + parameter_values: Parameters to be bound to the circuit. + parameters: Parameters of each of the quantum circuits. + Defaults to ``[circ.parameters for circ in circuits]``. + run_options: Backend runtime options used for circuit execution. + + Returns: + The job object of the result of the sampler. The i-th result corresponds to + ``circuits[i]`` evaluated with parameters bound as ``parameter_values[i]``. + + Raises: + QiskitError: Invalid arguments are given. + """ + # Support ndarray + if isinstance(parameter_values, np.ndarray): + parameter_values = parameter_values.tolist() + + # Allow optional + if parameter_values is None: + for i, circuit in enumerate(circuits): + if circuit.num_parameters != 0: + raise QiskitError( + f"The {i}-th circuit ({len(circuits)}) is parameterised," + "but parameter values are not given." + ) + parameter_values = [[]] * len(circuits) + + if parameters is None: + parameter_views = [circ.parameters for circ in circuits] + else: + parameter_views = [ParameterView(par) for par in parameters] + if len(self._parameters) != len(self._circuits): + raise QiskitError( + f"Different number of parameters ({len(self._parameters)}) and " + f"circuits ({len(self._circuits)})" + ) + for i, (circ, params) in enumerate(zip(self._circuits, self._parameters)): + if circ.num_parameters != len(params): + raise QiskitError( + f"Different numbers of parameters of {i}-th circuit: " + f"expected {circ.num_parameters}, actual {len(params)}." + ) + + # Validation + if len(circuits) != len(parameter_values): + raise QiskitError( + f"The number of circuits ({len(circuits)}) does not match " + f"the number of parameter value sets ({len(parameter_values)})." + ) + + for i, (circuit, parameter_value) in enumerate(zip(circuits, parameter_values)): + if len(parameter_value) != circuit.num_parameters: + raise QiskitError( + f"The number of values ({len(parameter_value)}) does not match " + f"the number of parameters ({circuit.num_parameters}) for the {i}-th circuit." + ) + + return self._run(circuits, parameter_values, parameter_views, **run_options) + @abstractmethod def _call( self, @@ -280,3 +369,16 @@ def _call( **run_options, ) -> SamplerResult: ... + + # This will be comment out after 0.22. (This is necessary for the compatibility.) + # @abstractmethod + def _run( + self, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[ParameterView], + **run_options, + ) -> Job: + raise NotImplementedError( + "_run method is not implemented. This method will be @abstractmethod after 0.22." + ) diff --git a/qiskit/primitives/estimator.py b/qiskit/primitives/estimator.py index 2bacbe60acf3..e0ae5c9924e3 100644 --- a/qiskit/primitives/estimator.py +++ b/qiskit/primitives/estimator.py @@ -16,10 +16,12 @@ from __future__ import annotations from collections.abc import Iterable, Sequence +from typing import Any import numpy as np from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.circuit.parametertable import ParameterView from qiskit.exceptions import QiskitError from qiskit.opflow import PauliSumOp from qiskit.quantum_info import Statevector @@ -27,6 +29,7 @@ from .base_estimator import BaseEstimator from .estimator_result import EstimatorResult +from .primitive_job import PrimitiveJob from .utils import init_circuit, init_observable @@ -48,21 +51,23 @@ class Estimator(BaseEstimator): def __init__( self, - circuits: QuantumCircuit | Iterable[QuantumCircuit], - observables: BaseOperator | PauliSumOp | Iterable[BaseOperator | PauliSumOp], + circuits: QuantumCircuit | Iterable[QuantumCircuit] | None = None, + observables: BaseOperator | PauliSumOp | Iterable[BaseOperator | PauliSumOp] | None = None, parameters: Iterable[Iterable[Parameter]] | None = None, ): if isinstance(circuits, QuantumCircuit): circuits = (circuits,) - circuits = tuple(init_circuit(circuit) for circuit in circuits) + if circuits is not None: + circuits = tuple(init_circuit(circuit) for circuit in circuits) if isinstance(observables, (PauliSumOp, BaseOperator)): observables = (observables,) - observables = tuple(init_observable(observable) for observable in observables) + if observables is not None: + observables = tuple(init_observable(observable) for observable in observables) super().__init__( circuits=circuits, - observables=observables, + observables=observables, # type: ignore parameters=parameters, ) self._is_closed = False @@ -87,7 +92,7 @@ def _call( rng = np.random.default_rng(seed) # Initialize metadata - metadata = [{}] * len(circuits) + metadata: list[dict[str, Any]] = [{}] * len(circuits) bound_circuits = [] for i, value in zip(circuits, parameter_values): @@ -126,3 +131,36 @@ def _call( def close(self): self._is_closed = True + + def _run( + self, + circuits: Sequence[QuantumCircuit], + observables: Sequence[BaseOperator | PauliSumOp], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[ParameterView], + **run_options, + ) -> PrimitiveJob: + circuit_indices = [] + for i, circuit in enumerate(circuits): + index = self._circuit_ids.get(id(circuit)) + if index is not None: + circuit_indices.append(index) + else: + circuit_indices.append(len(self._circuits)) + self._circuit_ids[id(circuit)] = len(self._circuits) + self._circuits.append(circuit) + self._parameters.append(parameters[i]) + observable_indices = [] + for observable in observables: + index = self._observable_ids.get(id(observable)) + if index is not None: + observable_indices.append(index) + else: + observable_indices.append(len(self._observables)) + self._observable_ids[id(observable)] = len(self._observables) + self._observables.append(init_observable(observable)) + job = PrimitiveJob( + self._call, circuit_indices, observable_indices, parameter_values, **run_options + ) + job.submit() + return job diff --git a/qiskit/primitives/estimator_result.py b/qiskit/primitives/estimator_result.py index 2c93de79dd9e..46ea00447acc 100644 --- a/qiskit/primitives/estimator_result.py +++ b/qiskit/primitives/estimator_result.py @@ -28,7 +28,7 @@ class EstimatorResult: .. code-block:: python - result = estimator(circuits, observables, params) + result = estimator.run(circuits, observables, params).result() where the i-th elements of ``result`` correspond to the circuit and observable given by ``circuits[i]``, ``observables[i]``, and the parameter values bounds by ``params[i]``. diff --git a/qiskit/primitives/primitive_job.py b/qiskit/primitives/primitive_job.py new file mode 100644 index 000000000000..dc8b07cc8f00 --- /dev/null +++ b/qiskit/primitives/primitive_job.py @@ -0,0 +1,68 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. +""" +Job implementation for the reference implementations of Primitives. +""" + +import uuid +from concurrent.futures import ThreadPoolExecutor + +from qiskit.providers import JobError, JobStatus, JobV1 + + +class PrimitiveJob(JobV1): + """ + PrimitiveJob class for the reference implemetations of Primitives. + """ + + def __init__(self, function, *args, **kwargs): + """ + Args: + function: a callable function to execute the job. + """ + job_id = str(uuid.uuid4()) + super().__init__(None, job_id) + self._future = None + self._function = function + self._args = args + self._kwargs = kwargs + + def submit(self): + if self._future is not None: + raise JobError("Primitive job has already been submitted.") + + with ThreadPoolExecutor(max_workers=1) as executor: + future = executor.submit(self._function, *self._args, **self._kwargs) + self._future = future + + def result(self): + """Return the results of the job.""" + self._check_submitted() + return self._future.result() + + def cancel(self): + self._check_submitted() + return self._future.cancel() + + def status(self): + self._check_submitted() + if self._future.running(): + return JobStatus.RUNNING + elif self._future.cancelled(): + return JobStatus.CANCELLED + elif self._future.done() and self._future._exception() is None: + return JobStatus.DONE + return JobStatus.ERROR + + def _check_submitted(self): + if self._future is None: + raise JobError("Job not submitted yet!. You have to .submit() first!") diff --git a/qiskit/primitives/sampler.py b/qiskit/primitives/sampler.py index 6a816ece357f..e5cee820ffce 100644 --- a/qiskit/primitives/sampler.py +++ b/qiskit/primitives/sampler.py @@ -16,15 +16,18 @@ from __future__ import annotations from collections.abc import Iterable, Sequence +from typing import Any, cast import numpy as np from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.circuit.parametertable import ParameterView from qiskit.exceptions import QiskitError from qiskit.quantum_info import Statevector from qiskit.result import QuasiDistribution from .base_sampler import BaseSampler +from .primitive_job import PrimitiveJob from .sampler_result import SamplerResult from .utils import final_measurement_mapping, init_circuit @@ -48,7 +51,7 @@ class Sampler(BaseSampler): def __init__( self, - circuits: QuantumCircuit | Iterable[QuantumCircuit], + circuits: QuantumCircuit | Iterable[QuantumCircuit] | None = None, parameters: Iterable[Iterable[Parameter]] | None = None, ): """ @@ -62,20 +65,16 @@ def __init__( """ if isinstance(circuits, QuantumCircuit): circuits = (circuits,) - circuits = tuple(init_circuit(circuit) for circuit in circuits) - q_c_mappings = [final_measurement_mapping(circuit) for circuit in circuits] self._qargs_list = [] - for circuit, q_c_mapping in zip(circuits, q_c_mappings): - if set(range(circuit.num_clbits)) != set(q_c_mapping.values()): - raise QiskitError( - "some classical bits are not used for measurements." - f" the number of classical bits {circuit.num_clbits}," - f" the used classical bits {set(q_c_mapping.values())}." - ) - c_q_mapping = sorted((c, q) for q, c in q_c_mapping.items()) - self._qargs_list.append([q for _, q in c_q_mapping]) - circuits = tuple(circuit.remove_final_measurements(inplace=False) for circuit in circuits) - super().__init__(circuits, parameters) + if circuits is not None: + preprocessed_circuits = [] + for circuit in circuits: + circuit, qargs = self._preprocess_circuit(circuit) + self._qargs_list.append(qargs) + preprocessed_circuits.append(circuit) + else: + preprocessed_circuits = None + super().__init__(preprocessed_circuits, parameters) self._is_closed = False def _call( @@ -97,23 +96,26 @@ def _call( rng = np.random.default_rng(seed) # Initialize metadata - metadata = [{}] * len(circuits) + metadata: list[dict[str, Any]] = [{}] * len(circuits) - bound_circuits_qargs = [] + bound_circuits = [] + qargs_list = [] for i, value in zip(circuits, parameter_values): if len(value) != len(self._parameters[i]): raise QiskitError( f"The number of values ({len(value)}) does not match " f"the number of parameters ({len(self._parameters[i])})." ) - bound_circuits_qargs.append( - ( - self._circuits[i].bind_parameters(dict(zip(self._parameters[i], value))), - self._qargs_list[i], - ) + bound_circuit = ( + self._circuits[i] + if len(value) == 0 + else self._circuits[i].bind_parameters(dict(zip(self._parameters[i], value))) ) + bound_circuits.append(bound_circuit) + qargs_list.append(self._qargs_list[i]) probabilities = [ - Statevector(circ).probabilities(qargs=qargs) for circ, qargs in bound_circuits_qargs + Statevector(circ).probabilities(qargs=qargs) + for circ, qargs in zip(bound_circuits, qargs_list) ] if shots is not None: probabilities = [ @@ -127,3 +129,41 @@ def _call( def close(self): self._is_closed = True + + def _run( + self, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[ParameterView], + **run_options, + ) -> PrimitiveJob: + circuit_indices = [] + for i, circuit in enumerate(circuits): + index = self._circuit_ids.get(id(circuit)) + if index is not None: + circuit_indices.append(index) + else: + circuit_indices.append(len(self._circuits)) + self._circuit_ids[id(circuit)] = len(self._circuits) + circuit, qargs = self._preprocess_circuit(circuit) + self._circuits.append(circuit) + self._qargs_list.append(qargs) + self._parameters.append(parameters[i]) + job = PrimitiveJob(self._call, circuit_indices, parameter_values, **run_options) + job.submit() + return job + + @staticmethod + def _preprocess_circuit(circuit: QuantumCircuit): + circuit = init_circuit(circuit) + q_c_mapping = final_measurement_mapping(circuit) + if set(range(circuit.num_clbits)) != set(q_c_mapping.values()): + raise QiskitError( + "some classical bits are not used for measurements." + f" the number of classical bits {circuit.num_clbits}," + f" the used classical bits {set(q_c_mapping.values())}." + ) + c_q_mapping = sorted((c, q) for q, c in q_c_mapping.items()) + qargs = [q for _, q in c_q_mapping] + circuit = cast(QuantumCircuit, circuit.remove_final_measurements(inplace=False)) + return circuit, qargs diff --git a/qiskit/primitives/sampler_result.py b/qiskit/primitives/sampler_result.py index 2f9af922be29..9581e951dc04 100644 --- a/qiskit/primitives/sampler_result.py +++ b/qiskit/primitives/sampler_result.py @@ -27,7 +27,7 @@ class SamplerResult: .. code-block:: python - result = sampler(circuits, params) + result = sampler.run(circuits, params).result() where the i-th elements of ``result`` correspond to the circuit given by ``circuits[i]``, and the parameter values bounds by ``params[i]``. diff --git a/qiskit/primitives/utils.py b/qiskit/primitives/utils.py index 590891cc783c..1e2c16ccad3c 100644 --- a/qiskit/primitives/utils.py +++ b/qiskit/primitives/utils.py @@ -15,9 +15,6 @@ from __future__ import annotations -from collections.abc import Iterator -from typing import TypeVar - from qiskit.circuit import ParameterExpression, QuantumCircuit from qiskit.extensions.quantum_initializer.initializer import Initialize from qiskit.opflow import PauliSumOp @@ -114,11 +111,3 @@ def final_measurement_mapping(circuit: QuantumCircuit) -> dict[int, int]: # Sort so that classical bits are in numeric order low->high. mapping = dict(sorted(mapping.items(), key=lambda item: item[1])) return mapping - - -T = TypeVar("T") # pylint: disable=invalid-name - - -def _finditer(obj: T, objects: list[T]) -> Iterator[int]: - """Return an iterator yielding the indices matching obj.""" - return map(lambda x: x[0], filter(lambda x: x[1] == obj, enumerate(objects))) diff --git a/qiskit/providers/job.py b/qiskit/providers/job.py index a5f3b0ad0b7f..fc797ba691b0 100644 --- a/qiskit/providers/job.py +++ b/qiskit/providers/job.py @@ -12,13 +12,14 @@ """Job abstract interface.""" +import time from abc import ABC, abstractmethod from typing import Callable, Optional -import time -from qiskit.providers.jobstatus import JobStatus, JOB_FINAL_STATES -from qiskit.providers.exceptions import JobTimeoutError +from qiskit.exceptions import QiskitError from qiskit.providers.backend import Backend +from qiskit.providers.exceptions import JobTimeoutError +from qiskit.providers.jobstatus import JOB_FINAL_STATES, JobStatus class Job: @@ -46,7 +47,7 @@ class JobV1(Job, ABC): version = 1 _async = True - def __init__(self, backend: Backend, job_id: str, **kwargs) -> None: + def __init__(self, backend: Optional[Backend], job_id: str, **kwargs) -> None: """Initializes the asynchronous job. Args: @@ -65,6 +66,8 @@ def job_id(self) -> str: def backend(self) -> Backend: """Return the backend where this job was executed.""" + if self._backend is None: + raise QiskitError("The job does not have any backend.") return self._backend def done(self) -> bool: diff --git a/releasenotes/notes/primitive-run-5d1afab3655330a6.yaml b/releasenotes/notes/primitive-run-5d1afab3655330a6.yaml new file mode 100644 index 000000000000..fd4c5965dc04 --- /dev/null +++ b/releasenotes/notes/primitive-run-5d1afab3655330a6.yaml @@ -0,0 +1,37 @@ +--- +features: + - | + Add methods `~qiskit.primitives.BaseSampler.run` and `~qiskit.primitives.BaseEstimator.run`. + These methods execute asynchronously and return `~qiskit.providers.JobV1`. + Circuits and observables can be appended that are not registered in the constructor. +deprecations: + - | + The method of executing primitives has been changed. + :meth:`~qiskit.primitives.BaseSampler.__call__` method was deprecated and + :meth:`~qiskit.primitives.BaseEstimator.__call__` method was deprecated. + For example:: + + .. code-block:: python + + estimator = Estimator(...) + result = estimator(circuits, observables, parameters) + + sampler = Sampler(...) + result = sampler(circuits, observables, parameters) + + are equivalent by rewriting + + .. code-block:: python + + estimator = Estimator(circuits, observables, parameters) + result = estimator.run(circuits, observables, parameter_values).result() + + sampler = Sampler(circuits, parameters) + result = sampler.run(circuits, parameter_values).result() + + Context manager for primitives was deprecated. + Not all Primitives have a context manager available. When available (e.g. qiskit-ibm-runtime), + the session's context manager provides equivalent functionality. + + ``circuits``, ``observables``, and ``parameters`` in the constructor was deprecated. + These objects can be passed from ``run`` methods. diff --git a/test/python/primitives/test_estimator.py b/test/python/primitives/test_estimator.py index c6ae728aecce..5ce1571867a9 100644 --- a/test/python/primitives/test_estimator.py +++ b/test/python/primitives/test_estimator.py @@ -21,6 +21,7 @@ from qiskit.exceptions import QiskitError from qiskit.opflow import PauliSumOp from qiskit.primitives import Estimator, EstimatorResult +from qiskit.providers import JobV1 from qiskit.quantum_info import Operator, SparsePauliOp from qiskit.test import QiskitTestCase @@ -42,13 +43,27 @@ def setUp(self): ) self.expvals = -1.0284380963435145, -1.284366511861733 + self.psi = (RealAmplitudes(num_qubits=2, reps=2), RealAmplitudes(num_qubits=2, reps=3)) + self.params = tuple(psi.parameters for psi in self.psi) + self.hamiltonian = ( + SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)]), + SparsePauliOp.from_list([("IZ", 1)]), + SparsePauliOp.from_list([("ZI", 1), ("ZZ", 1)]), + ) + self.theta = ( + [0, 1, 1, 2, 3, 5], + [0, 1, 1, 2, 3, 5, 8, 13], + [1, 2, 3, 4, 5, 6], + ) + def test_estimator(self): """test for a simple use case""" lst = [("XX", 1), ("YY", 2), ("ZZ", 3)] with self.subTest("PauliSumOp"): observable = PauliSumOp.from_list(lst) ansatz = RealAmplitudes(num_qubits=2, reps=2) - with Estimator([ansatz], [observable]) as est: + with self.assertWarns(DeprecationWarning): + est = Estimator([ansatz], [observable]) result = est([0], [0], parameter_values=[[0, 1, 1, 2, 3, 5]]) self.assertIsInstance(result, EstimatorResult) np.testing.assert_allclose(result.values, [1.84209213]) @@ -56,7 +71,8 @@ def test_estimator(self): with self.subTest("SparsePauliOp"): observable = SparsePauliOp.from_list(lst) ansatz = RealAmplitudes(num_qubits=2, reps=2) - with Estimator([ansatz], [observable]) as est: + with self.assertWarns(DeprecationWarning): + est = Estimator([ansatz], [observable]) result = est([0], [0], parameter_values=[[0, 1, 1, 2, 3, 5]]) self.assertIsInstance(result, EstimatorResult) np.testing.assert_allclose(result.values, [1.84209213]) @@ -65,7 +81,8 @@ def test_estimator_param_reverse(self): """test for the reverse parameter""" observable = PauliSumOp.from_list([("XX", 1), ("YY", 2), ("ZZ", 3)]) ansatz = RealAmplitudes(num_qubits=2, reps=2) - with Estimator([ansatz], [observable], [ansatz.parameters[::-1]]) as est: + with self.assertWarns(DeprecationWarning): + est = Estimator([ansatz], [observable], [ansatz.parameters[::-1]]) result = est([0], [0], parameter_values=[[0, 1, 1, 2, 3, 5][::-1]]) self.assertIsInstance(result, EstimatorResult) np.testing.assert_allclose(result.values, [1.84209213]) @@ -81,21 +98,24 @@ def test_init_observable_from_operator(self): [0.1809312, 0.0, 0.0, -1.06365335], ] ) - with Estimator([circuit], [matrix]) as est: + with self.assertWarns(DeprecationWarning): + est = Estimator([circuit], [matrix]) result = est([0], [0]) self.assertIsInstance(result, EstimatorResult) np.testing.assert_allclose(result.values, [-1.284366511861733]) def test_evaluate(self): """test for evaluate""" - with Estimator([self.ansatz], [self.observable]) as est: + with self.assertWarns(DeprecationWarning): + est = Estimator([self.ansatz], [self.observable]) result = est([0], [0], parameter_values=[[0, 1, 1, 2, 3, 5]]) self.assertIsInstance(result, EstimatorResult) np.testing.assert_allclose(result.values, [-1.284366511861733]) def test_evaluate_multi_params(self): """test for evaluate with multiple parameters""" - with Estimator([self.ansatz], [self.observable]) as est: + with self.assertWarns(DeprecationWarning): + est = Estimator([self.ansatz], [self.observable]) result = est( [0] * 2, [0] * 2, parameter_values=[[0, 1, 1, 2, 3, 5], [1, 1, 2, 3, 5, 8]] ) @@ -105,7 +125,8 @@ def test_evaluate_multi_params(self): def test_evaluate_no_params(self): """test for evaluate without parameters""" circuit = self.ansatz.bind_parameters([0, 1, 1, 2, 3, 5]) - with Estimator([circuit], [self.observable]) as est: + with self.assertWarns(DeprecationWarning): + est = Estimator([circuit], [self.observable]) result = est([0], [0]) self.assertIsInstance(result, EstimatorResult) np.testing.assert_allclose(result.values, [-1.284366511861733]) @@ -116,7 +137,8 @@ def test_run_with_multiple_observables_and_none_parameters(self): circuit.h(0) circuit.cx(0, 1) circuit.cx(1, 2) - with Estimator(circuit, ["ZZZ", "III"]) as est: + with self.assertWarns(DeprecationWarning): + est = Estimator(circuit, ["ZZZ", "III"]) result = est(circuits=[0, 0], observables=[0, 1]) self.assertIsInstance(result, EstimatorResult) np.testing.assert_allclose(result.values, [0.0, 1.0]) @@ -133,50 +155,57 @@ def test_estimator_example(self): op2 = SparsePauliOp.from_list([("IZ", 1)]) op3 = SparsePauliOp.from_list([("ZI", 1), ("ZZ", 1)]) - with Estimator([psi1, psi2], [op1, op2, op3], [params1, params2]) as est: - theta1 = [0, 1, 1, 2, 3, 5] - theta2 = [0, 1, 1, 2, 3, 5, 8, 13] - theta3 = [1, 2, 3, 4, 5, 6] + with self.assertWarns(DeprecationWarning): + est = Estimator([psi1, psi2], [op1, op2, op3], [params1, params2]) + theta1 = [0, 1, 1, 2, 3, 5] + theta2 = [0, 1, 1, 2, 3, 5, 8, 13] + theta3 = [1, 2, 3, 4, 5, 6] - # calculate [ ] + # calculate [ ] + with self.assertWarns(DeprecationWarning): result = est([0], [0], [theta1]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [1.5555572817900956]) - self.assertEqual(len(result.metadata), 1) + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [1.5555572817900956]) + self.assertEqual(len(result.metadata), 1) - # calculate [ , ] + # calculate [ , ] + with self.assertWarns(DeprecationWarning): result = est([0, 0], [1, 2], [theta1] * 2) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [-0.5516530027638437, 0.07535238795415422]) - self.assertEqual(len(result.metadata), 2) + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [-0.5516530027638437, 0.07535238795415422]) + self.assertEqual(len(result.metadata), 2) - # calculate [ ] + # calculate [ ] + with self.assertWarns(DeprecationWarning): result = est([1], [1], [theta2]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [0.17849238433885167]) - self.assertEqual(len(result.metadata), 1) + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [0.17849238433885167]) + self.assertEqual(len(result.metadata), 1) - # calculate [ , ] + # calculate [ , ] + with self.assertWarns(DeprecationWarning): result = est([0, 0], [0, 0], [theta1, theta3]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [1.5555572817900956, 1.0656325933346835]) - self.assertEqual(len(result.metadata), 2) + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [1.5555572817900956, 1.0656325933346835]) + self.assertEqual(len(result.metadata), 2) - # calculate [ , - # , - # ] + # calculate [ , + # , + # ] + with self.assertWarns(DeprecationWarning): result = est([0, 1, 0], [0, 1, 2], [theta1, theta2, theta3]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose( - result.values, [1.5555572817900956, 0.17849238433885167, -1.0876631752254926] - ) - self.assertEqual(len(result.metadata), 3) + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose( + result.values, [1.5555572817900956, 0.17849238433885167, -1.0876631752254926] + ) + self.assertEqual(len(result.metadata), 3) - # It is possible to pass objects. - # calculate [ ] + # It is possible to pass objects. + # calculate [ ] + with self.assertWarns(DeprecationWarning): result = est([psi2], [op2], [theta2]) - np.testing.assert_allclose(result.values, [0.17849238433885167]) - self.assertEqual(len(result.metadata), 1) + np.testing.assert_allclose(result.values, [0.17849238433885167]) + self.assertEqual(len(result.metadata), 1) def test_1qubit(self): """Test for 1-qubit cases""" @@ -187,22 +216,26 @@ def test_1qubit(self): op = SparsePauliOp.from_list([("I", 1)]) op2 = SparsePauliOp.from_list([("Z", 1)]) - with Estimator([qc, qc2], [op, op2], [[]] * 2) as est: + with self.assertWarns(DeprecationWarning): + est = Estimator([qc, qc2], [op, op2], [[]] * 2) result = est([0], [0], [[]]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [1]) + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [1]) + with self.assertWarns(DeprecationWarning): result = est([0], [1], [[]]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [1]) + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [1]) + with self.assertWarns(DeprecationWarning): result = est([1], [0], [[]]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [1]) + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [1]) + with self.assertWarns(DeprecationWarning): result = est([1], [1], [[]]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [-1]) + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [-1]) def test_2qubits(self): """Test for 2-qubit cases (to check endian)""" @@ -214,30 +247,36 @@ def test_2qubits(self): op2 = SparsePauliOp.from_list([("ZI", 1)]) op3 = SparsePauliOp.from_list([("IZ", 1)]) - with Estimator([qc, qc2], [op, op2, op3], [[]] * 2) as est: + with self.assertWarns(DeprecationWarning): + est = Estimator([qc, qc2], [op, op2, op3], [[]] * 2) result = est([0], [0], [[]]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [1]) + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [1]) + with self.assertWarns(DeprecationWarning): result = est([1], [0], [[]]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [1]) + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [1]) + with self.assertWarns(DeprecationWarning): result = est([0], [1], [[]]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [1]) + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [1]) + with self.assertWarns(DeprecationWarning): result = est([1], [1], [[]]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [1]) + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [1]) + with self.assertWarns(DeprecationWarning): result = est([0], [2], [[]]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [1]) + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [1]) + with self.assertWarns(DeprecationWarning): result = est([1], [2], [[]]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [-1]) + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [-1]) def test_errors(self): """Test for errors""" @@ -247,35 +286,39 @@ def test_errors(self): op = SparsePauliOp.from_list([("I", 1)]) op2 = SparsePauliOp.from_list([("II", 1)]) - with Estimator([qc, qc2], [op, op2], [[]] * 2) as est: - with self.assertRaises(QiskitError): - est([0], [1], [[]]) - with self.assertRaises(QiskitError): - est([1], [0], [[]]) - with self.assertRaises(QiskitError): - est([0], [0], [[1e4]]) - with self.assertRaises(QiskitError): - est([1], [1], [[1, 2]]) - with self.assertRaises(QiskitError): - est([0, 1], [1], [[1]]) - with self.assertRaises(QiskitError): - est([0], [0, 1], [[1]]) + with self.assertWarns(DeprecationWarning): + est = Estimator([qc, qc2], [op, op2], [[]] * 2) + with self.assertRaises(QiskitError), self.assertWarns(DeprecationWarning): + est([0], [1], [[]]) + with self.assertRaises(QiskitError), self.assertWarns(DeprecationWarning): + est([1], [0], [[]]) + with self.assertRaises(QiskitError), self.assertWarns(DeprecationWarning): + est([0], [0], [[1e4]]) + with self.assertRaises(QiskitError), self.assertWarns(DeprecationWarning): + est([1], [1], [[1, 2]]) + with self.assertRaises(QiskitError), self.assertWarns(DeprecationWarning): + est([0, 1], [1], [[1]]) + with self.assertRaises(QiskitError), self.assertWarns(DeprecationWarning): + est([0], [0, 1], [[1]]) def test_empty_parameter(self): """Test for empty parameter""" n = 2 qc = QuantumCircuit(n) op = SparsePauliOp.from_list([("I" * n, 1)]) - with Estimator(circuits=[qc] * 10, observables=[op] * 10) as estimator: - with self.subTest("one circuit"): + with self.assertWarns(DeprecationWarning): + estimator = Estimator(circuits=[qc] * 10, observables=[op] * 10) + with self.subTest("one circuit"): + with self.assertWarns(DeprecationWarning): result = estimator([0], [1], shots=1000) - np.testing.assert_allclose(result.values, [1]) - self.assertEqual(len(result.metadata), 1) + np.testing.assert_allclose(result.values, [1]) + self.assertEqual(len(result.metadata), 1) - with self.subTest("two circuits"): + with self.subTest("two circuits"): + with self.assertWarns(DeprecationWarning): result = estimator([2, 4], [3, 5], shots=1000) - np.testing.assert_allclose(result.values, [1, 1]) - self.assertEqual(len(result.metadata), 2) + np.testing.assert_allclose(result.values, [1, 1]) + self.assertEqual(len(result.metadata), 2) def test_numpy_params(self): """Test for numpy array as parameter values""" @@ -285,78 +328,246 @@ def test_numpy_params(self): params_array = np.random.rand(k, qc.num_parameters) params_list = params_array.tolist() params_list_array = list(params_array) - with Estimator(circuits=qc, observables=op) as estimator: + with self.assertWarns(DeprecationWarning): + estimator = Estimator(circuits=qc, observables=op) target = estimator([0] * k, [0] * k, params_list) - with self.subTest("ndarrary"): + with self.subTest("ndarrary"): + with self.assertWarns(DeprecationWarning): result = estimator([0] * k, [0] * k, params_array) - self.assertEqual(len(result.metadata), k) - np.testing.assert_allclose(result.values, target.values) + self.assertEqual(len(result.metadata), k) + np.testing.assert_allclose(result.values, target.values) - with self.subTest("list of ndarray"): + with self.subTest("list of ndarray"): + with self.assertWarns(DeprecationWarning): result = estimator([0] * k, [0] * k, params_list_array) - self.assertEqual(len(result.metadata), k) - np.testing.assert_allclose(result.values, target.values) + self.assertEqual(len(result.metadata), k) + np.testing.assert_allclose(result.values, target.values) def test_passing_objects(self): """Test passsing object for Estimator.""" with self.subTest("Valid test"): - with Estimator([self.ansatz], [self.observable]) as estimator: + with self.assertWarns(DeprecationWarning): + estimator = Estimator([self.ansatz], [self.observable]) result = estimator( circuits=[self.ansatz, self.ansatz], observables=[self.observable, self.observable], parameter_values=[list(range(6)), [0, 1, 1, 2, 3, 5]], ) - self.assertAlmostEqual(result.values[0], self.expvals[0]) - self.assertAlmostEqual(result.values[1], self.expvals[1]) + self.assertAlmostEqual(result.values[0], self.expvals[0]) + self.assertAlmostEqual(result.values[1], self.expvals[1]) with self.subTest("Invalid circuit test"): circuit = QuantumCircuit(2) - with Estimator([self.ansatz], [self.observable]) as estimator: - with self.assertRaises(QiskitError): - result = estimator( - circuits=[self.ansatz, circuit], - observables=[self.observable, self.observable], - parameter_values=[list(range(6)), [0, 1, 1, 2, 3, 5]], - ) + with self.assertWarns(DeprecationWarning): + estimator = Estimator([self.ansatz], [self.observable]) + with self.assertRaises(QiskitError), self.assertWarns(DeprecationWarning): + result = estimator( + circuits=[self.ansatz, circuit], + observables=[self.observable, self.observable], + parameter_values=[list(range(6)), [0, 1, 1, 2, 3, 5]], + ) with self.subTest("Invalid observable test"): observable = SparsePauliOp(["ZX"]) - with Estimator([self.ansatz], [self.observable]) as estimator: - with self.assertRaises(QiskitError): - result = estimator( - circuits=[self.ansatz, self.ansatz], - observables=[observable, self.observable], - parameter_values=[list(range(6)), [0, 1, 1, 2, 3, 5]], - ) + with self.assertWarns(DeprecationWarning): + estimator = Estimator([self.ansatz], [self.observable]) + with self.assertRaises(QiskitError), self.assertWarns(DeprecationWarning): + result = estimator( + circuits=[self.ansatz, self.ansatz], + observables=[observable, self.observable], + parameter_values=[list(range(6)), [0, 1, 1, 2, 3, 5]], + ) def test_deprecated_arguments(self): """test for deprecated arguments""" - with Estimator([self.ansatz], [self.observable]) as est: - with self.assertWarns(DeprecationWarning): - result = est( - circuit_indices=[0], - observable_indices=[0], - parameter_values=[[0, 1, 1, 2, 3, 5]], - ) + with self.assertWarns(DeprecationWarning): + est = Estimator([self.ansatz], [self.observable]) + result = est( + circuit_indices=[0], + observable_indices=[0], + parameter_values=[[0, 1, 1, 2, 3, 5]], + ) self.assertIsInstance(result, EstimatorResult) np.testing.assert_allclose(result.values, [-1.284366511861733]) def test_with_shots_option(self): """test with shots option.""" - with Estimator([self.ansatz], [self.observable]) as est: + with self.assertWarns(DeprecationWarning): + est = Estimator([self.ansatz], [self.observable]) result = est([0], [0], parameter_values=[[0, 1, 1, 2, 3, 5]], shots=1024, seed=15) self.assertIsInstance(result, EstimatorResult) np.testing.assert_allclose(result.values, [-1.307397243478641]) def test_with_shots_option_none(self): """test with shots=None option. Seed is ignored then.""" - with Estimator([self.ansatz], [self.observable]) as est: + with self.assertWarns(DeprecationWarning): + est = Estimator([self.ansatz], [self.observable]) result_42 = est([0], [0], parameter_values=[[0, 1, 1, 2, 3, 5]], shots=None, seed=42) result_15 = est([0], [0], parameter_values=[[0, 1, 1, 2, 3, 5]], shots=None, seed=15) np.testing.assert_allclose(result_42.values, result_15.values) + def test_estimator_run(self): + """Test Estimator.run()""" + psi1, psi2 = self.psi + hamiltonian1, hamiltonian2, hamiltonian3 = self.hamiltonian + theta1, theta2, theta3 = self.theta + estimator = Estimator() + + # Specify the circuit and observable by indices. + # calculate [ ] + job = estimator.run([psi1], [hamiltonian1], [theta1]) + self.assertIsInstance(job, JobV1) + result = job.result() + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [1.5555572817900956]) + + # Objects can be passed instead of indices. + # Note that passing objects has an overhead + # since the corresponding indices need to be searched. + # User can append a circuit and observable. + # calculate [ ] + result2 = estimator.run([psi2], [hamiltonian1], [theta2]).result() + np.testing.assert_allclose(result2.values, [2.97797666]) + + # calculate [ , ] + result3 = estimator.run([psi1, psi1], [hamiltonian2, hamiltonian3], [theta1] * 2).result() + np.testing.assert_allclose(result3.values, [-0.551653, 0.07535239]) + + # calculate [ , + # , + # ] + result4 = estimator.run( + [psi1, psi2, psi1], [hamiltonian1, hamiltonian2, hamiltonian3], [theta1, theta2, theta3] + ).result() + np.testing.assert_allclose(result4.values, [1.55555728, 0.17849238, -1.08766318]) + + def test_estiamtor_run_no_params(self): + """test for estimator without parameters""" + circuit = self.ansatz.bind_parameters([0, 1, 1, 2, 3, 5]) + est = Estimator() + result = est.run([circuit], [self.observable]).result() + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [-1.284366511861733]) + + def test_run_1qubit(self): + """Test for 1-qubit cases""" + qc = QuantumCircuit(1) + qc2 = QuantumCircuit(1) + qc2.x(0) + + op = SparsePauliOp.from_list([("I", 1)]) + op2 = SparsePauliOp.from_list([("Z", 1)]) + + est = Estimator() + result = est.run([qc], [op], [[]]).result() + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [1]) + + result = est.run([qc], [op2], [[]]).result() + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [1]) + + result = est.run([qc2], [op], [[]]).result() + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [1]) + + result = est.run([qc2], [op2], [[]]).result() + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [-1]) + + def test_run_2qubits(self): + """Test for 2-qubit cases (to check endian)""" + qc = QuantumCircuit(2) + qc2 = QuantumCircuit(2) + qc2.x(0) + + op = SparsePauliOp.from_list([("II", 1)]) + op2 = SparsePauliOp.from_list([("ZI", 1)]) + op3 = SparsePauliOp.from_list([("IZ", 1)]) + + est = Estimator() + result = est.run([qc], [op], [[]]).result() + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [1]) + + result = est.run([qc2], [op], [[]]).result() + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [1]) + + result = est.run([qc], [op2], [[]]).result() + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [1]) + + result = est.run([qc2], [op2], [[]]).result() + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [1]) + + result = est.run([qc], [op3], [[]]).result() + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [1]) + + result = est.run([qc2], [op3], [[]]).result() + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [-1]) + + def test_run_errors(self): + """Test for errors""" + qc = QuantumCircuit(1) + qc2 = QuantumCircuit(2) + + op = SparsePauliOp.from_list([("I", 1)]) + op2 = SparsePauliOp.from_list([("II", 1)]) + + est = Estimator() + with self.assertRaises(QiskitError): + est.run([qc], [op2], [[]]).result() + with self.assertRaises(QiskitError): + est.run([qc2], [op], [[]]).result() + with self.assertRaises(QiskitError): + est.run([qc], [op], [[1e4]]).result() + with self.assertRaises(QiskitError): + est.run([qc2], [op2], [[1, 2]]).result() + with self.assertRaises(QiskitError): + est.run([qc, qc2], [op2], [[1]]).result() + with self.assertRaises(QiskitError): + est.run([qc], [op, op2], [[1]]).result() + + def test_run_numpy_params(self): + """Test for numpy array as parameter values""" + qc = RealAmplitudes(num_qubits=2, reps=2) + op = SparsePauliOp.from_list([("IZ", 1), ("XI", 2), ("ZY", -1)]) + k = 5 + params_array = np.random.rand(k, qc.num_parameters) + params_list = params_array.tolist() + params_list_array = list(params_array) + estimator = Estimator() + target = estimator.run([qc] * k, [op] * k, params_list).result() + + with self.subTest("ndarrary"): + result = estimator.run([qc] * k, [op] * k, params_array).result() + self.assertEqual(len(result.metadata), k) + np.testing.assert_allclose(result.values, target.values) + + with self.subTest("list of ndarray"): + result = estimator.run([qc] * k, [op] * k, params_list_array).result() + self.assertEqual(len(result.metadata), k) + np.testing.assert_allclose(result.values, target.values) + + def test_run_with_shots_option(self): + """test with shots option.""" + est = Estimator() + result = est.run( + [self.ansatz], + [self.observable], + parameter_values=[[0, 1, 1, 2, 3, 5]], + shots=1024, + seed=15, + ).result() + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [-1.307397243478641]) + if __name__ == "__main__": unittest.main() diff --git a/test/python/primitives/test_sampler.py b/test/python/primitives/test_sampler.py index 3ee469a8cc15..7f258a644d9c 100644 --- a/test/python/primitives/test_sampler.py +++ b/test/python/primitives/test_sampler.py @@ -23,6 +23,7 @@ from qiskit.circuit.library import RealAmplitudes from qiskit.exceptions import QiskitError from qiskit.primitives import Sampler, SamplerResult +from qiskit.providers import JobV1 from qiskit.test import QiskitTestCase @@ -46,8 +47,15 @@ def setUp(self): ] self._pqc = RealAmplitudes(num_qubits=2, reps=2) self._pqc.measure_all() + self._pqc2 = RealAmplitudes(num_qubits=2, reps=3) + self._pqc2.measure_all() self._pqc_params = [[0.0] * 6, [1.0] * 6] self._pqc_target = [{0: 1}, {0: 0.0148, 1: 0.3449, 2: 0.0531, 3: 0.5872}] + self._theta = [ + [0, 1, 1, 2, 3, 5], + [1, 2, 3, 4, 5, 6], + [0, 1, 2, 3, 4, 5, 6, 7], + ] def _generate_circuits_target(self, indices): if isinstance(indices, list): @@ -69,6 +77,8 @@ def _generate_params_target(self, indices): return params, target def _compare_probs(self, prob, target): + if not isinstance(prob, list): + prob = [prob] if not isinstance(target, list): target = [target] self.assertEqual(len(prob), len(target)) @@ -83,26 +93,29 @@ def _compare_probs(self, prob, target): def test_sampler(self, indices): """test for sampler""" circuits, target = self._generate_circuits_target(indices) - with Sampler(circuits=circuits) as sampler: + with self.assertWarns(DeprecationWarning): + sampler = Sampler(circuits=circuits) result = sampler(list(range(len(indices))), parameter_values=[[] for _ in indices]) - self._compare_probs(result.quasi_dists, target) + self._compare_probs(result.quasi_dists, target) @combine(indices=[[0], [1], [0, 1]]) def test_sampler_pqc(self, indices): """test for sampler with a parametrized circuit""" params, target = self._generate_params_target(indices) - with Sampler(circuits=self._pqc) as sampler: + with self.assertWarns(DeprecationWarning): + sampler = Sampler(circuits=self._pqc) result = sampler([0] * len(params), params) - self._compare_probs(result.quasi_dists, target) + self._compare_probs(result.quasi_dists, target) @combine(indices=[[0, 0], [0, 1], [1, 1]]) def test_evaluate_two_pqcs(self, indices): """test for sampler with two parametrized circuits""" circs = [self._pqc, self._pqc] params, target = self._generate_params_target(indices) - with Sampler(circuits=circs) as sampler: + with self.assertWarns(DeprecationWarning): + sampler = Sampler(circuits=circs) result = sampler(indices, parameter_values=params) - self._compare_probs(result.quasi_dists, target) + self._compare_probs(result.quasi_dists, target) def test_sampler_example(self): """test for Sampler example""" @@ -113,35 +126,38 @@ def test_sampler_example(self): bell.measure_all() # executes a Bell circuit - with Sampler(circuits=[bell], parameters=[[]]) as sampler: + with self.assertWarns(DeprecationWarning): + sampler = Sampler(circuits=[bell], parameters=[[]]) result = sampler(parameter_values=[[]], circuits=[0]) - self.assertIsInstance(result, SamplerResult) - self.assertEqual(len(result.quasi_dists), 1) - keys, values = zip(*sorted(result.quasi_dists[0].items())) - self.assertTupleEqual(keys, tuple(range(4))) - np.testing.assert_allclose(values, [0.5, 0, 0, 0.5]) - self.assertEqual(len(result.metadata), 1) + self.assertIsInstance(result, SamplerResult) + self.assertEqual(len(result.quasi_dists), 1) + keys, values = zip(*sorted(result.quasi_dists[0].items())) + self.assertTupleEqual(keys, tuple(range(4))) + np.testing.assert_allclose(values, [0.5, 0, 0, 0.5]) + self.assertEqual(len(result.metadata), 1) # executes three Bell circuits - with Sampler([bell] * 3, [[]] * 3) as sampler: + with self.assertWarns(DeprecationWarning): + sampler = Sampler([bell] * 3, [[]] * 3) result = sampler([0, 1, 2], [[]] * 3) - self.assertIsInstance(result, SamplerResult) - self.assertEqual(len(result.quasi_dists), 3) - self.assertEqual(len(result.metadata), 3) - for dist in result.quasi_dists: - keys, values = zip(*sorted(dist.items())) - self.assertTupleEqual(keys, tuple(range(4))) - np.testing.assert_allclose(values, [0.5, 0, 0, 0.5]) - - with Sampler([bell]) as sampler: + self.assertIsInstance(result, SamplerResult) + self.assertEqual(len(result.quasi_dists), 3) + self.assertEqual(len(result.metadata), 3) + for dist in result.quasi_dists: + keys, values = zip(*sorted(dist.items())) + self.assertTupleEqual(keys, tuple(range(4))) + np.testing.assert_allclose(values, [0.5, 0, 0, 0.5]) + + with self.assertWarns(DeprecationWarning): + sampler = Sampler([bell]) result = sampler([bell, bell, bell]) - self.assertIsInstance(result, SamplerResult) - self.assertEqual(len(result.quasi_dists), 3) - self.assertEqual(len(result.metadata), 3) - for dist in result.quasi_dists: - keys, values = zip(*sorted(dist.items())) - self.assertTupleEqual(keys, tuple(range(4))) - np.testing.assert_allclose(values, [0.5, 0, 0, 0.5]) + self.assertIsInstance(result, SamplerResult) + self.assertEqual(len(result.quasi_dists), 3) + self.assertEqual(len(result.metadata), 3) + for dist in result.quasi_dists: + keys, values = zip(*sorted(dist.items())) + self.assertTupleEqual(keys, tuple(range(4))) + np.testing.assert_allclose(values, [0.5, 0, 0, 0.5]) # parametrized circuit pqc = RealAmplitudes(num_qubits=2, reps=2) @@ -153,37 +169,38 @@ def test_sampler_example(self): theta2 = [1, 2, 3, 4, 5, 6] theta3 = [0, 1, 2, 3, 4, 5, 6, 7] - with Sampler(circuits=[pqc, pqc2], parameters=[pqc.parameters, pqc2.parameters]) as sampler: + with self.assertWarns(DeprecationWarning): + sampler = Sampler(circuits=[pqc, pqc2], parameters=[pqc.parameters, pqc2.parameters]) result = sampler([0, 0, 1], [theta1, theta2, theta3]) - self.assertIsInstance(result, SamplerResult) - self.assertEqual(len(result.quasi_dists), 3) - self.assertEqual(len(result.metadata), 3) - - keys, values = zip(*sorted(result.quasi_dists[0].items())) - self.assertTupleEqual(keys, tuple(range(4))) - np.testing.assert_allclose( - values, - [0.13092484629757767, 0.3608720796028449, 0.09324865232050054, 0.414954421779077], - ) - - keys, values = zip(*sorted(result.quasi_dists[1].items())) - self.assertTupleEqual(keys, tuple(range(4))) - np.testing.assert_allclose( - values, - [0.06282290651933871, 0.02877144385576703, 0.606654494132085, 0.3017511554928095], - ) - - keys, values = zip(*sorted(result.quasi_dists[2].items())) - self.assertTupleEqual(keys, tuple(range(4))) - np.testing.assert_allclose( - values, - [ - 0.18802639943804164, - 0.6881971261189544, - 0.09326232720582446, - 0.030514147237179882, - ], - ) + self.assertIsInstance(result, SamplerResult) + self.assertEqual(len(result.quasi_dists), 3) + self.assertEqual(len(result.metadata), 3) + + keys, values = zip(*sorted(result.quasi_dists[0].items())) + self.assertTupleEqual(keys, tuple(range(4))) + np.testing.assert_allclose( + values, + [0.13092484629757767, 0.3608720796028449, 0.09324865232050054, 0.414954421779077], + ) + + keys, values = zip(*sorted(result.quasi_dists[1].items())) + self.assertTupleEqual(keys, tuple(range(4))) + np.testing.assert_allclose( + values, + [0.06282290651933871, 0.02877144385576703, 0.606654494132085, 0.3017511554928095], + ) + + keys, values = zip(*sorted(result.quasi_dists[2].items())) + self.assertTupleEqual(keys, tuple(range(4))) + np.testing.assert_allclose( + values, + [ + 0.18802639943804164, + 0.6881971261189544, + 0.09326232720582446, + 0.030514147237179882, + ], + ) def test_sampler_param_order(self): """test for sampler with different parameter orders""" @@ -198,30 +215,31 @@ def test_sampler_param_order(self): qc.measure(1, 1) qc.measure(2, 2) - with Sampler([qc, qc], [[x, y], [y, x]]) as sampler: + with self.assertWarns(DeprecationWarning): + sampler = Sampler([qc, qc], [[x, y], [y, x]]) result = sampler([0, 1, 0, 1], [[0, 0], [0, 0], [np.pi / 2, 0], [np.pi / 2, 0]]) - self.assertIsInstance(result, SamplerResult) - self.assertEqual(len(result.quasi_dists), 4) + self.assertIsInstance(result, SamplerResult) + self.assertEqual(len(result.quasi_dists), 4) - # qc({x: 0, y: 0}) - keys, values = zip(*sorted(result.quasi_dists[0].items())) - self.assertTupleEqual(keys, tuple(range(8))) - np.testing.assert_allclose(values, [0, 0, 0, 0, 1, 0, 0, 0]) + # qc({x: 0, y: 0}) + keys, values = zip(*sorted(result.quasi_dists[0].items())) + self.assertTupleEqual(keys, tuple(range(8))) + np.testing.assert_allclose(values, [0, 0, 0, 0, 1, 0, 0, 0]) - # qc({x: 0, y: 0}) - keys, values = zip(*sorted(result.quasi_dists[1].items())) - self.assertTupleEqual(keys, tuple(range(8))) - np.testing.assert_allclose(values, [0, 0, 0, 0, 1, 0, 0, 0]) + # qc({x: 0, y: 0}) + keys, values = zip(*sorted(result.quasi_dists[1].items())) + self.assertTupleEqual(keys, tuple(range(8))) + np.testing.assert_allclose(values, [0, 0, 0, 0, 1, 0, 0, 0]) - # qc({x: pi/2, y: 0}) - keys, values = zip(*sorted(result.quasi_dists[2].items())) - self.assertTupleEqual(keys, tuple(range(8))) - np.testing.assert_allclose(values, [0, 0, 0, 0, 0.5, 0.5, 0, 0]) + # qc({x: pi/2, y: 0}) + keys, values = zip(*sorted(result.quasi_dists[2].items())) + self.assertTupleEqual(keys, tuple(range(8))) + np.testing.assert_allclose(values, [0, 0, 0, 0, 0.5, 0.5, 0, 0]) - # qc({x: 0, y: pi/2}) - keys, values = zip(*sorted(result.quasi_dists[3].items())) - self.assertTupleEqual(keys, tuple(range(8))) - np.testing.assert_allclose(values, [0, 0, 0, 0, 0.5, 0, 0.5, 0]) + # qc({x: 0, y: pi/2}) + keys, values = zip(*sorted(result.quasi_dists[3].items())) + self.assertTupleEqual(keys, tuple(range(8))) + np.testing.assert_allclose(values, [0, 0, 0, 0, 0.5, 0, 0.5, 0]) def test_sampler_reverse_meas_order(self): """test for sampler with reverse measurement order""" @@ -236,30 +254,31 @@ def test_sampler_reverse_meas_order(self): qc.measure(1, 1) qc.measure(2, 0) - with Sampler([qc, qc], [[x, y], [y, x]]) as sampler: + with self.assertWarns(DeprecationWarning): + sampler = Sampler([qc, qc], [[x, y], [y, x]]) result = sampler([0, 1, 0, 1], [[0, 0], [0, 0], [np.pi / 2, 0], [np.pi / 2, 0]]) - self.assertIsInstance(result, SamplerResult) - self.assertEqual(len(result.quasi_dists), 4) + self.assertIsInstance(result, SamplerResult) + self.assertEqual(len(result.quasi_dists), 4) - # qc({x: 0, y: 0}) - keys, values = zip(*sorted(result.quasi_dists[0].items())) - self.assertTupleEqual(keys, tuple(range(8))) - np.testing.assert_allclose(values, [0, 1, 0, 0, 0, 0, 0, 0]) + # qc({x: 0, y: 0}) + keys, values = zip(*sorted(result.quasi_dists[0].items())) + self.assertTupleEqual(keys, tuple(range(8))) + np.testing.assert_allclose(values, [0, 1, 0, 0, 0, 0, 0, 0]) - # qc({x: 0, y: 0}) - keys, values = zip(*sorted(result.quasi_dists[1].items())) - self.assertTupleEqual(keys, tuple(range(8))) - np.testing.assert_allclose(values, [0, 1, 0, 0, 0, 0, 0, 0]) + # qc({x: 0, y: 0}) + keys, values = zip(*sorted(result.quasi_dists[1].items())) + self.assertTupleEqual(keys, tuple(range(8))) + np.testing.assert_allclose(values, [0, 1, 0, 0, 0, 0, 0, 0]) - # qc({x: pi/2, y: 0}) - keys, values = zip(*sorted(result.quasi_dists[2].items())) - self.assertTupleEqual(keys, tuple(range(8))) - np.testing.assert_allclose(values, [0, 0.5, 0, 0, 0, 0.5, 0, 0]) + # qc({x: pi/2, y: 0}) + keys, values = zip(*sorted(result.quasi_dists[2].items())) + self.assertTupleEqual(keys, tuple(range(8))) + np.testing.assert_allclose(values, [0, 0.5, 0, 0, 0, 0.5, 0, 0]) - # qc({x: 0, y: pi/2}) - keys, values = zip(*sorted(result.quasi_dists[3].items())) - self.assertTupleEqual(keys, tuple(range(8))) - np.testing.assert_allclose(values, [0, 0.5, 0, 0.5, 0, 0, 0, 0]) + # qc({x: 0, y: pi/2}) + keys, values = zip(*sorted(result.quasi_dists[3].items())) + self.assertTupleEqual(keys, tuple(range(8))) + np.testing.assert_allclose(values, [0, 0.5, 0, 0.5, 0, 0, 0, 0]) def test_1qubit(self): """test for 1-qubit cases""" @@ -269,18 +288,19 @@ def test_1qubit(self): qc2.x(0) qc2.measure_all() - with Sampler([qc, qc2], [qc.parameters, qc2.parameters]) as sampler: + with self.assertWarns(DeprecationWarning): + sampler = Sampler([qc, qc2], [qc.parameters, qc2.parameters]) result = sampler([0, 1], [[]] * 2) - self.assertIsInstance(result, SamplerResult) - self.assertEqual(len(result.quasi_dists), 2) + self.assertIsInstance(result, SamplerResult) + self.assertEqual(len(result.quasi_dists), 2) - keys, values = zip(*sorted(result.quasi_dists[0].items())) - self.assertTupleEqual(keys, tuple(range(2))) - np.testing.assert_allclose(values, [1, 0]) + keys, values = zip(*sorted(result.quasi_dists[0].items())) + self.assertTupleEqual(keys, tuple(range(2))) + np.testing.assert_allclose(values, [1, 0]) - keys, values = zip(*sorted(result.quasi_dists[1].items())) - self.assertTupleEqual(keys, tuple(range(2))) - np.testing.assert_allclose(values, [0, 1]) + keys, values = zip(*sorted(result.quasi_dists[1].items())) + self.assertTupleEqual(keys, tuple(range(2))) + np.testing.assert_allclose(values, [0, 1]) def test_2qubit(self): """test for 2-qubit cases""" @@ -296,28 +316,30 @@ def test_2qubit(self): qc3.x([0, 1]) qc3.measure_all() - with Sampler( - [qc0, qc1, qc2, qc3], [qc0.parameters, qc1.parameters, qc2.parameters, qc3.parameters] - ) as sampler: + with self.assertWarns(DeprecationWarning): + sampler = Sampler( + [qc0, qc1, qc2, qc3], + [qc0.parameters, qc1.parameters, qc2.parameters, qc3.parameters], + ) result = sampler([0, 1, 2, 3], [[]] * 4) - self.assertIsInstance(result, SamplerResult) - self.assertEqual(len(result.quasi_dists), 4) + self.assertIsInstance(result, SamplerResult) + self.assertEqual(len(result.quasi_dists), 4) - keys, values = zip(*sorted(result.quasi_dists[0].items())) - self.assertTupleEqual(keys, tuple(range(4))) - np.testing.assert_allclose(values, [1, 0, 0, 0]) + keys, values = zip(*sorted(result.quasi_dists[0].items())) + self.assertTupleEqual(keys, tuple(range(4))) + np.testing.assert_allclose(values, [1, 0, 0, 0]) - keys, values = zip(*sorted(result.quasi_dists[1].items())) - self.assertTupleEqual(keys, tuple(range(4))) - np.testing.assert_allclose(values, [0, 1, 0, 0]) + keys, values = zip(*sorted(result.quasi_dists[1].items())) + self.assertTupleEqual(keys, tuple(range(4))) + np.testing.assert_allclose(values, [0, 1, 0, 0]) - keys, values = zip(*sorted(result.quasi_dists[2].items())) - self.assertTupleEqual(keys, tuple(range(4))) - np.testing.assert_allclose(values, [0, 0, 1, 0]) + keys, values = zip(*sorted(result.quasi_dists[2].items())) + self.assertTupleEqual(keys, tuple(range(4))) + np.testing.assert_allclose(values, [0, 0, 1, 0]) - keys, values = zip(*sorted(result.quasi_dists[3].items())) - self.assertTupleEqual(keys, tuple(range(4))) - np.testing.assert_allclose(values, [0, 0, 0, 1]) + keys, values = zip(*sorted(result.quasi_dists[3].items())) + self.assertTupleEqual(keys, tuple(range(4))) + np.testing.assert_allclose(values, [0, 0, 0, 1]) def test_errors(self): """Test for errors""" @@ -326,35 +348,39 @@ def test_errors(self): qc2 = RealAmplitudes(num_qubits=1, reps=1) qc2.measure_all() - with Sampler([qc1, qc2], [qc1.parameters, qc2.parameters]) as sampler: - with self.assertRaises(QiskitError): - sampler([0], [[1e2]]) - with self.assertRaises(QiskitError): - sampler([1], [[]]) - with self.assertRaises(QiskitError): - sampler([1], [[1e2]]) + with self.assertWarns(DeprecationWarning): + sampler = Sampler([qc1, qc2], [qc1.parameters, qc2.parameters]) + with self.assertRaises(QiskitError), self.assertWarns(DeprecationWarning): + sampler([0], [[1e2]]) + with self.assertRaises(QiskitError), self.assertWarns(DeprecationWarning): + sampler([1], [[]]) + with self.assertRaises(QiskitError), self.assertWarns(DeprecationWarning): + sampler([1], [[1e2]]) def test_empty_parameter(self): """Test for empty parameter""" n = 5 qc = QuantumCircuit(n, n - 1) qc.measure(range(n - 1), range(n - 1)) - with Sampler(circuits=[qc] * 10) as sampler: - with self.subTest("one circuit"): + with self.assertWarns(DeprecationWarning): + sampler = Sampler(circuits=[qc] * 10) + with self.subTest("one circuit"): + with self.assertWarns(DeprecationWarning): result = sampler([0], shots=1000) - self.assertEqual(len(result.quasi_dists), 1) - for q_d in result.quasi_dists: - quasi_dist = {k: v for k, v in q_d.items() if v != 0.0} - self.assertDictEqual(quasi_dist, {0: 1.0}) - self.assertEqual(len(result.metadata), 1) + self.assertEqual(len(result.quasi_dists), 1) + for q_d in result.quasi_dists: + quasi_dist = {k: v for k, v in q_d.items() if v != 0.0} + self.assertDictEqual(quasi_dist, {0: 1.0}) + self.assertEqual(len(result.metadata), 1) - with self.subTest("two circuits"): + with self.subTest("two circuits"): + with self.assertWarns(DeprecationWarning): result = sampler([2, 4], shots=1000) - self.assertEqual(len(result.quasi_dists), 2) - for q_d in result.quasi_dists: - quasi_dist = {k: v for k, v in q_d.items() if v != 0.0} - self.assertDictEqual(quasi_dist, {0: 1.0}) - self.assertEqual(len(result.metadata), 2) + self.assertEqual(len(result.quasi_dists), 2) + for q_d in result.quasi_dists: + quasi_dist = {k: v for k, v in q_d.items() if v != 0.0} + self.assertDictEqual(quasi_dist, {0: 1.0}) + self.assertEqual(len(result.metadata), 2) def test_numpy_params(self): """Test for numpy array as parameter values""" @@ -364,20 +390,23 @@ def test_numpy_params(self): params_array = np.random.rand(k, qc.num_parameters) params_list = params_array.tolist() params_list_array = list(params_array) - with Sampler(circuits=qc) as sampler: + with self.assertWarns(DeprecationWarning): + sampler = Sampler(circuits=qc) target = sampler([0] * k, params_list) - with self.subTest("ndarrary"): + with self.subTest("ndarrary"): + with self.assertWarns(DeprecationWarning): result = sampler([0] * k, params_array) - self.assertEqual(len(result.metadata), k) - for i in range(k): - self.assertDictEqual(result.quasi_dists[i], target.quasi_dists[i]) + self.assertEqual(len(result.metadata), k) + for i in range(k): + self.assertDictEqual(result.quasi_dists[i], target.quasi_dists[i]) - with self.subTest("list of ndarray"): + with self.subTest("list of ndarray"): + with self.assertWarns(DeprecationWarning): result = sampler([0] * k, params_list_array) - self.assertEqual(len(result.metadata), k) - for i in range(k): - self.assertDictEqual(result.quasi_dists[i], target.quasi_dists[i]) + self.assertEqual(len(result.metadata), k) + for i in range(k): + self.assertDictEqual(result.quasi_dists[i], target.quasi_dists[i]) def test_passing_objects(self): """Test passing objects for Sampler.""" @@ -385,42 +414,243 @@ def test_passing_objects(self): params, target = self._generate_params_target([0]) with self.subTest("Valid test"): - with Sampler(circuits=self._pqc) as sampler: + with self.assertWarns(DeprecationWarning): + sampler = Sampler(circuits=self._pqc) result = sampler(circuits=[self._pqc], parameter_values=params) - self._compare_probs(result.quasi_dists, target) + self._compare_probs(result.quasi_dists, target) with self.subTest("Invalid circuit test"): circuit = QuantumCircuit(2) - with Sampler(circuits=self._pqc) as sampler: - with self.assertRaises(QiskitError): - result = sampler(circuits=[circuit], parameter_values=params) + with self.assertWarns(DeprecationWarning): + sampler = Sampler(circuits=self._pqc) + with self.assertRaises(QiskitError), self.assertWarns(DeprecationWarning): + result = sampler(circuits=[circuit], parameter_values=params) @combine(indices=[[0], [1], [0, 1]]) def test_deprecated_circuit_indices(self, indices): """Test for deprecated arguments""" circuits, target = self._generate_circuits_target(indices) - with Sampler(circuits=circuits) as sampler: - with self.assertWarns(DeprecationWarning): - result = sampler( - circuit_indices=list(range(len(indices))), - parameter_values=[[] for _ in indices], - ) - self._compare_probs(result.quasi_dists, target) + with self.assertWarns(DeprecationWarning): + sampler = Sampler(circuits=circuits) + result = sampler( + circuit_indices=list(range(len(indices))), + parameter_values=[[] for _ in indices], + ) + self._compare_probs(result.quasi_dists, target) def test_with_shots_option(self): """test with shots option.""" params, target = self._generate_params_target([1]) - with Sampler(circuits=self._pqc) as sampler: + with self.assertWarns(DeprecationWarning): + sampler = Sampler(circuits=self._pqc) result = sampler(circuits=[0], parameter_values=params, shots=1024, seed=15) - self._compare_probs(result.quasi_dists, target) + self._compare_probs(result.quasi_dists, target) def test_with_shots_option_none(self): """test with shots=None option. Seed is ignored then.""" - with Sampler([self._pqc]) as sampler: + with self.assertWarns(DeprecationWarning): + sampler = Sampler([self._pqc]) result_42 = sampler([0], parameter_values=[[0, 1, 1, 2, 3, 5]], shots=None, seed=42) result_15 = sampler([0], parameter_values=[[0, 1, 1, 2, 3, 5]], shots=None, seed=15) self.assertDictAlmostEqual(result_42.quasi_dists, result_15.quasi_dists) + def test_sampler_run(self): + """Test Sampler.run().""" + bell = self._circuit[1] + sampler = Sampler() + job = sampler.run(circuits=[bell]) + self.assertIsInstance(job, JobV1) + result = job.result() + self.assertIsInstance(result, SamplerResult) + # print([q.binary_probabilities() for q in result.quasi_dists]) + self._compare_probs(result.quasi_dists, self._target[1]) + + def test_sample_run_multiple_circuits(self): + """Test Sampler.run() with multiple circuits.""" + # executes three Bell circuits + # Argument `parameters` is optional. + bell = self._circuit[1] + sampler = Sampler() + result = sampler.run([bell, bell, bell]).result() + # print([q.binary_probabilities() for q in result.quasi_dists]) + self._compare_probs(result.quasi_dists[0], self._target[1]) + self._compare_probs(result.quasi_dists[1], self._target[1]) + self._compare_probs(result.quasi_dists[2], self._target[1]) + + def test_sampler_run_with_parameterized_circuits(self): + """Test Sampler.run() with parameterized circuits.""" + # parameterized circuit + + pqc = self._pqc + pqc2 = self._pqc2 + theta1, theta2, theta3 = self._theta + + sampler = Sampler() + result = sampler.run([pqc, pqc, pqc2], [theta1, theta2, theta3]).result() + + # result of pqc(theta1) + prob1 = { + "00": 0.1309248462975777, + "01": 0.3608720796028448, + "10": 0.09324865232050054, + "11": 0.41495442177907715, + } + self.assertDictAlmostEqual(result.quasi_dists[0].binary_probabilities(), prob1) + + # result of pqc(theta2) + prob2 = { + "00": 0.06282290651933871, + "01": 0.02877144385576705, + "10": 0.606654494132085, + "11": 0.3017511554928094, + } + self.assertDictAlmostEqual(result.quasi_dists[1].binary_probabilities(), prob2) + + # result of pqc2(theta3) + prob3 = { + "00": 0.1880263994380416, + "01": 0.6881971261189544, + "10": 0.09326232720582443, + "11": 0.030514147237179892, + } + self.assertDictAlmostEqual(result.quasi_dists[2].binary_probabilities(), prob3) + + def test_run_1qubit(self): + """test for 1-qubit cases""" + qc = QuantumCircuit(1) + qc.measure_all() + qc2 = QuantumCircuit(1) + qc2.x(0) + qc2.measure_all() + + sampler = Sampler() + result = sampler.run([qc, qc2]).result() + self.assertIsInstance(result, SamplerResult) + self.assertEqual(len(result.quasi_dists), 2) + + keys, values = zip(*sorted(result.quasi_dists[0].items())) + self.assertTupleEqual(keys, tuple(range(2))) + np.testing.assert_allclose(values, [1, 0]) + + keys, values = zip(*sorted(result.quasi_dists[1].items())) + self.assertTupleEqual(keys, tuple(range(2))) + np.testing.assert_allclose(values, [0, 1]) + + def test_run_2qubit(self): + """test for 2-qubit cases""" + qc0 = QuantumCircuit(2) + qc0.measure_all() + qc1 = QuantumCircuit(2) + qc1.x(0) + qc1.measure_all() + qc2 = QuantumCircuit(2) + qc2.x(1) + qc2.measure_all() + qc3 = QuantumCircuit(2) + qc3.x([0, 1]) + qc3.measure_all() + + sampler = Sampler() + result = sampler.run([qc0, qc1, qc2, qc3]).result() + self.assertIsInstance(result, SamplerResult) + self.assertEqual(len(result.quasi_dists), 4) + + keys, values = zip(*sorted(result.quasi_dists[0].items())) + self.assertTupleEqual(keys, tuple(range(4))) + np.testing.assert_allclose(values, [1, 0, 0, 0]) + + keys, values = zip(*sorted(result.quasi_dists[1].items())) + self.assertTupleEqual(keys, tuple(range(4))) + np.testing.assert_allclose(values, [0, 1, 0, 0]) + + keys, values = zip(*sorted(result.quasi_dists[2].items())) + self.assertTupleEqual(keys, tuple(range(4))) + np.testing.assert_allclose(values, [0, 0, 1, 0]) + + keys, values = zip(*sorted(result.quasi_dists[3].items())) + self.assertTupleEqual(keys, tuple(range(4))) + np.testing.assert_allclose(values, [0, 0, 0, 1]) + + def test_run_errors(self): + """Test for errors""" + qc1 = QuantumCircuit(1) + qc1.measure_all() + qc2 = RealAmplitudes(num_qubits=1, reps=1) + qc2.measure_all() + + sampler = Sampler() + with self.assertRaises(QiskitError): + sampler.run([qc1], [[1e2]]).result() + with self.assertRaises(QiskitError): + sampler.run([qc2], [[]]).result() + with self.assertRaises(QiskitError): + sampler.run([qc2], [[1e2]]).result() + + def test_run_empty_parameter(self): + """Test for empty parameter""" + n = 5 + qc = QuantumCircuit(n, n - 1) + qc.measure(range(n - 1), range(n - 1)) + sampler = Sampler() + with self.subTest("one circuit"): + result = sampler.run([qc], shots=1000).result() + self.assertEqual(len(result.quasi_dists), 1) + for q_d in result.quasi_dists: + quasi_dist = {k: v for k, v in q_d.items() if v != 0.0} + self.assertDictEqual(quasi_dist, {0: 1.0}) + self.assertEqual(len(result.metadata), 1) + + with self.subTest("two circuits"): + result = sampler.run([qc, qc], shots=1000).result() + self.assertEqual(len(result.quasi_dists), 2) + for q_d in result.quasi_dists: + quasi_dist = {k: v for k, v in q_d.items() if v != 0.0} + self.assertDictEqual(quasi_dist, {0: 1.0}) + self.assertEqual(len(result.metadata), 2) + + def test_run_numpy_params(self): + """Test for numpy array as parameter values""" + qc = RealAmplitudes(num_qubits=2, reps=2) + qc.measure_all() + k = 5 + params_array = np.random.rand(k, qc.num_parameters) + params_list = params_array.tolist() + params_list_array = list(params_array) + sampler = Sampler() + target = sampler.run([qc] * k, params_list).result() + + with self.subTest("ndarrary"): + result = sampler.run([qc] * k, params_array).result() + self.assertEqual(len(result.metadata), k) + for i in range(k): + self.assertDictEqual(result.quasi_dists[i], target.quasi_dists[i]) + + with self.subTest("list of ndarray"): + result = sampler.run([qc] * k, params_list_array).result() + self.assertEqual(len(result.metadata), k) + for i in range(k): + self.assertDictEqual(result.quasi_dists[i], target.quasi_dists[i]) + + def test_run_with_shots_option(self): + """test with shots option.""" + params, target = self._generate_params_target([1]) + sampler = Sampler() + result = sampler.run( + circuits=[self._pqc], parameter_values=params, shots=1024, seed=15 + ).result() + self._compare_probs(result.quasi_dists, target) + + def test_run_with_shots_option_none(self): + """test with shots=None option. Seed is ignored then.""" + sampler = Sampler() + result_42 = sampler.run( + [self._pqc], parameter_values=[[0, 1, 1, 2, 3, 5]], shots=None, seed=42 + ).result() + result_15 = sampler.run( + [self._pqc], parameter_values=[[0, 1, 1, 2, 3, 5]], shots=None, seed=15 + ).result() + self.assertDictAlmostEqual(result_42.quasi_dists, result_15.quasi_dists) + if __name__ == "__main__": unittest.main() From cdc53f382e2887aff7196f326c9ece42beffffa7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Aug 2022 12:50:40 +0000 Subject: [PATCH 34/82] Bump ahash from 0.7.6 to 0.8.0 (#8511) Bumps [ahash](https://github.com/tkaitchuck/ahash) from 0.7.6 to 0.8.0. - [Release notes](https://github.com/tkaitchuck/ahash/releases) - [Commits](https://github.com/tkaitchuck/ahash/compare/v0.7.6...v0.8.0) --- updated-dependencies: - dependency-name: ahash dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 16 ++++++++++++++-- Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4dd238e56295..9283953439c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,6 +13,18 @@ dependencies = [ "version_check", ] +[[package]] +name = "ahash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e6e951cfbb2db8de1828d49073a113a29fd7117b1596caa781a258c7e38d72" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -99,7 +111,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash", + "ahash 0.7.6", "rayon", ] @@ -356,7 +368,7 @@ dependencies = [ name = "qiskit-terra" version = "0.22.0" dependencies = [ - "ahash", + "ahash 0.8.0", "hashbrown", "indexmap", "ndarray", diff --git a/Cargo.toml b/Cargo.toml index a31dc0d82b94..881f7bd09a42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ numpy = "0.16.2" rand = "0.8" rand_pcg = "0.3" rand_distr = "0.4.3" -ahash = "0.7.6" +ahash = "0.8.0" num-complex = "0.4" num-bigint = "0.4" From f48aab2b01ae279ffc83ec424c0feb16d4159e95 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Wed, 10 Aug 2022 16:58:15 +0300 Subject: [PATCH 35/82] Refactoring commutativity analysis and a new commutative inverse cancellation transpiler pass (#8184) * experimenting with transpiler passes * removing some prints * minor cleanup * Improving external test file * Simplified commutative inverse cancellation pass * black * Removing duplicate code from CommutationAnalysis and DagDependency * black and lint * commutation_checker cleanup * Adding tests for CommutativeInverseCancellation pass The tests are adapted from CommutativeCancellation, InverseCancellation and issue 8020. * Removing external test python file * Update qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py Co-authored-by: Matthew Treinish * Update qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py Co-authored-by: Matthew Treinish * Removing the use of dag node classes and taking args and op separately * Removing runtime import * removing unnecessary pylint-disable * moving commutation_checker to qiskit.circuit and improving imports * Adding commutative_checker tests * running black * linting * Adding corner-cases to test_commutative_inverse_cancellation * release notes * black * release notes tweaks Co-authored-by: Matthew Treinish Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/circuit/__init__.py | 1 + qiskit/circuit/commutation_checker.py | 146 ++++ qiskit/dagcircuit/dagdependency.py | 85 +- qiskit/transpiler/passes/__init__.py | 2 + .../passes/optimization/__init__.py | 1 + .../optimization/commutation_analysis.py | 113 +-- .../commutative_inverse_cancellation.py | 97 +++ ...inverse-cancellation-a10e72d8e42ac74b.yaml | 56 ++ .../circuit/test_commutation_checker.py | 364 +++++++++ .../test_commutative_inverse_cancellation.py | 752 ++++++++++++++++++ 10 files changed, 1445 insertions(+), 172 deletions(-) create mode 100644 qiskit/circuit/commutation_checker.py create mode 100644 qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py create mode 100644 releasenotes/notes/commutative-inverse-cancellation-a10e72d8e42ac74b.yaml create mode 100644 test/python/circuit/test_commutation_checker.py create mode 100644 test/python/transpiler/test_commutative_inverse_cancellation.py diff --git a/qiskit/circuit/__init__.py b/qiskit/circuit/__init__.py index d516b3aa98dc..becb62be09ef 100644 --- a/qiskit/circuit/__init__.py +++ b/qiskit/circuit/__init__.py @@ -243,6 +243,7 @@ from .equivalence import EquivalenceLibrary from .classicalfunction.types import Int1, Int2 from .classicalfunction import classical_function, BooleanExpression +from .commutation_checker import CommutationChecker from .controlflow import ( ControlFlowOp, diff --git a/qiskit/circuit/commutation_checker.py b/qiskit/circuit/commutation_checker.py new file mode 100644 index 000000000000..4bc61ee33561 --- /dev/null +++ b/qiskit/circuit/commutation_checker.py @@ -0,0 +1,146 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2021. +# +# 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. + +"""Code from commutative_analysis pass that checks commutation relations between DAG nodes.""" + +from functools import lru_cache +from typing import List +import numpy as np + +from qiskit.circuit.operation import Operation +from qiskit.quantum_info.operators import Operator + + +@lru_cache(maxsize=None) +def _identity_op(num_qubits): + """Cached identity matrix""" + return Operator( + np.eye(2**num_qubits), input_dims=(2,) * num_qubits, output_dims=(2,) * num_qubits + ) + + +class CommutationChecker: + """This code is essentially copy-pasted from commutative_analysis.py. + This code cleverly hashes commutativity and non-commutativity results between DAG nodes and seems + quite efficient for large Clifford circuits. + They may be other possible efficiency improvements: using rule-based commutativity analysis, + evicting from the cache less useful entries, etc. + """ + + def __init__(self): + super().__init__() + self.cache = {} + + def _hashable_parameters(self, params): + """Convert the parameters of a gate into a hashable format for lookup in a dictionary. + + This aims to be fast in common cases, and is not intended to work outside of the lifetime of a + single commutation pass; it does not handle mutable state correctly if the state is actually + changed.""" + try: + hash(params) + return params + except TypeError: + pass + if isinstance(params, (list, tuple)): + return tuple(self._hashable_parameters(x) for x in params) + if isinstance(params, np.ndarray): + # We trust that the arrays will not be mutated during the commutation pass, since nothing + # would work if they were anyway. Using the id can potentially cause some additional cache + # misses if two UnitaryGate instances are being compared that have been separately + # constructed to have the same underlying matrix, but in practice the cost of string-ifying + # the matrix to get a cache key is far more expensive than just doing a small matmul. + return (np.ndarray, id(params)) + # Catch anything else with a slow conversion. + return ("fallback", str(params)) + + def commute( + self, op1: Operation, qargs1: List, cargs1: List, op2: Operation, qargs2: List, cargs2: List + ): + """ + Checks if two Operations commute. + + Args: + op1: first operation. + qargs1: first operation's qubits. + cargs1: first operation's clbits. + op2: second operation. + qargs2: second operation's qubits. + cargs2: second operation's clbits. + + Returns: + bool: whether two operations commute. + """ + # These lines are adapted from dag_dependency and say that two gates over + # different quantum and classical bits necessarily commute. This is more + # permissive that the check from commutation_analysis, as for example it + # allows to commute X(1) and Measure(0, 0). + # Presumably this check was not present in commutation_analysis as + # it was only called on pairs of connected nodes from DagCircuit. + intersection_q = set(qargs1).intersection(set(qargs2)) + intersection_c = set(cargs1).intersection(set(cargs2)) + if not (intersection_q or intersection_c): + return True + + # These lines are adapted from commutation_analysis, which is more restrictive + # than the check from dag_dependency when considering nodes with "_directive" + # or "condition". It would be nice to think which optimizations + # from dag_dependency can indeed be used. + for op in [op1, op2]: + if ( + getattr(op, "_directive", False) + or op.name in {"measure", "reset", "delay"} + or getattr(op, "condition", None) + or op.is_parameterized() + ): + return False + + # The main code is adapted from commutative analysis. + # Assign indices to each of the qubits such that all `node1`'s qubits come first, followed by + # any _additional_ qubits `node2` addresses. This helps later when we need to compose one + # operator with the other, since we can easily expand `node1` with a suitable identity. + qarg = {q: i for i, q in enumerate(qargs1)} + num_qubits = len(qarg) + for q in qargs2: + if q not in qarg: + qarg[q] = num_qubits + num_qubits += 1 + qarg1 = tuple(qarg[q] for q in qargs1) + qarg2 = tuple(qarg[q] for q in qargs2) + + node1_key = (op1.name, self._hashable_parameters(op1.params), qarg1) + node2_key = (op2.name, self._hashable_parameters(op2.params), qarg2) + try: + # We only need to try one orientation of the keys, since if we've seen the compound key + # before, we've set it in both orientations. + return self.cache[node1_key, node2_key] + except KeyError: + pass + + operator_1 = Operator(op1, input_dims=(2,) * len(qarg1), output_dims=(2,) * len(qarg1)) + operator_2 = Operator(op2, input_dims=(2,) * len(qarg2), output_dims=(2,) * len(qarg2)) + + if qarg1 == qarg2: + # Use full composition if possible to get the fastest matmul paths. + op12 = operator_1.compose(operator_2) + op21 = operator_2.compose(operator_1) + else: + # Expand operator_1 to be large enough to contain operator_2 as well; this relies on qargs1 + # being the lowest possible indices so the identity can be tensored before it. + extra_qarg2 = num_qubits - len(qarg1) + if extra_qarg2: + id_op = _identity_op(2**extra_qarg2) + operator_1 = id_op.tensor(operator_1) + op12 = operator_1.compose(operator_2, qargs=qarg2, front=False) + op21 = operator_1.compose(operator_2, qargs=qarg2, front=True) + self.cache[node1_key, node2_key] = self.cache[node2_key, node1_key] = ret = op12 == op21 + return ret diff --git a/qiskit/dagcircuit/dagdependency.py b/qiskit/dagcircuit/dagdependency.py index ceaa9df6b845..b645292ab1e3 100644 --- a/qiskit/dagcircuit/dagdependency.py +++ b/qiskit/dagcircuit/dagdependency.py @@ -18,15 +18,14 @@ from collections import OrderedDict, defaultdict import warnings -import numpy as np import retworkx as rx from qiskit.circuit.quantumregister import QuantumRegister, Qubit from qiskit.circuit.classicalregister import ClassicalRegister, Clbit from qiskit.dagcircuit.exceptions import DAGDependencyError from qiskit.dagcircuit.dagdepnode import DAGDepNode -from qiskit.quantum_info.operators import Operator from qiskit.exceptions import MissingOptionalLibraryError +from qiskit.circuit.commutation_checker import CommutationChecker class DAGDependency: @@ -94,6 +93,8 @@ def __init__(self): self.duration = None self.unit = "dt" + self.comm_checker = CommutationChecker() + @property def global_phase(self): """Return the global phase of the circuit.""" @@ -487,8 +488,14 @@ def _update_edges(self): self._multi_graph.get_node_data(current_node_id).reachable = True # Check the commutation relation with reachable node, it adds edges if it does not commute for prev_node_id in range(max_node_id - 1, -1, -1): - if self._multi_graph.get_node_data(prev_node_id).reachable and not _does_commute( - self._multi_graph.get_node_data(prev_node_id), max_node + prev_node = self._multi_graph.get_node_data(prev_node_id) + if prev_node.reachable and not self.comm_checker.commute( + prev_node.op, + prev_node.qargs, + prev_node.cargs, + max_node.op, + max_node.qargs, + max_node.cargs, ): self._multi_graph.add_edge(prev_node_id, max_node_id, {"commute": False}) self._list_pred(max_node_id) @@ -565,73 +572,3 @@ def merge_no_duplicates(*iterables): if val != last: last = val yield val - - -def _does_commute(node1, node2): - """Function to verify commutation relation between two nodes in the DAG. - - Args: - node1 (DAGnode): first node operation - node2 (DAGnode): second node operation - - Return: - bool: True if the nodes commute and false if it is not the case. - """ - - # Create set of qubits on which the operation acts - qarg1 = [node1.qargs[i] for i in range(0, len(node1.qargs))] - qarg2 = [node2.qargs[i] for i in range(0, len(node2.qargs))] - - # Create set of cbits on which the operation acts - carg1 = [node1.cargs[i] for i in range(0, len(node1.cargs))] - carg2 = [node2.cargs[i] for i in range(0, len(node2.cargs))] - - # Commutation for classical conditional gates - # if and only if the qubits are different. - # TODO: qubits can be the same if conditions are identical and - # the non-conditional gates commute. - if node1.type == "op" and node2.type == "op": - if node1.op.condition or node2.op.condition: - intersection = set(qarg1).intersection(set(qarg2)) - return not intersection - - # Commutation for non-unitary or parameterized or opaque ops - # (e.g. measure, reset, directives or pulse gates) - # if and only if the qubits and clbits are different. - non_unitaries = ["measure", "reset", "initialize", "delay"] - - def _unknown_commutator(n): - return n.op._directive or n.name in non_unitaries or n.op.is_parameterized() - - if _unknown_commutator(node1) or _unknown_commutator(node2): - intersection_q = set(qarg1).intersection(set(qarg2)) - intersection_c = set(carg1).intersection(set(carg2)) - return not (intersection_q or intersection_c) - - # Gates over disjoint sets of qubits commute - if not set(qarg1).intersection(set(qarg2)): - return True - - # Known non-commuting gates (TODO: add more). - non_commute_gates = [{"x", "y"}, {"x", "z"}] - if qarg1 == qarg2 and ({node1.name, node2.name} in non_commute_gates): - return False - - # Create matrices to check commutation relation if no other criteria are matched - qarg = list(set(node1.qargs + node2.qargs)) - qbit_num = len(qarg) - - qarg1 = [qarg.index(q) for q in node1.qargs] - qarg2 = [qarg.index(q) for q in node2.qargs] - - dim = 2**qbit_num - id_op = np.reshape(np.eye(dim), (2, 2) * qbit_num) - - op1 = np.reshape(node1.op.to_matrix(), (2, 2) * len(qarg1)) - op2 = np.reshape(node2.op.to_matrix(), (2, 2) * len(qarg2)) - - op = Operator._einsum_matmul(id_op, op1, qarg1) - op12 = Operator._einsum_matmul(op, op2, qarg2, right_mul=False) - op21 = Operator._einsum_matmul(op, op2, qarg2, shift=qbit_num, right_mul=True) - - return np.allclose(op12, op21) diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index e55d7bdfccde..4bed9b54803b 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -77,6 +77,7 @@ InverseCancellation CommutationAnalysis CommutativeCancellation + CommutativeInverseCancellation Optimize1qGatesSimpleCommutation RemoveDiagonalGatesBeforeMeasure RemoveResetInZeroState @@ -207,6 +208,7 @@ from .optimization import ConsolidateBlocks from .optimization import CommutationAnalysis from .optimization import CommutativeCancellation +from .optimization import CommutativeInverseCancellation from .optimization import CXCancellation from .optimization import Optimize1qGatesSimpleCommutation from .optimization import OptimizeSwapBeforeMeasure diff --git a/qiskit/transpiler/passes/optimization/__init__.py b/qiskit/transpiler/passes/optimization/__init__.py index 492224d5476c..bc922ba8cb0b 100644 --- a/qiskit/transpiler/passes/optimization/__init__.py +++ b/qiskit/transpiler/passes/optimization/__init__.py @@ -19,6 +19,7 @@ from .consolidate_blocks import ConsolidateBlocks from .commutation_analysis import CommutationAnalysis from .commutative_cancellation import CommutativeCancellation +from .commutative_inverse_cancellation import CommutativeInverseCancellation from .cx_cancellation import CXCancellation from .optimize_1q_commutation import Optimize1qGatesSimpleCommutation from .optimize_swap_before_measure import OptimizeSwapBeforeMeasure diff --git a/qiskit/transpiler/passes/optimization/commutation_analysis.py b/qiskit/transpiler/passes/optimization/commutation_analysis.py index 2226438f9ef7..b35a88a23dff 100644 --- a/qiskit/transpiler/passes/optimization/commutation_analysis.py +++ b/qiskit/transpiler/passes/optimization/commutation_analysis.py @@ -13,13 +13,10 @@ """Analysis pass to find commutation relations between DAG nodes.""" from collections import defaultdict -import numpy as np -from qiskit.transpiler.exceptions import TranspilerError -from qiskit.transpiler.basepasses import AnalysisPass -from qiskit.quantum_info.operators import Operator -from qiskit.dagcircuit import DAGOpNode -_CUTOFF_PRECISION = 1e-10 +from qiskit.dagcircuit import DAGOpNode +from qiskit.transpiler.basepasses import AnalysisPass +from qiskit.circuit.commutation_checker import CommutationChecker class CommutationAnalysis(AnalysisPass): @@ -35,7 +32,7 @@ class CommutationAnalysis(AnalysisPass): def __init__(self): super().__init__() - self.cache = {} + self.comm_checker = CommutationChecker() def run(self, dag): """Run the CommutationAnalysis pass on `dag`. @@ -73,101 +70,21 @@ def run(self, dag): if current_gate not in current_comm_set[-1]: prev_gate = current_comm_set[-1][-1] does_commute = False - try: - does_commute = _commute(current_gate, prev_gate, self.cache) - except TranspilerError: - pass + + if isinstance(current_gate, DAGOpNode) and isinstance(prev_gate, DAGOpNode): + does_commute = self.comm_checker.commute( + current_gate.op, + current_gate.qargs, + current_gate.cargs, + prev_gate.op, + prev_gate.qargs, + prev_gate.cargs, + ) + if does_commute: current_comm_set[-1].append(current_gate) - else: current_comm_set.append([current_gate]) temp_len = len(current_comm_set) self.property_set["commutation_set"][(current_gate, wire)] = temp_len - 1 - - -_COMMUTE_ID_OP = {} - - -def _hashable_parameters(params): - """Convert the parameters of a gate into a hashable format for lookup in a dictionary. - - This aims to be fast in common cases, and is not intended to work outside of the lifetime of a - single commutation pass; it does not handle mutable state correctly if the state is actually - changed.""" - try: - hash(params) - return params - except TypeError: - pass - if isinstance(params, (list, tuple)): - return tuple(_hashable_parameters(x) for x in params) - if isinstance(params, np.ndarray): - # We trust that the arrays will not be mutated during the commutation pass, since nothing - # would work if they were anyway. Using the id can potentially cause some additional cache - # misses if two UnitaryGate instances are being compared that have been separately - # constructed to have the same underlying matrix, but in practice the cost of string-ifying - # the matrix to get a cache key is far more expensive than just doing a small matmul. - return (np.ndarray, id(params)) - # Catch anything else with a slow conversion. - return ("fallback", str(params)) - - -def _commute(node1, node2, cache): - if not isinstance(node1, DAGOpNode) or not isinstance(node2, DAGOpNode): - return False - for nd in [node1, node2]: - if nd.op._directive or nd.name in {"measure", "reset", "delay"}: - return False - if node1.op.condition or node2.op.condition: - return False - if node1.op.is_parameterized() or node2.op.is_parameterized(): - return False - - # Assign indices to each of the qubits such that all `node1`'s qubits come first, followed by - # any _additional_ qubits `node2` addresses. This helps later when we need to compose one - # operator with the other, since we can easily expand `node1` with a suitable identity. - qarg = {q: i for i, q in enumerate(node1.qargs)} - num_qubits = len(qarg) - for q in node2.qargs: - if q not in qarg: - qarg[q] = num_qubits - num_qubits += 1 - qarg1 = tuple(qarg[q] for q in node1.qargs) - qarg2 = tuple(qarg[q] for q in node2.qargs) - - node1_key = (node1.op.name, _hashable_parameters(node1.op.params), qarg1) - node2_key = (node2.op.name, _hashable_parameters(node2.op.params), qarg2) - try: - # We only need to try one orientation of the keys, since if we've seen the compound key - # before, we've set it in both orientations. - return cache[node1_key, node2_key] - except KeyError: - pass - - operator_1 = Operator(node1.op, input_dims=(2,) * len(qarg1), output_dims=(2,) * len(qarg1)) - operator_2 = Operator(node2.op, input_dims=(2,) * len(qarg2), output_dims=(2,) * len(qarg2)) - - if qarg1 == qarg2: - # Use full composition if possible to get the fastest matmul paths. - op12 = operator_1.compose(operator_2) - op21 = operator_2.compose(operator_1) - else: - # Expand operator_1 to be large enough to contain operator_2 as well; this relies on qargs1 - # being the lowest possible indices so the identity can be tensored before it. - extra_qarg2 = num_qubits - len(qarg1) - if extra_qarg2: - try: - id_op = _COMMUTE_ID_OP[extra_qarg2] - except KeyError: - id_op = _COMMUTE_ID_OP[extra_qarg2] = Operator( - np.eye(2**extra_qarg2), - input_dims=(2,) * extra_qarg2, - output_dims=(2,) * extra_qarg2, - ) - operator_1 = id_op.tensor(operator_1) - op12 = operator_1.compose(operator_2, qargs=qarg2, front=False) - op21 = operator_1.compose(operator_2, qargs=qarg2, front=True) - cache[node1_key, node2_key] = cache[node2_key, node1_key] = ret = op12 == op21 - return ret diff --git a/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py b/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py new file mode 100644 index 000000000000..518ccbbef17e --- /dev/null +++ b/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py @@ -0,0 +1,97 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2021. +# +# 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. + +"""Cancel pairs of inverse gates exploiting commutation relations.""" + + +from qiskit.dagcircuit import DAGCircuit, DAGOpNode +from qiskit.transpiler.basepasses import TransformationPass +from qiskit.circuit.commutation_checker import CommutationChecker + + +class CommutativeInverseCancellation(TransformationPass): + """Cancel pairs of inverse gates exploiting commutation relations.""" + + def _skip_node(self, node): + """Returns True if we should skip this node for the analysis.""" + if not isinstance(node, DAGOpNode): + return True + + # We are currently taking an over-conservative approach with respect to which nodes + # can be inverses of which other nodes, and do not allow reductions for barriers, measures, + # conditional gates or parameterized gates. Possibly both this and commutativity + # checking can be extended to cover additional cases. + if getattr(node.op, "_directive", False) or node.name in {"measure", "reset", "delay"}: + return True + if getattr(node.op, "condition", None): + return True + if node.op.is_parameterized(): + return True + # ToDo: possibly also skip nodes on too many qubits + return False + + def run(self, dag: DAGCircuit): + """ + Run the CommutativeInverseCancellation pass on `dag`. + + Args: + dag: the directed acyclic graph to run on. + + Returns: + DAGCircuit: Transformed DAG. + """ + topo_sorted_nodes = list(dag.topological_op_nodes()) + + circ_size = len(topo_sorted_nodes) + + removed = [False for _ in range(circ_size)] + + cc = CommutationChecker() + + for idx1 in range(0, circ_size): + if self._skip_node(topo_sorted_nodes[idx1]): + continue + + matched_idx2 = -1 + + for idx2 in range(idx1 - 1, -1, -1): + if removed[idx2]: + continue + + if ( + not self._skip_node(topo_sorted_nodes[idx2]) + and topo_sorted_nodes[idx2].qargs == topo_sorted_nodes[idx1].qargs + and topo_sorted_nodes[idx2].cargs == topo_sorted_nodes[idx1].cargs + and topo_sorted_nodes[idx2].op == topo_sorted_nodes[idx1].op.inverse() + ): + matched_idx2 = idx2 + break + + if not cc.commute( + topo_sorted_nodes[idx1].op, + topo_sorted_nodes[idx1].qargs, + topo_sorted_nodes[idx1].cargs, + topo_sorted_nodes[idx2].op, + topo_sorted_nodes[idx2].qargs, + topo_sorted_nodes[idx2].cargs, + ): + break + + if matched_idx2 != -1: + removed[idx1] = True + removed[matched_idx2] = True + + for idx in range(circ_size): + if removed[idx]: + dag.remove_op_node(topo_sorted_nodes[idx]) + + return dag diff --git a/releasenotes/notes/commutative-inverse-cancellation-a10e72d8e42ac74b.yaml b/releasenotes/notes/commutative-inverse-cancellation-a10e72d8e42ac74b.yaml new file mode 100644 index 000000000000..53185ae3e80c --- /dev/null +++ b/releasenotes/notes/commutative-inverse-cancellation-a10e72d8e42ac74b.yaml @@ -0,0 +1,56 @@ +--- +features: + - | + Refactored gate commutativity analysis into a class :class:`~qiskit.circuit.CommutationChecker`. + It allows to check (based on matrix multiplication) whether two gates commute or do not commute, + and to cache the results (so that a similar check in the future will no longer require matrix + multiplication). + + For example we can now do:: + + from qiskit.circuit import QuantumRegister, CommutationChecker + + comm_checker = CommutationChecker() + qr = QuantumRegister(4) + + res = comm_checker.commute(CXGate(), [qr[1], qr[0]], [], CXGate(), [qr[1], qr[2]], []) + + As the two CX gates commute (the first CX gate is over qubits `qr[1]` and `qr[0]`, and the + second CX gate is over qubits `qr[1]` and `qr[2]`), we will have that `res` is `True`. + + This commutativity checking is over-conservative for conditional and parameterized gates, + and may return `False` even when such gates commute. + + - | + Added a new transpiler pass :class:`.CommutativeInverseCancellation` that cancels pairs of + inverse gates exploiting commutation relations between gates. This pass is a generalization + of the transpiler pass :class:`.InverseCancellation` as it detects a larger set of inverse + gates, and as it takes commutativity into account. The pass also avoids some problems + associated with the transpiler pass :class:`.CommutativeCancellation`, see + `issue #8020 `_ + + For example:: + + from qiskit.circuit import QuantumCircuit + from qiskit.transpiler import PassManager + from qiskit.transpiler.passes import CommutativeInverseCancellation + + circuit = QuantumCircuit(2) + circuit.z(0) + circuit.x(1) + circuit.cx(0, 1) + circuit.z(0) + circuit.x(1) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + + cancels the pair of self-inverse `Z`-gates, and the pair of self-inverse `X`-gates (as the + relevant gates commute with the `CX`-gate), producing a circuit consisting of a single `CX`-gate. + + The inverse checking is over-conservative for conditional and parameterized gates, + and may not cancel some of such gates. + + + + diff --git a/test/python/circuit/test_commutation_checker.py b/test/python/circuit/test_commutation_checker.py new file mode 100644 index 000000000000..9db5de33ff0f --- /dev/null +++ b/test/python/circuit/test_commutation_checker.py @@ -0,0 +1,364 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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 commutation checker class .""" + +import unittest +import numpy as np + +from qiskit import ClassicalRegister +from qiskit.test import QiskitTestCase + +from qiskit.circuit import QuantumRegister, Parameter +from qiskit.circuit import CommutationChecker +from qiskit.circuit.library import ( + ZGate, + XGate, + CXGate, + CCXGate, + RZGate, + Measure, + Barrier, + Reset, + LinearFunction, +) + + +class TestCommutationChecker(QiskitTestCase): + + """Test CommutationChecker class.""" + + def test_simple_gates(self): + """Check simple commutation relations between gates, experimenting with + different orders of gates, different orders of qubits, different sets of + qubits over which gates are defined, and so on.""" + comm_checker = CommutationChecker() + + # should commute + res = comm_checker.commute(ZGate(), [0], [], CXGate(), [0, 1], []) + self.assertTrue(res) + + # should not commute + res = comm_checker.commute(ZGate(), [1], [], CXGate(), [0, 1], []) + self.assertFalse(res) + + # should not commute + res = comm_checker.commute(XGate(), [0], [], CXGate(), [0, 1], []) + self.assertFalse(res) + + # should commute + res = comm_checker.commute(XGate(), [1], [], CXGate(), [0, 1], []) + self.assertTrue(res) + + # should not commute + res = comm_checker.commute(XGate(), [1], [], CXGate(), [1, 0], []) + self.assertFalse(res) + + # should commute + res = comm_checker.commute(XGate(), [0], [], CXGate(), [1, 0], []) + self.assertTrue(res) + + # should commute + res = comm_checker.commute(CXGate(), [1, 0], [], XGate(), [0], []) + self.assertTrue(res) + + # should not commute + res = comm_checker.commute(CXGate(), [1, 0], [], XGate(), [1], []) + self.assertFalse(res) + + # should commute + res = comm_checker.commute( + CXGate(), + [1, 0], + [], + CXGate(), + [1, 0], + [], + ) + self.assertTrue(res) + + # should not commute + res = comm_checker.commute( + CXGate(), + [1, 0], + [], + CXGate(), + [0, 1], + [], + ) + self.assertFalse(res) + + # should commute + res = comm_checker.commute( + CXGate(), + [1, 0], + [], + CXGate(), + [1, 2], + [], + ) + self.assertTrue(res) + + # should not commute + res = comm_checker.commute( + CXGate(), + [1, 0], + [], + CXGate(), + [2, 1], + [], + ) + self.assertFalse(res) + + # should commute + res = comm_checker.commute( + CXGate(), + [1, 0], + [], + CXGate(), + [2, 3], + [], + ) + self.assertTrue(res) + + res = comm_checker.commute(XGate(), [2], [], CCXGate(), [0, 1, 2], []) + self.assertTrue(res) + + res = comm_checker.commute(CCXGate(), [0, 1, 2], [], CCXGate(), [0, 2, 1], []) + self.assertFalse(res) + + def test_passing_quantum_registers(self): + """Check that passing QuantumRegisters works correctly.""" + comm_checker = CommutationChecker() + + qr = QuantumRegister(4) + + # should commute + res = comm_checker.commute(CXGate(), [qr[1], qr[0]], [], CXGate(), [qr[1], qr[2]], []) + self.assertTrue(res) + + # should not commute + res = comm_checker.commute(CXGate(), [qr[0], qr[1]], [], CXGate(), [qr[1], qr[2]], []) + self.assertFalse(res) + + def test_caching_positive_results(self): + """Check that hashing positive results in commutativity checker works as expected.""" + + comm_checker = CommutationChecker() + res = comm_checker.commute(ZGate(), [0], [], CXGate(), [0, 1], []) + self.assertTrue(res) + self.assertGreater(len(comm_checker.cache), 0) + + def test_caching_negative_results(self): + """Check that hashing negative results in commutativity checker works as expected.""" + + comm_checker = CommutationChecker() + res = comm_checker.commute(XGate(), [0], [], CXGate(), [0, 1], []) + self.assertFalse(res) + self.assertGreater(len(comm_checker.cache), 0) + + def test_caching_different_qubit_sets(self): + """Check that hashing same commutativity results over different qubit sets works as expected.""" + + comm_checker = CommutationChecker() + + # All the following should be cached in the same way + # though each relation gets cached twice: (A, B) and (B, A) + comm_checker.commute(XGate(), [0], [], CXGate(), [0, 1], []) + comm_checker.commute(XGate(), [10], [], CXGate(), [10, 20], []) + comm_checker.commute(XGate(), [10], [], CXGate(), [10, 5], []) + comm_checker.commute(XGate(), [5], [], CXGate(), [5, 7], []) + self.assertEqual(len(comm_checker.cache), 2) + + def test_gates_with_parameters(self): + """Check commutativity between (non-parameterized) gates with parameters.""" + + comm_checker = CommutationChecker() + res = comm_checker.commute(RZGate(0), [0], [], XGate(), [0], []) + self.assertTrue(res) + + res = comm_checker.commute(RZGate(np.pi / 2), [0], [], XGate(), [0], []) + self.assertFalse(res) + + res = comm_checker.commute(RZGate(np.pi / 2), [0], [], RZGate(0), [0], []) + self.assertTrue(res) + + def test_parameterized_gates(self): + """Check commutativity between parameterized gates, both with free and with + bound parameters.""" + + comm_checker = CommutationChecker() + + # gate that has parameters but is not considered parameterized + rz_gate = RZGate(np.pi / 2) + self.assertEqual(len(rz_gate.params), 1) + self.assertFalse(rz_gate.is_parameterized()) + + # gate that has parameters and is considered parameterized + rz_gate_theta = RZGate(Parameter("Theta")) + rz_gate_phi = RZGate(Parameter("Phi")) + self.assertEqual(len(rz_gate_theta.params), 1) + self.assertTrue(rz_gate_theta.is_parameterized()) + + # gate that has no parameters and is not considered parameterized + cx_gate = CXGate() + self.assertEqual(len(cx_gate.params), 0) + self.assertFalse(cx_gate.is_parameterized()) + + # We should detect that these gates commute + res = comm_checker.commute(rz_gate, [0], [], cx_gate, [0, 1], []) + self.assertTrue(res) + + # We should detect that these gates commute + res = comm_checker.commute(rz_gate, [0], [], rz_gate, [0], []) + self.assertTrue(res) + + # We should detect that parameterized gates over disjoint qubit subsets commute + res = comm_checker.commute(rz_gate_theta, [0], [], rz_gate_theta, [1], []) + self.assertTrue(res) + + # We should detect that parameterized gates over disjoint qubit subsets commute + res = comm_checker.commute(rz_gate_theta, [0], [], rz_gate_phi, [1], []) + self.assertTrue(res) + + # We should detect that parameterized gates over disjoint qubit subsets commute + res = comm_checker.commute(rz_gate_theta, [2], [], cx_gate, [1, 3], []) + self.assertTrue(res) + + # However, for now commutativity checker should return False when checking + # commutativity between a parameterized gate and some other gate, when + # the two gates are over intersecting qubit subsets. + # This check should be changed if commutativity checker is extended to + # handle parameterized gates better. + res = comm_checker.commute(rz_gate_theta, [0], [], cx_gate, [0, 1], []) + self.assertFalse(res) + + res = comm_checker.commute(rz_gate_theta, [0], [], rz_gate, [0], []) + self.assertFalse(res) + + def test_measure(self): + """Check commutativity involving measures.""" + + comm_checker = CommutationChecker() + + # Measure is over qubit 0, while gate is over a disjoint subset of qubits + # We should be able to swap these. + res = comm_checker.commute(Measure(), [0], [0], CXGate(), [1, 2], []) + self.assertTrue(res) + + # Measure and gate have intersecting set of qubits + # We should not be able to swap these. + res = comm_checker.commute(Measure(), [0], [0], CXGate(), [0, 2], []) + self.assertFalse(res) + + # Measures over different qubits and clbits + res = comm_checker.commute(Measure(), [0], [0], Measure(), [1], [1]) + self.assertTrue(res) + + # Measures over different qubits but same classical bit + # We should not be able to swap these. + res = comm_checker.commute(Measure(), [0], [0], Measure(), [1], [0]) + self.assertFalse(res) + + # Measures over same qubits but different classical bit + # ToDo: can we swap these? + # Currently checker takes the safe approach and returns False. + res = comm_checker.commute(Measure(), [0], [0], Measure(), [0], [1]) + self.assertFalse(res) + + def test_barrier(self): + """Check commutativity involving barriers.""" + + comm_checker = CommutationChecker() + + # A gate should not commute with a barrier + # (at least if these are over intersecting qubit sets). + res = comm_checker.commute(Barrier(4), [0, 1, 2, 3], [], CXGate(), [1, 2], []) + self.assertFalse(res) + + # Does it even make sense to have a barrier over a subset of qubits? + # Though in this case, it probably makes sense to say that barrier and gate can be swapped. + res = comm_checker.commute(Barrier(4), [0, 1, 2, 3], [], CXGate(), [5, 6], []) + self.assertTrue(res) + + def test_reset(self): + """Check commutativity involving resets.""" + + comm_checker = CommutationChecker() + + # A gate should not commute with reset when the qubits intersect. + res = comm_checker.commute(Reset(), [0], [], CXGate(), [0, 2], []) + self.assertFalse(res) + + # A gate should commute with reset when the qubits are disjoint. + res = comm_checker.commute(Reset(), [0], [], CXGate(), [1, 2], []) + self.assertTrue(res) + + def test_conditional_gates(self): + """Check commutativity involving conditional gates.""" + + comm_checker = CommutationChecker() + + qr = QuantumRegister(3) + cr = ClassicalRegister(2) + + # Different quantum bits (and empty classical bits). + # We should be able to swap these. + res = comm_checker.commute( + CXGate().c_if(cr[0], 0), [qr[0], qr[1]], [], XGate(), [qr[2]], [] + ) + self.assertTrue(res) + + # In all other cases, commutativity checker currently returns False. + # This is definitely suboptimal. + res = comm_checker.commute( + CXGate().c_if(cr[0], 0), [qr[0], qr[1]], [], XGate(), [qr[1]], [] + ) + self.assertFalse(res) + + res = comm_checker.commute( + CXGate().c_if(cr[0], 0), [qr[0], qr[1]], [], CXGate().c_if(cr[0], 0), [qr[0], qr[1]], [] + ) + self.assertFalse(res) + + res = comm_checker.commute( + XGate().c_if(cr[0], 0), [qr[0]], [], XGate().c_if(cr[0], 1), [qr[0]], [] + ) + self.assertFalse(res) + + res = comm_checker.commute(XGate().c_if(cr[0], 0), [qr[0]], [], XGate(), [qr[0]], []) + self.assertFalse(res) + + def test_complex_gates(self): + """Check commutativity involving more complex gates.""" + + comm_checker = CommutationChecker() + + lf1 = LinearFunction([[0, 1, 0], [1, 0, 0], [0, 0, 1]]) + lf2 = LinearFunction([[1, 0, 0], [0, 0, 1], [0, 1, 0]]) + + # lf1 is equivalent to swap(0, 1), and lf2 to swap(1, 2). + # These do not commute. + res = comm_checker.commute(lf1, [0, 1, 2], [], lf2, [0, 1, 2], []) + self.assertFalse(res) + + lf3 = LinearFunction([[0, 1, 0], [0, 0, 1], [1, 0, 0]]) + lf4 = LinearFunction([[0, 0, 1], [1, 0, 0], [0, 1, 0]]) + # lf3 is permutation 1->2, 2->3, 3->1. + # lf3 is the inverse permutation 1->3, 2->1, 3->2. + # These commute. + res = comm_checker.commute(lf3, [0, 1, 2], [], lf4, [0, 1, 2], []) + self.assertTrue(res) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/transpiler/test_commutative_inverse_cancellation.py b/test/python/transpiler/test_commutative_inverse_cancellation.py new file mode 100644 index 000000000000..bafb92f3425d --- /dev/null +++ b/test/python/transpiler/test_commutative_inverse_cancellation.py @@ -0,0 +1,752 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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 transpiler pass that cancels inverse gates while exploiting the commutation relations.""" + +import unittest +import numpy as np +from qiskit.test import QiskitTestCase + +from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.circuit.library import RZGate +from qiskit.transpiler import PassManager +from qiskit.transpiler.passes import CommutativeInverseCancellation + + +class TestCommutativeInverseCancellation(QiskitTestCase): + + """Test the CommutativeInverseCancellation pass.""" + + # The first suite of tests is adapted from CommutativeCancellation, + # excluding/modifying the tests the combine rotations gates or do + # basis priority change. + + def test_commutative_circuit1(self): + """A simple circuit where three CNOTs commute, the first and the last cancel. + + 0:----.---------------.-- 0:------------ + | | + 1:---(+)-----(+)-----(+)- = 1:-------(+)-- + | | + 2:---[H]------.---------- 2:---[H]--.--- + """ + circuit = QuantumCircuit(3) + circuit.cx(0, 1) + circuit.h(2) + circuit.cx(2, 1) + circuit.cx(0, 1) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + + expected = QuantumCircuit(3) + expected.h(2) + expected.cx(2, 1) + + self.assertEqual(expected, new_circuit) + + def test_consecutive_cnots(self): + """A simple circuit equals identity + + 0:----.- ----.-- 0:------------ + | | + 1:---(+)----(+)- = 1:------------ + """ + + circuit = QuantumCircuit(2) + circuit.cx(0, 1) + circuit.cx(0, 1) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + + expected = QuantumCircuit(2) + + self.assertEqual(expected, new_circuit) + + def test_consecutive_cnots2(self): + """ + Both CNOTs and rotations should cancel out. + """ + circuit = QuantumCircuit(2) + circuit.rx(np.pi / 2, 0) + circuit.cx(0, 1) + circuit.cx(0, 1) + circuit.rx(-np.pi / 2, 0) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + + expected = QuantumCircuit(2) + self.assertEqual(expected, new_circuit) + + def test_2_alternating_cnots(self): + """A simple circuit where nothing should be cancelled. + + 0:----.- ---(+)- 0:----.----(+)- + | | | | + 1:---(+)-----.-- = 1:---(+)----.-- + + """ + + circuit = QuantumCircuit(2) + circuit.cx(0, 1) + circuit.cx(1, 0) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + + expected = QuantumCircuit(2) + expected.cx(0, 1) + expected.cx(1, 0) + + self.assertEqual(expected, new_circuit) + + def test_control_bit_of_cnot(self): + """A simple circuit where nothing should be cancelled. + + 0:----.------[X]------.-- 0:----.------[X]------.-- + | | | | + 1:---(+)-------------(+)- = 1:---(+)-------------(+)- + """ + + circuit = QuantumCircuit(2) + circuit.cx(0, 1) + circuit.x(0) + circuit.cx(0, 1) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + + expected = QuantumCircuit(2) + expected.cx(0, 1) + expected.x(0) + expected.cx(0, 1) + + self.assertEqual(expected, new_circuit) + + def test_control_bit_of_cnot1(self): + """A simple circuit where the two cnots should be cancelled. + + 0:----.------[Z]------.-- 0:---[Z]--- + | | + 1:---(+)-------------(+)- = 1:--------- + """ + + circuit = QuantumCircuit(2) + circuit.cx(0, 1) + circuit.z(0) + circuit.cx(0, 1) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + + expected = QuantumCircuit(2) + expected.z(0) + + self.assertEqual(expected, new_circuit) + + def test_control_bit_of_cnot2(self): + """A simple circuit where the two cnots should be cancelled. + + 0:----.------[T]------.-- 0:---[T]--- + | | + 1:---(+)-------------(+)- = 1:--------- + """ + + circuit = QuantumCircuit(2) + circuit.cx(0, 1) + circuit.t(0) + circuit.cx(0, 1) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + + expected = QuantumCircuit(2) + expected.t(0) + + self.assertEqual(expected, new_circuit) + + def test_control_bit_of_cnot3(self): + """A simple circuit where the two cnots should be cancelled. + + 0:----.------[Rz]------.-- 0:---[Rz]--- + | | + 1:---(+)--------------(+)- = 1:---------- + """ + + circuit = QuantumCircuit(2) + circuit.cx(0, 1) + circuit.rz(np.pi / 3, 0) + circuit.cx(0, 1) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + + expected = QuantumCircuit(2) + expected.rz(np.pi / 3, 0) + + self.assertEqual(expected, new_circuit) + + def test_control_bit_of_cnot4(self): + """A simple circuit where the two cnots should be cancelled. + + 0:----.------[T]------.-- 0:---[T]--- + | | + 1:---(+)-------------(+)- = 1:--------- + """ + + circuit = QuantumCircuit(2) + circuit.cx(0, 1) + circuit.t(0) + circuit.cx(0, 1) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + + expected = QuantumCircuit(2) + expected.t(0) + + self.assertEqual(expected, new_circuit) + + def test_target_bit_of_cnot(self): + """A simple circuit where nothing should be cancelled. + + 0:----.---------------.-- 0:----.---------------.-- + | | | | + 1:---(+)-----[Z]-----(+)- = 1:---(+)----[Z]------(+)- + """ + + circuit = QuantumCircuit(2) + circuit.cx(0, 1) + circuit.z(1) + circuit.cx(0, 1) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + + expected = QuantumCircuit(2) + expected.cx(0, 1) + expected.z(1) + expected.cx(0, 1) + + self.assertEqual(expected, new_circuit) + + def test_target_bit_of_cnot1(self): + """A simple circuit where nothing should be cancelled. + + 0:----.---------------.-- 0:----.---------------.-- + | | | | + 1:---(+)-----[T]-----(+)- = 1:---(+)----[T]------(+)- + """ + + circuit = QuantumCircuit(2) + circuit.cx(0, 1) + circuit.t(1) + circuit.cx(0, 1) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + + expected = QuantumCircuit(2) + expected.cx(0, 1) + expected.t(1) + expected.cx(0, 1) + + self.assertEqual(expected, new_circuit) + + def test_target_bit_of_cnot2(self): + """A simple circuit where nothing should be cancelled. + + 0:----.---------------.-- 0:----.---------------.-- + | | | | + 1:---(+)-----[Rz]----(+)- = 1:---(+)----[Rz]-----(+)- + """ + + circuit = QuantumCircuit(2) + circuit.cx(0, 1) + circuit.rz(np.pi / 3, 1) + circuit.cx(0, 1) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + + expected = QuantumCircuit(2) + expected.cx(0, 1) + expected.rz(np.pi / 3, 1) + expected.cx(0, 1) + + self.assertEqual(expected, new_circuit) + + def test_commutative_circuit2(self): + """ + A simple circuit where three CNOTs commute, the first and the last cancel, + also two X gates cancel. + """ + + circuit = QuantumCircuit(3) + circuit.cx(0, 1) + circuit.rz(np.pi / 3, 2) + circuit.cx(2, 1) + circuit.rz(np.pi / 3, 2) + circuit.t(2) + circuit.s(2) + circuit.x(1) + circuit.cx(0, 1) + circuit.x(1) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + + expected = QuantumCircuit(3) + expected.rz(np.pi / 3, 2) + expected.cx(2, 1) + expected.rz(np.pi / 3, 2) + expected.t(2) + expected.s(2) + + self.assertEqual(expected, new_circuit) + + def test_commutative_circuit3(self): + """ + A simple circuit where three CNOTs commute, the first and the last cancel, + also two X gates cancel and two RX gates cancel. + """ + + circuit = QuantumCircuit(4) + + circuit.cx(0, 1) + circuit.rz(np.pi / 3, 2) + circuit.rz(np.pi / 3, 3) + circuit.x(3) + circuit.cx(2, 3) + circuit.cx(2, 1) + circuit.cx(2, 3) + circuit.rz(-np.pi / 3, 2) + circuit.x(3) + circuit.rz(-np.pi / 3, 3) + circuit.x(1) + circuit.cx(0, 1) + circuit.x(1) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + + expected = QuantumCircuit(4) + expected.cx(2, 1) + + self.assertEqual(expected, new_circuit) + + def test_cnot_cascade(self): + """ + A cascade of CNOTs that equals identity. + """ + + circuit = QuantumCircuit(10) + circuit.cx(0, 1) + circuit.cx(1, 2) + circuit.cx(2, 3) + circuit.cx(3, 4) + circuit.cx(4, 5) + circuit.cx(5, 6) + circuit.cx(6, 7) + circuit.cx(7, 8) + circuit.cx(8, 9) + + circuit.cx(8, 9) + circuit.cx(7, 8) + circuit.cx(6, 7) + circuit.cx(5, 6) + circuit.cx(4, 5) + circuit.cx(3, 4) + circuit.cx(2, 3) + circuit.cx(1, 2) + circuit.cx(0, 1) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + + expected = QuantumCircuit(10) + + self.assertEqual(expected, new_circuit) + + def test_conditional_gates_dont_commute(self): + """Conditional gates do not commute and do not cancel""" + + # ┌───┐┌─┐ + # q_0: ┤ H ├┤M├───────────── + # └───┘└╥┘ ┌─┐ + # q_1: ──■───╫────■───┤M├─── + # ┌─┴─┐ ║ ┌─┴─┐ └╥┘┌─┐ + # q_2: ┤ X ├─╫──┤ X ├──╫─┤M├ + # └───┘ ║ └─╥─┘ ║ └╥┘ + # ║ ┌──╨──┐ ║ ║ + # c: 2/══════╩═╡ 0x0 ╞═╩══╩═ + # 0 └─────┘ 0 1 + circuit = QuantumCircuit(3, 2) + circuit.h(0) + circuit.measure(0, 0) + circuit.cx(1, 2) + circuit.cx(1, 2).c_if(circuit.cregs[0], 0) + circuit.measure([1, 2], [0, 1]) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + + self.assertEqual(circuit, new_circuit) + + # The second suite of tests is adapted from InverseCancellation, + # modifying tests where more nonconsecutive gates cancel. + + def test_basic_self_inverse(self): + """Test that a single self-inverse gate as input can be cancelled.""" + circuit = QuantumCircuit(2, 2) + circuit.h(0) + circuit.h(0) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + gates_after = new_circuit.count_ops() + + self.assertNotIn("h", gates_after) + + def test_odd_number_self_inverse(self): + """Test that an odd number of self-inverse gates leaves one gate remaining.""" + circuit = QuantumCircuit(2, 2) + circuit.h(0) + circuit.h(0) + circuit.h(0) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + gates_after = new_circuit.count_ops() + + self.assertIn("h", gates_after) + self.assertEqual(gates_after["h"], 1) + + def test_basic_cx_self_inverse(self): + """Test that a single self-inverse cx gate as input can be cancelled.""" + circuit = QuantumCircuit(2, 2) + circuit.cx(0, 1) + circuit.cx(0, 1) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + gates_after = new_circuit.count_ops() + + self.assertNotIn("cx", gates_after) + + def test_basic_gate_inverse(self): + """Test that a basic pair of gate inverse can be cancelled.""" + circuit = QuantumCircuit(2, 2) + circuit.rx(np.pi / 4, 0) + circuit.rx(-np.pi / 4, 0) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + gates_after = new_circuit.count_ops() + + self.assertNotIn("rx", gates_after) + + def test_non_inverse_do_not_cancel(self): + """Test that non-inverse gate pairs do not cancel.""" + circuit = QuantumCircuit(2, 2) + circuit.rx(np.pi / 4, 0) + circuit.rx(np.pi / 4, 0) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + gates_after = new_circuit.count_ops() + + self.assertIn("rx", gates_after) + self.assertEqual(gates_after["rx"], 2) + + def test_non_consecutive_gates(self): + """Test that non-consecutive gates cancel as well.""" + circuit = QuantumCircuit(2, 2) + circuit.h(0) + circuit.h(0) + circuit.h(0) + circuit.cx(0, 1) + circuit.cx(0, 1) + circuit.h(0) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + gates_after = new_circuit.count_ops() + + self.assertNotIn("cx", gates_after) + self.assertNotIn("h", gates_after) + + def test_gate_inverse_phase_gate(self): + """Test that an inverse pair of a PhaseGate can be cancelled.""" + circuit = QuantumCircuit(2, 2) + circuit.p(np.pi / 4, 0) + circuit.p(-np.pi / 4, 0) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + gates_after = new_circuit.count_ops() + + self.assertNotIn("p", gates_after) + + def test_self_inverse_on_different_qubits(self): + """Test that self_inverse gates cancel on the correct qubits.""" + circuit = QuantumCircuit(2, 2) + circuit.h(0) + circuit.h(1) + circuit.h(0) + circuit.h(1) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + gates_after = new_circuit.count_ops() + + self.assertNotIn("h", gates_after) + + def test_consecutive_self_inverse_h_x_gate(self): + """Test that consecutive self-inverse gates cancel.""" + circuit = QuantumCircuit(2, 2) + circuit.h(0) + circuit.h(0) + circuit.h(0) + circuit.x(0) + circuit.x(0) + circuit.h(0) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + gates_after = new_circuit.count_ops() + + self.assertNotIn("x", gates_after) + self.assertNotIn("h", gates_after) + + def test_inverse_with_different_names(self): + """Test that inverse gates that have different names.""" + circuit = QuantumCircuit(2, 2) + circuit.t(0) + circuit.tdg(0) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + gates_after = new_circuit.count_ops() + + self.assertNotIn("t", gates_after) + self.assertNotIn("tdg", gates_after) + + def test_three_alternating_inverse_gates(self): + """Test that inverse cancellation works correctly for alternating sequences + of inverse gates of odd-length.""" + circuit = QuantumCircuit(2, 2) + circuit.p(np.pi / 4, 0) + circuit.p(-np.pi / 4, 0) + circuit.p(np.pi / 4, 0) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + gates_after = new_circuit.count_ops() + + self.assertIn("p", gates_after) + self.assertEqual(gates_after["p"], 1) + + def test_four_alternating_inverse_gates(self): + """Test that inverse cancellation works correctly for alternating sequences + of inverse gates of even-length.""" + circuit = QuantumCircuit(2, 2) + circuit.p(np.pi / 4, 0) + circuit.p(-np.pi / 4, 0) + circuit.p(np.pi / 4, 0) + circuit.p(-np.pi / 4, 0) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + gates_after = new_circuit.count_ops() + + self.assertNotIn("p", gates_after) + + def test_five_alternating_inverse_gates(self): + """Test that inverse cancellation works correctly for alternating sequences + of inverse gates of odd-length.""" + circuit = QuantumCircuit(2, 2) + circuit.p(np.pi / 4, 0) + circuit.p(-np.pi / 4, 0) + circuit.p(np.pi / 4, 0) + circuit.p(-np.pi / 4, 0) + circuit.p(np.pi / 4, 0) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + gates_after = new_circuit.count_ops() + + self.assertIn("p", gates_after) + self.assertEqual(gates_after["p"], 1) + + def test_sequence_of_inverse_gates_1(self): + """Test that inverse cancellation works correctly for more general sequences + of inverse gates. In this test two pairs of inverse gates are supposed to + cancel out.""" + circuit = QuantumCircuit(2, 2) + circuit.p(np.pi / 4, 0) + circuit.p(-np.pi / 4, 0) + circuit.p(-np.pi / 4, 0) + circuit.p(np.pi / 4, 0) + circuit.p(np.pi / 4, 0) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + gates_after = new_circuit.count_ops() + + self.assertIn("p", gates_after) + self.assertEqual(gates_after["p"], 1) + + def test_sequence_of_inverse_gates_2(self): + """Test that inverse cancellation works correctly for more general sequences + of inverse gates. In this test, in theory three pairs of inverse gates can + cancel out, but in practice only two pairs are back-to-back.""" + circuit = QuantumCircuit(2, 2) + circuit.p(np.pi / 4, 0) + circuit.p(np.pi / 4, 0) + circuit.p(-np.pi / 4, 0) + circuit.p(-np.pi / 4, 0) + circuit.p(-np.pi / 4, 0) + circuit.p(np.pi / 4, 0) + circuit.p(np.pi / 4, 0) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + gates_after = new_circuit.count_ops() + + self.assertIn("p", gates_after) + self.assertEqual(gates_after["p"] % 2, 1) + + def test_cx_do_not_wrongly_cancel(self): + """Test that CX(0,1) and CX(1, 0) do not cancel out, when (CX, CX) is passed + as an inverse pair.""" + circuit = QuantumCircuit(2, 0) + circuit.cx(0, 1) + circuit.cx(1, 0) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + gates_after = new_circuit.count_ops() + + self.assertIn("cx", gates_after) + self.assertEqual(gates_after["cx"], 2) + + # A few more tests from issue 8020 + + def test_cancel_both_x_and_z(self): + """Test that Z commutes with control qubit of CX, and X commutes with the target qubit.""" + circuit = QuantumCircuit(2) + circuit.z(0) + circuit.x(1) + circuit.cx(0, 1) + circuit.z(0) + circuit.x(1) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + + expected = QuantumCircuit(2) + expected.cx(0, 1) + + self.assertEqual(expected, new_circuit) + + def test_gates_do_not_wrongly_cancel(self): + """Test that X gates do not cancel for X-I-H-I-X.""" + circuit = QuantumCircuit(1) + circuit.x(0) + circuit.i(0) + circuit.h(0) + circuit.i(0) + circuit.x(0) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + + expected = QuantumCircuit(1) + expected.x(0) + expected.h(0) + expected.x(0) + + self.assertEqual(expected, new_circuit) + + # More tests to cover corner-cases: parameterized gates, directives, reset, etc. + + def test_no_cancellation_across_barrier(self): + """Test that barrier prevents cancellation.""" + circuit = QuantumCircuit(2) + circuit.cx(0, 1) + circuit.barrier() + circuit.cx(0, 1) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + + self.assertEqual(circuit, new_circuit) + + def test_no_cancellation_across_measure(self): + """Test that barrier prevents cancellation.""" + circuit = QuantumCircuit(2, 1) + circuit.cx(0, 1) + circuit.measure(0, 0) + circuit.cx(0, 1) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + + self.assertEqual(circuit, new_circuit) + + def test_no_cancellation_across_reset(self): + """Test that reset prevents cancellation.""" + circuit = QuantumCircuit(2) + circuit.cx(0, 1) + circuit.reset(0) + circuit.cx(0, 1) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + + self.assertEqual(circuit, new_circuit) + + def test_no_cancellation_across_parameterized_gates(self): + """Test that parameterized gates prevent cancellation. + This test should be modified when inverse and commutativity checking + get improved to handle parameterized gates. + """ + circuit = QuantumCircuit(1) + circuit.rz(np.pi / 2, 0) + circuit.rz(Parameter("Theta"), 0) + circuit.rz(-np.pi / 2, 0) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + self.assertEqual(circuit, new_circuit) + + def test_parameterized_gates_do_not_cancel(self): + """Test that parameterized gates do not cancel. + This test should be modified when inverse and commutativity checking + get improved to handle parameterized gates. + """ + gate = RZGate(Parameter("Theta")) + + circuit = QuantumCircuit(1) + circuit.append(gate, [0]) + circuit.append(gate.inverse(), [0]) + + passmanager = PassManager(CommutativeInverseCancellation()) + new_circuit = passmanager.run(circuit) + self.assertEqual(circuit, new_circuit) + + +if __name__ == "__main__": + unittest.main() From dadacb8050a6fdad656e234fbc1bc8cbca895b4e Mon Sep 17 00:00:00 2001 From: Ikko Hamamura Date: Thu, 11 Aug 2022 00:09:00 +0900 Subject: [PATCH 36/82] Improve the performance of taper in opflow (#8410) * reduce * add releasenote Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/opflow/primitive_ops/tapered_pauli_sum_op.py | 1 + releasenotes/notes/taper-performance-6da355c04da5b648.yaml | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 releasenotes/notes/taper-performance-6da355c04da5b648.yaml diff --git a/qiskit/opflow/primitive_ops/tapered_pauli_sum_op.py b/qiskit/opflow/primitive_ops/tapered_pauli_sum_op.py index 4bc0d70d134d..0b4abf1713f1 100644 --- a/qiskit/opflow/primitive_ops/tapered_pauli_sum_op.py +++ b/qiskit/opflow/primitive_ops/tapered_pauli_sum_op.py @@ -380,6 +380,7 @@ def taper(self, operator: PauliSumOp) -> OperatorBase: if not operator.is_zero(): for clifford in self.cliffords: operator = cast(PauliSumOp, clifford @ operator @ clifford) + operator = operator.reduce(atol=0) if self._tapering_values is None: tapered_ops_list = [ diff --git a/releasenotes/notes/taper-performance-6da355c04da5b648.yaml b/releasenotes/notes/taper-performance-6da355c04da5b648.yaml new file mode 100644 index 000000000000..c59721149da3 --- /dev/null +++ b/releasenotes/notes/taper-performance-6da355c04da5b648.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + The :meth:`~qiskit.opflow.Z2Symmetries.taper` has significantly improved + performance because intermediate data is simplified. From 19fdd32affd37afb58774f40bcf509b16480fe63 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Wed, 10 Aug 2022 22:41:43 +0300 Subject: [PATCH 37/82] Adding Cliffords to QuantumCircuits as Operations (#7966) * Adding Clifford to a QuantumCircuit natively; adding some tests; adding a preliminary Clifford optimization pass * Making Instruction.condition into a property * improving tests * adding release notes * A few changes based on review * Removing definition from Operation interface. Instead adding HighLevelSynthesis transpiler pass, and incorporating it into the preset pass managers * minor lint fix * moving all of broadcast functionality to a separate file; removing broadcast from Operation interface and in particular from Clifford * moving HighLevelSynthesis pass from Decompose to QuantumCircuit.decompose; slightly changing Decompose pass to skip nodes with definition attribute * Fixing broadcasting for Cliffords * making sure that transpile decomposes cliffords * lint * As per code review, replacing x._direction by getattr(x, '_direction', False) * Removing condition from Operation API. Removing previously added getter and setter to instruction.py. As per code review, starting to replace condition getters and setters. For now, only dagcircuit and circuit_to_instruction. * pass over condition in transpiler code * more refactoring of condition * finishing condition pass * minor fixes * adding OptimizeClifford pass to __init__ * Improving release notes * considering DAG nodes in topological order; adding a simple test to see this * typo * release notes fixes * Adding TODO comment to HighLevelSynthesis pass * another attempt to fix docs build * Fix based on code review * Only construction Operator from Instruction (as before) and from Clifford (for backward compatibility) --- qiskit/assembler/assemble_circuits.py | 4 +- qiskit/circuit/__init__.py | 2 + qiskit/circuit/controlflow/builder.py | 2 +- qiskit/circuit/instruction.py | 3 +- qiskit/circuit/instructionset.py | 6 +- qiskit/circuit/quantumcircuit.py | 80 +++-- qiskit/circuit/quantumcircuitdata.py | 7 +- qiskit/converters/circuit_to_instruction.py | 2 +- qiskit/dagcircuit/dagcircuit.py | 63 ++-- qiskit/dagcircuit/dagdependency.py | 4 +- qiskit/dagcircuit/dagdepnode.py | 6 +- qiskit/dagcircuit/dagnode.py | 2 +- qiskit/qpy/binary_io/circuits.py | 2 +- qiskit/quantum_info/operators/operator.py | 27 +- .../operators/symplectic/clifford.py | 3 +- qiskit/transpiler/passes/__init__.py | 4 + .../passes/basis/basis_translator.py | 2 +- qiskit/transpiler/passes/basis/decompose.py | 2 +- .../passes/basis/unroll_custom_definitions.py | 2 +- qiskit/transpiler/passes/basis/unroller.py | 2 +- .../passes/optimization/__init__.py | 1 + .../optimization/collect_linear_functions.py | 2 +- .../optimization/collect_multiqubit_blocks.py | 4 +- .../optimization/commutative_cancellation.py | 2 +- .../passes/optimization/consolidate_blocks.py | 4 +- .../passes/optimization/optimize_1q_gates.py | 2 +- .../passes/optimization/optimize_cliffords.py | 87 ++++++ .../optimize_swap_before_measure.py | 2 +- .../template_matching/backward_match.py | 9 +- .../template_matching/forward_match.py | 9 +- .../transpiler/passes/synthesis/__init__.py | 1 + .../passes/synthesis/high_level_synthesis.py | 45 +++ .../transpiler/preset_passmanagers/common.py | 5 + qiskit/visualization/dag_visualization.py | 4 +- qiskit/visualization/latex.py | 16 +- qiskit/visualization/matplotlib.py | 14 +- qiskit/visualization/text.py | 8 +- qiskit/visualization/utils.py | 4 +- ...-abstract-base-class-c5efe020aa9caf46.yaml | 100 ++++++ test/python/circuit/test_instructions.py | 2 +- test/python/circuit/test_operation.py | 11 +- test/python/dagcircuit/test_dagcircuit.py | 4 +- .../python/transpiler/test_clifford_passes.py | 289 ++++++++++++++++++ 43 files changed, 738 insertions(+), 112 deletions(-) create mode 100644 qiskit/transpiler/passes/optimization/optimize_cliffords.py create mode 100644 qiskit/transpiler/passes/synthesis/high_level_synthesis.py create mode 100644 releasenotes/notes/operation-abstract-base-class-c5efe020aa9caf46.yaml create mode 100644 test/python/transpiler/test_clifford_passes.py diff --git a/qiskit/assembler/assemble_circuits.py b/qiskit/assembler/assemble_circuits.py index bf2261453147..d1c90d5cc026 100644 --- a/qiskit/assembler/assemble_circuits.py +++ b/qiskit/assembler/assemble_circuits.py @@ -111,7 +111,9 @@ def _assemble_circuit( # their clbit_index, create a new register slot for every conditional gate # and add a bfunc to map the creg=val mask onto the gating register bit. - is_conditional_experiment = any(instruction.operation.condition for instruction in circuit.data) + is_conditional_experiment = any( + getattr(instruction.operation, "condition", None) for instruction in circuit.data + ) max_conditional_idx = 0 instructions = [] diff --git a/qiskit/circuit/__init__.py b/qiskit/circuit/__init__.py index becb62be09ef..bea6c844ebf7 100644 --- a/qiskit/circuit/__init__.py +++ b/qiskit/circuit/__init__.py @@ -190,6 +190,7 @@ Delay Instruction InstructionSet + Operation EquivalenceLibrary Control Flow Operations @@ -232,6 +233,7 @@ from .controlledgate import ControlledGate from .instruction import Instruction from .instructionset import InstructionSet +from .operation import Operation from .barrier import Barrier from .delay import Delay from .measure import Measure diff --git a/qiskit/circuit/controlflow/builder.py b/qiskit/circuit/controlflow/builder.py index 9c58df51d459..ee1ee58e2f6e 100644 --- a/qiskit/circuit/controlflow/builder.py +++ b/qiskit/circuit/controlflow/builder.py @@ -426,7 +426,7 @@ def build( # a register is already present, so we use our own tracking. self.add_register(register) out.add_register(register) - if instruction.operation.condition is not None: + if getattr(instruction.operation, "condition", None) is not None: for register in condition_registers(instruction.operation.condition): if register not in self.registers: self.add_register(register) diff --git a/qiskit/circuit/instruction.py b/qiskit/circuit/instruction.py index 04926d665ab1..1f9d09419a63 100644 --- a/qiskit/circuit/instruction.py +++ b/qiskit/circuit/instruction.py @@ -42,12 +42,13 @@ from qiskit.circuit.classicalregister import ClassicalRegister, Clbit from qiskit.qobj.qasm_qobj import QasmQobjInstruction from qiskit.circuit.parameter import ParameterExpression +from qiskit.circuit.operation import Operation from .tools import pi_check _CUTOFF_PRECISION = 1e-10 -class Instruction: +class Instruction(Operation): """Generic quantum instruction.""" # Class attribute to treat like barrier for transpiler, unroller, drawer diff --git a/qiskit/circuit/instructionset.py b/qiskit/circuit/instructionset.py index c15161229b9f..5163b4146082 100644 --- a/qiskit/circuit/instructionset.py +++ b/qiskit/circuit/instructionset.py @@ -19,8 +19,8 @@ from typing import Callable, Optional, Tuple, Union from qiskit.circuit.exceptions import CircuitError -from .instruction import Instruction from .classicalregister import Clbit, ClassicalRegister +from .operation import Operation from .quantumcircuitdata import CircuitInstruction @@ -150,8 +150,8 @@ def __getitem__(self, i): def add(self, instruction, qargs=None, cargs=None): """Add an instruction and its context (where it is attached).""" if not isinstance(instruction, CircuitInstruction): - if not isinstance(instruction, Instruction): - raise CircuitError("attempt to add non-Instruction to InstructionSet") + if not isinstance(instruction, Operation): + raise CircuitError("attempt to add non-Operation to InstructionSet") if qargs is None or cargs is None: raise CircuitError("missing qargs or cargs in old-style InstructionSet.add") instruction = CircuitInstruction(instruction, tuple(qargs), tuple(cargs)) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 4e18db09b41e..2254a9b0ce7a 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -52,6 +52,7 @@ from .parametertable import ParameterReferences, ParameterTable, ParameterView from .parametervector import ParameterVector, ParameterVectorElement from .instructionset import InstructionSet +from .operation import Operation from .register import Register from .bit import Bit from .quantumcircuitdata import QuantumCircuitData, CircuitInstruction @@ -947,7 +948,7 @@ def compose( n_cargs = [edge_map[carg] for carg in instr.clbits] n_instr = instr.operation.copy() - if instr.operation.condition is not None: + if getattr(instr.operation, "condition", None) is not None: from qiskit.dagcircuit import DAGCircuit # pylint: disable=cyclic-import n_instr.condition = DAGCircuit._map_condition( @@ -1216,7 +1217,7 @@ def _resolve_classical_resource(self, specifier): def append( self, - instruction: Union[Instruction, CircuitInstruction], + instruction: Union[Operation, CircuitInstruction], qargs: Optional[Sequence[QubitSpecifier]] = None, cargs: Optional[Sequence[ClbitSpecifier]] = None, ) -> InstructionSet: @@ -1252,20 +1253,20 @@ def append( else: operation = instruction # Convert input to instruction - if not isinstance(operation, Instruction) and not hasattr(operation, "to_instruction"): - if issubclass(operation, Instruction): + if not isinstance(operation, Operation) and not hasattr(operation, "to_instruction"): + if issubclass(operation, Operation): raise CircuitError( - "Object is a subclass of Instruction, please add () to " + "Object is a subclass of Operation, please add () to " "pass an instance of this object." ) raise CircuitError( - "Object to append must be an Instruction or have a to_instruction() method." + "Object to append must be an Operation or have a to_instruction() method." ) - if not isinstance(operation, Instruction) and hasattr(operation, "to_instruction"): + if not isinstance(operation, Operation) and hasattr(operation, "to_instruction"): operation = operation.to_instruction() - if not isinstance(operation, Instruction): - raise CircuitError("object is not an Instruction.") + if not isinstance(operation, Operation): + raise CircuitError("object is not an Operation.") # Make copy of parameterized gate instances if hasattr(operation, "params"): @@ -1283,11 +1284,21 @@ def append( appender = self._append requester = self._resolve_classical_resource instructions = InstructionSet(resource_requester=requester) - for qarg, carg in operation.broadcast_arguments(expanded_qargs, expanded_cargs): - self._check_dups(qarg) - instruction = CircuitInstruction(operation, qarg, carg) - appender(instruction) - instructions.add(instruction) + if isinstance(operation, Instruction): + for qarg, carg in operation.broadcast_arguments(expanded_qargs, expanded_cargs): + self._check_dups(qarg) + instruction = CircuitInstruction(operation, qarg, carg) + appender(instruction) + instructions.add(instruction) + else: + # For Operations that are non-Instructions, we use the Instruction's default method + for qarg, carg in Instruction.broadcast_arguments( + operation, expanded_qargs, expanded_cargs + ): + self._check_dups(qarg) + instruction = CircuitInstruction(operation, qarg, carg) + appender(instruction) + instructions.add(instruction) return instructions # Preferred new style. @@ -1301,10 +1312,10 @@ def _append( @typing.overload def _append( self, - operation: Instruction, + operation: Operation, qargs: Sequence[Qubit], cargs: Sequence[Clbit], - ) -> Instruction: + ) -> Operation: ... def _append(self, instruction, qargs=None, cargs=None): @@ -1328,12 +1339,12 @@ def _append(self, instruction, qargs=None, cargs=None): constructs of the control-flow builder interface. Args: - instruction: Instruction instance to append + instruction: Operation instance to append qargs: Qubits to attach the instruction to. cargs: Clbits to attach the instruction to. Returns: - Instruction: a handle to the instruction that was just added + Operation: a handle to the instruction that was just added :meta public: """ @@ -1341,7 +1352,9 @@ def _append(self, instruction, qargs=None, cargs=None): if old_style: instruction = CircuitInstruction(instruction, qargs, cargs) self._data.append(instruction) - self._update_parameter_table(instruction) + if isinstance(instruction.operation, Instruction): + self._update_parameter_table(instruction) + # mark as normal circuit if a new instruction is added self.duration = None self.unit = "dt" @@ -1567,11 +1580,13 @@ def decompose( """ # pylint: disable=cyclic-import from qiskit.transpiler.passes.basis.decompose import Decompose + from qiskit.transpiler.passes.synthesis import HighLevelSynthesis from qiskit.converters.circuit_to_dag import circuit_to_dag from qiskit.converters.dag_to_circuit import dag_to_circuit - pass_ = Decompose(gates_to_decompose) dag = circuit_to_dag(self) + dag = HighLevelSynthesis().run(dag) + pass_ = Decompose(gates_to_decompose) for _ in range(reps): dag = pass_.run(dag) return dag_to_circuit(dag) @@ -1936,7 +1951,10 @@ def draw( ) def size( - self, filter_function: Optional[callable] = lambda x: not x.operation._directive + self, + filter_function: Optional[callable] = lambda x: not getattr( + x.operation, "_directive", False + ), ) -> int: """Returns total number of instructions in circuit. @@ -1951,7 +1969,10 @@ def size( return sum(map(filter_function, self._data)) def depth( - self, filter_function: Optional[callable] = lambda x: not x.operation._directive + self, + filter_function: Optional[callable] = lambda x: not getattr( + x.operation, "_directive", False + ), ) -> int: """Return circuit depth (i.e., length of critical path). @@ -2000,7 +2021,7 @@ def depth( levels.append(op_stack[reg_ints[ind]]) # Assuming here that there is no conditional # snapshots or barriers ever. - if instruction.operation.condition: + if getattr(instruction.operation, "condition", None): # Controls operate over all bits of a classical register # or over a single bit if isinstance(instruction.operation.condition[0], Clbit): @@ -2064,7 +2085,9 @@ def num_nonlocal_gates(self) -> int: """ multi_qubit_gates = 0 for instruction in self._data: - if instruction.operation.num_qubits > 1 and not instruction.operation._directive: + if instruction.operation.num_qubits > 1 and not getattr( + instruction.operation, "_directive", False + ): multi_qubit_gates += 1 return multi_qubit_gates @@ -2105,9 +2128,11 @@ def num_connected_components(self, unitary_only: bool = False) -> int: num_qargs = len(args) else: args = instruction.qubits + instruction.clbits - num_qargs = len(args) + (1 if instruction.operation.condition else 0) + num_qargs = len(args) + ( + 1 if getattr(instruction.operation, "condition", None) else 0 + ) - if num_qargs >= 2 and not instruction.operation._directive: + if num_qargs >= 2 and not getattr(instruction.operation, "_directive", False): graphs_touched = [] num_touched = 0 # Controls necessarily join all the cbits in the @@ -4206,7 +4231,8 @@ def _pop_previous_instruction_in_scope(self) -> CircuitInstruction: if not self._data: raise CircuitError("This circuit contains no instructions.") instruction = self._data.pop() - self._update_parameter_table_on_instruction_removal(instruction) + if isinstance(instruction.operation, Instruction): + self._update_parameter_table_on_instruction_removal(instruction) return instruction def _update_parameter_table_on_instruction_removal(self, instruction: CircuitInstruction): diff --git a/qiskit/circuit/quantumcircuitdata.py b/qiskit/circuit/quantumcircuitdata.py index 41e7b4e26f94..614ecd5e8ddc 100644 --- a/qiskit/circuit/quantumcircuitdata.py +++ b/qiskit/circuit/quantumcircuitdata.py @@ -154,7 +154,12 @@ def _resolve_legacy_value(self, operation, qargs, cargs) -> CircuitInstruction: expanded_qargs = [self._circuit.qbit_argument_conversion(qarg) for qarg in qargs or []] expanded_cargs = [self._circuit.cbit_argument_conversion(carg) for carg in cargs or []] - broadcast_args = list(operation.broadcast_arguments(expanded_qargs, expanded_cargs)) + if isinstance(operation, Instruction): + broadcast_args = list(operation.broadcast_arguments(expanded_qargs, expanded_cargs)) + else: + broadcast_args = list( + Instruction.broadcast_arguments(operation, expanded_qargs, expanded_cargs) + ) if len(broadcast_args) > 1: raise CircuitError( diff --git a/qiskit/converters/circuit_to_instruction.py b/qiskit/converters/circuit_to_instruction.py index a30a3576503b..a655c5141b3d 100644 --- a/qiskit/converters/circuit_to_instruction.py +++ b/qiskit/converters/circuit_to_instruction.py @@ -110,7 +110,7 @@ def circuit_to_instruction(circuit, parameter_map=None, equivalence_library=None # fix condition for rule in definition: - condition = rule.operation.condition + condition = getattr(rule.operation, "condition", None) if condition: reg, val = condition if isinstance(reg, Clbit): diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index 60f6085a9241..3f0fa48be8dc 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -32,6 +32,7 @@ from qiskit.circuit.quantumregister import QuantumRegister, Qubit from qiskit.circuit.classicalregister import ClassicalRegister, Clbit from qiskit.circuit.gate import Gate +from qiskit.circuit.instruction import Instruction from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.dagcircuit.exceptions import DAGCircuitError from qiskit.dagcircuit.dagnode import DAGNode, DAGOpNode, DAGInNode, DAGOutNode @@ -554,10 +555,10 @@ def apply_operation_back(self, op, qargs=(), cargs=()): qargs = tuple(qargs) if qargs is not None else () cargs = tuple(cargs) if cargs is not None else () - all_cbits = self._bits_in_condition(op.condition) + all_cbits = self._bits_in_condition(getattr(op, "condition", None)) all_cbits = set(all_cbits).union(cargs) - self._check_condition(op.name, op.condition) + self._check_condition(op.name, getattr(op, "condition", None)) self._check_bits(qargs, self.output_map) self._check_bits(all_cbits, self.output_map) @@ -586,10 +587,10 @@ def apply_operation_front(self, op, qargs=(), cargs=()): Raises: DAGCircuitError: if initial nodes connected to multiple out edges """ - all_cbits = self._bits_in_condition(op.condition) + all_cbits = self._bits_in_condition(getattr(op, "condition", None)) all_cbits.extend(cargs) - self._check_condition(op.name, op.condition) + self._check_condition(op.name, getattr(op, "condition", None)) self._check_bits(qargs, self.input_map) self._check_bits(all_cbits, self.input_map) node_index = self._add_op_node(op, qargs, cargs) @@ -835,11 +836,15 @@ def compose(self, other, qubits=None, clbits=None, front=False, inplace=True): # ignore output nodes pass elif isinstance(nd, DAGOpNode): - condition = dag._map_condition(edge_map, nd.op.condition, dag.cregs.values()) + condition = dag._map_condition( + edge_map, getattr(nd.op, "condition", None), dag.cregs.values() + ) dag._check_condition(nd.op.name, condition) m_qargs = list(map(lambda x: edge_map.get(x, x), nd.qargs)) m_cargs = list(map(lambda x: edge_map.get(x, x), nd.cargs)) op = nd.op.copy() + if condition and not isinstance(op, Instruction): + raise DAGCircuitError("Cannot add a condition on a generic Operation.") op.condition = condition dag.apply_operation_back(op, m_qargs, m_cargs) else: @@ -954,8 +959,8 @@ def _check_wires_list(self, wires, node): raise DAGCircuitError("duplicate wires") wire_tot = len(node.qargs) + len(node.cargs) - if node.op.condition is not None: - wire_tot += node.op.condition[0].size + if getattr(node.op, "condition", None) is not None: + wire_tot += getattr(node.op, "condition", None)[0].size if len(wires) != wire_tot: raise DAGCircuitError("expected %d wires, got %d" % (wire_tot, len(wires))) @@ -1083,7 +1088,7 @@ def replace_block_with_op(self, node_block, op, wire_pos_map, cycle_check=True): for nd in node_block: block_qargs |= set(nd.qargs) - if isinstance(nd, DAGOpNode) and nd.op.condition: + if isinstance(nd, DAGOpNode) and getattr(nd.op, "condition", None): block_cargs |= set(nd.cargs) # Create replacement node @@ -1128,13 +1133,17 @@ def substitute_node_with_dag(self, node, input_dag, wires=None): # the dag must be amended if used in a # conditional context. delete the op nodes and replay # them with the condition. - if node.op.condition: + if getattr(node.op, "condition", None): in_dag = copy.deepcopy(input_dag) - in_dag.add_creg(node.op.condition[0]) + in_dag.add_creg(getattr(node.op, "condition", None)[0]) to_replay = [] for sorted_node in in_dag.topological_nodes(): if isinstance(sorted_node, DAGOpNode): - sorted_node.op.condition = node.op.condition + if getattr(node.op, "condition", None) and not isinstance( + sorted_node.op, Instruction + ): + raise DAGCircuitError("Cannot add a condition on a generic Operation.") + sorted_node.op.condition = getattr(node.op, "condition", None) to_replay.append(sorted_node) for input_node in in_dag.op_nodes(): in_dag.remove_op_node(input_node) @@ -1166,7 +1175,7 @@ def substitute_node_with_dag(self, node, input_dag, wires=None): if not isinstance(node, DAGOpNode): raise DAGCircuitError("expected node DAGOpNode, got %s" % type(node)) - condition_bit_list = self._bits_in_condition(node.op.condition) + condition_bit_list = self._bits_in_condition(getattr(node.op, "condition", None)) new_wires = list(node.qargs) + list(node.cargs) + list(condition_bit_list) @@ -1258,11 +1267,15 @@ def edge_weight_map(wire): for old_node_index, new_node_index in node_map.items(): # update node attributes old_node = in_dag._multi_graph[old_node_index] - condition = self._map_condition(wire_map, old_node.op.condition, self.cregs.values()) + condition = self._map_condition( + wire_map, getattr(old_node.op, "condition", None), self.cregs.values() + ) m_qargs = [wire_map.get(x, x) for x in old_node.qargs] m_cargs = [wire_map.get(x, x) for x in old_node.cargs] new_node = DAGOpNode(old_node.op, qargs=m_qargs, cargs=m_cargs) new_node._node_id = new_node_index + if condition and not isinstance(new_node.op, Instruction): + raise DAGCircuitError("Cannot add a condition on a generic Operation.") new_node.op.condition = condition self._multi_graph[new_node_index] = new_node self._increment_op(new_node.op) @@ -1306,14 +1319,18 @@ def substitute_node(self, node, op, inplace=False): if op.name != node.op.name: self._increment_op(op) self._decrement_op(node.op) - save_condition = node.op.condition + save_condition = getattr(node.op, "condition", None) node.op = op + if save_condition and not isinstance(op, Instruction): + raise DAGCircuitError("Cannot add a condition on a generic Operation.") node.op.condition = save_condition return node new_node = copy.copy(node) - save_condition = new_node.op.condition + save_condition = getattr(new_node.op, "condition", None) new_node.op = op + if save_condition and not isinstance(new_node.op, Instruction): + raise DAGCircuitError("Cannot add a condition on a generic Operation.") new_node.op.condition = save_condition self._multi_graph[node._node_id] = new_node if op.name != node.op.name: @@ -1379,7 +1396,7 @@ def op_nodes(self, op=None, include_directives=True): nodes = [] for node in self._multi_graph.nodes(): if isinstance(node, DAGOpNode): - if not include_directives and node.op._directive: + if not include_directives and getattr(node.op, "_directive", False): continue if op is None or isinstance(node.op, op): nodes.append(node) @@ -1584,7 +1601,9 @@ def layers(self): # The quantum registers that have an operation in this layer. support_list = [ - op_node.qargs for op_node in new_layer.op_nodes() if not op_node.op._directive + op_node.qargs + for op_node in new_layer.op_nodes() + if not getattr(op_node.op, "_directive", False) ] yield {"graph": new_layer, "partition": support_list} @@ -1604,13 +1623,13 @@ def serial_layers(self): op = copy.copy(next_node.op) qargs = copy.copy(next_node.qargs) cargs = copy.copy(next_node.cargs) - condition = copy.copy(next_node.op.condition) + condition = copy.copy(getattr(next_node.op, "condition", None)) _ = self._bits_in_condition(condition) # Add node to new_layer new_layer.apply_operation_back(op, qargs, cargs) # Add operation to partition - if not next_node.op._directive: + if not getattr(next_node.op, "_directive", False): support_list.append(list(qargs)) l_dict = {"graph": new_layer, "partition": support_list} yield l_dict @@ -1637,7 +1656,7 @@ def filter_fn(node): return ( isinstance(node, DAGOpNode) and node.op.name in namelist - and node.op.condition is None + and getattr(node.op, "condition", None) is None ) group_list = rx.collect_runs(self._multi_graph, filter_fn) @@ -1651,7 +1670,7 @@ def filter_fn(node): isinstance(node, DAGOpNode) and len(node.qargs) == 1 and len(node.cargs) == 0 - and node.op.condition is None + and getattr(node.op, "condition", None) is None and not node.op.is_parameterized() and isinstance(node.op, Gate) and hasattr(node.op, "__array__") @@ -1671,7 +1690,7 @@ def filter_fn(node): return ( isinstance(node.op, Gate) and len(node.qargs) <= 2 - and not node.op.condition + and not getattr(node.op, "condition", None) and not node.op.is_parameterized() ) else: diff --git a/qiskit/dagcircuit/dagdependency.py b/qiskit/dagcircuit/dagdependency.py index b645292ab1e3..5ed41ca6f800 100644 --- a/qiskit/dagcircuit/dagdependency.py +++ b/qiskit/dagcircuit/dagdependency.py @@ -389,11 +389,11 @@ def add_op_node(self, operation, qargs, cargs): cargs (list[Clbit]): list of classical wires to attach to. """ directives = ["measure"] - if not operation._directive and operation.name not in directives: + if not getattr(operation, "_directive", False) and operation.name not in directives: qindices_list = [] for elem in qargs: qindices_list.append(self.qubits.index(elem)) - if operation.condition: + if getattr(operation, "condition", None): for clbit in self.clbits: if clbit in operation.condition[0]: initial = self.clbits.index(clbit) diff --git a/qiskit/dagcircuit/dagdepnode.py b/qiskit/dagcircuit/dagdepnode.py index 3c370aeaa02b..c4038db24f8c 100644 --- a/qiskit/dagcircuit/dagdepnode.py +++ b/qiskit/dagcircuit/dagdepnode.py @@ -111,7 +111,7 @@ def condition(self): DeprecationWarning, 2, ) - return self._op.condition + return getattr(self._op, "condition", None) @condition.setter def condition(self, new_condition): @@ -162,7 +162,9 @@ def semantic_eq(node1, node2): if node1._qargs == node2._qargs: if node1.cargs == node2.cargs: if node1.type == "op": - if node1._op.condition != node2._op.condition: + if getattr(node1._op, "condition", None) != getattr( + node2._op, "condition", None + ): return False return True return False diff --git a/qiskit/dagcircuit/dagnode.py b/qiskit/dagcircuit/dagnode.py index b0ead7b4cbd8..ed6cfe15da24 100644 --- a/qiskit/dagcircuit/dagnode.py +++ b/qiskit/dagcircuit/dagnode.py @@ -79,7 +79,7 @@ def semantic_eq(node1, node2, bit_indices1=None, bit_indices2=None): if node1_qargs == node2_qargs: if node1_cargs == node2_cargs: - if node1.op.condition == node2.op.condition: + if getattr(node1.op, "condition", None) == getattr(node2.op, "condition", None): if node1.op == node2.op: return True elif (isinstance(node1, DAGInNode) and isinstance(node2, DAGInNode)) or ( diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index f851679179d8..ff30f00e21c4 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -513,7 +513,7 @@ def _write_instruction(file_obj, instruction, custom_operations, index_map): has_condition = False condition_register = b"" condition_value = 0 - if instruction.operation.condition: + if getattr(instruction.operation, "condition", None): has_condition = True if isinstance(instruction.operation.condition[0], Clbit): bit_index = index_map["c"][instruction.operation.condition[0]] diff --git a/qiskit/quantum_info/operators/operator.py b/qiskit/quantum_info/operators/operator.py index 46e717b8b5d4..413731e6fe9f 100644 --- a/qiskit/quantum_info/operators/operator.py +++ b/qiskit/quantum_info/operators/operator.py @@ -22,6 +22,7 @@ from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.instruction import Instruction +from qiskit.circuit.operation import Operation from qiskit.circuit.library.standard_gates import IGate, XGate, YGate, ZGate, HGate, SGate, TGate from qiskit.exceptions import QiskitError from qiskit.quantum_info.operators.predicates import is_unitary_matrix, matrix_equal @@ -53,7 +54,7 @@ def __init__(self, data, input_dims=None, output_dims=None): Args: data (QuantumCircuit or - Instruction or + Operation or BaseOperator or matrix): data to initialize operator. input_dims (tuple): the input subsystem dimensions. @@ -75,8 +76,8 @@ def __init__(self, data, input_dims=None, output_dims=None): if isinstance(data, (list, np.ndarray)): # Default initialization from list or numpy array matrix self._data = np.asarray(data, dtype=complex) - elif isinstance(data, (QuantumCircuit, Instruction)): - # If the input is a Terra QuantumCircuit or Instruction we + elif isinstance(data, (QuantumCircuit, Operation)): + # If the input is a Terra QuantumCircuit or Operation we # perform a simulation to construct the unitary operator. # This will only work if the circuit or instruction can be # defined in terms of unitary gate instructions which have a @@ -498,7 +499,7 @@ def _einsum_matmul(cls, tensor, mat, indices, shift=0, right_mul=False): @classmethod def _init_instruction(cls, instruction): - """Convert a QuantumCircuit or Instruction to an Operator.""" + """Convert a QuantumCircuit or Operation to an Operator.""" # Initialize an identity operator of the correct size of the circuit if hasattr(instruction, "__array__"): return Operator(np.array(instruction, dtype=complex)) @@ -514,8 +515,16 @@ def _init_instruction(cls, instruction): @classmethod def _instruction_to_matrix(cls, obj): """Return Operator for instruction if defined or None otherwise.""" - if not isinstance(obj, Instruction): - raise QiskitError("Input is not an instruction.") + # Note: to_matrix() is not a required method for Operations, so for now + # we do not allow constructing matrices for general Operations. + # However, for backward compatibility we need to support constructing matrices + # for Cliffords, which happen to have a to_matrix() method. + + # pylint: disable=cyclic-import + from qiskit.quantum_info import Clifford + + if not isinstance(obj, (Instruction, Clifford)): + raise QiskitError("Input is neither an Instruction nor Clifford.") mat = None if hasattr(obj, "to_matrix"): # If instruction is a gate first we see if it has a @@ -544,10 +553,10 @@ def _append_instruction(self, obj, qargs=None): # circuit decomposition definition if it exists, otherwise we # cannot compose this gate and raise an error. if obj.definition is None: - raise QiskitError(f"Cannot apply Instruction: {obj.name}") + raise QiskitError(f"Cannot apply Operation: {obj.name}") if not isinstance(obj.definition, QuantumCircuit): raise QiskitError( - 'Instruction "{}" ' + 'Operation "{}" ' "definition is {} but expected QuantumCircuit.".format( obj.name, type(obj.definition) ) @@ -569,7 +578,7 @@ def _append_instruction(self, obj, qargs=None): for instruction in flat_instr: if instruction.clbits: raise QiskitError( - "Cannot apply instruction with classical bits:" + "Cannot apply operation with classical bits:" f" {instruction.operation.name}" ) # Get the integer position of the flat register diff --git a/qiskit/quantum_info/operators/symplectic/clifford.py b/qiskit/quantum_info/operators/symplectic/clifford.py index cbfb5c7337ea..20d0d7063668 100644 --- a/qiskit/quantum_info/operators/symplectic/clifford.py +++ b/qiskit/quantum_info/operators/symplectic/clifford.py @@ -22,12 +22,13 @@ from qiskit.quantum_info.operators.operator import Operator from qiskit.quantum_info.operators.scalar_op import ScalarOp from qiskit.quantum_info.operators.mixins import generate_apidocs, AdjointMixin +from qiskit.circuit.operation import Operation from qiskit.quantum_info.operators.symplectic.base_pauli import _count_y from .stabilizer_table import StabilizerTable from .clifford_circuits import _append_circuit -class Clifford(BaseOperator, AdjointMixin): +class Clifford(BaseOperator, AdjointMixin, Operation): """An N-qubit unitary operator from the Clifford group. **Representation** diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index 4bed9b54803b..733a72c1a006 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -85,6 +85,7 @@ HoareOptimizer TemplateOptimization EchoRZXWeylDecomposition + OptimizeCliffords Calibration ============= @@ -139,6 +140,7 @@ UnitarySynthesis LinearFunctionsSynthesis LinearFunctionsToPermutations + HighLevelSynthesis Post Layout (Post transpile qubit selection) ============================================ @@ -220,6 +222,7 @@ from .optimization import InverseCancellation from .optimization import EchoRZXWeylDecomposition from .optimization import CollectLinearFunctions +from .optimization import OptimizeCliffords # circuit analysis from .analysis import ResourceEstimation @@ -236,6 +239,7 @@ from .synthesis import unitary_synthesis_plugin_names from .synthesis import LinearFunctionsSynthesis from .synthesis import LinearFunctionsToPermutations +from .synthesis import HighLevelSynthesis # calibration from .calibration import PulseGates diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index 93dc08067889..67131298f1b7 100644 --- a/qiskit/transpiler/passes/basis/basis_translator.py +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -277,7 +277,7 @@ def _replace_node(self, dag, node, instr_map): dag_op = bound_target_dag.op_nodes()[0].op # dag_op may be the same instance as other ops in the dag, # so if there is a condition, need to copy - if node.op.condition: + if getattr(node.op, "condition", None): dag_op = dag_op.copy() dag.substitute_node(node, dag_op, inplace=True) diff --git a/qiskit/transpiler/passes/basis/decompose.py b/qiskit/transpiler/passes/basis/decompose.py index fb5a929e0f37..408a95271917 100644 --- a/qiskit/transpiler/passes/basis/decompose.py +++ b/qiskit/transpiler/passes/basis/decompose.py @@ -87,7 +87,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: # Walk through the DAG and expand each non-basis node for node in dag.op_nodes(): if self._should_decompose(node): - if node.op.definition is None: + if getattr(node.op, "definition", None) is None: continue # TODO: allow choosing among multiple decomposition rules rule = node.op.definition.data diff --git a/qiskit/transpiler/passes/basis/unroll_custom_definitions.py b/qiskit/transpiler/passes/basis/unroll_custom_definitions.py index 8f8248e93fd3..185460ab9506 100644 --- a/qiskit/transpiler/passes/basis/unroll_custom_definitions.py +++ b/qiskit/transpiler/passes/basis/unroll_custom_definitions.py @@ -57,7 +57,7 @@ def run(self, dag): for node in dag.op_nodes(): - if node.op._directive: + if getattr(node.op, "_directive", False): continue if dag.has_calibration_for(node): diff --git a/qiskit/transpiler/passes/basis/unroller.py b/qiskit/transpiler/passes/basis/unroller.py index d4bb93134fca..035f87455783 100644 --- a/qiskit/transpiler/passes/basis/unroller.py +++ b/qiskit/transpiler/passes/basis/unroller.py @@ -54,7 +54,7 @@ def run(self, dag): # Walk through the DAG and expand each non-basis node basic_insts = ["measure", "reset", "barrier", "snapshot", "delay"] for node in dag.op_nodes(): - if node.op._directive: + if getattr(node.op, "_directive", False): continue if node.name in basic_insts: diff --git a/qiskit/transpiler/passes/optimization/__init__.py b/qiskit/transpiler/passes/optimization/__init__.py index bc922ba8cb0b..48888639f027 100644 --- a/qiskit/transpiler/passes/optimization/__init__.py +++ b/qiskit/transpiler/passes/optimization/__init__.py @@ -32,3 +32,4 @@ from .collect_1q_runs import Collect1qRuns from .echo_rzx_weyl_decomposition import EchoRZXWeylDecomposition from .collect_linear_functions import CollectLinearFunctions +from .optimize_cliffords import OptimizeCliffords diff --git a/qiskit/transpiler/passes/optimization/collect_linear_functions.py b/qiskit/transpiler/passes/optimization/collect_linear_functions.py index 0a1303a2325e..455115511bbc 100644 --- a/qiskit/transpiler/passes/optimization/collect_linear_functions.py +++ b/qiskit/transpiler/passes/optimization/collect_linear_functions.py @@ -30,7 +30,7 @@ def collect_linear_blocks(dag): pending_non_linear_ops = deque() def is_linear(op): - return op.name in ("cx", "swap") and op.condition is None + return op.name in ("cx", "swap") and getattr(op, "condition", None) is None def process_node(node): for suc in dag.successors(node): diff --git a/qiskit/transpiler/passes/optimization/collect_multiqubit_blocks.py b/qiskit/transpiler/passes/optimization/collect_multiqubit_blocks.py index 527cdbfd087e..e7afc7788510 100644 --- a/qiskit/transpiler/passes/optimization/collect_multiqubit_blocks.py +++ b/qiskit/transpiler/passes/optimization/collect_multiqubit_blocks.py @@ -120,7 +120,7 @@ def collect_key(x): if not isinstance(x, DAGOpNode): return "d" if isinstance(x.op, Gate): - if x.op.is_parameterized() or x.op.condition is not None: + if x.op.is_parameterized() or getattr(x.op, "condition", None) is not None: return "c" return "b" + chr(ord("a") + len(x.qargs)) return "d" @@ -134,7 +134,7 @@ def collect_key(x): # check if the node is a gate and if it is parameterized if ( - nd.op.condition is not None + getattr(nd.op, "condition", None) is not None or nd.op.is_parameterized() or not isinstance(nd.op, Gate) ): diff --git a/qiskit/transpiler/passes/optimization/commutative_cancellation.py b/qiskit/transpiler/passes/optimization/commutative_cancellation.py index c3e956a9bbb8..7db4d37fc769 100644 --- a/qiskit/transpiler/passes/optimization/commutative_cancellation.py +++ b/qiskit/transpiler/passes/optimization/commutative_cancellation.py @@ -136,7 +136,7 @@ def run(self, dag): total_phase = 0.0 for current_node in run: if ( - current_node.op.condition is not None + getattr(current_node.op, "condition", None) is not None or len(current_node.qargs) != 1 or current_node.qargs[0] != run_qarg ): diff --git a/qiskit/transpiler/passes/optimization/consolidate_blocks.py b/qiskit/transpiler/passes/optimization/consolidate_blocks.py index c9f2d540b60e..4168ed17f0e1 100644 --- a/qiskit/transpiler/passes/optimization/consolidate_blocks.py +++ b/qiskit/transpiler/passes/optimization/consolidate_blocks.py @@ -87,8 +87,8 @@ def run(self, dag): block_cargs = set() for nd in block: block_qargs |= set(nd.qargs) - if isinstance(nd, DAGOpNode) and nd.op.condition: - block_cargs |= set(nd.op.condition[0]) + if isinstance(nd, DAGOpNode) and getattr(nd.op, "condition", None): + block_cargs |= set(getattr(nd.op, "condition", None)[0]) all_block_gates.add(nd) q = QuantumRegister(len(block_qargs)) qc = QuantumCircuit(q) diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_gates.py b/qiskit/transpiler/passes/optimization/optimize_1q_gates.py index 8a77ec696b45..9aa9cd6aa2f6 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_gates.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_gates.py @@ -73,7 +73,7 @@ def run(self, dag): for current_node in run: left_name = current_node.name if ( - current_node.op.condition is not None + getattr(current_node.op, "condition", None) is not None or len(current_node.qargs) != 1 or left_name not in ["p", "u1", "u2", "u3", "u", "id"] ): diff --git a/qiskit/transpiler/passes/optimization/optimize_cliffords.py b/qiskit/transpiler/passes/optimization/optimize_cliffords.py new file mode 100644 index 000000000000..c81539b60637 --- /dev/null +++ b/qiskit/transpiler/passes/optimization/optimize_cliffords.py @@ -0,0 +1,87 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2021. +# +# 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. + +"""Combine consecutive Cliffords over the same qubits.""" + +from qiskit.transpiler.basepasses import TransformationPass +from qiskit.quantum_info.operators import Clifford + + +class OptimizeCliffords(TransformationPass): + """Combine consecutive Cliffords over the same qubits. + This serves as an example of extra capabilities enabled by storing + Cliffords natively on the circuit. + """ + + def run(self, dag): + """Run the OptimizeCliffords pass on `dag`. + + Args: + dag (DAGCircuit): the DAG to be optimized. + + Returns: + DAGCircuit: the optimized DAG. + """ + + blocks = [] + prev_node = None + cur_block = [] + + # Iterate over all nodes and collect consecutive Cliffords over the + # same qubits. In this very first proof-of-concept implementation + # we require the same ordering of qubits, but this restriction will + # be shortly removed. An interesting question is whether we may also + # want to compose Cliffords over different sets of qubits, such as + # cliff1 over qubits [1, 2, 3] and cliff2 over [2, 3, 4]. + for node in dag.topological_op_nodes(): + if isinstance(node.op, Clifford): + if prev_node is None: + blocks.append(cur_block) + cur_block = [node] + else: + if prev_node.qargs == node.qargs: + cur_block.append(node) + else: + blocks.append(cur_block) + cur_block = [node] + + prev_node = node + + else: + # not a clifford + if cur_block: + blocks.append(cur_block) + prev_node = None + cur_block = [] + + if cur_block: + blocks.append(cur_block) + + # Replace every discovered block of cliffords by a single clifford + # based on the Cliffords' compose function. + for cur_nodes in blocks: + # Create clifford functions only out of blocks with at least 2 gates + if len(cur_nodes) <= 1: + continue + + wire_pos_map = dict((qb, ix) for ix, qb in enumerate(cur_nodes[0].qargs)) + + # Construct a linear circuit + cliff = cur_nodes[0].op + for i, node in enumerate(cur_nodes): + if i > 0: + cliff = Clifford.compose(node.op, cliff, front=True) + + # Replace the block by the composed clifford + dag.replace_block_with_op(cur_nodes, cliff, wire_pos_map, cycle_check=False) + + return dag diff --git a/qiskit/transpiler/passes/optimization/optimize_swap_before_measure.py b/qiskit/transpiler/passes/optimization/optimize_swap_before_measure.py index a736b9438639..f4bc30d0cdb3 100644 --- a/qiskit/transpiler/passes/optimization/optimize_swap_before_measure.py +++ b/qiskit/transpiler/passes/optimization/optimize_swap_before_measure.py @@ -37,7 +37,7 @@ def run(self, dag): """ swaps = dag.op_nodes(SwapGate) for swap in swaps[::-1]: - if swap.op.condition is not None: + if getattr(swap.op, "condition", None) is not None: continue final_successor = [] for successor in dag.successors(swap): diff --git a/qiskit/transpiler/passes/optimization/template_matching/backward_match.py b/qiskit/transpiler/passes/optimization/template_matching/backward_match.py index df817c701f92..a4b11a33de2d 100644 --- a/qiskit/transpiler/passes/optimization/template_matching/backward_match.py +++ b/qiskit/transpiler/passes/optimization/template_matching/backward_match.py @@ -304,13 +304,16 @@ def _is_same_c_conf(self, node_circuit, node_template, carg_circuit): """ if ( node_circuit.type == "op" - and node_circuit.op.condition + and getattr(node_circuit.op, "condition", None) and node_template.type == "op" - and node_template.op.condition + and getattr(node_template.op, "condition", None) ): if set(carg_circuit) != set(node_template.cindices): return False - if node_circuit.op.condition[1] != node_template.op.conditon[1]: + if ( + getattr(node_circuit.op, "condition", None)[1] + != getattr(node_template.op, "condition", None)[1] + ): return False return True diff --git a/qiskit/transpiler/passes/optimization/template_matching/forward_match.py b/qiskit/transpiler/passes/optimization/template_matching/forward_match.py index 3e996a572004..6c5380539701 100644 --- a/qiskit/transpiler/passes/optimization/template_matching/forward_match.py +++ b/qiskit/transpiler/passes/optimization/template_matching/forward_match.py @@ -313,13 +313,16 @@ def _is_same_c_conf(self, node_circuit, node_template): """ if ( node_circuit.type == "op" - and node_circuit.op.condition + and getattr(node_circuit.op, "condition", None) and node_template.type == "op" - and node_template.op.conditon + and getattr(node_template.op, "condition", None) ): if set(self.carg_indices) != set(node_template.cindices): return False - if node_circuit.op.condition[1] != node_template.op.conditon[1]: + if ( + getattr(node_circuit.op, "condition", None)[1] + != getattr(node_template.op, "condition", None)[1] + ): return False return True diff --git a/qiskit/transpiler/passes/synthesis/__init__.py b/qiskit/transpiler/passes/synthesis/__init__.py index f32a51674d3c..8869f403c61e 100644 --- a/qiskit/transpiler/passes/synthesis/__init__.py +++ b/qiskit/transpiler/passes/synthesis/__init__.py @@ -15,3 +15,4 @@ from .unitary_synthesis import UnitarySynthesis from .plugin import unitary_synthesis_plugin_names from .linear_functions_synthesis import LinearFunctionsSynthesis, LinearFunctionsToPermutations +from .high_level_synthesis import HighLevelSynthesis diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py new file mode 100644 index 000000000000..c1b937842112 --- /dev/null +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -0,0 +1,45 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. + + +"""Synthesize high-level objects.""" + +from qiskit.converters import circuit_to_dag +from qiskit.transpiler.basepasses import TransformationPass +from qiskit.dagcircuit.dagcircuit import DAGCircuit +from qiskit.quantum_info.synthesis.clifford_decompose import decompose_clifford + + +class HighLevelSynthesis(TransformationPass): + """Synthesize high-level objects by choosing the appropriate synthesis method based on + the object's name. + """ + + # TODO: Currently, this class only contains the minimal functionality required to transpile + # Cliffords. In the near future, this class will be expanded to cover other higher-level + # objects (as these become available). Additionally, the plan is to make HighLevelSynthesis + # "pluggable", so that the users would be able to "plug in" their own synthesis methods + # for higher-level objects (which would be called during transpilation). + + def run(self, dag: DAGCircuit) -> DAGCircuit: + """Run the HighLevelSynthesis pass on `dag`. + Args: + dag: input dag. + Returns: + Output dag with high level objects synthesized. + """ + + for node in dag.named_nodes("clifford"): + decomposition = circuit_to_dag(decompose_clifford(node.op)) + dag.substitute_node_with_dag(node, decomposition) + + return dag diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index ce01dce6a26e..d66037e616b9 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -25,6 +25,7 @@ from qiskit.transpiler.passes import Collect1qRuns from qiskit.transpiler.passes import ConsolidateBlocks from qiskit.transpiler.passes import UnitarySynthesis +from qiskit.transpiler.passes import HighLevelSynthesis from qiskit.transpiler.passes import CheckMap from qiskit.transpiler.passes import GateDirection from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements @@ -82,6 +83,7 @@ def generate_unroll_3q( target=target, ) ) + unroll_3q.append(HighLevelSynthesis()) unroll_3q.append(Unroll3qOrMore(target=target, basis_gates=basis_gates)) return unroll_3q @@ -275,6 +277,7 @@ def generate_translation_passmanager( method=unitary_synthesis_method, target=target, ), + HighLevelSynthesis(), UnrollCustomDefinitions(sel, basis_gates), BasisTranslator(sel, basis_gates, target), ] @@ -292,6 +295,7 @@ def generate_translation_passmanager( min_qubits=3, target=target, ), + HighLevelSynthesis(), Unroll3qOrMore(target=target, basis_gates=basis_gates), Collect2qBlocks(), Collect1qRuns(), @@ -305,6 +309,7 @@ def generate_translation_passmanager( method=unitary_synthesis_method, target=target, ), + HighLevelSynthesis(), ] else: raise TranspilerError("Invalid translation method %s." % method) diff --git a/qiskit/visualization/dag_visualization.py b/qiskit/visualization/dag_visualization.py index c016e8b292dc..3c513ba73dd9 100644 --- a/qiskit/visualization/dag_visualization.py +++ b/qiskit/visualization/dag_visualization.py @@ -89,11 +89,11 @@ def node_attr_func(node): n["color"] = "black" n["style"] = "filled" n["fillcolor"] = "green" - if node.op._directive: + if getattr(node.op, "_directive", False): n["color"] = "black" n["style"] = "filled" n["fillcolor"] = "red" - if node.op.condition: + if getattr(node.op, "condition", None): n["label"] = str(node.node_id) + ": " + str(node.name) + " (conditional)" n["color"] = "black" n["style"] = "filled" diff --git a/qiskit/visualization/latex.py b/qiskit/visualization/latex.py index 1c2471081983..968111352051 100644 --- a/qiskit/visualization/latex.py +++ b/qiskit/visualization/latex.py @@ -286,7 +286,10 @@ def _get_image_depth(self): if self._cregbundle and ( self._nodes and self._nodes[0] - and (self._nodes[0][0].op.name == "measure" or self._nodes[0][0].op.condition) + and ( + self._nodes[0][0].op.name == "measure" + or getattr(self._nodes[0][0].op, "condition", None) + ) ): columns += 1 @@ -410,7 +413,10 @@ def _build_latex_array(self): if self._cregbundle and ( self._nodes and self._nodes[0] - and (self._nodes[0][0].op.name == "measure" or self._nodes[0][0].op.condition) + and ( + self._nodes[0][0].op.name == "measure" + or getattr(self._nodes[0][0].op, "condition", None) + ) ): column += 1 @@ -421,13 +427,13 @@ def _build_latex_array(self): op = node.op num_cols_op = 1 wire_list = [self._wire_map[qarg] for qarg in node.qargs if qarg in self._qubits] - if op.condition: + if getattr(op, "condition", None): self._add_condition(op, wire_list, column) if isinstance(op, Measure): self._build_measure(node, column) - elif op._directive: # barrier, snapshot, etc. + elif getattr(op, "_directive", False): # barrier, snapshot, etc. self._build_barrier(node, column) else: @@ -563,7 +569,7 @@ def _build_measure(self, node, col): self._latex[wire1][col] = "\\meter" idx_str = "" - cond_offset = 1.5 if node.op.condition else 0.0 + cond_offset = 1.5 if getattr(node.op, "condition", None) else 0.0 if self._cregbundle: register = get_bit_register(self._circuit, node.cargs[0]) if register is not None: diff --git a/qiskit/visualization/matplotlib.py b/qiskit/visualization/matplotlib.py index fd648c780f63..463a1e5b629c 100644 --- a/qiskit/visualization/matplotlib.py +++ b/qiskit/visualization/matplotlib.py @@ -414,7 +414,7 @@ def _get_layer_widths(self): self._data[node] = {} self._data[node]["width"] = WID num_ctrl_qubits = 0 if not hasattr(op, "num_ctrl_qubits") else op.num_ctrl_qubits - if op._directive or isinstance(op, Measure): + if getattr(op, "_directive", False) or isinstance(op, Measure): self._data[node]["raw_gate_text"] = op.name continue @@ -604,7 +604,9 @@ def _get_coords(self, n_lines): barrier_offset = 0 if not self._plot_barriers: # only adjust if everything in the layer wasn't plotted - barrier_offset = -1 if all(nd.op._directive for nd in layer) else 0 + barrier_offset = ( + -1 if all(getattr(nd.op, "_directive", False) for nd in layer) else 0 + ) prev_x_index = anc_x_index + layer_width + barrier_offset - 1 return prev_x_index + 1 @@ -793,7 +795,7 @@ def _draw_ops(self, verbose=False): print(op) # add conditional - if op.condition: + if getattr(op, "condition", None): cond_xy = [ self._c_anchors[ii].plot_coord(anc_x_index, layer_width, self._x_offset) for ii in self._clbits_dict @@ -809,7 +811,7 @@ def _draw_ops(self, verbose=False): self._measure(node) # draw barriers, snapshots, etc. - elif op._directive: + elif getattr(op, "_directive", False): if self._plot_barriers: self._barrier(node) @@ -829,7 +831,9 @@ def _draw_ops(self, verbose=False): barrier_offset = 0 if not self._plot_barriers: # only adjust if everything in the layer wasn't plotted - barrier_offset = -1 if all(nd.op._directive for nd in layer) else 0 + barrier_offset = ( + -1 if all(getattr(nd.op, "_directive", False) for nd in layer) else 0 + ) prev_x_index = anc_x_index + layer_width + barrier_offset - 1 diff --git a/qiskit/visualization/text.py b/qiskit/visualization/text.py index ac8814673ad3..51ca5c9d568f 100644 --- a/qiskit/visualization/text.py +++ b/qiskit/visualization/text.py @@ -1035,7 +1035,7 @@ def _set_ctrl_state(self, node, conditional, ctrl_text, bottom): for i in range(len(ctrl_qubits)): # For sidetext gate alignment, need to set every Bullet with # conditional on if there's a condition. - if op.condition is not None: + if getattr(op, "condition", None) is not None: conditional = True if cstate[i] == "1": gates.append(Bullet(conditional=conditional, label=ctrl_text, bottom=bottom)) @@ -1054,12 +1054,12 @@ def _node_to_gate(self, node, layer): base_gate = getattr(op, "base_gate", None) params = get_param_str(op, "text", ndigits=5) - if not isinstance(op, (Measure, SwapGate, Reset)) and not op._directive: + if not isinstance(op, (Measure, SwapGate, Reset)) and not getattr(op, "_directive", False): gate_text, ctrl_text, _ = get_gate_ctrl_text(op, "text") gate_text = TextDrawing.special_label(op) or gate_text gate_text = gate_text + params - if op.condition is not None: + if getattr(op, "condition", None) is not None: # conditional current_cons_cond += layer.set_cl_multibox(op.condition, top_connect="╨") conditional = True @@ -1084,7 +1084,7 @@ def add_connected_gate(node, gates, layer, current_cons): else: layer.set_clbit(node.cargs[0], MeasureTo()) - elif op._directive: + elif getattr(op, "_directive", False): # barrier if not self.plotbarriers: return layer, current_cons, current_cons_cond, connection_label diff --git a/qiskit/visualization/utils.py b/qiskit/visualization/utils.py index 1ebabb6c3fbc..0ae413981a20 100644 --- a/qiskit/visualization/utils.py +++ b/qiskit/visualization/utils.py @@ -488,7 +488,7 @@ def _get_gate_span(qubits, node): if index > max_index: max_index = index - if node.cargs or node.op.condition: + if node.cargs or getattr(node.op, "condition", None): return qubits[min_index : len(qubits)] return qubits[min_index : max_index + 1] @@ -563,7 +563,7 @@ def slide_from_left(self, node, index): curr_index = index last_insertable_index = -1 index_stop = -1 - if node.op.condition: + if getattr(node.op, "condition", None): if isinstance(node.op.condition[0], Clbit): cond_bit = [clbit for clbit in self.clbits if node.op.condition[0] == clbit] index_stop = self.measure_map[cond_bit[0]] diff --git a/releasenotes/notes/operation-abstract-base-class-c5efe020aa9caf46.yaml b/releasenotes/notes/operation-abstract-base-class-c5efe020aa9caf46.yaml new file mode 100644 index 000000000000..ef5e619684d7 --- /dev/null +++ b/releasenotes/notes/operation-abstract-base-class-c5efe020aa9caf46.yaml @@ -0,0 +1,100 @@ +--- + +features: + - | + Currently, only subclasses of :class:`qiskit.circuit.Instruction` can be put on + :class:`.QuantumCircuit`, but this interface has become unwieldy and includes too many methods + and attributes for general-purpose objects. + + A new :class:`.Operation` base class provides a lightweight abstract interface + for objects that can be put on :class:`.QuantumCircuit`. This allows to store "higher-level" + objects directly on a circuit (for instance, :class:`.Clifford` objects), to directly combine such objects + (for instance, to compose several consecutive :class:`.Clifford` objects over the same qubits), and + to synthesize such objects at run time (for instance, to synthesize :class:`.Clifford` in + a way that optimizes depth and/or exploits device connectivity). + + The new :class:`.Operation` interface includes ``name``, ``num_qubits`` and ``num_clbits`` + (in the future this may be slightly adjusted), but importantly does not include ``definition`` + or ``_define`` (and thus does not tie synthesis to the object), does not include ``condition`` + (this should be part of separate classical control flow), and does not include ``duration`` and + ``unit`` (as these are properties of the output of the transpiler). + + As of now, :class:`.Operation` includes :class:`.Gate`, :class:`.Reset`, :class:`.Barrier`, + :class:`.Measure`, and "higher-level" objects such as :class:`.Clifford`. This list of + "higher-level" objects will grow in the future. + + - | + A :class:`.Clifford` is now added to a quantum circuit as an :class:`.Operation`, without first + synthesizing a subcircuit implementing this Clifford. The actual synthesis is postponed + to a later :class:`.HighLevelSynthesis` transpilation pass. + + For example, the following code:: + + from qiskit import QuantumCircuit + from qiskit.quantum_info import random_clifford + + qc = QuantumCircuit(3) + cliff = random_clifford(2) + qc.append(cliff, [0, 1]) + + no longer converts ``cliff`` to :class:`qiskit.circuit.Instruction` when it is appended to ``qc``. + + - | + Added a new transpiler pass :class:`.OptimizeCliffords` that collects blocks of consecutive + :class:`.Clifford` objects in a circuit, and replaces each block with a single :class:`.Clifford`. + + For example, the following code:: + + from qiskit import QuantumCircuit + from qiskit.quantum_info import random_clifford + from qiskit.transpiler.passes import OptimizeCliffords + from qiskit.transpiler import PassManager + + qc = QuantumCircuit(3) + cliff1 = random_clifford(2) + cliff2 = random_clifford(2) + qc.append(cliff1, [2, 1]) + qc.append(cliff2, [2, 1]) + qc_optimized = PassManager(OptimizeCliffords()).run(qc) + + first stores the two Cliffords ``cliff1`` and ``cliff2`` on ``qc`` as "higher-level" objects, + and then the transpiler pass :class:`.OptimizeCliffords` optimizes the circuit by composing + these two Cliffords into a single Clifford. Note that the resulting Clifford is still stored + on ``qc`` as a higher-level object. This pass is not yet included in any of preset pass + managers. + + - | + Added a new transpiler pass :class:`.HighLevelSynthesis` that synthesizes higher-level objects + (for instance, :class:`.Clifford` objects). + + As of now, :class:`.HighLevelSynthesis` is only limited to :class:`.Clifford` objects, but it will be + expanded to cover other higher-level objects (as more higher-level objects will become available). + In addition, the plan is to make :class:`.HighLevelSynthesis` "pluggable", so that the users + can "plug in" their own synthesis methods for higher-level objects at transpilation run time. + + For example, the following code:: + + from qiskit import QuantumCircuit + from qiskit.quantum_info import random_clifford + from qiskit.transpiler import PassManager + from qiskit.transpiler.passes import HighLevelSynthesis + + qc = QuantumCircuit(3) + qc.h(0) + cliff = random_clifford(2) + qc.append(cliff, [0, 1]) + + qc_synthesized = PassManager(HighLevelSynthesis()).run(qc) + + will synthesize the higher-level Clifford stored in ``qc`` using the default + :func:`~qiskit.quantum_info.decompose_clifford` function. + + This new transpiler pass :class:`.HighLevelSynthesis` is integrated into the preset pass managers, + running right after :class:`.UnitarySynthesis` pass. Thus, ``qiskit.compiler.transpile()`` will + synthesize all higher-level Cliffords present in the circuit. + + It is important to note that the work done to store :class:`.Clifford` objects as "higher-level" + objects and to transpile these objects using :class:`.HighLevelSynthesis` pass should be completely + transparent to the users, absolutely no code changes are required. However, as explained before, + the users are now able to optimize consecutive Cliffords using the new :class:`.OptimizeCliffords` + pass, and in the future would be able to plug in their own synthesis methods for Cliffords. diff --git a/test/python/circuit/test_instructions.py b/test/python/circuit/test_instructions.py index 905fb4d7d293..95466917d9f1 100644 --- a/test/python/circuit/test_instructions.py +++ b/test/python/circuit/test_instructions.py @@ -400,7 +400,7 @@ def test_instance_of_instruction(self): qr = QuantumRegister(2) qc = QuantumCircuit(qr) - with self.assertRaisesRegex(CircuitError, r"Object is a subclass of Instruction"): + with self.assertRaisesRegex(CircuitError, r"Object is a subclass of Operation"): qc.append(HGate, qr[:], []) def test_repr_of_instructions(self): diff --git a/test/python/circuit/test_operation.py b/test/python/circuit/test_operation.py index 35f417a8aefd..86db2c0ac456 100644 --- a/test/python/circuit/test_operation.py +++ b/test/python/circuit/test_operation.py @@ -17,7 +17,7 @@ import numpy as np from qiskit.test import QiskitTestCase -from qiskit.circuit import QuantumCircuit, Barrier, Measure, Reset, Gate +from qiskit.circuit import QuantumCircuit, Barrier, Measure, Reset, Gate, Operation from qiskit.circuit.library import XGate, CXGate from qiskit.quantum_info.operators import Clifford, CNOTDihedral, Pauli from qiskit.extensions.quantum_initializer import Initialize, Isometry @@ -35,6 +35,7 @@ def test_measure_as_operation(self): self.assertTrue(op.name == "measure") self.assertTrue(op.num_qubits == 1) self.assertTrue(op.num_clbits == 1) + self.assertIsInstance(op, Operation) def test_reset_as_operation(self): """Test that we can instantiate an object of class @@ -45,6 +46,7 @@ def test_reset_as_operation(self): self.assertTrue(op.name == "reset") self.assertTrue(op.num_qubits == 1) self.assertTrue(op.num_clbits == 0) + self.assertIsInstance(op, Operation) def test_barrier_as_operation(self): """Test that we can instantiate an object of class @@ -56,6 +58,7 @@ def test_barrier_as_operation(self): self.assertTrue(op.name == "barrier") self.assertTrue(op.num_qubits == num_qubits) self.assertTrue(op.num_clbits == 0) + self.assertIsInstance(op, Operation) def test_clifford_as_operation(self): """Test that we can instantiate an object of class @@ -70,6 +73,7 @@ def test_clifford_as_operation(self): self.assertTrue(op.name == "clifford") self.assertTrue(op.num_qubits == num_qubits) self.assertTrue(op.num_clbits == 0) + self.assertIsInstance(op, Operation) def test_cnotdihedral_as_operation(self): """Test that we can instantiate an object of class @@ -106,6 +110,7 @@ def test_isometry_as_operation(self): self.assertTrue(op.name == "isometry") self.assertTrue(op.num_qubits == 7) self.assertTrue(op.num_clbits == 0) + self.assertIsInstance(op, Operation) def test_initialize_as_operation(self): """Test that we can instantiate an object of class @@ -117,6 +122,7 @@ def test_initialize_as_operation(self): self.assertTrue(op.name == "initialize") self.assertTrue(op.num_qubits == 2) self.assertTrue(op.num_clbits == 0) + self.assertIsInstance(op, Operation) def test_gate_as_operation(self): """Test that we can instantiate an object of class @@ -129,6 +135,7 @@ def test_gate_as_operation(self): self.assertTrue(op.name == name) self.assertTrue(op.num_qubits == num_qubits) self.assertTrue(op.num_clbits == 0) + self.assertIsInstance(op, Operation) def test_xgate_as_operation(self): """Test that we can instantiate an object of class @@ -139,6 +146,7 @@ def test_xgate_as_operation(self): self.assertTrue(op.name == "x") self.assertTrue(op.num_qubits == 1) self.assertTrue(op.num_clbits == 0) + self.assertIsInstance(op, Operation) def test_cxgate_as_operation(self): """Test that we can instantiate an object of class @@ -149,6 +157,7 @@ def test_cxgate_as_operation(self): self.assertTrue(op.name == "cx") self.assertTrue(op.num_qubits == 2) self.assertTrue(op.num_clbits == 0) + self.assertIsInstance(op, Operation) def test_can_append_to_quantum_circuit(self): """Test that we can add various objects with Operation interface to a Quantum Circuit.""" diff --git a/test/python/dagcircuit/test_dagcircuit.py b/test/python/dagcircuit/test_dagcircuit.py index 5d6e1b71bf9a..8885dcf31a40 100644 --- a/test/python/dagcircuit/test_dagcircuit.py +++ b/test/python/dagcircuit/test_dagcircuit.py @@ -120,7 +120,9 @@ def raise_if_dagcircuit_invalid(dag): in_wires = {data for src, dest, data in in_edges} out_wires = {data for src, dest, data in out_edges} - node_cond_bits = set(node.op.condition[0][:] if node.op.condition is not None else []) + node_cond_bits = set( + node.op.condition[0][:] if getattr(node.op, "condition", None) is not None else [] + ) node_qubits = set(node.qargs) node_clbits = set(node.cargs) diff --git a/test/python/transpiler/test_clifford_passes.py b/test/python/transpiler/test_clifford_passes.py new file mode 100644 index 000000000000..faf0db738008 --- /dev/null +++ b/test/python/transpiler/test_clifford_passes.py @@ -0,0 +1,289 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2021. +# +# 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 transpiler passes and conversion methods that deal with Cliffords.""" + +import unittest +import numpy as np + +from qiskit.circuit import QuantumCircuit, Gate +from qiskit.converters import dag_to_circuit, circuit_to_dag +from qiskit.dagcircuit import DAGOpNode +from qiskit.transpiler.passes import HighLevelSynthesis +from qiskit.transpiler.passes import OptimizeCliffords +from qiskit.test import QiskitTestCase +from qiskit.quantum_info.operators import Clifford +from qiskit.transpiler import PassManager +from qiskit.quantum_info import Operator, random_clifford +from qiskit.compiler.transpiler import transpile + + +class TestCliffordPasses(QiskitTestCase): + """Tests to verify correctness of transpiler passes and + conversion methods that deal with Cliffords.""" + + def create_cliff1(self): + """Creates a simple Clifford.""" + qc = QuantumCircuit(3) + qc.h(0) + qc.cx(0, 1) + qc.cx(1, 2) + qc.s(2) + return Clifford(qc) + + def create_cliff2(self): + """Creates another simple Clifford.""" + qc = QuantumCircuit(3) + qc.cx(0, 1) + qc.h(0) + qc.h(1) + qc.h(2) + qc.cx(1, 2) + qc.s(2) + return Clifford(qc) + + def create_cliff3(self): + """Creates a third Clifford which is the composition of the previous two.""" + qc = QuantumCircuit(3) + qc.h(0) + qc.cx(0, 1) + qc.cx(1, 2) + qc.s(2) + qc.cx(0, 1) + qc.h(0) + qc.h(1) + qc.h(2) + qc.cx(1, 2) + qc.s(2) + return Clifford(qc) + + def test_circuit_with_cliffords(self): + """Test that Cliffords get stored natively on a QuantumCircuit, + and that QuantumCircuit's decompose() replaces Clifford with gates.""" + + # Create a circuit with 2 cliffords and four other gates + cliff1 = self.create_cliff1() + cliff2 = self.create_cliff2() + qc = QuantumCircuit(4) + qc.h(0) + qc.cx(2, 0) + qc.append(cliff1, [3, 0, 2]) + qc.swap(1, 3) + qc.append(cliff2, [1, 2, 3]) + qc.h(3) + + # Check that there are indeed two Clifford objects in the circuit, + # and that these are not gates. + cliffords = [inst for inst, _, _ in qc.data if isinstance(inst, Clifford)] + gates = [inst for inst, _, _ in qc.data if isinstance(inst, Gate)] + self.assertEqual(len(cliffords), 2) + self.assertEqual(len(gates), 4) + + # Check that calling QuantumCircuit's decompose(), no Clifford objects remain + qc2 = qc.decompose() + cliffords2 = [inst for inst, _, _ in qc2.data if isinstance(inst, Clifford)] + self.assertEqual(len(cliffords2), 0) + + def test_can_construct_operator(self): + """Test that we can construct an Operator from a circuit that + contains a Clifford gate.""" + + cliff = self.create_cliff1() + qc = QuantumCircuit(4) + qc.append(cliff, [3, 1, 2]) + + # Create an operator from the decomposition of qc into gates + op1 = Operator(qc.decompose()) + + # Create an operator from qc directly + op2 = Operator(qc) + + # Check that the two operators are equal + self.assertTrue(op1.equiv(op2)) + + def test_can_combine_cliffords(self): + """Test that we can combine a pair of Cliffords over the same qubits + using OptimizeCliffords transpiler pass.""" + + cliff1 = self.create_cliff1() + cliff2 = self.create_cliff2() + cliff3 = self.create_cliff3() + + # Create a circuit with two consective cliffords + qc1 = QuantumCircuit(4) + qc1.append(cliff1, [3, 1, 2]) + qc1.append(cliff2, [3, 1, 2]) + self.assertEqual(qc1.count_ops()["clifford"], 2) + + # Run OptimizeCliffords pass, and check that only one Clifford remains + qc1opt = PassManager(OptimizeCliffords()).run(qc1) + self.assertEqual(qc1opt.count_ops()["clifford"], 1) + + # Create the expected circuit + qc2 = QuantumCircuit(4) + qc2.append(cliff3, [3, 1, 2]) + + # Check that all possible operators are equal + self.assertTrue(Operator(qc1).equiv(Operator(qc1.decompose()))) + self.assertTrue(Operator(qc1opt).equiv(Operator(qc1opt.decompose()))) + self.assertTrue(Operator(qc1).equiv(Operator(qc1opt))) + self.assertTrue(Operator(qc2).equiv(Operator(qc2.decompose()))) + self.assertTrue(Operator(qc1opt).equiv(Operator(qc2))) + + def test_cannot_combine(self): + """Test that currently we cannot combine a pair of Cliffords. + The result will be changed after pass is updated""" + + cliff1 = self.create_cliff1() + cliff2 = self.create_cliff2() + qc1 = QuantumCircuit(4) + qc1.append(cliff1, [3, 1, 2]) + qc1.append(cliff2, [3, 2, 1]) + qc1 = PassManager(OptimizeCliffords()).run(qc1) + self.assertEqual(qc1.count_ops()["clifford"], 2) + + def test_circuit_to_dag_conversion_and_back(self): + """Test that converting a circuit containing Clifford to a DAG + and back preserves the Clifford. + """ + # Create a Clifford + cliff_circ = QuantumCircuit(3) + cliff_circ.cx(0, 1) + cliff_circ.h(0) + cliff_circ.s(1) + cliff_circ.swap(1, 2) + cliff = Clifford(cliff_circ) + + # Add this Clifford to a Quantum Circuit, and check that it remains a Clifford + circ0 = QuantumCircuit(4) + circ0.append(cliff, [0, 1, 2]) + circ0_cliffords = [inst for inst, _, _ in circ0.data if isinstance(inst, Clifford)] + circ0_gates = [inst for inst, _, _ in circ0.data if isinstance(inst, Gate)] + self.assertEqual(len(circ0_cliffords), 1) + self.assertEqual(len(circ0_gates), 0) + + # Check that converting circuit to DAG preserves Clifford. + dag0 = circuit_to_dag(circ0) + dag0_cliffords = [ + node + for node in dag0.topological_nodes() + if isinstance(node, DAGOpNode) and isinstance(node.op, Clifford) + ] + self.assertEqual(len(dag0_cliffords), 1) + + # Check that converted DAG to a circuit also preserves Clifford. + circ1 = dag_to_circuit(dag0) + circ1_cliffords = [inst for inst, _, _ in circ1.data if isinstance(inst, Clifford)] + circ1_gates = [inst for inst, _, _ in circ1.data if isinstance(inst, Gate)] + self.assertEqual(len(circ1_cliffords), 1) + self.assertEqual(len(circ1_gates), 0) + + # However, test that running an unrolling pass on the DAG replaces Clifford + # by gates. + dag1 = HighLevelSynthesis().run(dag0) + dag1_cliffords = [ + node + for node in dag1.topological_nodes() + if isinstance(node, DAGOpNode) and isinstance(node.op, Clifford) + ] + self.assertEqual(len(dag1_cliffords), 0) + + def test_optimize_cliffords(self): + """Test OptimizeCliffords pass.""" + + rng = np.random.default_rng(1234) + for _ in range(20): + # Create several random Cliffords + cliffs = [random_clifford(3, rng) for _ in range(5)] + + # The first circuit contains these cliffords + qc1 = QuantumCircuit(5) + for cliff in cliffs: + qc1.append(cliff, [4, 0, 2]) + self.assertEqual(qc1.count_ops()["clifford"], 5) + + # The second circuit is obtained by running the OptimizeCliffords pass. + qc2 = PassManager(OptimizeCliffords()).run(qc1) + self.assertEqual(qc2.count_ops()["clifford"], 1) + + # The third circuit contains the decompositions of Cliffods. + qc3 = QuantumCircuit(5) + for cliff in cliffs: + qc3.append(cliff.to_circuit(), [4, 0, 2]) + self.assertNotIn("clifford", qc3.count_ops()) + + # Check that qc1, qc2 and qc3 and their decompositions are all equivalent. + self.assertTrue(Operator(qc1).equiv(Operator(qc1.decompose()))) + self.assertTrue(Operator(qc2).equiv(Operator(qc2.decompose()))) + self.assertTrue(Operator(qc3).equiv(Operator(qc3.decompose()))) + self.assertTrue(Operator(qc1).equiv(Operator(qc2))) + self.assertTrue(Operator(qc1).equiv(Operator(qc3))) + + def test_topological_ordering(self): + """Test that Clifford optimization pass optimizes Cliffords across a gate + on a different qubit.""" + + cliff1 = self.create_cliff1() + cliff2 = self.create_cliff1() + + qc1 = QuantumCircuit(5) + qc1.append(cliff1, [0, 1, 2]) + qc1.h(4) + qc1.append(cliff2, [0, 1, 2]) + + # The second circuit is obtained by running the OptimizeCliffords pass. + qc2 = PassManager(OptimizeCliffords()).run(qc1) + self.assertEqual(qc2.count_ops()["clifford"], 1) + + def test_transpile_level_0(self): + """Make sure that transpile with optimization_level=0 transpiles + the Clifford.""" + cliff1 = self.create_cliff1() + qc = QuantumCircuit(3) + qc.append(cliff1, [0, 1, 2]) + self.assertIn("clifford", qc.count_ops()) + qc2 = transpile(qc, optimization_level=0) + self.assertNotIn("clifford", qc2.count_ops()) + + def test_transpile_level_1(self): + """Make sure that transpile with optimization_level=1 transpiles + the Clifford.""" + cliff1 = self.create_cliff1() + qc = QuantumCircuit(3) + qc.append(cliff1, [0, 1, 2]) + self.assertIn("clifford", qc.count_ops()) + qc2 = transpile(qc, optimization_level=1) + self.assertNotIn("clifford", qc2.count_ops()) + + def test_transpile_level_2(self): + """Make sure that transpile with optimization_level=2 transpiles + the Clifford.""" + cliff1 = self.create_cliff1() + qc = QuantumCircuit(3) + qc.append(cliff1, [0, 1, 2]) + self.assertIn("clifford", qc.count_ops()) + qc2 = transpile(qc, optimization_level=2) + self.assertNotIn("clifford", qc2.count_ops()) + + def test_transpile_level_3(self): + """Make sure that transpile with optimization_level=2 transpiles + the Clifford.""" + cliff1 = self.create_cliff1() + qc = QuantumCircuit(3) + qc.append(cliff1, [0, 1, 2]) + self.assertIn("clifford", qc.count_ops()) + qc2 = transpile(qc, optimization_level=3) + self.assertNotIn("clifford", qc2.count_ops()) + + +if __name__ == "__main__": + unittest.main() From aa2d72c8543295743c5f318b74157f93e052b26d Mon Sep 17 00:00:00 2001 From: Guillermo-Mijares-Vilarino <106545082+Guillermo-Mijares-Vilarino@users.noreply.github.com> Date: Thu, 11 Aug 2022 14:26:28 +0200 Subject: [PATCH 38/82] Added plot bloch multivector API reference code example (#8349) * changed plot_bloch_multivector code examples in API reference to better show the different arguments * Simplified circuit and removed reference to Aer and transpile * simplified state commands * Removed extra imports and matplotlib inline * Added comment * remove spaces around = in function arguments * import numpy before importing qiskit * Revert "import numpy before importing qiskit" This reverts commit e46e50aeacce8dfe967fe72f2f6f757aab9b73d4. Co-authored-by: Junye Huang Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/visualization/state_visualization.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/qiskit/visualization/state_visualization.py b/qiskit/visualization/state_visualization.py index b47b1fa2f416..e4491a4eaf16 100644 --- a/qiskit/visualization/state_visualization.py +++ b/qiskit/visualization/state_visualization.py @@ -260,20 +260,35 @@ def plot_bloch_multivector( MissingOptionalLibraryError: Requires matplotlib. VisualizationError: if input is not a valid N-qubit state. - Example: + Examples: .. jupyter-execute:: from qiskit import QuantumCircuit from qiskit.quantum_info import Statevector from qiskit.visualization import plot_bloch_multivector - %matplotlib inline qc = QuantumCircuit(2) qc.h(0) qc.x(1) - state = Statevector.from_instruction(qc) + state = Statevector(qc) plot_bloch_multivector(state) + + .. jupyter-execute:: + + # You can reverse the order of the qubits. + + from qiskit.quantum_info import DensityMatrix + + qc = QuantumCircuit(2) + qc.h([0, 1]) + qc.t(1) + qc.s(0) + qc.cx(0,1) + + matrix = DensityMatrix(qc) + plot_bloch_multivector(matrix, title='My Bloch Spheres', reverse_bits=True) + """ from matplotlib import pyplot as plt From a2780470a22f2050d6bb38316369dd2cd518fdc8 Mon Sep 17 00:00:00 2001 From: a-matsuo <47442626+a-matsuo@users.noreply.github.com> Date: Thu, 11 Aug 2022 22:48:27 +0900 Subject: [PATCH 39/82] fix primitive job's status (#8521) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/primitives/primitive_job.py | 2 +- test/python/primitives/test_sampler.py | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/qiskit/primitives/primitive_job.py b/qiskit/primitives/primitive_job.py index dc8b07cc8f00..f8d295b662b5 100644 --- a/qiskit/primitives/primitive_job.py +++ b/qiskit/primitives/primitive_job.py @@ -59,7 +59,7 @@ def status(self): return JobStatus.RUNNING elif self._future.cancelled(): return JobStatus.CANCELLED - elif self._future.done() and self._future._exception() is None: + elif self._future.done() and self._future.exception() is None: return JobStatus.DONE return JobStatus.ERROR diff --git a/test/python/primitives/test_sampler.py b/test/python/primitives/test_sampler.py index 7f258a644d9c..b126f927de97 100644 --- a/test/python/primitives/test_sampler.py +++ b/test/python/primitives/test_sampler.py @@ -23,7 +23,7 @@ from qiskit.circuit.library import RealAmplitudes from qiskit.exceptions import QiskitError from qiskit.primitives import Sampler, SamplerResult -from qiskit.providers import JobV1 +from qiskit.providers import JobStatus, JobV1 from qiskit.test import QiskitTestCase @@ -651,6 +651,13 @@ def test_run_with_shots_option_none(self): ).result() self.assertDictAlmostEqual(result_42.quasi_dists, result_15.quasi_dists) + def test_primitive_job_status_done(self): + """test primitive job's status""" + bell = self._circuit[1] + sampler = Sampler() + job = sampler.run(circuits=[bell]) + self.assertEqual(job.status(), JobStatus.DONE) + if __name__ == "__main__": unittest.main() From 82e38d1de0ea950457d647955471404b044910b8 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 12 Aug 2022 03:13:59 -0400 Subject: [PATCH 40/82] Pin setuptools in CI (#8526) * Pin setuptools in CI The recently released setuptools 64.0.0 release introduced a regression that prevents editable installs from working (see pypa/setuptools#3498). This is blocking CI as we use editable installs to build and install terra for testing. When there is an upstream release fixing this issue we can remove the pins. * Remove pip/setuptools/wheel manual install step * Try venv instead of virtualenv * Revert "Try venv instead of virtualenv" This reverts commit 3ada81933069ee2fff8904863533fbad68c5e923. * Revert "Remove pip/setuptools/wheel manual install step" This reverts commit 831bc6e0dbc828e0437b5ecbb7cf86bdb2bfadfd. * Pin in constraints.txt too * Lower version further * Pin setuptools-rust too * Set editable install to legacy mode via env var * Set env variable correctly everywhere we build terra * Add missing env variable setting for image tests --- .azure/docs-linux.yml | 6 ++++-- .azure/lint-linux.yml | 4 +++- .azure/test-linux.yml | 7 ++++++- .azure/test-macos.yml | 4 +++- .azure/test-windows.yml | 4 +++- .azure/tutorials-linux.yml | 2 ++ .github/workflows/coverage.yml | 2 ++ .github/workflows/randomized_tests.yml | 2 ++ constraints.txt | 4 ++++ pyproject.toml | 2 +- requirements-dev.txt | 2 +- tox.ini | 2 +- 12 files changed, 32 insertions(+), 9 deletions(-) diff --git a/.azure/docs-linux.yml b/.azure/docs-linux.yml index 4f1f9206a11c..0f8e3015c0eb 100644 --- a/.azure/docs-linux.yml +++ b/.azure/docs-linux.yml @@ -30,8 +30,8 @@ jobs: - bash: | set -e - python -m pip install --upgrade pip setuptools wheel - pip install -U tox + python -m pip install --upgrade pip 'setuptools<64.0.0' wheel -c constraints.txt + pip install -U tox -c constraints.txt sudo apt-get update sudo apt-get install -y graphviz displayName: 'Install dependencies' @@ -39,6 +39,8 @@ jobs: - bash: | tox -edocs displayName: 'Run Docs build' + env: + SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" - task: ArchiveFiles@2 inputs: diff --git a/.azure/lint-linux.yml b/.azure/lint-linux.yml index 830acdc1821e..5fddd6afef3f 100644 --- a/.azure/lint-linux.yml +++ b/.azure/lint-linux.yml @@ -28,7 +28,7 @@ jobs: - bash: | set -e - python -m pip install --upgrade pip setuptools wheel virtualenv + python -m pip install --upgrade pip 'setuptools<64.0.0' wheel virtualenv -c constraints.txt virtualenv test-job source test-job/bin/activate pip install -U -r requirements.txt -r requirements-dev.txt -c constraints.txt @@ -36,6 +36,8 @@ jobs: pip install -U "qiskit-aer" -c constraints.txt python setup.py build_ext --inplace displayName: 'Install dependencies' + env: + SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" - bash: | set -e diff --git a/.azure/test-linux.yml b/.azure/test-linux.yml index 6389dafc8604..14d1c090f822 100644 --- a/.azure/test-linux.yml +++ b/.azure/test-linux.yml @@ -62,7 +62,7 @@ jobs: - bash: | set -e - python -m pip install --upgrade pip setuptools wheel virtualenv + python -m pip install --upgrade pip 'setuptools<64.0.0' wheel virtualenv -c constraints.txt virtualenv test-job displayName: "Prepare venv" @@ -82,6 +82,9 @@ jobs: pip install -U -r requirements.txt -r requirements-dev.txt -c constraints.txt pip install -U -c constraints.txt -e . displayName: "Install Terra directly" + env: + SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" + - bash: | set -e @@ -165,6 +168,8 @@ jobs: sudo apt-get install -y graphviz pandoc image_tests/bin/pip check displayName: 'Install dependencies' + env: + SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" - bash: image_tests/bin/python -m unittest discover -v test/ipynb displayName: 'Run image test' diff --git a/.azure/test-macos.yml b/.azure/test-macos.yml index c0e05f4990ff..4417dd8add68 100644 --- a/.azure/test-macos.yml +++ b/.azure/test-macos.yml @@ -41,7 +41,7 @@ jobs: - bash: | set -e - python -m pip install --upgrade pip setuptools wheel virtualenv + python -m pip install --upgrade pip 'setuptools<64.0.0' wheel virtualenv -c constraints.txt virtualenv test-job source test-job/bin/activate pip install -U -r requirements.txt -r requirements-dev.txt -c constraints.txt @@ -49,6 +49,8 @@ jobs: python setup.py build_ext --inplace pip check displayName: 'Install dependencies' + env: + SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" - bash: | set -e diff --git a/.azure/test-windows.yml b/.azure/test-windows.yml index 9bac8b2683bf..8140a6ce4fbb 100644 --- a/.azure/test-windows.yml +++ b/.azure/test-windows.yml @@ -30,7 +30,7 @@ jobs: - bash: | set -e - python -m pip install --upgrade pip setuptools wheel virtualenv + python -m pip install --upgrade pip 'setuptools<64.0.0' wheel virtualenv -c constraints.txt virtualenv test-job source test-job/Scripts/activate pip install -r requirements.txt -r requirements-dev.txt -c constraints.txt @@ -39,6 +39,8 @@ jobs: python setup.py build_ext --inplace pip check displayName: 'Install dependencies' + env: + SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" - bash: | set -e diff --git a/.azure/tutorials-linux.yml b/.azure/tutorials-linux.yml index 03eac35bc193..2ce7cb9026c8 100644 --- a/.azure/tutorials-linux.yml +++ b/.azure/tutorials-linux.yml @@ -39,6 +39,8 @@ jobs: sudo apt-get install -y graphviz pandoc pip check displayName: 'Install dependencies' + env: + SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" - bash: | set -e diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 6e149a0d39c7..08861cb1def9 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -20,6 +20,8 @@ jobs: run: pip install tox coveragepy-lcov - name: Run coverage report run: tox -ecoverage + env: + SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" - name: Convert to lcov run: coveragepy-lcov --output_file_path coveralls.info - name: Coveralls diff --git a/.github/workflows/randomized_tests.yml b/.github/workflows/randomized_tests.yml index 5465e499b105..f96f84e0ed78 100644 --- a/.github/workflows/randomized_tests.yml +++ b/.github/workflows/randomized_tests.yml @@ -20,6 +20,8 @@ jobs: pip install -c constraints.txt -e . pip install "qiskit-ibmq-provider" -c constraints.txt pip install "qiskit-aer" + env: + SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" - name: Run randomized tests run: make test_randomized - name: Create comment on failed test run diff --git a/constraints.txt b/constraints.txt index 3cb8598cb345..bf4e304596c5 100644 --- a/constraints.txt +++ b/constraints.txt @@ -13,3 +13,7 @@ pyparsing<3.0.0 # to work with the new jinja version (the jinja maintainers aren't going to # fix things) pin to the previous working version. jinja2==3.0.3 + +# setuptools 64.0.0 breaks editable installs. Pin to an old version until +# see https://github.com/pypa/setuptools/issues/3498 +setuptools==63.3.0 diff --git a/pyproject.toml b/pyproject.toml index 9282c3b8c8d0..728e2a2be55a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools", "wheel", "setuptools-rust"] +requires = ["setuptools", "wheel", "setuptools-rust<1.5.0"] build-backend = "setuptools.build_meta" [tool.black] diff --git a/requirements-dev.txt b/requirements-dev.txt index eef50c6b96e1..fb5e2cbeae18 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,4 @@ -setuptools-rust +setuptools-rust<1.5.0 coverage>=4.4.0 hypothesis>=4.24.3 python-constraint>=1.4 diff --git a/tox.ini b/tox.ini index f457cab047e0..0561b10732a7 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,7 @@ setenv = QISKIT_SUPRESS_PACKAGING_WARNINGS=Y QISKIT_TEST_CAPTURE_STREAMS=1 QISKIT_PARALLEL=FALSE -passenv = RAYON_NUM_THREADS OMP_NUM_THREADS QISKIT_PARALLEL +passenv = RAYON_NUM_THREADS OMP_NUM_THREADS QISKIT_PARALLEL SETUPTOOLS_ENABLE_FEATURES deps = -r{toxinidir}/requirements.txt -r{toxinidir}/requirements-dev.txt commands = From 82c85d859be00659b0ab0214bee89456a9cee195 Mon Sep 17 00:00:00 2001 From: "Israel F. Araujo" Date: Sun, 14 Aug 2022 12:37:03 -0300 Subject: [PATCH 41/82] Fix global phase in inverse of UCGate (#8508) * Defines the inverse of the global phase The inverse of UCGate is defined by simply inverting the existing decomposition, but the inverse of the global phase was missing. * Tests the UCGate.inverse() function * Lint * Seed for building the matrices * Replace `unitary_simulator` by using `qiskit.quantum_info.Operator`. Co-authored-by: Julien Gacon * Import `Operator` class * Release notes * Lint * Test the phase of isometry decomposition * Removes the `prelude` from the release notes file * Replaces `matrix_equal` with `np.allclose` Co-authored-by: Julien Gacon --- qiskit/extensions/quantum_initializer/uc.py | 3 +++ ...-inverse-global_phase-c9655c13c22e5cf4.yaml | 9 +++++++++ test/python/circuit/test_isometry.py | 5 ++--- test/python/circuit/test_uc.py | 18 +++++++++++++++++- 4 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/bugfix-ucgate-inverse-global_phase-c9655c13c22e5cf4.yaml diff --git a/qiskit/extensions/quantum_initializer/uc.py b/qiskit/extensions/quantum_initializer/uc.py index 61618efb3563..d859ed4a96de 100644 --- a/qiskit/extensions/quantum_initializer/uc.py +++ b/qiskit/extensions/quantum_initializer/uc.py @@ -110,6 +110,9 @@ def inverse(self): definition = QuantumCircuit(*self.definition.qregs) for inst in reversed(self._definition): definition._append(inst.replace(operation=inst.operation.inverse())) + + definition.global_phase = -self.definition.global_phase + inverse_gate.definition = definition return inverse_gate diff --git a/releasenotes/notes/bugfix-ucgate-inverse-global_phase-c9655c13c22e5cf4.yaml b/releasenotes/notes/bugfix-ucgate-inverse-global_phase-c9655c13c22e5cf4.yaml new file mode 100644 index 000000000000..eae30452782b --- /dev/null +++ b/releasenotes/notes/bugfix-ucgate-inverse-global_phase-c9655c13c22e5cf4.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + Fixes the :meth:`.UCGate.inverse` method which previously did not invert the + global phase. + - | + Fixes the global phase problem of the isometry decomposition. Refer to + `#4687 ` for more + details. diff --git a/test/python/circuit/test_isometry.py b/test/python/circuit/test_isometry.py index c411836da5fb..bffd9c69bac3 100644 --- a/test/python/circuit/test_isometry.py +++ b/test/python/circuit/test_isometry.py @@ -24,7 +24,6 @@ from qiskit import execute from qiskit.test import QiskitTestCase from qiskit.compiler import transpile -from qiskit.quantum_info.operators.predicates import matrix_equal from qiskit.quantum_info import Operator from qiskit.extensions.quantum_initializer.isometry import Isometry @@ -69,7 +68,7 @@ def test_isometry(self, iso): unitary = result.get_unitary(qc) iso_from_circuit = unitary[::, 0 : 2**num_q_input] iso_desired = iso - self.assertTrue(matrix_equal(iso_from_circuit, iso_desired, ignore_phase=True)) + self.assertTrue(np.allclose(iso_from_circuit, iso_desired)) @data( np.eye(2, 2), @@ -108,7 +107,7 @@ def test_isometry_tolerance(self, iso): result = execute(qc, simulator).result() unitary = result.get_unitary(qc) iso_from_circuit = unitary[::, 0 : 2**num_q_input] - self.assertTrue(matrix_equal(iso_from_circuit, iso, ignore_phase=True)) + self.assertTrue(np.allclose(iso_from_circuit, iso)) @data( np.eye(2, 2), diff --git a/test/python/circuit/test_uc.py b/test/python/circuit/test_uc.py index cedff9f4ac41..1f50b650623c 100644 --- a/test/python/circuit/test_uc.py +++ b/test/python/circuit/test_uc.py @@ -30,6 +30,7 @@ from qiskit.quantum_info.random import random_unitary from qiskit.compiler import transpile from qiskit.quantum_info.operators.predicates import matrix_equal +from qiskit.quantum_info import Operator _id = np.eye(2, 2) _not = np.matrix([[0, 1], [1, 0]]) @@ -71,7 +72,7 @@ def test_ucg(self, squs, up_to_diagonal): self.assertTrue(matrix_equal(unitary_desired, unitary, ignore_phase=True)) def test_global_phase_ucg(self): - """ "Test global phase of uniformly controlled gates""" + """Test global phase of uniformly controlled gates""" gates = [random_unitary(2).data for _ in range(2**2)] num_con = int(np.log2(len(gates))) q = QuantumRegister(num_con + 1) @@ -85,6 +86,21 @@ def test_global_phase_ucg(self): self.assertTrue(np.allclose(unitary_desired, unitary)) + def test_inverse_ucg(self): + """Test inverse function of uniformly controlled gates""" + gates = [random_unitary(2, seed=42 + s).data for s in range(2**2)] + num_con = int(np.log2(len(gates))) + q = QuantumRegister(num_con + 1) + qc = QuantumCircuit(q) + + qc.uc(gates, q[1:], q[0], up_to_diagonal=False) + qc.append(qc.inverse(), qc.qubits) + + unitary = Operator(qc).data + unitary_desired = np.identity(2**qc.num_qubits) + + self.assertTrue(np.allclose(unitary_desired, unitary)) + def _get_ucg_matrix(squs): return block_diag(*squs) From eaf55816470fd64c934b36b7684a02e133dfbc5f Mon Sep 17 00:00:00 2001 From: Rohit Taeja <108047286+r-taeja@users.noreply.github.com> Date: Mon, 15 Aug 2022 19:39:32 +0530 Subject: [PATCH 42/82] Fixed Issue 8244 by updating unitary_simulator error message (#8249) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/providers/basicaer/unitary_simulator.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/qiskit/providers/basicaer/unitary_simulator.py b/qiskit/providers/basicaer/unitary_simulator.py index a4a0328a1007..63745f7967ee 100644 --- a/qiskit/providers/basicaer/unitary_simulator.py +++ b/qiskit/providers/basicaer/unitary_simulator.py @@ -392,8 +392,6 @@ def _validate(self, qobj): for operation in experiment.instructions: if operation.name in ["measure", "reset"]: raise BasicAerError( - 'Unsupported "%s" instruction "%s" ' + 'in circuit "%s" ', - self.name(), - operation.name, - name, + f'Unsupported "{self.name()}" instruction "{operation.name}"' + f' in circuit "{name}".' ) From d68e15547a4e0ee07fd0d33baee7bc60a335f3d0 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 15 Aug 2022 13:23:34 -0400 Subject: [PATCH 43/82] Update macOS image version in azure pipelines ci (#8431) The macOS 10.15 image we're using in azure pipelines has been deprecated for some time (as Apple has dropped support for the version) and they've started periodic brownouts on the version to accelerate the transition off of the image. We have stayed pinned at the older version because we had compatibility issues with the newer releases in the past. But, since this is no longer an option this commit bumps us one version from 10.15 to 11. This doesn't go straight to 12 as Apple proactively disables support for older platforms in newer OS releases and in Qiskit we try to maximize platform support, even those using older Apple hardware, so the minimal version update is made. Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .azure/test-macos.yml | 2 +- azure-pipelines.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.azure/test-macos.yml b/.azure/test-macos.yml index 4417dd8add68..d7d98c3ef3be 100644 --- a/.azure/test-macos.yml +++ b/.azure/test-macos.yml @@ -6,7 +6,7 @@ parameters: jobs: - job: "MacOS_Tests_Python${{ replace(parameters.pythonVersion, '.', '') }}" displayName: "Test macOS Python ${{ parameters.pythonVersion }}" - pool: {vmImage: 'macOS-10.15'} + pool: {vmImage: 'macOS-11'} variables: QISKIT_SUPPRESS_PACKAGING_WARNINGS: Y diff --git a/azure-pipelines.yml b/azure-pipelines.yml index a0947861a159..de6655e02348 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -212,12 +212,12 @@ stages: - template: ".azure/wheels.yml" parameters: jobName: "macos" - pool: {vmImage: 'macOS-10.15'} + pool: {vmImage: 'macOS-11'} - template: ".azure/wheels.yml" parameters: jobName: "macos_arm" - pool: {vmImage: 'macOS-10.15'} + pool: {vmImage: 'macOS-11'} env: CIBW_BEFORE_ALL: rustup target add aarch64-apple-darwin CIBW_ARCHS_MACOS: arm64 universal2 From f37dcf9d9c879867140b3072483a3dbd36cb5c1f Mon Sep 17 00:00:00 2001 From: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> Date: Mon, 15 Aug 2022 14:29:38 -0400 Subject: [PATCH 44/82] Add Minimizer to API ref. (#8533) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/algorithms/optimizers/__init__.py | 3 +++ qiskit/algorithms/optimizers/optimizer.py | 20 +++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/qiskit/algorithms/optimizers/__init__.py b/qiskit/algorithms/optimizers/__init__.py index a6efc11c49d8..c0f30d86dd32 100644 --- a/qiskit/algorithms/optimizers/__init__.py +++ b/qiskit/algorithms/optimizers/__init__.py @@ -37,6 +37,7 @@ OptimizerResult OptimizerSupportLevel Optimizer + Minimizer Local Optimizers ================ @@ -133,6 +134,8 @@ __all__ = [ "Optimizer", "OptimizerSupportLevel", + "OptimizerResult", + "Minimizer", "ADAM", "AQGD", "CG", diff --git a/qiskit/algorithms/optimizers/optimizer.py b/qiskit/algorithms/optimizers/optimizer.py index b8acefbf9d2c..6f2e4e1077f9 100644 --- a/qiskit/algorithms/optimizers/optimizer.py +++ b/qiskit/algorithms/optimizers/optimizer.py @@ -110,7 +110,25 @@ def nit(self, nit: Optional[int]) -> None: class Minimizer(Protocol): - """Callback Protocol for minimizer.""" + """Callable Protocol for minimizer. + + This interface is based on `SciPy's optimize module + `__. + + This protocol defines a callable taking the following parameters: + + fun + The objective function to minimize (for example the energy in the case of the VQE). + x0 + The initial point for the optimization. + jac + The gradient of the objective function. + bounds + Parameters bounds for the optimization. Note that these might not be supported + by all optimizers. + + and which returns a minimization result object (either SciPy's or Qiskit's). + """ # pylint: disable=invalid-name def __call__( From 4fac3f4bea52aae80797cd01ca86545cbfb32e9b Mon Sep 17 00:00:00 2001 From: Emilio <63567458+epelaaez@users.noreply.github.com> Date: Tue, 16 Aug 2022 11:07:13 -0500 Subject: [PATCH 45/82] accept single qubit in initialize to fix #8408 (#8463) * accept single qubit in initialize * only call conversion when not iterable * add single Qubit to if statement * add test * fix imports * modify docs; add release note * accept single qubit in state preparation Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .../library/data_preparation/state_preparation.py | 7 ++++--- qiskit/extensions/quantum_initializer/initializer.py | 6 ++++-- ...ze-and-prepare-single-qubit-e25dacc8f873bc01.yaml | 5 +++++ .../python/circuit/library/test_state_preparation.py | 11 ++++++++++- test/python/circuit/test_registerless_circuit.py | 12 ++++++++++++ 5 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/circuit-initialize-and-prepare-single-qubit-e25dacc8f873bc01.yaml diff --git a/qiskit/circuit/library/data_preparation/state_preparation.py b/qiskit/circuit/library/data_preparation/state_preparation.py index 43592514da1f..943e74f445b5 100644 --- a/qiskit/circuit/library/data_preparation/state_preparation.py +++ b/qiskit/circuit/library/data_preparation/state_preparation.py @@ -17,7 +17,7 @@ import numpy as np from qiskit.exceptions import QiskitError -from qiskit.circuit import QuantumCircuit, QuantumRegister +from qiskit.circuit import QuantumCircuit, QuantumRegister, Qubit from qiskit.circuit.gate import Gate from qiskit.circuit.library.standard_gates.x import CXGate, XGate from qiskit.circuit.library.standard_gates.h import HGate @@ -427,8 +427,9 @@ def prepare_state(self, state, qubits=None, label=None): to :math:`|1\rangle`. Example: setting params to 5 would initialize qubit 0 and qubit 2 to :math:`|1\rangle` and qubit 1 to :math:`|0\rangle`. - qubits (QuantumRegister or int): + qubits (QuantumRegister or Qubit or int): * QuantumRegister: A list of qubits to be initialized [Default: None]. + * Qubit: Single qubit to be initialized [Default: None]. * int: Index of qubit to be initialized [Default: None]. * list: Indexes of qubits to be initialized [Default: None]. label (str): An optional label for the gate @@ -505,7 +506,7 @@ def prepare_state(self, state, qubits=None, label=None): if qubits is None: qubits = self.qubits - elif isinstance(qubits, (int, np.integer, slice)): + elif isinstance(qubits, (int, np.integer, slice, Qubit)): qubits = [qubits] num_qubits = len(qubits) if isinstance(state, int) else None diff --git a/qiskit/extensions/quantum_initializer/initializer.py b/qiskit/extensions/quantum_initializer/initializer.py index 67b1dfa4baf6..7d6c658174ad 100644 --- a/qiskit/extensions/quantum_initializer/initializer.py +++ b/qiskit/extensions/quantum_initializer/initializer.py @@ -18,6 +18,7 @@ from qiskit.circuit import QuantumCircuit from qiskit.circuit import QuantumRegister from qiskit.circuit import Instruction +from qiskit.circuit import Qubit from qiskit.circuit.library.data_preparation import StatePreparation _EPS = 1e-10 # global variable used to chop very small numbers to zero @@ -106,8 +107,9 @@ class to prepare the qubits in a specified state. to :math:`|1\rangle`. Example: setting params to 5 would initialize qubit 0 and qubit 2 to :math:`|1\rangle` and qubit 1 to :math:`|0\rangle`. - qubits (QuantumRegister or int): + qubits (QuantumRegister or Qubit or int): * QuantumRegister: A list of qubits to be initialized [Default: None]. + * Qubit: Single qubit to be initialized [Default: None]. * int: Index of qubit to be initialized [Default: None]. * list: Indexes of qubits to be initialized [Default: None]. @@ -182,7 +184,7 @@ class to prepare the qubits in a specified state. """ if qubits is None: qubits = self.qubits - elif isinstance(qubits, (int, np.integer, slice)): + elif isinstance(qubits, (int, np.integer, slice, Qubit)): qubits = [qubits] num_qubits = len(qubits) if isinstance(params, int) else None diff --git a/releasenotes/notes/circuit-initialize-and-prepare-single-qubit-e25dacc8f873bc01.yaml b/releasenotes/notes/circuit-initialize-and-prepare-single-qubit-e25dacc8f873bc01.yaml new file mode 100644 index 000000000000..0648666c6147 --- /dev/null +++ b/releasenotes/notes/circuit-initialize-and-prepare-single-qubit-e25dacc8f873bc01.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixed a bug in :meth:`.QuantumCircuit.initialize` and :meth:`.QuantumCircuit.prepare_state` + that caused them to not accept a single :class:`Qubit` as argument to initialize. diff --git a/test/python/circuit/library/test_state_preparation.py b/test/python/circuit/library/test_state_preparation.py index 430d462fa745..f34141ce596c 100644 --- a/test/python/circuit/library/test_state_preparation.py +++ b/test/python/circuit/library/test_state_preparation.py @@ -19,7 +19,7 @@ import numpy as np from ddt import ddt, data -from qiskit import QuantumCircuit +from qiskit import QuantumCircuit, QuantumRegister from qiskit.quantum_info import Statevector, Operator from qiskit.test import QiskitTestCase from qiskit.exceptions import QiskitError @@ -54,6 +54,15 @@ def test_prepare_from_list(self): actual_sv = Statevector(qc) self.assertTrue(desired_sv == actual_sv) + def test_prepare_single_qubit(self): + """Prepare state in single qubit.""" + qreg = QuantumRegister(2) + circuit = QuantumCircuit(qreg) + circuit.prepare_state([1 / math.sqrt(2), 1 / math.sqrt(2)], qreg[1]) + expected = QuantumCircuit(qreg) + expected.prepare_state([1 / math.sqrt(2), 1 / math.sqrt(2)], [qreg[1]]) + self.assertEqual(circuit, expected) + def test_nonzero_state_incorrect(self): """Test final state incorrect if initial state not zero""" desired_sv = Statevector([1 / math.sqrt(2), 0, 0, 1 / math.sqrt(2)]) diff --git a/test/python/circuit/test_registerless_circuit.py b/test/python/circuit/test_registerless_circuit.py index 6a4d0a401744..865b80e78034 100644 --- a/test/python/circuit/test_registerless_circuit.py +++ b/test/python/circuit/test_registerless_circuit.py @@ -230,6 +230,18 @@ def test_circuit_initialize(self): self.assertEqual(circuit, expected) + def test_circuit_initialize_single_qubit(self): + """Test initialize on single qubit.""" + init_vector = [numpy.sqrt(0.5), numpy.sqrt(0.5)] + qreg = QuantumRegister(2) + circuit = QuantumCircuit(qreg) + circuit.initialize(init_vector, qreg[0]) + + expected = QuantumCircuit(qreg) + expected.initialize(init_vector, [qreg[0]]) + + self.assertEqual(circuit, expected) + def test_mixed_register_and_registerless_indexing(self): """Test indexing if circuit contains bits in and out of registers.""" From 9bcc9d8b11abc59e834d95a6faf7e7a093d7d521 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Tue, 16 Aug 2022 20:31:42 +0200 Subject: [PATCH 46/82] Detailed explanation of circuit parameter order in docs (#8404) * Detailed explanation of circuit parameter order * try to fix sphinx 1/? * rm return type doc Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/circuit/quantumcircuit.py | 94 ++++++++++++++++++++++++++------ 1 file changed, 77 insertions(+), 17 deletions(-) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 2254a9b0ce7a..381d9cba68b3 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -2499,7 +2499,63 @@ def global_phase(self, angle: ParameterValueType): @property def parameters(self) -> ParameterView: - """Convenience function to get the parameters defined in the parameter table.""" + """The parameters defined in the circuit. + + This attribute returns the :class:`.Parameter` objects in the circuit sorted + alphabetically. Note that parameters instantiated with a :class:`.ParameterVector` + are still sorted numerically. + + Examples: + + The snippet below shows that insertion order of parameters does not matter. + + .. code-block:: python + + >>> from qiskit.circuit import QuantumCircuit, Parameter + >>> a, b, elephant = Parameter("a"), Parameter("b"), Parameter("elephant") + >>> circuit = QuantumCircuit(1) + >>> circuit.rx(b, 0) + >>> circuit.rz(elephant, 0) + >>> circuit.ry(a, 0) + >>> circuit.parameters # sorted alphabetically! + ParameterView([Parameter(a), Parameter(b), Parameter(elephant)]) + + Bear in mind that alphabetical sorting might be unituitive when it comes to numbers. + The literal "10" comes before "2" in strict alphabetical sorting. + + .. code-block:: python + + >>> from qiskit.circuit import QuantumCircuit, Parameter + >>> angles = [Parameter("angle_1"), Parameter("angle_2"), Parameter("angle_10")] + >>> circuit = QuantumCircuit(1) + >>> circuit.u(*angles, 0) + >>> circuit.draw() + ┌─────────────────────────────┐ + q: ┤ U(angle_1,angle_2,angle_10) ├ + └─────────────────────────────┘ + >>> circuit.parameters + ParameterView([Parameter(angle_1), Parameter(angle_10), Parameter(angle_2)]) + + To respect numerical sorting, a :class:`.ParameterVector` can be used. + + .. code-block:: python + + >>> from qiskit.circuit import QuantumCircuit, Parameter, ParameterVector + >>> x = ParameterVector("x", 12) + >>> circuit = QuantumCircuit(1) + >>> for x_i in x: + ... circuit.rx(x_i, 0) + >>> circuit.parameters + ParameterView([ + ParameterVectorElement(x[0]), ParameterVectorElement(x[1]), + ParameterVectorElement(x[2]), ParameterVectorElement(x[3]), + ..., ParameterVectorElement(x[11]) + ]) + + + Returns: + The sorted :class:`.Parameter` objects in the circuit. + """ # parameters from gates if self._parameters is None: unsorted = self._unsorted_parameters() @@ -2510,7 +2566,7 @@ def parameters(self) -> ParameterView: @property def num_parameters(self) -> int: - """Convenience function to get the number of parameter objects in the circuit.""" + """The number of parameter objects in the circuit.""" return len(self._unsorted_parameters()) def _unsorted_parameters(self) -> Set[Parameter]: @@ -2528,18 +2584,20 @@ def assign_parameters( ) -> Optional["QuantumCircuit"]: """Assign parameters to new parameters or values. - The keys of the parameter dictionary must be Parameter instances in the current circuit. The - values of the dictionary can either be numeric values or new parameter objects. + If ``parameters`` is passed as a dictionary, the keys must be :class:`.Parameter` + instances in the current circuit. The values of the dictionary can either be numeric values + or new parameter objects. + + If ``parameters`` is passed as a list or array, the elements are assigned to the + current parameters in the order of :attr:`parameters` which is sorted + alphabetically (while respecting the ordering in :class:`.ParameterVector` objects). + The values can be assigned to the current circuit object or to a copy of it. Args: - parameters (dict or iterable): Either a dictionary or iterable specifying the new - parameter values. If a dict, it specifies the mapping from ``current_parameter`` to - ``new_parameter``, where ``new_parameter`` can be a new parameter object or a - numeric value. If an iterable, the elements are assigned to the existing parameters - in the order of ``QuantumCircuit.parameters``. - inplace (bool): If False, a copy of the circuit with the bound parameters is - returned. If True the circuit instance itself is modified. + parameters: Either a dictionary or iterable specifying the new parameter values. + inplace: If False, a copy of the circuit with the bound parameters is returned. + If True the circuit instance itself is modified. Raises: CircuitError: If parameters is a dict and contains parameters not present in the @@ -2548,8 +2606,7 @@ def assign_parameters( parameters in the circuit. Returns: - Optional(QuantumCircuit): A copy of the circuit with bound parameters, if - ``inplace`` is False, otherwise None. + A copy of the circuit with bound parameters, if ``inplace`` is False, otherwise None. Examples: @@ -2572,7 +2629,7 @@ def assign_parameters( print('Assigned in-place:') print(circuit.draw()) - Bind the values out-of-place and get a copy of the original circuit. + Bind the values out-of-place by list and get a copy of the original circuit. .. jupyter-execute:: @@ -2583,7 +2640,7 @@ def assign_parameters( circuit.ry(params[0], 0) circuit.crx(params[1], 0, 1) - bound_circuit = circuit.assign_parameters({params[0]: 1, params[1]: 2}) + bound_circuit = circuit.assign_parameters([1, 2]) print('Bound circuit:') print(bound_circuit.draw()) @@ -2638,18 +2695,21 @@ def bind_parameters( ) -> "QuantumCircuit": """Assign numeric parameters to values yielding a new circuit. + If the values are given as list or array they are bound to the circuit in the order + of :attr:`parameters` (see the docstring for more details). + To assign new Parameter objects or bind the values in-place, without yielding a new circuit, use the :meth:`assign_parameters` method. Args: - values (dict or iterable): {parameter: value, ...} or [value1, value2, ...] + values: ``{parameter: value, ...}`` or ``[value1, value2, ...]`` Raises: CircuitError: If values is a dict and contains parameters not present in the circuit. TypeError: If values contains a ParameterExpression. Returns: - QuantumCircuit: copy of self with assignment substitution. + Copy of self with assignment substitution. """ if isinstance(values, dict): if any(isinstance(value, ParameterExpression) for value in values.values()): From f2d228ecd288c928d074b1f61702b96eb5b66e87 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 16 Aug 2022 16:01:43 -0400 Subject: [PATCH 47/82] Remove stale dev dependencies from requirements-dev.txt (#8501) * Remove stale dev dependencies from requirements-dev.txt In looking at the requirements-dev.txt list as part of #8498 there were a few entries which were not needed and/or are being used anymore. This commit cleans those up and also removes an unused script for reporting ci failures. The report script has been completed supersceded by native comment actions in azure pipelines and github actions so we no longer need to manually call the github api to report CI failures on periodic jobs. * Use pip to install extension for lint * Update CI config to avoid explicit setup.py usage where not needed Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .azure/lint-linux.yml | 2 +- .azure/test-linux.yml | 2 + .azure/test-macos.yml | 1 - .azure/test-windows.yml | 1 - .azure/tutorials-linux.yml | 1 - requirements-dev.txt | 3 - tools/report_ci_failure.py | 158 ------------------------------------- 7 files changed, 3 insertions(+), 165 deletions(-) delete mode 100644 tools/report_ci_failure.py diff --git a/.azure/lint-linux.yml b/.azure/lint-linux.yml index 5fddd6afef3f..f184ccd7470f 100644 --- a/.azure/lint-linux.yml +++ b/.azure/lint-linux.yml @@ -34,7 +34,7 @@ jobs: pip install -U -r requirements.txt -r requirements-dev.txt -c constraints.txt pip install -U -c constraints.txt -e . pip install -U "qiskit-aer" -c constraints.txt - python setup.py build_ext --inplace + pip install -e . displayName: 'Install dependencies' env: SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" diff --git a/.azure/test-linux.yml b/.azure/test-linux.yml index 14d1c090f822..57b18e698373 100644 --- a/.azure/test-linux.yml +++ b/.azure/test-linux.yml @@ -71,6 +71,8 @@ jobs: set -e source test-job/bin/activate pip install -U -r requirements.txt -r requirements-dev.txt -c constraints.txt + # Install setuptools-rust for building sdist + pip install -U -c constraints.txt setuptools-rust python setup.py sdist pip install -U -c constraints.txt dist/qiskit-terra*.tar.gz displayName: "Install Terra from sdist" diff --git a/.azure/test-macos.yml b/.azure/test-macos.yml index d7d98c3ef3be..895197098f5e 100644 --- a/.azure/test-macos.yml +++ b/.azure/test-macos.yml @@ -46,7 +46,6 @@ jobs: source test-job/bin/activate pip install -U -r requirements.txt -r requirements-dev.txt -c constraints.txt pip install -U -c constraints.txt -e . - python setup.py build_ext --inplace pip check displayName: 'Install dependencies' env: diff --git a/.azure/test-windows.yml b/.azure/test-windows.yml index 8140a6ce4fbb..31cca7ba2ed6 100644 --- a/.azure/test-windows.yml +++ b/.azure/test-windows.yml @@ -36,7 +36,6 @@ jobs: pip install -r requirements.txt -r requirements-dev.txt -c constraints.txt pip install -c constraints.txt -e . pip install "z3-solver" -c constraints.txt - python setup.py build_ext --inplace pip check displayName: 'Install dependencies' env: diff --git a/.azure/tutorials-linux.yml b/.azure/tutorials-linux.yml index 2ce7cb9026c8..fe49db79bf1d 100644 --- a/.azure/tutorials-linux.yml +++ b/.azure/tutorials-linux.yml @@ -34,7 +34,6 @@ jobs: pip install -U -r requirements.txt -r requirements-dev.txt -c constraints.txt pip install -c constraints.txt -e . pip install "qiskit-ibmq-provider" "qiskit-aer" "z3-solver" "qiskit-ignis" "matplotlib>=3.3.0" sphinx nbsphinx sphinx_rtd_theme cvxpy -c constraints.txt - python setup.py build_ext --inplace sudo apt-get update sudo apt-get install -y graphviz pandoc pip check diff --git a/requirements-dev.txt b/requirements-dev.txt index fb5e2cbeae18..72da159e4ff5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,3 @@ -setuptools-rust<1.5.0 coverage>=4.4.0 hypothesis>=4.24.3 python-constraint>=1.4 @@ -13,8 +12,6 @@ pydot astroid==2.5.6 pylint==2.8.3 stestr>=2.0.0 -PyGithub -wheel pylatexenc>=1.4 ddt>=1.2.0,!=1.4.0,!=1.4.3 seaborn>=0.9.0 diff --git a/tools/report_ci_failure.py b/tools/report_ci_failure.py deleted file mode 100644 index b9be8243560d..000000000000 --- a/tools/report_ci_failure.py +++ /dev/null @@ -1,158 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2018. -# -# 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. -"""Utility module to open an issue on the repository when CIs fail.""" - -import os -import re - -from github import Github - - -class CIFailureReporter: - """Instances of this class can report to GitHub that the CI is failing.""" - - stable_branch_regex = re.compile(r"^stable/\d+\.\d+") - - def __init__(self, repository, token): - """ - Args: - repository (str): a string in the form 'owner/repository-name' - indicating the GitHub repository to report against. - token (str): a GitHub token obtained following: - https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/ - """ - self._repo = repository - self._api = Github(token) - - def report(self, branch, commit, infourl=None, job_name=None): - """Report on GitHub that the specified branch is failing to build at - the specified commit. The method will open an issue indicating that - the branch is failing. If there is an issue already open, it will add a - comment avoiding to report twice about the same failure. - - Args: - branch (str): branch name to report about. - commit (str): commit hash at which the build fails. - infourl (str): URL with extra info about the failure such as the - build logs. - job_name (str): name of the failed ci job. - """ - if branch != "main" and not self.stable_branch_regex.search(branch): - return - key_label = self._key_label(branch, job_name) - issue_number = self._get_report_issue_number(key_label) - if issue_number: - self._report_as_comment(issue_number, branch, commit, infourl) - else: - self._report_as_issue(branch, commit, infourl, job_name) - - def _key_label(self, branch_name, job_name): - if job_name == "Randomized tests": - return "randomized test" - elif job_name == "Benchmarks": - return "benchmarks failing" - elif branch_name == "main": - return "main failing" - elif branch_name.startswith("stable/"): - return "stable failing" - else: - return "" - - def _get_report_issue_number(self, key_label): - query = f'state:open label:"{key_label}" repo:{self._repo}' - results = self._api.search_issues(query=query) - try: - return results[0].number - except IndexError: - return None - - def _report_as_comment(self, issue_number, branch, commit, infourl): - stamp = _branch_is_failing_stamp(branch, commit) - report_exists = self._check_report_existence(issue_number, stamp) - if not report_exists: - _, body = _branch_is_failing_template(branch, commit, infourl) - message_body = f"{stamp}\n{body}" - self._post_new_comment(issue_number, message_body) - - def _check_report_existence(self, issue_number, target): - repo = self._api.get_repo(self._repo) - issue = repo.get_issue(issue_number) - if target in issue.body: - return True - - for comment in issue.get_comments(): - if target in comment.body: - return True - - return False - - def _report_as_issue(self, branch, commit, infourl, key_label): - repo = self._api.get_repo(self._repo) - stamp = _branch_is_failing_stamp(branch, commit) - title, body = _branch_is_failing_template(branch, commit, infourl) - message_body = f"{stamp}\n{body}" - repo.create_issue(title=title, body=message_body, labels=[key_label]) - - def _post_new_comment(self, issue_number, body): - repo = self._api.get_repo(self._repo) - issue = repo.get_issue(issue_number) - issue.create_comment(body) - - -def _branch_is_failing_template(branch, commit, infourl): - title = f"Branch `{branch}` is failing" - body = f"Trying to build `{branch}` at commit {commit} failed." - if infourl: - body += f"\nMore info at: {infourl}" - return title, body - - -def _branch_is_failing_stamp(branch, commit): - return f"" - - -_REPOSITORY = "Qiskit/qiskit-terra" -_GH_TOKEN = os.getenv("GH_TOKEN") - - -def _get_repo_name(): - return os.getenv("TRAVIS_REPO_SLUG") or os.getenv("APPVEYOR_REPO_NAME") - - -def _get_branch_name(): - return os.getenv("TRAVIS_BRANCH") or os.getenv("APPVEYOR_REPO_BRANCH") - - -def _get_commit_hash(): - return os.getenv("TRAVIS_COMMIT") or os.getenv("APPVEYOR_REPO_COMMIT") - - -def _get_job_name(): - return os.getenv("TRAVIS_JOB_NAME") or os.getenv("APPVEYOR_JOB_NAME") - - -def _get_info_url(): - if os.getenv("TRAVIS"): - job_id = os.getenv("TRAVIS_JOB_ID") - return f"https://travis-ci.com/{_REPOSITORY}/jobs/{job_id}" - - if os.getenv("APPVEYOR"): - build_id = os.getenv("APPVEYOR_BUILD_ID") - return f"https://ci.appveyor.com/project/{_REPOSITORY}/build/{build_id}" - - return None - - -if __name__ == "__main__": - if os.getenv("TRAVIS_EVENT_TYPE", "") == "push": - _REPORTER = CIFailureReporter(_get_repo_name(), _GH_TOKEN) - _REPORTER.report(_get_branch_name(), _get_commit_hash(), _get_info_url(), _get_job_name()) From 74c27f831ccd966965caff6e6892c6528c08f35b Mon Sep 17 00:00:00 2001 From: Guillermo-Mijares-Vilarino <106545082+Guillermo-Mijares-Vilarino@users.noreply.github.com> Date: Tue, 16 Aug 2022 23:39:01 +0200 Subject: [PATCH 48/82] Added Hinton explanation (#8412) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/visualization/state_visualization.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/qiskit/visualization/state_visualization.py b/qiskit/visualization/state_visualization.py index e4491a4eaf16..0f91ee078e42 100644 --- a/qiskit/visualization/state_visualization.py +++ b/qiskit/visualization/state_visualization.py @@ -43,6 +43,11 @@ def plot_state_hinton( ): """Plot a hinton diagram for the density matrix of a quantum state. + The hinton diagram represents the values of a matrix using + squares, whose size indicate the magnitude of their corresponding value + and their color, its sign. A white square means the value is positive and + a black one means negative. + Args: state (Statevector or DensityMatrix or ndarray): An N-qubit quantum state. title (str): a string that represents the plot title From 074d2ded68cf90b164fabb132d5e5b5c27983eb9 Mon Sep 17 00:00:00 2001 From: choerst-ibm <62256845+choerst-ibm@users.noreply.github.com> Date: Wed, 17 Aug 2022 01:13:59 +0200 Subject: [PATCH 49/82] Fix typo in qasm3 exporter docstring (#8557) Co-authored-by: Jake Lishman --- qiskit/qasm3/exporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index 74452ddde4f9..e0a956d7bfae 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -110,7 +110,7 @@ class Exporter: - """QASM3 expoter main class.""" + """QASM3 exporter main class.""" def __init__( self, From 844d426ba684373d013c3a409df65647cbf15585 Mon Sep 17 00:00:00 2001 From: Guillermo-Mijares-Vilarino <106545082+Guillermo-Mijares-Vilarino@users.noreply.github.com> Date: Wed, 17 Aug 2022 05:14:57 +0200 Subject: [PATCH 50/82] Changed plot state city code example in API reference and added another one (#8352) * changed plot_state_city code examples in API reference to better showcase the different arguments * removed refernce to Aer and transpile * removed extra imports and matplotlib inline * Added comments and removed figsize * unwrap comments and codes, remove spaces around = in function arguments, import order * reduced length to 100 Co-authored-by: Junye Huang Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/visualization/state_visualization.py | 28 +++++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/qiskit/visualization/state_visualization.py b/qiskit/visualization/state_visualization.py index 0f91ee078e42..215397633e86 100644 --- a/qiskit/visualization/state_visualization.py +++ b/qiskit/visualization/state_visualization.py @@ -365,21 +365,39 @@ def plot_state_city( ValueError: When 'color' is not a list of len=2. VisualizationError: if input is not a valid N-qubit state. - Example: + Examples: .. jupyter-execute:: + # You can choose different colors for the real and imaginary parts of the density matrix. + from qiskit import QuantumCircuit from qiskit.quantum_info import DensityMatrix from qiskit.visualization import plot_state_city - %matplotlib inline qc = QuantumCircuit(2) qc.h(0) qc.cx(0, 1) - state = DensityMatrix.from_instruction(qc) - plot_state_city(state, color=['midnightblue', 'midnightblue'], - title="New State City") + state = DensityMatrix(qc) + plot_state_city(state, color=['midnightblue', 'crimson'], title="New State City") + + .. jupyter-execute:: + + # You can make the bars more transparent to better see the ones that are behind + # if they overlap. + + import numpy as np + from qiskit.quantum_info import Statevector + + qc = QuantumCircuit(2) + qc.h([0, 1]) + qc.cz(0,1) + qc.ry(np.pi/3, 0) + qc.rx(np.pi/5, 1) + + state = Statevector(qc) + plot_state_city(state, alpha=0.6) + """ from matplotlib import pyplot as plt from mpl_toolkits.mplot3d.art3d import Poly3DCollection From 14a0aaf8364e47b5c24761bcd48a7d5252ed2a8b Mon Sep 17 00:00:00 2001 From: Adenilton Silva <7927558+adjs@users.noreply.github.com> Date: Wed, 17 Aug 2022 02:48:58 -0300 Subject: [PATCH 51/82] Remove obsolete global_phase hack in UnitaryGate.control (#8537) * remove global phase hack * Create remove_hack-7f2c765e5d8d8799.yaml * Remove release note Co-authored-by: Jake Lishman Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/extensions/unitary.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/qiskit/extensions/unitary.py b/qiskit/extensions/unitary.py index ff166044e82a..b47bd80c7017 100644 --- a/qiskit/extensions/unitary.py +++ b/qiskit/extensions/unitary.py @@ -156,7 +156,7 @@ def control(self, num_ctrl_qubits=1, label=None, ctrl_state=None): mat = self.to_matrix() cmat = _compute_control_matrix(mat, num_ctrl_qubits, ctrl_state=None) iso = isometry.Isometry(cmat, 0, 0) - cunitary = ControlledGate( + return ControlledGate( "c-unitary", num_qubits=self.num_qubits + num_ctrl_qubits, params=[mat], @@ -166,18 +166,6 @@ def control(self, num_ctrl_qubits=1, label=None, ctrl_state=None): ctrl_state=ctrl_state, base_gate=self.copy(), ) - from qiskit.quantum_info import Operator - - # hack to correct global phase; should fix to prevent need for correction here - pmat = Operator(iso.inverse()).data @ cmat - diag = numpy.diag(pmat) - if not numpy.allclose(diag, diag[0]): - raise ExtensionError("controlled unitary generation failed") - phase = numpy.angle(diag[0]) - if phase: - # need to apply to _definition since open controls creates temporary definition - cunitary._definition.global_phase = phase - return cunitary def qasm(self): """The qasm for a custom unitary gate From a2ae2ed1d3af5b6a70da97b9e0bd427337521d95 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Wed, 17 Aug 2022 22:40:48 +0300 Subject: [PATCH 52/82] Fix internal transpiler error in CommutativeCancellation with classical conditions (#8556) * Bug fix related to issue 8553 * Shrink regression test to minimal reproducer * Correct comments after condition change Co-authored-by: Jake Lishman Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/circuit/commutation_checker.py | 13 ++++++++----- test/python/circuit/test_commutation_checker.py | 8 +++----- .../transpiler/test_commutative_cancellation.py | 11 +++++++++++ 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/qiskit/circuit/commutation_checker.py b/qiskit/circuit/commutation_checker.py index 4bc61ee33561..a975e20aec9b 100644 --- a/qiskit/circuit/commutation_checker.py +++ b/qiskit/circuit/commutation_checker.py @@ -80,6 +80,11 @@ def commute( Returns: bool: whether two operations commute. """ + # We don't support commutation of conditional gates for now due to bugs in + # CommutativeCancellation. See gh-8553. + if getattr(op1, "condition") is not None or getattr(op2, "condition") is not None: + return False + # These lines are adapted from dag_dependency and say that two gates over # different quantum and classical bits necessarily commute. This is more # permissive that the check from commutation_analysis, as for example it @@ -91,15 +96,13 @@ def commute( if not (intersection_q or intersection_c): return True - # These lines are adapted from commutation_analysis, which is more restrictive - # than the check from dag_dependency when considering nodes with "_directive" - # or "condition". It would be nice to think which optimizations - # from dag_dependency can indeed be used. + # These lines are adapted from commutation_analysis, which is more restrictive than the + # check from dag_dependency when considering nodes with "_directive". It would be nice to + # think which optimizations from dag_dependency can indeed be used. for op in [op1, op2]: if ( getattr(op, "_directive", False) or op.name in {"measure", "reset", "delay"} - or getattr(op, "condition", None) or op.is_parameterized() ): return False diff --git a/test/python/circuit/test_commutation_checker.py b/test/python/circuit/test_commutation_checker.py index 9db5de33ff0f..c44252a64f3e 100644 --- a/test/python/circuit/test_commutation_checker.py +++ b/test/python/circuit/test_commutation_checker.py @@ -311,15 +311,13 @@ def test_conditional_gates(self): qr = QuantumRegister(3) cr = ClassicalRegister(2) - # Different quantum bits (and empty classical bits). - # We should be able to swap these. + # Currently, in all cases commutativity checker should returns False. + # This is definitely suboptimal. res = comm_checker.commute( CXGate().c_if(cr[0], 0), [qr[0], qr[1]], [], XGate(), [qr[2]], [] ) - self.assertTrue(res) + self.assertFalse(res) - # In all other cases, commutativity checker currently returns False. - # This is definitely suboptimal. res = comm_checker.commute( CXGate().c_if(cr[0], 0), [qr[0], qr[1]], [], XGate(), [qr[1]], [] ) diff --git a/test/python/transpiler/test_commutative_cancellation.py b/test/python/transpiler/test_commutative_cancellation.py index 86e166ffded9..2d513951a2de 100644 --- a/test/python/transpiler/test_commutative_cancellation.py +++ b/test/python/transpiler/test_commutative_cancellation.py @@ -621,6 +621,17 @@ def test_basis_global_phase_03(self): ccirc = passmanager.run(circ) self.assertEqual(Operator(circ), Operator(ccirc)) + def test_basic_classical_wires(self): + """Test that transpile runs without internal errors when dealing with commutable operations + with classical controls. Regression test for gh-8553.""" + original = QuantumCircuit(2, 1) + original.x(0).c_if(original.cregs[0], 0) + original.x(1).c_if(original.cregs[0], 0) + # This transpilation shouldn't change anything, but it should succeed. At one point it was + # triggering an internal logic error and crashing. + transpiled = PassManager([CommutativeCancellation()]).run(original) + self.assertEqual(original, transpiled) + if __name__ == "__main__": unittest.main() From a0964c10aae11c490157fb8992108078229891a1 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 18 Aug 2022 13:34:08 +0100 Subject: [PATCH 53/82] Fix QPY serialisation of ControlledGate with open controls (#8571) * Fix QPY serialisation of ControlledGate with open controls Previously, an incorrect definition and name would be re-instated on QPY deserialisation of a `ControlledGate` instance with open controls. The base name would include the dynamic `_o{ctrl_state}` suffix, causing the suffix to later be duplicated, and the definition would duplicate the logic that added the open controls. This fixes both by stripping the suffix on re-read before it is assigned, and serialising only the "11...1" state definition, since this is what is required and stored by `ControlledGate`. * Add QPY backwards compatibility test Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/qpy/binary_io/circuits.py | 16 +++++++++--- ...ledgate-open-control-35c8ccb4c7466f4c.yaml | 6 +++++ .../circuit/test_circuit_load_from_qpy.py | 11 ++++++++ test/qpy_compat/test_qpy.py | 26 +++++++++++++++++++ 4 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/fix-qpy-controlledgate-open-control-35c8ccb4c7466f4c.yaml diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index ff30f00e21c4..49ca334e4a33 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -320,6 +320,9 @@ def _parse_custom_operation(custom_operations, gate_name, params, version, vecto base_gate = _read_instruction( base_gate_obj, None, registers, custom_operations, version, vectors ) + if ctrl_state < 2**num_ctrl_qubits - 1: + # If open controls, we need to discard the control suffix when setting the name. + gate_name = gate_name.rsplit("_", 1)[0] inst_obj = ControlledGate( gate_name, num_qubits, @@ -623,14 +626,21 @@ def _write_custom_operation(file_obj, name, operation, custom_operations): has_definition = True data = common.data_to_binary(operation, _write_pauli_evolution_gate) size = len(data) - elif operation.definition is not None: + elif type_key == type_keys.CircuitInstruction.CONTROLLED_GATE: + # For ControlledGate, we have to access and store the private `_definition` rather than the + # public one, because the public one is mutated to include additional logic if the control + # state is open, and the definition setter (during a subsequent read) uses the "fully + # excited" control definition only. has_definition = True - data = common.data_to_binary(operation.definition, write_circuit) + data = common.data_to_binary(operation._definition, write_circuit) size = len(data) - if type_key == type_keys.CircuitInstruction.CONTROLLED_GATE: num_ctrl_qubits = operation.num_ctrl_qubits ctrl_state = operation.ctrl_state base_gate = operation.base_gate + elif operation.definition is not None: + has_definition = True + data = common.data_to_binary(operation.definition, write_circuit) + size = len(data) if base_gate is None: base_gate_raw = b"" else: diff --git a/releasenotes/notes/fix-qpy-controlledgate-open-control-35c8ccb4c7466f4c.yaml b/releasenotes/notes/fix-qpy-controlledgate-open-control-35c8ccb4c7466f4c.yaml new file mode 100644 index 000000000000..d7a45238b2f4 --- /dev/null +++ b/releasenotes/notes/fix-qpy-controlledgate-open-control-35c8ccb4c7466f4c.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixed QPY serialisation and deserialisation of :class:`.ControlledGate` + with open controls (*i.e.* those whose ``ctrl_state`` is not all ones). + Fixed `#8549 `__. diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 8292cee07780..9cc5f277580c 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -998,6 +998,17 @@ def test_controlled_gate(self): new_circuit = load(qpy_file)[0] self.assertEqual(qc, new_circuit) + def test_controlled_gate_open_controls(self): + """Test a controlled gate with open controls round-trips exactly.""" + qc = QuantumCircuit(3) + controlled_gate = DCXGate().control(1, ctrl_state=0) + qc.append(controlled_gate, [0, 1, 2]) + qpy_file = io.BytesIO() + dump(qc, qpy_file) + qpy_file.seek(0) + new_circuit = load(qpy_file)[0] + self.assertEqual(qc, new_circuit) + def test_nested_controlled_gate(self): """Test a custom nested controlled gate.""" custom_gate = Gate("black_box", 1, []) diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index 8c23af7fdb0f..418202f59d49 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -488,6 +488,30 @@ def generate_controlled_gates(): return circuits +def generate_open_controlled_gates(): + """Test QPY serialization with custom ControlledGates with open controls.""" + circuits = [] + qc = QuantumCircuit(3) + controlled_gate = DCXGate().control(1, ctrl_state=0) + qc.append(controlled_gate, [0, 1, 2]) + circuits.append(qc) + + custom_gate = Gate("black_box", 1, []) + custom_definition = QuantumCircuit(1) + custom_definition.h(0) + custom_definition.rz(1.5, 0) + custom_definition.sdg(0) + custom_gate.definition = custom_definition + nested_qc = QuantumCircuit(3) + nested_qc.append(custom_gate, [0]) + controlled_gate = custom_gate.control(2, ctrl_state=1) + nested_qc.append(controlled_gate, [0, 1, 2]) + nested_qc.measure_all() + circuits.append(nested_qc) + + return circuits + + def generate_circuits(version_str=None): """Generate reference circuits.""" version_parts = None @@ -525,6 +549,8 @@ def generate_circuits(version_str=None): output_circuits["controlled_gates.qpy"] = generate_controlled_gates() output_circuits["schedule_blocks.qpy"] = generate_schedule_blocks() output_circuits["pulse_gates.qpy"] = generate_calibrated_circuits() + if version_parts >= (0, 21, 2): + output_circuits["open_controlled_gates.qpy"] = generate_open_controlled_gates() return output_circuits From 26ba58ad54a23b4246553980476c4a27002e320f Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 18 Aug 2022 14:55:39 +0100 Subject: [PATCH 54/82] Fix QuantumCircuit.compose within control-flow scopes (#8570) * Fix QuantumCircuit.compose within control-flow scopes Previously, `QuantumCircuit.compose` with `inplace=True` would insert the appended instructions outside the scope due to unchecked use of `QuantumCircuit._append` (and direct modification of the `data` attribute). This now respects any active control-flow scopes. Using `front=True` in conjunction with `inplace=True` within a control-flow scope has no clear meaning, and so the behaviour is forbidden. Emitting a new circuit (`inplace=False`) while in an active scope would produce a broken circuit - the active scope would need to be cloned to ensure equivalence, but it wouldn't be part of any Python-managed context, so the scope could never be closed. * Finish writing documentation sentence * Add missing docstring in test * Add missing Sphinx directive Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/circuit/quantumcircuit.py | 33 ++++++-- ...-control-flow-scopes-a8aad3b87efbe77c.yaml | 7 ++ .../circuit/test_control_flow_builders.py | 83 +++++++++++++++++++ 3 files changed, 117 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/fix-QuantumCircuit.compose-in-control-flow-scopes-a8aad3b87efbe77c.yaml diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 381d9cba68b3..9d6cf84c86cb 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -835,7 +835,8 @@ def compose( this can be anything that :obj:`.append` will accept. qubits (list[Qubit|int]): qubits of self to compose onto. clbits (list[Clbit|int]): clbits of self to compose onto. - front (bool): If True, front composition will be performed (not implemented yet). + front (bool): If True, front composition will be performed. This is not possible within + control-flow builder context managers. inplace (bool): If True, modify the object. Otherwise return composed circuit. wrap (bool): If True, wraps the other circuit into a gate (or instruction, depending on whether it contains only unitary instructions) before composing it onto self. @@ -844,12 +845,18 @@ def compose( QuantumCircuit: the composed circuit (returns None if inplace==True). Raises: - CircuitError: if composing on the front. - QiskitError: if ``other`` is wider or there are duplicate edge mappings. + CircuitError: if no correct wire mapping can be made between the two circuits, such as + if ``other`` is wider than ``self``. + CircuitError: if trying to emit a new circuit while ``self`` has a partially built + control-flow context active, such as the context-manager forms of :meth:`if_test`, + :meth:`for_loop` and :meth:`while_loop`. + CircuitError: if trying to compose to the front of a circuit when a control-flow builder + block is active; there is no clear meaning to this action. - Examples:: + Examples: + .. code-block:: python - lhs.compose(rhs, qubits=[3, 2], inplace=True) + >>> lhs.compose(rhs, qubits=[3, 2], inplace=True) .. parsed-literal:: @@ -869,6 +876,19 @@ def compose( lcr_1: 0 ═══════════ lcr_1: 0 ═══════════════════════ """ + if inplace and front and self._control_flow_scopes: + # If we're composing onto ourselves while in a stateful control-flow builder context, + # there's no clear meaning to composition to the "front" of the circuit. + raise CircuitError( + "Cannot compose to the front of a circuit while a control-flow context is active." + ) + if not inplace and self._control_flow_scopes: + # If we're inside a stateful control-flow builder scope, even if we successfully cloned + # the partial builder scope (not simple), the scope wouldn't be controlled by an active + # `with` statement, so the output circuit would be permanently broken. + raise CircuitError( + "Cannot emit a new composed circuit while a control-flow context is active." + ) if inplace: dest = self @@ -962,8 +982,9 @@ def compose( mapped_instrs += dest.data dest.data.clear() dest._parameter_table.clear() + append = dest._control_flow_scopes[-1].append if dest._control_flow_scopes else dest._append for instr in mapped_instrs: - dest._append(instr) + append(instr) for gate, cals in other.calibrations.items(): dest._calibrations[gate].update(cals) diff --git a/releasenotes/notes/fix-QuantumCircuit.compose-in-control-flow-scopes-a8aad3b87efbe77c.yaml b/releasenotes/notes/fix-QuantumCircuit.compose-in-control-flow-scopes-a8aad3b87efbe77c.yaml new file mode 100644 index 000000000000..c6674cf888df --- /dev/null +++ b/releasenotes/notes/fix-QuantumCircuit.compose-in-control-flow-scopes-a8aad3b87efbe77c.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + :meth:`.QuantumCircuit.compose` will now function correctly when used with + the ``inplace=True`` argument within control-flow builder contexts. + Previously the instructions would be added outside the control-flow scope. + Fixed `#8433 `__. diff --git a/test/python/circuit/test_control_flow_builders.py b/test/python/circuit/test_control_flow_builders.py index 369237684aac..0452ef8a701e 100644 --- a/test/python/circuit/test_control_flow_builders.py +++ b/test/python/circuit/test_control_flow_builders.py @@ -2062,6 +2062,64 @@ def test_copy_of_instruction_parameters(self): self.assertEqual(while_body, copy.copy(while_body)) self.assertEqual(while_body, copy.deepcopy(while_body)) + def test_inplace_compose_within_builder(self): + """Test that QuantumCircuit.compose used in-place works as expected within control-flow + scopes.""" + inner = QuantumCircuit(1) + inner.x(0) + + base = QuantumCircuit(1, 1) + base.h(0) + base.measure(0, 0) + + with self.subTest("if"): + outer = base.copy() + with outer.if_test((outer.clbits[0], 1)): + outer.compose(inner, inplace=True) + + expected = base.copy() + with expected.if_test((expected.clbits[0], 1)): + expected.x(0) + + self.assertCircuitsEquivalent(outer, expected) + + with self.subTest("else"): + outer = base.copy() + with outer.if_test((outer.clbits[0], 1)) as else_: + outer.compose(inner, inplace=True) + with else_: + outer.compose(inner, inplace=True) + + expected = base.copy() + with expected.if_test((expected.clbits[0], 1)) as else_: + expected.x(0) + with else_: + expected.x(0) + + self.assertCircuitsEquivalent(outer, expected) + + with self.subTest("for"): + outer = base.copy() + with outer.for_loop(range(3)): + outer.compose(inner, inplace=True) + + expected = base.copy() + with expected.for_loop(range(3)): + expected.x(0) + + self.assertCircuitsEquivalent(outer, expected) + + with self.subTest("while"): + outer = base.copy() + with outer.while_loop((outer.clbits[0], 0)): + outer.compose(inner, inplace=True) + + expected = base.copy() + with expected.while_loop((outer.clbits[0], 0)): + expected.x(0) + + self.assertCircuitsEquivalent(outer, expected) + @ddt.ddt class TestControlFlowBuildersFailurePaths(QiskitTestCase): @@ -2458,3 +2516,28 @@ def dummy_requester(resource): ) with self.assertRaisesRegex(TypeError, r"Can only add qubits or classical bits.*"): builder_block.add_bits([bit]) + + def test_compose_front_inplace_invalid_within_builder(self): + """Test that `QuantumCircuit.compose` raises a sensible error when called within a + control-flow builder block.""" + inner = QuantumCircuit(1) + inner.x(0) + + outer = QuantumCircuit(1, 1) + outer.measure(0, 0) + outer.compose(inner, front=True, inplace=True) + with outer.if_test((outer.clbits[0], 1)): + with self.assertRaisesRegex(CircuitError, r"Cannot compose to the front.*"): + outer.compose(inner, front=True, inplace=True) + + def test_compose_new_invalid_within_builder(self): + """Test that `QuantumCircuit.compose` raises a sensible error when called within a + control-flow builder block if trying to emit a new circuit.""" + inner = QuantumCircuit(1) + inner.x(0) + + outer = QuantumCircuit(1, 1) + outer.measure(0, 0) + with outer.if_test((outer.clbits[0], 1)): + with self.assertRaisesRegex(CircuitError, r"Cannot emit a new composed circuit.*"): + outer.compose(inner, inplace=False) From 93e2fc52f98246c84b3079e1655ab2e44d943749 Mon Sep 17 00:00:00 2001 From: Edwin Navarro Date: Fri, 19 Aug 2022 08:49:15 -0700 Subject: [PATCH 55/82] Add barrier labels and display in 3 circuit drawers (#8440) * Add barrier labels to mpl and text drawers * Release note * Add import * Lint * Add barrier labels to latex drawer * Remove utils changes * Cleanup * Fix merge conflict * Lint * Remove label property for barriers and snapshots and increase mpl label font size Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/circuit/barrier.py | 15 +++++++-- qiskit/circuit/quantumcircuit.py | 8 +++-- qiskit/extensions/simulator/snapshot.py | 20 ------------ qiskit/visualization/latex.py | 5 ++- qiskit/visualization/matplotlib.py | 30 +++++++++++++++--- qiskit/visualization/text.py | 11 ++++--- .../add-barrier-label-8e677979cb37461e.yaml | 18 +++++++++++ .../mpl/circuit/references/barrier_label.png | Bin 0 -> 9059 bytes .../circuit/test_circuit_matplotlib_drawer.py | 11 +++++++ .../references/test_latex_barrier_label.tex | 12 +++++++ .../test_latex_plot_barriers_true.tex | 2 +- .../visualization/test_circuit_latex.py | 18 ++++++++++- .../visualization/test_circuit_text_drawer.py | 22 +++++++++++++ 13 files changed, 135 insertions(+), 37 deletions(-) create mode 100644 releasenotes/notes/add-barrier-label-8e677979cb37461e.yaml create mode 100644 test/ipynb/mpl/circuit/references/barrier_label.png create mode 100644 test/python/visualization/references/test_latex_barrier_label.tex diff --git a/qiskit/circuit/barrier.py b/qiskit/circuit/barrier.py index 62ecc04512e4..7081fbcd6604 100644 --- a/qiskit/circuit/barrier.py +++ b/qiskit/circuit/barrier.py @@ -25,9 +25,18 @@ class Barrier(Instruction): _directive = True - def __init__(self, num_qubits): - """Create new barrier instruction.""" - super().__init__("barrier", num_qubits, 0, []) + def __init__(self, num_qubits, label=None): + """Create new barrier instruction. + + Args: + num_qubits (int): the number of qubits for the barrier type [Default: 0]. + label (str): the barrier label + + Raises: + TypeError: if barrier label is invalid. + """ + self._label = label + super().__init__("barrier", num_qubits, 0, [], label=label) def inverse(self): """Special case. Return self.""" diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 9d6cf84c86cb..60ba8b80c6f3 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -2863,10 +2863,14 @@ def _rebind_definition( inner.operation.params[idx] = param.bind({parameter: value}) self._rebind_definition(inner.operation, parameter, value) - def barrier(self, *qargs: QubitSpecifier) -> InstructionSet: + def barrier(self, *qargs: QubitSpecifier, label=None) -> InstructionSet: """Apply :class:`~qiskit.circuit.Barrier`. If qargs is empty, applies to all qubits in the circuit. + Args: + qargs (QubitSpecifier): Specification for one or more qubit arguments. + label (str): The string label of the barrier. + Returns: qiskit.circuit.InstructionSet: handle to the added instructions. """ @@ -2889,7 +2893,7 @@ def barrier(self, *qargs: QubitSpecifier) -> InstructionSet: else: qubits.append(qarg) - return self.append(Barrier(len(qubits)), qubits, []) + return self.append(Barrier(len(qubits), label=label), qubits, []) def delay( self, diff --git a/qiskit/extensions/simulator/snapshot.py b/qiskit/extensions/simulator/snapshot.py index a5806da879e8..434b6da7c344 100644 --- a/qiskit/extensions/simulator/snapshot.py +++ b/qiskit/extensions/simulator/snapshot.py @@ -60,26 +60,6 @@ def snapshot_type(self): """Return snapshot type""" return self._snapshot_type - @property - def label(self): - """Return snapshot label""" - return self._label - - @label.setter - def label(self, name): - """Set snapshot label to name - - Args: - name (str or None): label to assign unitary - - Raises: - TypeError: name is not string or None. - """ - if isinstance(name, str): - self._label = name - else: - raise TypeError("label expects a string") - def c_if(self, classical, val): raise QiskitError("Snapshots are simulator directives and cannot be conditional.") diff --git a/qiskit/visualization/latex.py b/qiskit/visualization/latex.py index 968111352051..2818365a3904 100644 --- a/qiskit/visualization/latex.py +++ b/qiskit/visualization/latex.py @@ -603,7 +603,10 @@ def _build_barrier(self, node, col): first = last = index pos = self._wire_map[self._qubits[first]] self._latex[pos][col - 1] += " \\barrier[0em]{" + str(last - first) + "}" - self._latex[pos][col] = "\\qw" + if node.op.label is not None: + pos = indexes[0] + label = node.op.label.replace(" ", "\\,") + self._latex[pos][col] = "\\cds{0}{^{\\mathrm{%s}}}" % label def _add_controls(self, wire_list, ctrlqargs, ctrl_state, col): """Add one or more controls to a gate""" diff --git a/qiskit/visualization/matplotlib.py b/qiskit/visualization/matplotlib.py index 463a1e5b629c..5f157b42b22d 100644 --- a/qiskit/visualization/matplotlib.py +++ b/qiskit/visualization/matplotlib.py @@ -414,7 +414,9 @@ def _get_layer_widths(self): self._data[node] = {} self._data[node]["width"] = WID num_ctrl_qubits = 0 if not hasattr(op, "num_ctrl_qubits") else op.num_ctrl_qubits - if getattr(op, "_directive", False) or isinstance(op, Measure): + if ( + getattr(op, "_directive", False) and (not op.label or not self._plot_barriers) + ) or isinstance(op, Measure): self._data[node]["raw_gate_text"] = op.name continue @@ -1013,11 +1015,16 @@ def _measure(self, node): def _barrier(self, node): """Draw a barrier""" - for xy in self._data[node]["q_xy"]: + for i, xy in enumerate(self._data[node]["q_xy"]): xpos, ypos = xy + # For the topmost barrier, reduce the rectangle if there's a label to allow for the text. + if i == 0 and node.op.label is not None: + ypos_adj = -0.35 + else: + ypos_adj = 0.0 self._ax.plot( [xpos, xpos], - [ypos + 0.5, ypos - 0.5], + [ypos + 0.5 + ypos_adj, ypos - 0.5], linewidth=self._lwidth1, linestyle="dashed", color=self._style["lc"], @@ -1026,7 +1033,7 @@ def _barrier(self, node): box = self._patches_mod.Rectangle( xy=(xpos - (0.3 * WID), ypos - 0.5), width=0.6 * WID, - height=1, + height=1.0 + ypos_adj, fc=self._style["bc"], ec=None, alpha=0.6, @@ -1035,6 +1042,21 @@ def _barrier(self, node): ) self._ax.add_patch(box) + # display the barrier label at the top if there is one + if i == 0 and node.op.label is not None: + dir_ypos = ypos + 0.65 * HIG + self._ax.text( + xpos, + dir_ypos, + node.op.label, + ha="center", + va="top", + fontsize=self._fs, + color=self._data[node]["tc"], + clip_on=True, + zorder=PORDER_TEXT, + ) + def _gate(self, node, xy=None): """Draw a 1-qubit gate""" if xy is None: diff --git a/qiskit/visualization/text.py b/qiskit/visualization/text.py index 51ca5c9d568f..6fa431a1b2ad 100644 --- a/qiskit/visualization/text.py +++ b/qiskit/visualization/text.py @@ -376,18 +376,18 @@ def __init__(self, label=""): class Barrier(DirectOnQuWire): - """Draws a barrier. + """Draws a barrier with a label at the top if there is one. :: - top: ░ ░ + top: ░ label mid: ─░─ ───░─── bot: ░ ░ """ def __init__(self, label=""): super().__init__("░") - self.top_connect = "░" + self.top_connect = label if label else "░" self.bot_connect = "░" self.top_connector = {} self.bot_connector = {} @@ -1089,9 +1089,10 @@ def add_connected_gate(node, gates, layer, current_cons): if not self.plotbarriers: return layer, current_cons, current_cons_cond, connection_label - for qubit in node.qargs: + for i, qubit in enumerate(node.qargs): if qubit in self.qubits: - layer.set_qubit(qubit, Barrier()) + label = op.label if i == 0 else "" + layer.set_qubit(qubit, Barrier(label)) elif isinstance(op, SwapGate): # swap diff --git a/releasenotes/notes/add-barrier-label-8e677979cb37461e.yaml b/releasenotes/notes/add-barrier-label-8e677979cb37461e.yaml new file mode 100644 index 000000000000..9687f19bcc8e --- /dev/null +++ b/releasenotes/notes/add-barrier-label-8e677979cb37461e.yaml @@ -0,0 +1,18 @@ +--- +features: + - | + Added a ``label`` parameter to the :class:`.Barrier` which now allows + a user to enter a label for the ``barrier`` directive and the label + will be printed at the top of the ``barrier`` in the `mpl`, `latex`, + and `text` circuit drawers. Printing of existing ``snapshot`` labels + to the 3 circuit drawers was also added. + + .. code-block:: python + + from qiskit import QuantumCircuit + + circuit = QuantumCircuit(2) + circuit.h(0) + circuit.h(1) + circuit.barrier(label="After H") + circuit.draw('mpl') diff --git a/test/ipynb/mpl/circuit/references/barrier_label.png b/test/ipynb/mpl/circuit/references/barrier_label.png new file mode 100644 index 0000000000000000000000000000000000000000..c1db46dccfdddaed24bce54d1feef44752a5ec68 GIT binary patch literal 9059 zcmdsdX&{ts+y6xrArui6WhmJqOO&N#8KSag8zR}V@4GfyP>nVFzB5ery|RRi-Pp-C z_Q^JO^FOEid7k^e|IhP2AKwqJFUD!EYtHL9kK_0)$Go|(u0(g7?KlKMba3T68W2Rz z2|;9JR7b(et&itVf?tv@3J+a0?Vq@~KXNjM)E>DwJhgXuYGumlX71!{Wp8&)P)tyS zpVQLC#lcxhNXYhIZxFP1vJko=&Y=KqLhYce=L|tC;D#(G$t)7P!AD)}gx|TPg}R6Q$t+sNPxrMd7)VqtH~<{n6cs0k({*wx z2)Y^ykwZ{0*I$G*e)G;p0S`?w;R!4 zV3eDZauKEaM*r>?2#R?%_}QWjohL=~CLU~s^r3Bs#kpZH%ZAsCC4L8Ze_|DXTX*o8 zYhTBROP@O)>qQ=d#l_{a@knz$uce~qh5h|x`5mtM<7d`^Zxset_!{MhV%mt>L^eH$BmcA#CY31r<#@LlWaYsnn-<=2+^sUp0lEV|Oz`AQFa3ZF4s^ zT=XK-4eV>QB_$=#y$J z$$MCI8*A)e!{3I4&CJd&G_r7={+v`A9lN+? zXVNWv57o>U1~R)GcfQF-Yt8$NnGc_C&8E-_?v2#R+aYD7VD+!tqYmHb^h>NGj?$m!hkO0{a;&ew=tjPA9Rd!ABh!BTxNGFxF17%6 zQnRr0jpHy0L~yZ{52Xc%ztJf$oWI-L8y*#v85~TD+dFHf7x@Gem%TDtWplW{!zSxj zUFnWTpzx#NjB=fqpMObG$<3`1{N@Z77RDF~#>np0T5ar;lKS*hl@h$FbDKcf^`G*{ z-HmxJ_)w*5F3f6cWpsCY>b;of+H~Q4hs{3STpMh`E@9YyQTv{$X-dYu#DZA+TD`In zr*{Q4grO-+k*xurZb2`~|6ox{cB;5XT~k}T&yfq(^%i=r_(0z%ztm=YwUV}DxymU( zpW|Gs!n)G9L`VX0r^6qCQpmz3c-svunmwAH`0@gohN|6FLAo67;5giuU$G#v_V@R_ zu;B>_f^gUV&7VOW*2KeuM~kx7Y!Y^HJ~D|?ZcAD4FdnP=05Xi|gW|U<)G!EQ%PJW4 zsu@UhZhXrug|A-kEwRB))s|XzT@)}Vy<0~-z;BK?cVs6e$=#FLNn2$bZK9%ru-iT| zIEfldW6on|4W^3B+ajlG2_R$3cWc(x)ZitJeYOi%s_-(Kb6xUNO(9|9c&wVl zXj-;rrZuT)thk>P*rq0{7jtxUjJe@nFpG)LEib>0Y9qVYUF++eo}O-niIcn{AmBRY zzqedgV%5Wgt?d}A@s3DLG+x|j<9dHHWwgrOX7qWD3g3QQxT|OUrOnlJl)t z6&*pNMJ(t8-N!sHP38}zE?bpv__}QRXx=w6%AcNoJTpCgnUAk+(J9!Jf+8R{bo49L z#I%Zvis+-S6r&zfA&4l`-$41k*mhXwz07VF*4*QHmC1iZ(IP7502`1#pg+x?JtAiW z(8cf+3ri*?&1ny#qXC!eQJ4RO8(vnZfLBNe3-Xg2-WDZ9?_EmS$)vDL^&D*2$ZgOZ zd7J+IyN%T&8XMBjlahbmi}onUIYAsNwD6UQHbwu}uSeQ+bw=7)TCG0+@oW=?#y1_P zJgq*IVi2ZoMx3%v@@bX^r&FK*wpo@tik-hI$*->B*qFyPmv*ru%nwVeKk`v5yoV;y z-Q60XV_&&;O>=pugw$cBTo=*+3R$)qN$9>(uuD=+riA$0 zC&wuaO}Tkm{q5c68bi;ajT1C+6<_4!o>|>f9rwM|*xV-TSk2!iqQ&drM8Oa>Jv}{F zI&9bT?8q?~9F%l#{j(!tUK^IAEU+G}EP>5`R65_KDpWIV7!?`WL{G`G{TaV=DDf@R zi8(KicF(I2t??`~L$y?ZoBNet7)8nll)I4Lfe1I8Fqw{)MR7R@8KzCl3f;zzxMt>k z9y*iDm_<+inr6&rT}^ub*EeKZbTlhK1qE-y(gOhBPugSB72cgD!QzXfuL5QL_lC!% zux|LV(Xg1<*iQREy=0af?&r2*lcl{2ip?@%E&leAd1hs#H_>9I{SOpXWM&H-^=9m+>@1Fs!mITwI|9N z^YZfU7Bp4c>g&g}IK_$?<7xFi`NH4?q&ZrG`DLgC^LIqG7V`1tynykzOhVtl2!O@j zUajXxM?1C_JDczP0wlz?>8}4ry ziRBizSzTS#ul14i4q#;vXa4;7(^7|cV5rUAnr%FrK<+tBp^$74z8Fd83=)(9F5zLM zu5}^c4yUZF@jDiwNLYGWnk8UTmuhAQUR1*7Nf3iAFtU-h$6?}IF&Ldp^>`*wy@cgq zo4L;9-H8|UBfPbr(E8Yi<3(mcIn85y%L5m9i`5&fdxokep(sgB;V((#_hwoJ`S<-6 z(v{qIjvC<|U&)P9aWZ=Re8mzsMe3y>vu@6ldO60i%x;v4o!xsxa%X2}cd)}R1Avw~ zNPKI6@r0Gp+*dR#SGc(3KU;LTr610=Mh)uf;DbBg)p&Embx?fMi|*+bVq_s-KtzbO zshYx7ZhTi){l8@(0y2W*@3UWKrM)5~)Zr=Zv;E7<$o<#EEnIo&CI^WW{|sbz0e`1w zW-j*W8uwAM7$!|lno9qwKN2JDUAj`cXGg-KC_^WBgZ~LQ+SZ`4v9Z9XUpT$5ufT8Y z{S3Q91o~#k@ZQ-s}Y~Ca+w!BZt)3d^^?!XP+or|hvTNV9rc(BJ+ z&;k?M9gFHxb*sBeIqoau(v}Y+j=Hy45BGZrXt-=vGkK0jMMW*<1g%T~)0uuM86O_5g}-`VOKbWpw)Ajn2bYKqJ$EBKIywu0>wMm4 z43fz@pzd%Jb>;Hq&SM-tNxYgF*L!3?UJ(@3F(~tY81GkU%HqBLkYJ+lid?9ciI5Oq zcKeWc#=JUmy+hV`mtZh8UUq@!ZZyNHg_gFq)nb1>^7h_julUz3v+3DcMZ>TM=QnFW zR`%g*S-iUDZ|)pLSX2j^rDWsMzwg!NzN{HwGoXoUYisM*`T5GL*!M4mB(@=T{gJR-f7!Auc2fOTJr>UV24QwBds;aBIh0+{9&O}9_ zUQXQ(p#58^3CeaieKt!a=)}2&{X9~w;m2wo;gl{)N)CF8hZq2&+2=adT;Mm`GT@B( z=yu+_(=hTH|1!Hr{89;?cYE~`8doQ1oH&M-O0Y@G0M&JwSLO8d@<_$C$&SkaUq5%q z@WCrL`wh4dAGSK>=@ksa;D`-h%~NX9LJDJBc3Y&Qs-3NKw|h%UGOvxfpJzFwT$D*9 zf}jS%OE76A(Pm#NvT*Qruh19A&qz& zqWBK-u{q%*e!#X|Y3I@iYC%{}10)*g3bF^hdLe~5dqJ3+>vv1s9zBSmwi}@~A1-ws zNl|!LIE-OW5k6fj#?#|F&ZMB(q55&FW*BVIcwB->^C}(SMr>~Ewr*~vGKbq!h0f&9 z0%qG|GZUn}bIQvf3XA#inH?^#y zU3>UCJS|==QN}2YN6o1qUm&2@$L}tvbp5`fn1g|su-&IVZdVF%wHJL4N?_sV)S$t}tRR zUbXDx1f=Xb>d3Qz{ye=@z0Xgqetvy5RN&lzM_5Z`N~VueV#ovbHe8thqzCBmX{{=hPQN;K}oeX=y`P zDF^`s7rTUAZ&L{Sx0Mr6%;y)!=mVv3G@J5}FefYR0-y)32rPl2(rl#ycRt=3Ly{bQ zbP^*;U%%dlYXB9g`2Ku8P;16-Lqht@-b?onm>+>UEu)_cn+%uQ?#{;A!{Be;yjcK8 z9gpJyj>vp>I%%si+Y?8O06qfJ-3?k6D2RQ(Isn=8LtzeN&@XZ>&ZE7C#E+4YZrulr zpgfa1WcKcYmIS11YHM3t1QMBYK|XK}2%AjB5D}u5E(DDkse0mCU;}SZL0;zt~TM;vkl7ZFv5Tqlh=z; zQ&SHWK6-&QhngB18?C-Pr$j_CM!dH9lf{$X#mvmiCh3%>78Ifp)*6$B^UCRFDV`oRAWhm4i%R;u#b zH(j-S$2^PzaY)l`KDR5$_<7G>j`-(_kMtt-o146hj+!v}()V&`Ui~vNK;J-Y-_iW8 zbDdxQvS&(45W_$A+?T8SKka#{`8{@fb!zbKxy+X!roh3u`z?@8Q$gVscIFN<@awP? zbmIa#@$sth=zdB>k6kI`c zLKEz-|H}mww;x+_rEad6OL+%Ur~Rxi{v2?${^G9w;i;u6(UZs&s7plD5m70x+Mf?! z9wqGOQFtS{q z=3&k28FmvAln#V}Q}|EsGWt2$1Y2T9CUE|6td=WtJAuH{bWj7L4j2cEu4e*-H+;S^ zV*h=kxw=A|3w`fwvmI5EaO|OCK)kvFl891;f>UtpfsFsnGj{(1IfQiJGy$WHGfS#S z1=o`*1F&5ELX&5t`srM!Q!)g*{L_IZsb5{-yY$uh`$t59^gYoz2X<>lN0p+p$#Ix9 zF|-9symvTawPxhg1xHa=uVySo9rD1}LfVkTs2A*2KQvXYvi24jRmGW=3wdl8=HS`* zBEprIJ2Za$z&&nk?DO?APtR$7tXky;w;j6tq@M|0AZ`VOJW!iKwMs*(J!-$2P7)9wN(YQB@%Eh zjMvpAcuZ19goj%KLW)dlXt?9LGBP;Tni}?ZfjV_;^{c{hyTgkv?MCjcQn~R(Y|ok| zI%;uyjP?IX<0{fV<9P{9ro_da9d#SFm2#K}fMo*`&UW1)iG&60_SFya4ay>6zx|#0 z^r*PS?(Xhw+neNvOKh6LJwP^RQSEs-?70m(hpN}O+zYesloM*DFl1&U`*FNxpklF{ zQO=uSXw|F^M-s>Q_2BRfRSNaj&?FN0-J0~W`-L!G9v(%_jJsWcv5?$L5?SaZ%*bij)5+3)NUR`V2-1SuF;m`k(d zkw~Pd|Dg|1lXF#UG(0`s-FG90`Cq9u^6T;JeGW;U0_5PZO0mtlxmVQ;5OBb#RMgbC z=gv%aV8FGt9Ar10U`&;NRkoxGl{38Rx%T*+q|+UwsN>|FQJ?j8*Mwm}89+1SUjjz; z$j`ft6fch7jNq#({I!=Ib$)b)-KD0PS=|2*VO$X@WL&Vl?SX2OCSB?3s51jv` zoani28=ch`RY7biU_wzg?pKv||3j#YvKMGrjM&FWU0;s8tosvD}gNgLzsF`?mM{P zn?d&pDd$POx!?^NN_W9%G7IkT&d_1W5+%*dVvj*TC5}CxJH`v~wi-XW;+Z|ac>so5 z;Yj4keKj*vPvp{c`M=s*^Z?5j&ObxQaE zgwL?fW={|EhJ)zI>x|^e6u^>NKAY&+bY`mMR;j&5lXhs9-Mls; zhsABDIlSNJgV#(ed{U=>ObjlRSu(0^4=g-M0$)BB4Ps}r}lX*%* z$Uqa7oSe|>hHPcwI=Z=wP4A5kepnWpx1HuzXn9@@Ohm=~GXRCX2!j?tgpYaIG|;S_ z2R8I;84R9HA^GKd7rT6DCtH41mcA3|pJ0p_T?@U8OBlh?^gT;otwG>$c zZoQ(m{&H#4Txa&uk82%THY}&pKTR~n-!HYj(JY`}OjR{geSyvFr-7MB-_{p=PVM-z zD6eoa`vU#Ly-JBR#bDtP~Oyu7=+waL-X-^f{8TMyV4gV7dg z^70KsO+ii?;&~C4vO$_P_HMT)Wm7QJ3q+b{1FQnt@CZ9j?4tJyUpLlR6}hixW?oWo zaLc)t-H$hSu`$_ZwmM~^V6-G5DDXFcexOo|m(m6*AriJc_*scGDHOSOjWoAIB2U&3 z5BD$(;~VomVpiQSoIW{KOf2X{`G}gKMUfb>y?3_89P{M@w<)@D0CYxmBg z7V^B)e@ireVxrS_P`3i;gu>(}lW)B^%6sWjv*$S~isC2jY`~UQC5;v^@i$2HALu6e zGiS~mryxhB0r0D;+d3NXMBt&e*+ROu>JFaWz1t?w+5h7Ba#>&jaawcGlaHagCuMhu_#?f$0CwCcb)H6kQj9dTPdG95f2hu&56v~nR$=i?|aHlk*OK; zjl~@f-+2G+_BDFc-i1#AEL5C=E#JT2S5$1G?$`l?zBn`Rz^Q<_jyg7JR2h@ zJ>@hNJ0#AMH6+E*Xi;-ohWvYx+e*s%8P@1SOG zo(|Mqs<2s878>2X=oFx+1fopRTRaR@mSAK|(pz2Ybj@Ja1;+f045Dw-qFG5}Lomt_ z%R|A0{AdQUw z{7OZD%@saAF~5C}5DwXde{yLO7X}&&h!vpua|a^XGAVxJ?w@Mu-{i_aWCjGq>HpV& g{V#o?V}y8YIx5%qeZ!}62>iem)bAABHhKR400@Uo(EtDd literal 0 HcmV?d00001 diff --git a/test/ipynb/mpl/circuit/test_circuit_matplotlib_drawer.py b/test/ipynb/mpl/circuit/test_circuit_matplotlib_drawer.py index 68ecb4455e58..656d0a584a60 100644 --- a/test/ipynb/mpl/circuit/test_circuit_matplotlib_drawer.py +++ b/test/ipynb/mpl/circuit/test_circuit_matplotlib_drawer.py @@ -920,6 +920,17 @@ def test_wire_order(self): filename="wire_order.png", ) + def test_barrier_label(self): + """Test the barrier label""" + circuit = QuantumCircuit(2) + circuit.x(0) + circuit.y(1) + circuit.barrier() + circuit.y(0) + circuit.x(1) + circuit.barrier(label="End Y/X") + self.circuit_drawer(circuit, filename="barrier_label.png") + if __name__ == "__main__": unittest.main(verbosity=1) diff --git a/test/python/visualization/references/test_latex_barrier_label.tex b/test/python/visualization/references/test_latex_barrier_label.tex new file mode 100644 index 000000000000..c5f434fa903c --- /dev/null +++ b/test/python/visualization/references/test_latex_barrier_label.tex @@ -0,0 +1,12 @@ +\documentclass[border=2px]{standalone} + +\usepackage[braket, qm]{qcircuit} +\usepackage{graphicx} + +\begin{document} +\scalebox{1.0}{ +\Qcircuit @C=1.0em @R=0.2em @!R { \\ + \nghost{{q}_{0} : } & \lstick{{q}_{0} : } & \gate{\mathrm{X}} \barrier[0em]{1} & \qw & \gate{\mathrm{Y}} \barrier[0em]{1} & \cds{0}{^{\mathrm{End\,Y/X}}} & \qw & \qw\\ + \nghost{{q}_{1} : } & \lstick{{q}_{1} : } & \gate{\mathrm{Y}} & \qw & \gate{\mathrm{X}} & \qw & \qw & \qw\\ +\\ }} +\end{document} \ No newline at end of file diff --git a/test/python/visualization/references/test_latex_plot_barriers_true.tex b/test/python/visualization/references/test_latex_plot_barriers_true.tex index 67618f16e7f8..b7679d27cd39 100644 --- a/test/python/visualization/references/test_latex_plot_barriers_true.tex +++ b/test/python/visualization/references/test_latex_plot_barriers_true.tex @@ -6,7 +6,7 @@ \begin{document} \scalebox{1.0}{ \Qcircuit @C=1.0em @R=0.2em @!R { \\ - \nghost{{q}_{0} : } & \lstick{{q}_{0} : } & \gate{\mathrm{H}} \barrier[0em]{1} & \qw & \qw \barrier[0em]{1} & \qw & \qw & \qw\\ + \nghost{{q}_{0} : } & \lstick{{q}_{0} : } & \gate{\mathrm{H}} \barrier[0em]{1} & \qw & \qw \barrier[0em]{1} & \cds{0}{^{\mathrm{sn\,1}}} & \qw & \qw\\ \nghost{{q}_{1} : } & \lstick{{q}_{1} : } & \qw & \qw & \gate{\mathrm{H}} & \qw & \qw & \qw\\ \nghost{\mathrm{{c} : }} & \lstick{\mathrm{{c} : }} & \lstick{/_{_{2}}} \cw & \cw & \cw & \cw & \cw & \cw\\ \\ }} diff --git a/test/python/visualization/test_circuit_latex.py b/test/python/visualization/test_circuit_latex.py index 3930725a5f38..447a173392b8 100644 --- a/test/python/visualization/test_circuit_latex.py +++ b/test/python/visualization/test_circuit_latex.py @@ -241,7 +241,7 @@ def test_plot_barriers(self): # this import appears to be unused, but is actually needed to get snapshot instruction import qiskit.extensions.simulator # pylint: disable=unused-import - circuit.snapshot("1") + circuit.snapshot("sn 1") # check the barriers plot properly when plot_barriers= True circuit_drawer(circuit, filename=filename1, output="latex_source", plot_barriers=True) @@ -265,6 +265,22 @@ def test_no_barriers_false(self): self.assertEqualToReference(filename) + def test_barrier_label(self): + """Test the barrier label""" + filename = self._get_resource_path("test_latex_barrier_label.tex") + qr = QuantumRegister(2, "q") + circuit = QuantumCircuit(qr) + circuit.x(0) + circuit.y(1) + circuit.barrier() + circuit.y(0) + circuit.x(1) + circuit.barrier(label="End Y/X") + + circuit_drawer(circuit, filename=filename, output="latex_source") + + self.assertEqualToReference(filename) + def test_big_gates(self): """Test large gates with params""" filename = self._get_resource_path("test_latex_big_gates.tex") diff --git a/test/python/visualization/test_circuit_text_drawer.py b/test/python/visualization/test_circuit_text_drawer.py index 25a2d79ce952..1ef9430c9a4d 100644 --- a/test/python/visualization/test_circuit_text_drawer.py +++ b/test/python/visualization/test_circuit_text_drawer.py @@ -1052,6 +1052,28 @@ def test_text_justify_right_barrier(self): circuit.h(qr1[1]) self.assertEqual(str(_text_circuit_drawer(circuit, justify="right")), expected) + def test_text_barrier_label(self): + """Show barrier label""" + expected = "\n".join( + [ + " ┌───┐ ░ ┌───┐ End Y/X ", + "q_0: |0>┤ X ├─░─┤ Y ├────░────", + " ├───┤ ░ ├───┤ ░ ", + "q_1: |0>┤ Y ├─░─┤ X ├────░────", + " └───┘ ░ └───┘ ░ ", + ] + ) + + qr = QuantumRegister(2, "q") + circuit = QuantumCircuit(qr) + circuit.x(0) + circuit.y(1) + circuit.barrier() + circuit.y(0) + circuit.x(1) + circuit.barrier(label="End Y/X") + self.assertEqual(str(_text_circuit_drawer(circuit)), expected) + def test_text_overlap_cx(self): """Overlapping CX gates are drawn not overlapping""" expected = "\n".join( From 8ce6e0a2566c4c045d8053687503b87643b46dae Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 22 Aug 2022 11:46:23 -0400 Subject: [PATCH 56/82] Further oxidize sabre (#8388) * Further oxidize sabre In #7977 we started the process of oxidizing SabreSwap by replacing the inner-most scoring heuristic loop with a rust routine. This greatly improved the overall performance and scaling of the transpiler pass. Continuing from where that started this commit migrates more of the pass into the Rust domain so that almost all the pass's operations are done inside a rust module and all that is returned is a list of swaps to run prior to each 2q gate. This should further improve the runtime performance of the pass and scaling because the only steps performed in Python are generating the input data structures and then replaying the circuit with SWAPs inserted at the appropriate points. While we could have stuck with #7977 as the performance of the pass was more than sufficient after it. What this commit really enables by moving most of the pass to the rust domain is to expand with improvments and expansion of the sabre algorithm which will require multithreaded to be efficiently implemented. So while this will have some modest performance improvements this is more about setting the stage for introducing variants of SabreSwap that do more thorough analysis in the future (which were previously preculded by the parallelism limitations of python). * Fix most test failures This commit fixes a small typo/logic error in the algorithm implementation that was preventing sabre from making forward progress because it wasn't correctly identifying successors for the next layer. By fixing this all the hard errors in the SabreSwap tests are fixed. The only failures left seem to be related to a different layout which hopefully is not a correctness issue but just caused by different ordering. * Rework circuit reconstruction to use layer order In some tests there were subtle differences in the relative positioning of the 1q gates relative to inserted swaps (i.e. a 1q gate which was before the swap previously could move to after it). This was caused by different topological ordering being used between the hybrid python sabre implementation and the mostly rust sabre implementations. To ensure a consistent ordering fater moving mostly to rust this changes the swap insertion loop to iterate over the circuit layers which mirrors how the old sabre implementation worked. * Differentiate between empty extended_set and none * Simplify arguments passing to remove adjacency matrix storage * Only check env variables once in rust * Rust side NLayout.copy() * Preserve SabreSwap execution order This commit fixes an issue where in some cases the topological order the DAGCircuit is traversed is different from the topological order that sabre uses internally. The build_swap_map sabre swap function is only valid if the 2q gates are replayed in the same exact order when rebuilding the DAGCircuit. If a 2q gate gets replayed in a different order the layout mapping will cause the circuit to diverge and potentially be invalid. This commit updates the replay logic in the python side to detect when the topological order over the dagcircuit differs from the sabre traversal order and attempts to correct it. * Rework SabreDAG to include full DAGCircuit structure Previously we attempted to just have the rust component of sabre deal solely with the 2q component of the input circuit. However, while this works for ~80% of the cases it fails to account ordering and interactions between non-2q gates or instructions with classical bits. To address this the sabre dag structure is modified to contain all isntructions in the input circuit and structurally match the DAGCircuit's edges. This fixes most of the issues related to gate ordering the previous implementation was encountering. It also simplifies the swap insertion/replay of the circuit in the python side as we now get an exact application order from the rust code. * Switch back to topological_op_nodes() for SabreDAG creation * Fix lint * Fix extended set construction * Fix typo in application of decay rate * Remove unused QubitsDecay class * Remove unused EdgeList class * Remove unnecessary SabreRNG class * Cleanup SabreDAG docstring and comments * Remove unused edge weights from SabreDAG The edge weights in the SabreDAG struct were set to the qubit indices from the input DAGCircuit because the edges represent the flow of data on the qubit. However, we never actually inspect the edge weights and all having them present does is use extra memory. This commit changes SabreDAG to just not set any weight for edges as all we need is the source and target nodes for the algorithm to work. * s/_bit_indices/_qubit_indices/g * Fix sabre rust class signatures --- Cargo.lock | 106 ++++-- Cargo.toml | 1 + .../transpiler/passes/routing/sabre_swap.py | 285 +++------------- src/nlayout.rs | 4 + src/sabre_swap/edge_list.rs | 101 ------ src/sabre_swap/mod.rs | 318 +++++++++++++++--- src/sabre_swap/qubits_decay.rs | 85 ----- src/sabre_swap/sabre_dag.rs | 69 ++++ src/sabre_swap/sabre_rng.rs | 35 -- src/sabre_swap/swap_map.rs | 48 +++ tox.ini | 2 +- 11 files changed, 517 insertions(+), 537 deletions(-) delete mode 100644 src/sabre_swap/edge_list.rs delete mode 100644 src/sabre_swap/qubits_decay.rs create mode 100644 src/sabre_swap/sabre_dag.rs delete mode 100644 src/sabre_swap/sabre_rng.rs create mode 100644 src/sabre_swap/swap_map.rs diff --git a/Cargo.lock b/Cargo.lock index 9283953439c7..0f821d94ffdb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,9 +45,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "crossbeam-channel" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c02a4d71819009c192cf4872265391563fd6a84c81ff2c0f2a7026ca4c1d85c" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" dependencies = [ "cfg-if", "crossbeam-utils", @@ -55,9 +55,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" dependencies = [ "cfg-if", "crossbeam-epoch", @@ -66,9 +66,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07db9d94cbd326813772c968ccd25999e5f8ae22f4f8d1b11effa37ef6ce281d" +checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1" dependencies = [ "autocfg", "cfg-if", @@ -80,9 +80,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83" +checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" dependencies = [ "cfg-if", "once_cell", @@ -90,9 +90,15 @@ dependencies = [ [[package]] name = "either" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "getrandom" @@ -105,6 +111,16 @@ dependencies = [ "wasi", ] +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash 0.7.6", + "rayon", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -131,27 +147,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", "rayon", ] [[package]] name = "indoc" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05a0bd019339e5d968b37855180087b7b9d512c5046fbd244cf8c95687927d6e" +checksum = "adab1eaa3408fb7f0c777a73e7465fd5656136fc93b670eb6df3c88c2c1344e3" [[package]] name = "libc" -version = "0.2.126" +version = "0.2.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" [[package]] name = "libm" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33a33a362ce288760ec6a508b94caaec573ae7d3bbbd91b87aa0bad4456839db" +checksum = "292a948cd991e376cf75541fe5b97a1081d713c618b4f1b9500f8844e49eb565" [[package]] name = "lock_api" @@ -260,9 +276,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" +checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e" [[package]] name = "parking_lot" @@ -287,6 +303,16 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "petgraph" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5014253a1331579ce62aa67443b4a658c5e7dd03d4bc6d302b94474888143" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "ppv-lite86" version = "0.2.16" @@ -295,9 +321,9 @@ checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" [[package]] name = "proc-macro2" -version = "1.0.40" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" dependencies = [ "unicode-ident", ] @@ -309,7 +335,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e6302e85060011447471887705bb7838f14aba43fcb06957d823739a496b3dc" dependencies = [ "cfg-if", - "hashbrown", + "hashbrown 0.12.3", "indoc", "libc", "num-bigint", @@ -369,7 +395,7 @@ name = "qiskit-terra" version = "0.22.0" dependencies = [ "ahash 0.8.0", - "hashbrown", + "hashbrown 0.12.3", "indexmap", "ndarray", "num-bigint", @@ -380,13 +406,14 @@ dependencies = [ "rand_distr", "rand_pcg", "rayon", + "retworkx-core", ] [[package]] name = "quote" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ "proc-macro2", ] @@ -472,13 +499,26 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.13" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags", ] +[[package]] +name = "retworkx-core" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353bcdcdab6c754ea32bce39ee7a763c8a3c16c91a8dd648befd14fbcb0d5b68" +dependencies = [ + "ahash 0.7.6", + "hashbrown 0.11.2", + "indexmap", + "petgraph", + "rayon", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -493,9 +533,9 @@ checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" [[package]] name = "syn" -version = "1.0.98" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" dependencies = [ "proc-macro2", "quote", @@ -510,15 +550,15 @@ checksum = "c02424087780c9b71cc96799eaeddff35af2bc513278cda5c99fc1f5d026d3c1" [[package]] name = "unicode-ident" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" +checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" [[package]] name = "unindent" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52fee519a3e570f7df377a06a1a7775cdbfb7aa460be7e08de2b1f0e69973a44" +checksum = "58ee9362deb4a96cef4d437d1ad49cffc9b9e92d202b6995674e928ce684f112" [[package]] name = "version_check" diff --git a/Cargo.toml b/Cargo.toml index 881f7bd09a42..d0d3882db611 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ rand_distr = "0.4.3" ahash = "0.8.0" num-complex = "0.4" num-bigint = "0.4" +retworkx-core = "0.11" [dependencies.pyo3] version = "0.16.5" diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py index 3f83d83b04dd..275864ba7fba 100644 --- a/qiskit/transpiler/passes/routing/sabre_swap.py +++ b/qiskit/transpiler/passes/routing/sabre_swap.py @@ -13,7 +13,6 @@ """Routing via SWAP insertion using the SABRE method from Li et al.""" import logging -from collections import defaultdict from copy import copy, deepcopy import numpy as np @@ -27,22 +26,15 @@ # pylint: disable=import-error from qiskit._accelerate.sabre_swap import ( - sabre_score_heuristic, + build_swap_map, Heuristic, - EdgeList, - QubitsDecay, NeighborTable, - SabreRng, + SabreDAG, ) from qiskit._accelerate.stochastic_swap import NLayout # pylint: disable=import-error logger = logging.getLogger(__name__) -EXTENDED_SET_SIZE = 20 # Size of lookahead window. TODO: set dynamically to len(current_layout) - -DECAY_RATE = 0.001 # Decay coefficient for penalizing serial swaps. -DECAY_RESET_INTERVAL = 5 # How often to reset all decay rates to 1. - class SabreSwap(TransformationPass): r"""Map input circuit onto a backend topology via insertion of SWAPs. @@ -167,9 +159,8 @@ def __init__( else: self.seed = seed self.fake_run = fake_run - self.required_predecessors = None - self.qubits_decay = None - self._bit_indices = None + self._qubit_indices = None + self._clbit_indices = None self.dist_matrix = None def run(self, dag): @@ -189,18 +180,8 @@ def run(self, dag): if len(dag.qubits) > self.coupling_map.size(): raise TranspilerError("More virtual qubits exist than physical.") - max_iterations_without_progress = 10 * len(dag.qubits) # Arbitrary. - ops_since_progress = [] - extended_set = None - - # Normally this isn't necessary, but here we want to log some objects that have some - # non-trivial cost to create. - do_expensive_logging = logger.isEnabledFor(logging.DEBUG) - self.dist_matrix = self.coupling_map.distance_matrix - rng = SabreRng(self.seed) - # Preserve input DAG's name, regs, wire_map, etc. but replace the graph. mapped_dag = None if not self.fake_run: @@ -208,244 +189,68 @@ def run(self, dag): canonical_register = dag.qregs["q"] current_layout = Layout.generate_trivial_layout(canonical_register) - self._bit_indices = {bit: idx for idx, bit in enumerate(canonical_register)} + self._qubit_indices = {bit: idx for idx, bit in enumerate(canonical_register)} + self._clbit_indices = {bit: idx for idx, bit in enumerate(dag.clbits)} layout_mapping = { - self._bit_indices[k]: v for k, v in current_layout.get_virtual_bits().items() + self._qubit_indices[k]: v for k, v in current_layout.get_virtual_bits().items() } layout = NLayout(layout_mapping, len(dag.qubits), self.coupling_map.size()) - - # A decay factor for each qubit used to heuristically penalize recently - # used qubits (to encourage parallelism). - self.qubits_decay = QubitsDecay(len(dag.qubits)) - - # Start algorithm from the front layer and iterate until all gates done. - self.required_predecessors = self._build_required_predecessors(dag) - num_search_steps = 0 - front_layer = dag.front_layer() - - while front_layer: - execute_gate_list = [] - - # Remove as many immediately applicable gates as possible - new_front_layer = [] - for node in front_layer: - if len(node.qargs) == 2: - v0 = self._bit_indices[node.qargs[0]] - v1 = self._bit_indices[node.qargs[1]] - if self.coupling_map.graph.has_edge( - layout.logical_to_physical(v0), layout.logical_to_physical(v1) - ): - execute_gate_list.append(node) - else: - new_front_layer.append(node) - else: # Single-qubit gates as well as barriers are free - execute_gate_list.append(node) - front_layer = new_front_layer - - if not execute_gate_list and len(ops_since_progress) > max_iterations_without_progress: - # Backtrack to the last time we made progress, then greedily insert swaps to route - # the gate with the smallest distance between its arguments. This is a release - # valve for the algorithm to avoid infinite loops only, and should generally not - # come into play for most circuits. - self._undo_operations(ops_since_progress, mapped_dag, layout) - self._add_greedy_swaps(front_layer, mapped_dag, layout, canonical_register) - continue - - if execute_gate_list: - for node in execute_gate_list: - self._apply_gate(mapped_dag, node, layout, canonical_register) - for successor in self._successors(node, dag): - self.required_predecessors[successor] -= 1 - if self._is_resolved(successor): - front_layer.append(successor) - - if node.qargs: - self.qubits_decay.reset() - - # Diagnostics - if do_expensive_logging: - logger.debug( - "free! %s", - [ - (n.name if isinstance(n, DAGOpNode) else None, n.qargs) - for n in execute_gate_list - ], - ) - logger.debug( - "front_layer: %s", - [ - (n.name if isinstance(n, DAGOpNode) else None, n.qargs) - for n in front_layer - ], - ) - - ops_since_progress = [] - extended_set = None - continue - - # After all free gates are exhausted, heuristically find - # the best swap and insert it. When two or more swaps tie - # for best score, pick one randomly. - - if extended_set is None: - extended_set = self._obtain_extended_set(dag, front_layer) - extended_set_list = EdgeList(len(extended_set)) - for x in extended_set: - extended_set_list.append( - self._bit_indices[x.qargs[0]], self._bit_indices[x.qargs[1]] - ) - - front_layer_list = EdgeList(len(front_layer)) - for x in front_layer: - front_layer_list.append( - self._bit_indices[x.qargs[0]], self._bit_indices[x.qargs[1]] + original_layout = layout.copy() + + dag_list = [] + for node in dag.topological_op_nodes(): + dag_list.append( + ( + node._node_id, + [self._qubit_indices[x] for x in node.qargs], + [self._clbit_indices[x] for x in node.cargs], ) - best_swap = sabre_score_heuristic( - front_layer_list, - layout, - self._neighbor_table, - extended_set_list, - self.dist_matrix, - self.qubits_decay, - self.heuristic, - rng, ) - best_swap_qargs = [canonical_register[best_swap[0]], canonical_register[best_swap[1]]] - swap_node = self._apply_gate( - mapped_dag, - DAGOpNode(op=SwapGate(), qargs=best_swap_qargs), - layout, - canonical_register, - ) - layout.swap_logical(*best_swap) - ops_since_progress.append(swap_node) - - num_search_steps += 1 - if num_search_steps % DECAY_RESET_INTERVAL == 0: - self.qubits_decay.reset() - else: - self.qubits_decay[best_swap[0]] += DECAY_RATE - self.qubits_decay[best_swap[1]] += DECAY_RATE - - # Diagnostics - if do_expensive_logging: - logger.debug("SWAP Selection...") - logger.debug("extended_set: %s", [(n.name, n.qargs) for n in extended_set]) - logger.debug("best swap: %s", best_swap) - logger.debug("qubits decay: %s", self.qubits_decay) + front_layer = np.asarray([x._node_id for x in dag.front_layer()], dtype=np.uintp) + sabre_dag = SabreDAG(len(dag.qubits), len(dag.clbits), dag_list, front_layer) + swap_map, gate_order = build_swap_map( + len(dag.qubits), + sabre_dag, + self._neighbor_table, + self.dist_matrix, + self.heuristic, + self.seed, + layout, + ) + layout_mapping = layout.layout_mapping() output_layout = Layout({dag.qubits[k]: v for (k, v) in layout_mapping}) self.property_set["final_layout"] = output_layout if not self.fake_run: + for node_id in gate_order: + node = dag._multi_graph[node_id] + self._process_swaps(swap_map, node, mapped_dag, original_layout, canonical_register) + self._apply_gate(mapped_dag, node, original_layout, canonical_register) return mapped_dag return dag + def _process_swaps(self, swap_map, node, mapped_dag, current_layout, canonical_register): + if node._node_id in swap_map: + for swap in swap_map[node._node_id]: + swap_qargs = [canonical_register[swap[0]], canonical_register[swap[1]]] + self._apply_gate( + mapped_dag, + DAGOpNode(op=SwapGate(), qargs=swap_qargs), + current_layout, + canonical_register, + ) + current_layout.swap_logical(*swap) + def _apply_gate(self, mapped_dag, node, current_layout, canonical_register): new_node = self._transform_gate_for_layout(node, current_layout, canonical_register) if self.fake_run: return new_node return mapped_dag.apply_operation_back(new_node.op, new_node.qargs, new_node.cargs) - def _build_required_predecessors(self, dag): - out = defaultdict(int) - # We don't need to count in- or out-wires: outs can never be predecessors, and all input - # wires are automatically satisfied at the start. - for node in dag.op_nodes(): - for successor in self._successors(node, dag): - out[successor] += 1 - return out - - def _successors(self, node, dag): - """Return an iterable of the successors along each wire from the given node. - - This yields the same successor multiple times if there are parallel wires (e.g. two adjacent - operations that have one clbit and qubit in common), which is important in the swapping - algorithm for detecting if each wire has been accounted for.""" - for _, successor, _ in dag.edges(node): - if isinstance(successor, DAGOpNode): - yield successor - - def _is_resolved(self, node): - """Return True if all of a node's predecessors in dag are applied.""" - return self.required_predecessors[node] == 0 - - def _obtain_extended_set(self, dag, front_layer): - """Populate extended_set by looking ahead a fixed number of gates. - For each existing element add a successor until reaching limit. - """ - extended_set = [] - decremented = [] - tmp_front_layer = front_layer - done = False - while tmp_front_layer and not done: - new_tmp_front_layer = [] - for node in tmp_front_layer: - for successor in self._successors(node, dag): - decremented.append(successor) - self.required_predecessors[successor] -= 1 - if self._is_resolved(successor): - new_tmp_front_layer.append(successor) - if len(successor.qargs) == 2: - extended_set.append(successor) - if len(extended_set) >= EXTENDED_SET_SIZE: - done = True - break - tmp_front_layer = new_tmp_front_layer - for node in decremented: - self.required_predecessors[node] += 1 - return extended_set - - def _add_greedy_swaps(self, front_layer, dag, layout, qubits): - """Mutate ``dag`` and ``layout`` by applying greedy swaps to ensure that at least one gate - can be routed.""" - target_node = min( - front_layer, - key=lambda node: self.dist_matrix[ - layout.logical_to_physical(self._bit_indices[node.qargs[0]]), - layout.logical_to_physical(self._bit_indices[node.qargs[1]]), - ], - ) - for pair in _shortest_swap_path( - tuple(target_node.qargs), self.coupling_map, layout, qubits - ): - self._apply_gate(dag, DAGOpNode(op=SwapGate(), qargs=pair), layout, qubits) - layout.swap_logical(*[self._bit_indices[x] for x in pair]) - - def _undo_operations(self, operations, dag, layout): - """Mutate ``dag`` and ``layout`` by undoing the swap gates listed in ``operations``.""" - if dag is None: - for operation in reversed(operations): - layout.swap_logical(*[self._bit_indices[x] for x in operation.qargs]) - else: - for operation in reversed(operations): - dag.remove_op_node(operation) - p0 = self._bit_indices[operation.qargs[0]] - p1 = self._bit_indices[operation.qargs[1]] - layout.swap_logical(p0, p1) - def _transform_gate_for_layout(self, op_node, layout, device_qreg): """Return node implementing a virtual op on given layout.""" mapped_op_node = copy(op_node) mapped_op_node.qargs = tuple( - device_qreg[layout.logical_to_physical(self._bit_indices[x])] for x in op_node.qargs + device_qreg[layout.logical_to_physical(self._qubit_indices[x])] for x in op_node.qargs ) return mapped_op_node - - -def _shortest_swap_path(target_qubits, coupling_map, layout, qreg): - """Return an iterator that yields the swaps between virtual qubits needed to bring the two - virtual qubits in ``target_qubits`` together in the coupling map.""" - v_start, v_goal = target_qubits - start, goal = layout.logical_to_physical(qreg.index(v_start)), layout.logical_to_physical( - qreg.index(v_goal) - ) - # TODO: remove the list call once using retworkx 0.12, as the return value can be sliced. - path = list(retworkx.dijkstra_shortest_paths(coupling_map.graph, start, target=goal)[goal]) - # Swap both qubits towards the "centre" (as opposed to applying the same swaps to one) to - # parallelise and reduce depth. - split = len(path) // 2 - forwards, backwards = path[1:split], reversed(path[split:-1]) - for swap in forwards: - yield v_start, qreg[layout.physical_to_logical(swap)] - for swap in backwards: - yield v_goal, qreg[layout.physical_to_logical(swap)] diff --git a/src/nlayout.rs b/src/nlayout.rs index e4ca1223b33d..2d4ff5a29880 100644 --- a/src/nlayout.rs +++ b/src/nlayout.rs @@ -112,4 +112,8 @@ impl NLayout { pub fn swap_physical(&mut self, bit_a: usize, bit_b: usize) { self.swap(bit_a, bit_b) } + + pub fn copy(&self) -> NLayout { + self.clone() + } } diff --git a/src/sabre_swap/edge_list.rs b/src/sabre_swap/edge_list.rs deleted file mode 100644 index a1dbf0fb55e7..000000000000 --- a/src/sabre_swap/edge_list.rs +++ /dev/null @@ -1,101 +0,0 @@ -// This code is part of Qiskit. -// -// (C) Copyright IBM 2022 -// -// 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. - -use pyo3::exceptions::PyIndexError; -use pyo3::prelude::*; - -/// A simple container that contains a vector representing edges in the -/// coupling map that are found to be optimal by the swap mapper. -#[pyclass(module = "qiskit._accelerate.sabre_swap")] -#[pyo3(text_signature = "(/)")] -#[derive(Clone, Debug)] -pub struct EdgeList { - pub edges: Vec<[usize; 2]>, -} - -impl Default for EdgeList { - fn default() -> Self { - Self::new(None) - } -} - -#[pymethods] -impl EdgeList { - #[new] - pub fn new(capacity: Option) -> Self { - match capacity { - Some(size) => EdgeList { - edges: Vec::with_capacity(size), - }, - None => EdgeList { edges: Vec::new() }, - } - } - - /// Append an edge to the list. - /// - /// Args: - /// edge_start (int): The start qubit of the edge. - /// edge_end (int): The end qubit of the edge. - #[pyo3(text_signature = "(self, edge_start, edge_end, /)")] - pub fn append(&mut self, edge_start: usize, edge_end: usize) { - self.edges.push([edge_start, edge_end]); - } - - pub fn __iter__(slf: PyRef) -> PyResult> { - let iter = EdgeListIter { - inner: slf.edges.clone().into_iter(), - }; - Py::new(slf.py(), iter) - } - - pub fn __len__(&self) -> usize { - self.edges.len() - } - - pub fn __contains__(&self, object: [usize; 2]) -> bool { - self.edges.contains(&object) - } - - pub fn __getitem__(&self, object: usize) -> PyResult<[usize; 2]> { - if object >= self.edges.len() { - return Err(PyIndexError::new_err(format!( - "Index {} out of range for this EdgeList", - object - ))); - } - Ok(self.edges[object]) - } - - fn __getstate__(&self) -> Vec<[usize; 2]> { - self.edges.clone() - } - - fn __setstate__(&mut self, state: Vec<[usize; 2]>) { - self.edges = state - } -} - -#[pyclass] -pub struct EdgeListIter { - inner: std::vec::IntoIter<[usize; 2]>, -} - -#[pymethods] -impl EdgeListIter { - fn __iter__(slf: PyRef) -> PyRef { - slf - } - - fn __next__(mut slf: PyRefMut) -> Option<[usize; 2]> { - slf.inner.next() - } -} diff --git a/src/sabre_swap/mod.rs b/src/sabre_swap/mod.rs index 73323cd446d4..a2301c56e31f 100644 --- a/src/sabre_swap/mod.rs +++ b/src/sabre_swap/mod.rs @@ -12,29 +12,38 @@ #![allow(clippy::too_many_arguments)] -pub mod edge_list; pub mod neighbor_table; -pub mod qubits_decay; -pub mod sabre_rng; +pub mod sabre_dag; +pub mod swap_map; +use std::cmp::Ordering; + +use hashbrown::{HashMap, HashSet}; use ndarray::prelude::*; +use numpy::IntoPyArray; use numpy::PyReadonlyArray2; use pyo3::prelude::*; use pyo3::wrap_pyfunction; use pyo3::Python; - -use hashbrown::HashSet; use rand::prelude::SliceRandom; +use rand::prelude::*; +use rand_pcg::Pcg64Mcg; use rayon::prelude::*; +use retworkx_core::dictmap::*; +use retworkx_core::petgraph::prelude::*; +use retworkx_core::petgraph::visit::EdgeRef; +use retworkx_core::shortest_path::dijkstra; use crate::getenv_use_multiple_threads; use crate::nlayout::NLayout; -use edge_list::EdgeList; use neighbor_table::NeighborTable; -use qubits_decay::QubitsDecay; -use sabre_rng::SabreRng; +use sabre_dag::SabreDAG; +use swap_map::SwapMap; +const EXTENDED_SET_SIZE: usize = 20; // Size of lookahead window. +const DECAY_RATE: f64 = 0.001; // Decay coefficient for penalizing serial swaps. +const DECAY_RESET_INTERVAL: u8 = 5; // How often to reset all decay rates to 1. const EXTENDED_SET_WEIGHT: f64 = 0.5; // Weight of lookahead window compared to front_layer. #[pyclass] @@ -53,16 +62,15 @@ pub enum Heuristic { /// /// Candidate swaps are sorted so SWAP(i,j) and SWAP(j,i) are not duplicated. fn obtain_swaps( - front_layer: &EdgeList, + front_layer: &[[usize; 2]], neighbors: &NeighborTable, layout: &NLayout, ) -> HashSet<[usize; 2]> { // This will likely under allocate as it's a function of the number of // neighbors for the qubits in the layer too, but this is basically a // minimum allocation assuming each qubit has only 1 unique neighbor - let mut candidate_swaps: HashSet<[usize; 2]> = - HashSet::with_capacity(2 * front_layer.edges.len()); - for node in &front_layer.edges { + let mut candidate_swaps: HashSet<[usize; 2]> = HashSet::with_capacity(2 * front_layer.len()); + for node in front_layer { for v in node { let physical = layout.logic_to_phys[*v]; for neighbor in &neighbors.neighbors[physical] { @@ -79,49 +87,274 @@ fn obtain_swaps( candidate_swaps } -/// Run the sabre heuristic scoring +fn obtain_extended_set( + dag: &SabreDAG, + front_layer: &[NodeIndex], + required_predecessors: &mut [u32], +) -> Vec<[usize; 2]> { + let mut extended_set: Vec<[usize; 2]> = Vec::new(); + let mut decremented: Vec = Vec::new(); + let mut tmp_front_layer: Vec = front_layer.to_vec(); + let mut done: bool = false; + while !tmp_front_layer.is_empty() && !done { + let mut new_tmp_front_layer = Vec::new(); + for node in tmp_front_layer { + for edge in dag.dag.edges(node) { + let successor_index = edge.target(); + let successor = successor_index.index(); + decremented.push(successor); + required_predecessors[successor] -= 1; + if required_predecessors[successor] == 0 { + new_tmp_front_layer.push(successor_index); + let node_weight = dag.dag.node_weight(successor_index).unwrap(); + let qargs = &node_weight.1; + if qargs.len() == 2 { + let extended_set_edges: [usize; 2] = [qargs[0], qargs[1]]; + extended_set.push(extended_set_edges); + } + } + } + if extended_set.len() >= EXTENDED_SET_SIZE { + done = true; + break; + } + } + tmp_front_layer = new_tmp_front_layer; + } + for node in decremented { + required_predecessors[node] += 1; + } + extended_set +} + +fn cmap_from_neighor_table(neighbor_table: &NeighborTable) -> DiGraph<(), ()> { + DiGraph::<(), ()>::from_edges(neighbor_table.neighbors.iter().enumerate().flat_map( + |(u, targets)| { + targets + .iter() + .map(move |v| (NodeIndex::new(u), NodeIndex::new(*v))) + }, + )) +} + +/// Run sabre swap on a circuit /// -/// Args: -/// layers (EdgeList): The input layer edge list to score and find the -/// best swaps -/// layout (NLayout): The current layout -/// neighbor_table (NeighborTable): The table of neighbors for each node -/// in the coupling graph -/// extended_set (EdgeList): The extended set -/// distance_matrix (ndarray): The 2D array distance matrix for the coupling -/// graph -/// qubits_decay (QubitsDecay): The current qubit decay factors for -/// heuristic (Heuristic): The chosen heuristic method to use /// Returns: -/// ndarray: A 2d array of the best swap candidates all with the minimum score +/// (SwapMap, gate_order): A tuple where the first element is a mapping of +/// DAGCircuit node ids to a list of virtual qubit swaps that should be +/// added before that operation. The second element is a numpy array of +/// node ids that represents the traversal order used by sabre. #[pyfunction] +pub fn build_swap_map( + py: Python, + num_qubits: usize, + dag: &SabreDAG, + neighbor_table: &NeighborTable, + distance_matrix: PyReadonlyArray2, + heuristic: &Heuristic, + seed: u64, + layout: &mut NLayout, +) -> PyResult<(SwapMap, PyObject)> { + let mut gate_order: Vec = Vec::with_capacity(dag.dag.node_count()); + let run_in_parallel = getenv_use_multiple_threads(); + let mut out_map: HashMap> = HashMap::new(); + let mut front_layer: Vec = dag.first_layer.clone(); + let max_iterations_without_progress = 10 * neighbor_table.neighbors.len(); + let mut ops_since_progress: Vec<[usize; 2]> = Vec::new(); + let mut required_predecessors: Vec = vec![0; dag.dag.node_count()]; + let mut extended_set: Option> = None; + let mut num_search_steps: u8 = 0; + let dist = distance_matrix.as_array(); + let coupling_graph: DiGraph<(), ()> = cmap_from_neighor_table(neighbor_table); + let mut qubits_decay: Vec = vec![1.; num_qubits]; + let mut rng = Pcg64Mcg::seed_from_u64(seed); + + for node in dag.dag.node_indices() { + for edge in dag.dag.edges(node) { + required_predecessors[edge.target().index()] += 1; + } + } + while !front_layer.is_empty() { + let mut execute_gate_list: Vec = Vec::new(); + // Remove as many immediately applicable gates as possible + let mut new_front_layer: Vec = Vec::new(); + for node in front_layer { + let node_weight = dag.dag.node_weight(node).unwrap(); + let qargs = &node_weight.1; + if qargs.len() == 2 { + let physical_qargs: [usize; 2] = [ + layout.logic_to_phys[qargs[0]], + layout.logic_to_phys[qargs[1]], + ]; + if coupling_graph + .find_edge( + NodeIndex::new(physical_qargs[0]), + NodeIndex::new(physical_qargs[1]), + ) + .is_none() + { + new_front_layer.push(node); + } else { + execute_gate_list.push(node); + } + } else { + execute_gate_list.push(node); + } + } + front_layer = new_front_layer.clone(); + + // Backtrack to the last time we made progress, then greedily insert swaps to route + // the gate with the smallest distance between its arguments. This is f block a release + // valve for the algorithm to avoid infinite loops only, and should generally not + // come into play for most circuits. + if execute_gate_list.is_empty() + && ops_since_progress.len() > max_iterations_without_progress + { + // If we're stuck in a loop without making progress first undo swaps: + ops_since_progress + .drain(..) + .rev() + .for_each(|swap| layout.swap_logical(swap[0], swap[1])); + // Then pick the closest pair in the current layer + let target_qubits = front_layer + .iter() + .map(|n| { + let node_weight = dag.dag.node_weight(*n).unwrap(); + let qargs = &node_weight.1; + [qargs[0], qargs[1]] + }) + .min_by(|qargs_a, qargs_b| { + let dist_a = dist[[ + layout.logic_to_phys[qargs_a[0]], + layout.logic_to_phys[qargs_a[1]], + ]]; + let dist_b = dist[[ + layout.logic_to_phys[qargs_b[0]], + layout.logic_to_phys[qargs_b[1]], + ]]; + dist_a.partial_cmp(&dist_b).unwrap_or(Ordering::Equal) + }) + .unwrap(); + // find Shortest path between target qubits + let mut shortest_paths: DictMap> = DictMap::new(); + let u = layout.logic_to_phys[target_qubits[0]]; + let v = layout.logic_to_phys[target_qubits[1]]; + (dijkstra( + &coupling_graph, + NodeIndex::::new(u), + Some(NodeIndex::::new(v)), + |_| Ok(1.), + Some(&mut shortest_paths), + ) as PyResult>>)?; + let shortest_path: Vec = shortest_paths + .get(&NodeIndex::new(v)) + .unwrap() + .iter() + .map(|n| n.index()) + .collect(); + // Insert greedy swaps along that shortest path + let split: usize = shortest_path.len() / 2; + let forwards = &shortest_path[1..split]; + let backwards = &shortest_path[split..shortest_path.len() - 1]; + let mut greedy_swaps: Vec<[usize; 2]> = Vec::with_capacity(split); + for swap in forwards { + let logical_swap_bit = layout.phys_to_logic[*swap]; + greedy_swaps.push([target_qubits[0], logical_swap_bit]); + layout.swap_logical(target_qubits[0], logical_swap_bit); + } + backwards.iter().rev().for_each(|swap| { + let logical_swap_bit = layout.phys_to_logic[*swap]; + greedy_swaps.push([target_qubits[1], logical_swap_bit]); + layout.swap_logical(target_qubits[1], logical_swap_bit); + }); + ops_since_progress = greedy_swaps; + continue; + } + if !execute_gate_list.is_empty() { + for node in execute_gate_list { + let node_weight = dag.dag.node_weight(node).unwrap(); + gate_order.push(node_weight.0); + let out_swaps: Vec<[usize; 2]> = ops_since_progress.drain(..).collect(); + if !out_swaps.is_empty() { + out_map.insert(dag.dag.node_weight(node).unwrap().0, out_swaps); + } + for edge in dag.dag.edges(node) { + let successor = edge.target().index(); + required_predecessors[successor] -= 1; + if required_predecessors[successor] == 0 { + front_layer.push(edge.target()); + } + } + } + qubits_decay.fill_with(|| 1.); + extended_set = None; + continue; + } + let first_layer: Vec<[usize; 2]> = front_layer + .iter() + .map(|n| { + let node_weight = dag.dag.node_weight(*n).unwrap(); + let qargs = &node_weight.1; + [qargs[0], qargs[1]] + }) + .collect(); + if extended_set.is_none() { + extended_set = Some(obtain_extended_set( + dag, + &front_layer, + &mut required_predecessors, + )); + } + + let best_swap = sabre_score_heuristic( + &first_layer, + layout, + neighbor_table, + extended_set.as_ref().unwrap(), + &dist, + &qubits_decay, + heuristic, + &mut rng, + run_in_parallel, + ); + num_search_steps += 1; + if num_search_steps % DECAY_RESET_INTERVAL == 0 { + qubits_decay.fill_with(|| 1.); + } else { + qubits_decay[best_swap[0]] += DECAY_RATE; + qubits_decay[best_swap[1]] += DECAY_RATE; + } + ops_since_progress.push(best_swap); + } + Ok((SwapMap { map: out_map }, gate_order.into_pyarray(py).into())) +} + pub fn sabre_score_heuristic( - layer: EdgeList, + layer: &[[usize; 2]], layout: &mut NLayout, neighbor_table: &NeighborTable, - extended_set: EdgeList, - distance_matrix: PyReadonlyArray2, - qubits_decay: QubitsDecay, + extended_set: &[[usize; 2]], + dist: &ArrayView2, + qubits_decay: &[f64], heuristic: &Heuristic, - rng: &mut SabreRng, + rng: &mut Pcg64Mcg, + run_in_parallel: bool, ) -> [usize; 2] { // Run in parallel only if we're not already in a multiprocessing context // unless force threads is set. - let run_in_parallel = getenv_use_multiple_threads(); - let dist = distance_matrix.as_array(); - let candidate_swaps = obtain_swaps(&layer, neighbor_table, layout); + let candidate_swaps = obtain_swaps(layer, neighbor_table, layout); let mut min_score = f64::MAX; let mut best_swaps: Vec<[usize; 2]> = Vec::new(); for swap_qubits in candidate_swaps { layout.swap_logical(swap_qubits[0], swap_qubits[1]); let score = score_heuristic( heuristic, - &layer.edges, - &extended_set.edges, + layer, + extended_set, layout, &swap_qubits, - &dist, - &qubits_decay.decay, + dist, + qubits_decay, ); if score < min_score { min_score = score; @@ -137,7 +370,9 @@ pub fn sabre_score_heuristic( } else { best_swaps.sort_unstable(); } - *best_swaps.choose(&mut rng.rng).unwrap() + let best_swap = *best_swaps.choose(rng).unwrap(); + layout.swap_logical(best_swap[0], best_swap[1]); + best_swap } #[inline] @@ -196,11 +431,10 @@ fn score_heuristic( #[pymodule] pub fn sabre_swap(_py: Python, m: &PyModule) -> PyResult<()> { - m.add_wrapped(wrap_pyfunction!(sabre_score_heuristic))?; + m.add_wrapped(wrap_pyfunction!(build_swap_map))?; m.add_class::()?; - m.add_class::()?; - m.add_class::()?; m.add_class::()?; - m.add_class::()?; + m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/src/sabre_swap/qubits_decay.rs b/src/sabre_swap/qubits_decay.rs deleted file mode 100644 index 0a5899af1bc5..000000000000 --- a/src/sabre_swap/qubits_decay.rs +++ /dev/null @@ -1,85 +0,0 @@ -// This code is part of Qiskit. -// -// (C) Copyright IBM 2022 -// -// 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. - -use numpy::IntoPyArray; -use pyo3::exceptions::PyIndexError; -use pyo3::prelude::*; -use pyo3::Python; - -/// A container for qubit decay values for each qubit -/// -/// This class tracks the qubit decay for the sabre heuristic. When initialized -/// all qubits are set to a value of ``1.``. This class implements the sequence -/// protocol and can be modified in place like any python sequence. -/// -/// Args: -/// qubit_count (int): The number of qubits -#[pyclass(module = "qiskit._accelerate.sabre_swap")] -#[pyo3(text_signature = "(qubit_indices, logical_qubits, physical_qubits, /)")] -#[derive(Clone, Debug)] -pub struct QubitsDecay { - pub decay: Vec, -} - -#[pymethods] -impl QubitsDecay { - #[new] - pub fn new(qubit_count: usize) -> Self { - QubitsDecay { - decay: vec![1.; qubit_count], - } - } - - // Mapping Protocol - pub fn __len__(&self) -> usize { - self.decay.len() - } - - pub fn __contains__(&self, object: f64) -> bool { - self.decay.contains(&object) - } - - pub fn __getitem__(&self, object: usize) -> PyResult { - match self.decay.get(object) { - Some(val) => Ok(*val), - None => Err(PyIndexError::new_err(format!( - "Index {} out of range for this EdgeList", - object - ))), - } - } - - pub fn __setitem__(mut slf: PyRefMut, object: usize, value: f64) -> PyResult<()> { - if object >= slf.decay.len() { - return Err(PyIndexError::new_err(format!( - "Index {} out of range for this EdgeList", - object - ))); - } - slf.decay[object] = value; - Ok(()) - } - - pub fn __array__(&self, py: Python) -> PyObject { - self.decay.clone().into_pyarray(py).into() - } - - pub fn __str__(&self) -> PyResult { - Ok(format!("{:?}", self.decay)) - } - - /// Reset decay for all qubits back to default ``1.`` - #[pyo3(text_signature = "(self, /)")] - pub fn reset(mut slf: PyRefMut) { - slf.decay.fill_with(|| 1.); - } -} diff --git a/src/sabre_swap/sabre_dag.rs b/src/sabre_swap/sabre_dag.rs new file mode 100644 index 000000000000..bb60b990b2f9 --- /dev/null +++ b/src/sabre_swap/sabre_dag.rs @@ -0,0 +1,69 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2022 +// +// 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. + +use hashbrown::HashMap; +use numpy::PyReadonlyArray1; +use pyo3::prelude::*; +use retworkx_core::petgraph::prelude::*; + +/// A DAG object used to represent the data interactions from a DAGCircuit +/// to run the the sabre algorithm. This is structurally identical to the input +/// DAGCircuit, but the contents of the node are a tuple of DAGCircuit node ids, +/// a list of qargs and a list of cargs +#[pyclass(module = "qiskit._accelerate.sabre_swap")] +#[pyo3(text_signature = "(num_qubits, num_clbits, nodes, front_layer, /)")] +#[derive(Clone, Debug)] +pub struct SabreDAG { + pub dag: DiGraph<(usize, Vec, Vec), ()>, + pub first_layer: Vec, +} + +#[pymethods] +impl SabreDAG { + #[new] + pub fn new( + num_qubits: usize, + num_clbits: usize, + nodes: Vec<(usize, Vec, Vec)>, + front_layer: PyReadonlyArray1, + ) -> PyResult { + let mut qubit_pos: Vec = vec![usize::MAX; num_qubits]; + let mut clbit_pos: Vec = vec![usize::MAX; num_clbits]; + let mut reverse_index_map: HashMap = HashMap::with_capacity(nodes.len()); + let mut dag: DiGraph<(usize, Vec, Vec), ()> = + Graph::with_capacity(nodes.len(), 2 * nodes.len()); + for node in &nodes { + let qargs = &node.1; + let cargs = &node.2; + let gate_index = dag.add_node(node.clone()); + reverse_index_map.insert(node.0, gate_index); + for x in qargs { + if qubit_pos[*x] != usize::MAX { + dag.add_edge(NodeIndex::new(qubit_pos[*x]), gate_index, ()); + } + qubit_pos[*x] = gate_index.index(); + } + for x in cargs { + if clbit_pos[*x] != usize::MAX { + dag.add_edge(NodeIndex::new(qubit_pos[*x]), gate_index, ()); + } + clbit_pos[*x] = gate_index.index(); + } + } + let first_layer = front_layer + .as_slice()? + .iter() + .map(|x| reverse_index_map[x]) + .collect(); + Ok(SabreDAG { dag, first_layer }) + } +} diff --git a/src/sabre_swap/sabre_rng.rs b/src/sabre_swap/sabre_rng.rs deleted file mode 100644 index 79a4a70acb13..000000000000 --- a/src/sabre_swap/sabre_rng.rs +++ /dev/null @@ -1,35 +0,0 @@ -// This code is part of Qiskit. -// -// (C) Copyright IBM 2022 -// -// 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. - -use pyo3::prelude::*; -use rand::prelude::*; -use rand_pcg::Pcg64Mcg; - -/// A rng container that shares an rng state between python and sabre's rust -/// code. It should be initialized once and passed to -/// ``sabre_score_heuristic`` to avoid recreating a rng on the inner loop -#[pyclass(module = "qiskit._accelerate.sabre_swap")] -#[pyo3(text_signature = "(/)")] -#[derive(Clone, Debug)] -pub struct SabreRng { - pub rng: Pcg64Mcg, -} - -#[pymethods] -impl SabreRng { - #[new] - pub fn new(seed: u64) -> Self { - SabreRng { - rng: Pcg64Mcg::seed_from_u64(seed), - } - } -} diff --git a/src/sabre_swap/swap_map.rs b/src/sabre_swap/swap_map.rs new file mode 100644 index 000000000000..b14d9c72ecdc --- /dev/null +++ b/src/sabre_swap/swap_map.rs @@ -0,0 +1,48 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2022 +// +// 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. + +use hashbrown::HashMap; +use pyo3::exceptions::PyIndexError; +use pyo3::prelude::*; + +/// A container for required swaps before a gate qubit +#[pyclass(module = "qiskit._accelerate.sabre_swap")] +#[derive(Clone, Debug)] +pub struct SwapMap { + pub map: HashMap>, +} + +#[pymethods] +impl SwapMap { + // Mapping Protocol + pub fn __len__(&self) -> usize { + self.map.len() + } + + pub fn __contains__(&self, object: usize) -> bool { + self.map.contains_key(&object) + } + + pub fn __getitem__(&self, object: usize) -> PyResult> { + match self.map.get(&object) { + Some(val) => Ok(val.clone()), + None => Err(PyIndexError::new_err(format!( + "Node index {} not in swap mapping", + object + ))), + } + } + + pub fn __str__(&self) -> PyResult { + Ok(format!("{:?}", self.map)) + } +} diff --git a/tox.ini b/tox.ini index 0561b10732a7..d2bf7ad9f7a5 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,7 @@ setenv = QISKIT_SUPRESS_PACKAGING_WARNINGS=Y QISKIT_TEST_CAPTURE_STREAMS=1 QISKIT_PARALLEL=FALSE -passenv = RAYON_NUM_THREADS OMP_NUM_THREADS QISKIT_PARALLEL SETUPTOOLS_ENABLE_FEATURES +passenv = RAYON_NUM_THREADS OMP_NUM_THREADS QISKIT_PARALLEL RUST_BACKTRACE SETUPTOOLS_ENABLE_FEATURES deps = -r{toxinidir}/requirements.txt -r{toxinidir}/requirements-dev.txt commands = From f3d885e84a96d757c0a936b9d2e0fcddce9af830 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 22 Aug 2022 15:10:34 -0400 Subject: [PATCH 57/82] Remove time_taken assertion from amplitude estimator tests (#8582) This commit removes the assertions in `test.python.algorithms.test_amplitude_estimators` for testing that time_taken is > 0. This assertion is not necessary or important for the validation of the amplitude estimation functionality being tested. Additionally, they exact behavior is dependent on local execution speed and the granularity of the system calls used for time of the particular local environment the tests are running on. We have been encountering occasional failures on Windows because of the fragile nature of these assertions. Since the assertions aren't really critical to validating the functionality and are problematic on some environments this commit just opts to remove the assertions. Fixes #8577 Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- test/python/algorithms/test_amplitude_estimators.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/python/algorithms/test_amplitude_estimators.py b/test/python/algorithms/test_amplitude_estimators.py index ce7bb6ecb599..0b41e863aed2 100644 --- a/test/python/algorithms/test_amplitude_estimators.py +++ b/test/python/algorithms/test_amplitude_estimators.py @@ -130,7 +130,6 @@ def test_statevector(self, prob, qae, expect): problem = EstimationProblem(BernoulliStateIn(prob), 0, BernoulliGrover(prob)) result = qae.estimate(problem) - self.assertGreaterEqual(self._statevector.time_taken, 0.0) self._statevector.reset_execution_results() for key, value in expect.items(): self.assertAlmostEqual( @@ -350,7 +349,6 @@ def test_statevector(self, n, qae, expect): # result = qae.run(self._statevector) result = qae.estimate(estimation_problem) - self.assertGreaterEqual(self._statevector.time_taken, 0.0) self._statevector.reset_execution_results() for key, value in expect.items(): self.assertAlmostEqual( @@ -409,7 +407,6 @@ def test_confidence_intervals(self, qae, key, expect): # statevector simulator result = qae.estimate(estimation_problem) - self.assertGreater(self._statevector.time_taken, 0.0) self._statevector.reset_execution_results() methods = ["lr", "fi", "oi"] # short for likelihood_ratio, fisher, observed_fisher alphas = [0.1, 0.00001, 0.9] # alpha shouldn't matter in statevector @@ -438,7 +435,6 @@ def test_iqae_confidence_intervals(self): # statevector simulator result = qae.estimate(estimation_problem) - self.assertGreaterEqual(self._statevector.time_taken, 0.0) self._statevector.reset_execution_results() confint = result.confidence_interval # confidence interval based on statevector should be empty, as we are sure of the result From dfca1fb90d2ba18ca6f9d27f311a468fc1f3e1bf Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 22 Aug 2022 17:20:09 -0400 Subject: [PATCH 58/82] Revert "Pin setuptools in CI (#8526)" (#8530) * Revert "Pin setuptools in CI (#8526)" With the release of setuptools 64.0.1 the issues previously blocking CI and editable installs more generally should have fixed now. This commit reverts the pins previously introduced to unblock CI and work around the broken release. This reverts commit 82e38d1de0ea950457d647955471404b044910b8. * Add back SETUPTOOLS_ENABLE_FEATURES env var for legacy editable install Co-authored-by: Jake Lishman Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .azure/docs-linux.yml | 4 ++-- .azure/lint-linux.yml | 2 +- .azure/test-linux.yml | 3 +-- .azure/test-macos.yml | 2 +- .azure/test-windows.yml | 2 +- constraints.txt | 4 ---- pyproject.toml | 2 +- 7 files changed, 7 insertions(+), 12 deletions(-) diff --git a/.azure/docs-linux.yml b/.azure/docs-linux.yml index 0f8e3015c0eb..83026963219d 100644 --- a/.azure/docs-linux.yml +++ b/.azure/docs-linux.yml @@ -30,8 +30,8 @@ jobs: - bash: | set -e - python -m pip install --upgrade pip 'setuptools<64.0.0' wheel -c constraints.txt - pip install -U tox -c constraints.txt + python -m pip install --upgrade pip setuptools wheel + pip install -U tox sudo apt-get update sudo apt-get install -y graphviz displayName: 'Install dependencies' diff --git a/.azure/lint-linux.yml b/.azure/lint-linux.yml index f184ccd7470f..fef32e8acdb7 100644 --- a/.azure/lint-linux.yml +++ b/.azure/lint-linux.yml @@ -28,7 +28,7 @@ jobs: - bash: | set -e - python -m pip install --upgrade pip 'setuptools<64.0.0' wheel virtualenv -c constraints.txt + python -m pip install --upgrade pip setuptools wheel virtualenv virtualenv test-job source test-job/bin/activate pip install -U -r requirements.txt -r requirements-dev.txt -c constraints.txt diff --git a/.azure/test-linux.yml b/.azure/test-linux.yml index 57b18e698373..4ea728d46032 100644 --- a/.azure/test-linux.yml +++ b/.azure/test-linux.yml @@ -62,7 +62,7 @@ jobs: - bash: | set -e - python -m pip install --upgrade pip 'setuptools<64.0.0' wheel virtualenv -c constraints.txt + python -m pip install --upgrade pip setuptools wheel virtualenv virtualenv test-job displayName: "Prepare venv" @@ -87,7 +87,6 @@ jobs: env: SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" - - bash: | set -e source test-job/bin/activate diff --git a/.azure/test-macos.yml b/.azure/test-macos.yml index 895197098f5e..69aca4e0c1e0 100644 --- a/.azure/test-macos.yml +++ b/.azure/test-macos.yml @@ -41,7 +41,7 @@ jobs: - bash: | set -e - python -m pip install --upgrade pip 'setuptools<64.0.0' wheel virtualenv -c constraints.txt + python -m pip install --upgrade pip setuptools wheel virtualenv virtualenv test-job source test-job/bin/activate pip install -U -r requirements.txt -r requirements-dev.txt -c constraints.txt diff --git a/.azure/test-windows.yml b/.azure/test-windows.yml index 31cca7ba2ed6..a1e4e85a1f9b 100644 --- a/.azure/test-windows.yml +++ b/.azure/test-windows.yml @@ -30,7 +30,7 @@ jobs: - bash: | set -e - python -m pip install --upgrade pip 'setuptools<64.0.0' wheel virtualenv -c constraints.txt + python -m pip install --upgrade pip setuptools wheel virtualenv virtualenv test-job source test-job/Scripts/activate pip install -r requirements.txt -r requirements-dev.txt -c constraints.txt diff --git a/constraints.txt b/constraints.txt index bf4e304596c5..3cb8598cb345 100644 --- a/constraints.txt +++ b/constraints.txt @@ -13,7 +13,3 @@ pyparsing<3.0.0 # to work with the new jinja version (the jinja maintainers aren't going to # fix things) pin to the previous working version. jinja2==3.0.3 - -# setuptools 64.0.0 breaks editable installs. Pin to an old version until -# see https://github.com/pypa/setuptools/issues/3498 -setuptools==63.3.0 diff --git a/pyproject.toml b/pyproject.toml index 728e2a2be55a..9282c3b8c8d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools", "wheel", "setuptools-rust<1.5.0"] +requires = ["setuptools", "wheel", "setuptools-rust"] build-backend = "setuptools.build_meta" [tool.black] From 3f9edfc83e6effdacbcce063361878d96b5ed1d5 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Tue, 23 Aug 2022 07:35:42 +0900 Subject: [PATCH 59/82] Add logic to return channels to FakeBackendV2 (#8444) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/providers/backend.py | 2 +- .../providers/fake_provider/fake_backend.py | 101 +++++++++++++++++- ...-in--fake-backend-v2-82f0650006495fbe.yaml | 7 ++ test/python/providers/test_backend_v2.py | 44 ++++++++ 4 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/support-channels-in--fake-backend-v2-82f0650006495fbe.yaml diff --git a/qiskit/providers/backend.py b/qiskit/providers/backend.py index 2690583bd575..7ed69cadfa12 100644 --- a/qiskit/providers/backend.py +++ b/qiskit/providers/backend.py @@ -553,7 +553,7 @@ def control_channel(self, qubits: Iterable[int]): ``(control_qubit, target_qubit)``. Returns: - List[ControlChannel]: The Qubit measurement acquisition line. + List[ControlChannel]: The multi qubit control line. Raises: NotImplementedError: if the backend doesn't support querying the diff --git a/qiskit/providers/fake_provider/fake_backend.py b/qiskit/providers/fake_provider/fake_backend.py index 5bae916970f0..7554f819dce5 100644 --- a/qiskit/providers/fake_provider/fake_backend.py +++ b/qiskit/providers/fake_provider/fake_backend.py @@ -17,10 +17,12 @@ """ import warnings +import collections import json import os +import re -from typing import List +from typing import List, Iterable from qiskit import circuit from qiskit.providers.models import BackendProperties @@ -84,6 +86,36 @@ def __init__(self): self._target = None self.sim = None + if "channels" in self._conf_dict: + self._parse_channels(self._conf_dict["channels"]) + + def _parse_channels(self, channels): + type_map = { + "acquire": pulse.AcquireChannel, + "drive": pulse.DriveChannel, + "measure": pulse.MeasureChannel, + "control": pulse.ControlChannel, + } + identifier_pattern = re.compile(r"\D+(?P\d+)") + + channels_map = { + "acquire": collections.defaultdict(list), + "drive": collections.defaultdict(list), + "measure": collections.defaultdict(list), + "control": collections.defaultdict(list), + } + for identifier, spec in channels.items(): + channel_type = spec["type"] + out = re.match(identifier_pattern, identifier) + if out is None: + # Identifier is not a valid channel name format + continue + channel_index = int(out.groupdict()["index"]) + qubit_index = tuple(spec["operates"]["qubits"]) + chan_obj = type_map[channel_type](channel_index) + channels_map[channel_type][qubit_index].append(chan_obj) + setattr(self, "channels_map", channels_map) + def _setup_sim(self): if _optionals.HAS_AER: from qiskit.providers import aer @@ -193,6 +225,73 @@ def meas_map(self) -> List[List[int]]: """ return self._conf_dict.get("meas_map") + def drive_channel(self, qubit: int): + """Return the drive channel for the given qubit. + + This is required to be implemented if the backend supports Pulse + scheduling. + + Returns: + DriveChannel: The Qubit drive channel + """ + drive_channels_map = getattr(self, "channels_map", {}).get("drive", {}) + qubits = (qubit,) + if qubits in drive_channels_map: + return drive_channels_map[qubits][0] + return None + + def measure_channel(self, qubit: int): + """Return the measure stimulus channel for the given qubit. + + This is required to be implemented if the backend supports Pulse + scheduling. + + Returns: + MeasureChannel: The Qubit measurement stimulus line + """ + measure_channels_map = getattr(self, "channels_map", {}).get("measure", {}) + qubits = (qubit,) + if qubits in measure_channels_map: + return measure_channels_map[qubits][0] + return None + + def acquire_channel(self, qubit: int): + """Return the acquisition channel for the given qubit. + + This is required to be implemented if the backend supports Pulse + scheduling. + + Returns: + AcquireChannel: The Qubit measurement acquisition line. + """ + acquire_channels_map = getattr(self, "channels_map", {}).get("acquire", {}) + qubits = (qubit,) + if qubits in acquire_channels_map: + return acquire_channels_map[qubits][0] + return None + + def control_channel(self, qubits: Iterable[int]): + """Return the secondary drive channel for the given qubit + + This is typically utilized for controlling multiqubit interactions. + This channel is derived from other channels. + + This is required to be implemented if the backend supports Pulse + scheduling. + + Args: + qubits: Tuple or list of qubits of the form + ``(control_qubit, target_qubit)``. + + Returns: + List[ControlChannel]: The multi qubit control line. + """ + control_channels_map = getattr(self, "channels_map", {}).get("control", {}) + qubits = tuple(qubits) + if qubits in control_channels_map: + return control_channels_map[qubits] + return [] + def run(self, run_input, **options): """Run on the fake backend using a simulator. diff --git a/releasenotes/notes/support-channels-in--fake-backend-v2-82f0650006495fbe.yaml b/releasenotes/notes/support-channels-in--fake-backend-v2-82f0650006495fbe.yaml new file mode 100644 index 000000000000..9d01cc969f05 --- /dev/null +++ b/releasenotes/notes/support-channels-in--fake-backend-v2-82f0650006495fbe.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + All fake backends in :mod:`qiskit.providers.fake_provider.backends` have been + updated to return the corresponding pulse channel objects with the method call of + :meth:`~BackendV2.drive_channel`, :meth:`~BackendV2.measure_channel`, + :meth:`~BackendV2.acquire_channel`, :meth:`~BackendV2.control_channel`. diff --git a/test/python/providers/test_backend_v2.py b/test/python/providers/test_backend_v2.py index 1be5c96950b3..016e6feb41b7 100644 --- a/test/python/providers/test_backend_v2.py +++ b/test/python/providers/test_backend_v2.py @@ -31,7 +31,9 @@ FakeBackendSimple, FakeBackendV2LegacyQubitProps, ) +from qiskit.providers.fake_provider.backends import FakeBogotaV2 from qiskit.quantum_info import Operator +from qiskit.pulse import channels @ddt @@ -190,3 +192,45 @@ def test_transpile_parse_inst_map(self): """Test that transpiler._parse_inst_map() supports BackendV2.""" inst_map = _parse_inst_map(inst_map=None, backend=self.backend) self.assertIsInstance(inst_map, InstructionScheduleMap) + + @data(0, 1, 2, 3, 4) + def test_drive_channel(self, qubit): + """Test getting drive channel with qubit index.""" + backend = FakeBogotaV2() + chan = backend.drive_channel(qubit) + ref = channels.DriveChannel(qubit) + self.assertEqual(chan, ref) + + @data(0, 1, 2, 3, 4) + def test_measure_channel(self, qubit): + """Test getting measure channel with qubit index.""" + backend = FakeBogotaV2() + chan = backend.measure_channel(qubit) + ref = channels.MeasureChannel(qubit) + self.assertEqual(chan, ref) + + @data(0, 1, 2, 3, 4) + def test_acquire_channel(self, qubit): + """Test getting acquire channel with qubit index.""" + backend = FakeBogotaV2() + chan = backend.acquire_channel(qubit) + ref = channels.AcquireChannel(qubit) + self.assertEqual(chan, ref) + + @data((4, 3), (3, 4), (3, 2), (2, 3), (1, 2), (2, 1), (1, 0), (0, 1)) + def test_control_channel(self, qubits): + """Test getting acquire channel with qubit index.""" + bogota_cr_channels_map = { + (4, 3): 7, + (3, 4): 6, + (3, 2): 5, + (2, 3): 4, + (1, 2): 2, + (2, 1): 3, + (1, 0): 1, + (0, 1): 0, + } + backend = FakeBogotaV2() + chan = backend.control_channel(qubits)[0] + ref = channels.ControlChannel(bogota_cr_channels_map[qubits]) + self.assertEqual(chan, ref) From e6e5ff6ac6d104613666b6144092b7451cd786e6 Mon Sep 17 00:00:00 2001 From: dlasecki Date: Tue, 23 Aug 2022 08:55:07 -0700 Subject: [PATCH 60/82] Variational Quantum Time Evolution algorithm. (#8152) * Fixed requirements-dev.txt * Fixed some pylint. * Fixed some pylint. * Update Documentation variational principles * updates documentation * minor documentation * Added unit tests docs and some package docs. * Code refactoring. * Code refactoring. * Code refactoring. * Update qiskit/opflow/evolutions/pauli_trotter_evolution.py Co-authored-by: Julien Gacon * Renamed the main folder. * Code refactoring. * Update qiskit/algorithms/quantum_time_evolution/variational/calculators/evolution_grad_calculator.py Co-authored-by: Julien Gacon * Code refactoring. * Update qiskit/algorithms/time_evolution/variational/error_calculators/gradient_errors/error_calculator.py Co-authored-by: Julien Gacon * Code refactoring. * Lint fixes. * Code refactoring. * Code refactoring. * Code refactoring. * Code refactoring. * Code refactoring, docs fixed. * Code refactoring, docs fixed. * Code refactoring and docs improved. * Exposed optimizer tolerance as an argument. * Exposed allowed imaginary part as an argument. * Exposed allowed numerical instability as an argument. * Code refactoring. * Introduced evolution_result.py class. * Minor bugfix. * Integrated evolution result to VarQte algorithms. * Code refactoring. * Black formatting fix. * Fixed signatures. * Fixed random seed setup. * Fixed too long lines. * Deleted unnecessary files. * Some fixes in test_gradients.py * Copyright date updated. * Refactored getting rid of flags. * Updated unit tests after refactoring. * Removed a duplicated argument. * Implemented general Quantum Time Evolution Framework interfaces. * Updated docs. * Reno added. * Improved reno. * Code refactoring. * Code refactoring. * Typehints added. * Made variational_principle.py stateless. * Updated copyright years. * Simplified var_qte.py class. * Refactored var_qte_linear_solver.py * Refactored abstract_ode_function_generator.py * Refactored var_qte_linear_solver.py * Refactored var_qte.py * Code formatting. * ODE solvers and optimizers as objects, not strings. * Update qiskit/algorithms/time_evolution/evolution_base.py Co-authored-by: Julien Gacon * Code refactoring. * Introduced evolution problem classes. * Code refactoring. * Apply suggestions from code review Co-authored-by: Julien Gacon * Added unit tests. * Lint fixed. * Code refactoring. * Removed error_based_ode_function_generator.py for MVP. * Code refactoring * Code refactoring * Code refactoring * Code refactoring * Code refactoring * Code review changes. * Removed gradient code for now. * Evolving observable removed. Evaluating observables added. * Improved naming. * Improved folder structure; filled evolvers init file. * Added Evolvers to algorithms init file. * Fixed missing imports. * Code refactoring * Fixed cyclic imports. * Extracted ListOrDict. * Code refactoring. * Code refactoring. * Fixed release note. * Fixed inheritance order. * Code refactoring. * Code refactoring. * Fixed cyclic imports. * Name fix. * Updated the algorithm to the latest version of interfaces. * Code refactoring. * Adapted unit tests to evolution problem. * Implemented aux_ops evaluation. * Fixed position of quantum_instance. * Added algorithms to algorithms init. * Imports refactoring. * Imports refactoring. * Updated code to the latest gradient framework. * Code refactoring. * Imports refactoring. * Added gradient files. * Code review addressed. Fixed tests. * Switched to 1 sampler. * Added unit tests for expected errors. * Improved docs. * Changed folder structure. * Added test_evolution_grad_calculator.py unit test with bases. * Updated interfaces. * Added VarQite unit test with aux ops. * Added VarQite unit test with aux ops. * Added VarQrte unit test with aux ops. * Update releasenotes/notes/add-variational-quantum-time-evolution-112ffeaf62782fea.yaml Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> * Code refactoring. * Code refactoring. * Code refactoring. * Improved docs of variational principles. * Code refactoring. * Code refactoring. * Simplified var principles folder structure. * Opened VarQte algorithms for field modification. * Improved Sphinx docs. * Code refactoring. * Introduced ode_function_factory.py * Removed hardcoded rcond. * Renamed hamiltonian_value_dict. * Renamed hamiltonian_value_dict. * Extracted lengthy expected results in tests. * Updated unit tests. * Apply suggestions from code review Co-authored-by: Julien Gacon * Apply suggestions from code review * Extended release notes. * Removed dead code. * Improved ode solver types. * Moved evolve method for now. * Shortened Var Principles names. * Updated metric_tensor_calculator.py * Updated docs * Removed ordered_parameters. * Added and corrected code examples. * Extended unit tests. * Extended unit tests. * Extended unit tests. * Improved init files. * Improved init files. * Improved init files. * Improved init files. * Improved docs. * Import fix * Renamed sle to lse. * Code refactoring. * Replaced metric tensor calculator. * Replaced evolution gradient calculator. * Code refactoring. * Removed evolution_grad_calculator.py * Removed evolution_grad_calculator.py * Evolution grad calculator removal; code refactoring; dirty bug fix. * Added docs. * Code refactoring * Improved performance of evolution grad. * Improved performance of evolution grad, code refactoring; bug fixed. * Improved LSE solver handling. * Improved docs. * Unindented reno * Mitigated some cyclic imports. * Unindented reno * Improved docs * Lint resolutions * Fix some line-too-long in tests * Fixed lint. * Fixed lint. * Apply suggestions from code review Co-authored-by: Julien Gacon * Code review changes. * Added reno for a bug fix. * Fixed lint. * Updated docs. * Moved lse_solver to ODE factory. * Made ODE factory optional and defaults set. * Added expectation method argument and set defaults to PauliExpectation. * Limited supported methods for real_time_dependent_principle.py. * Fix imports. * Updated docs. * Implemented the Forward Euler ODE solver. Implemented a unit test. * Set Forward Euler ODE solver as a default. Updated unit tests. * Sync with main. * Exposed ODE number of steps. * Code refactoring * Code refactoring * Code refactoring * Exposed ode_num_t_steps to the user; reduced default ode_num_t_steps; added ForwardEulerSolver to docs. * Updated tests. * Fixed CI. * Fixed docs. * Hides OdeFunction from the user. * Update qiskit/algorithms/evolvers/variational/var_qrte.py Co-authored-by: Julien Gacon * Update releasenotes/notes/add-variational-quantum-time-evolution-112ffeaf62782fea.yaml Co-authored-by: Julien Gacon * Removes unsupported error-based method flag for now. * Switched num time steps to deltas in ODEs. * Code refactoring. * Fixed typhint. * Switched back to num_steps for ODE. * Slow tests and code refactoring. * Code refactoring. * Removed TD variational principle. * Apply suggestions from code review Co-authored-by: Julien Gacon * Code refactoring. * Improved signatures; support for list of parameter values. * Code refactoring. * Improved variable name. * Updated docs. Co-authored-by: CZ Co-authored-by: Julien Gacon Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> Co-authored-by: woodsp-ibm Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/algorithms/__init__.py | 18 ++ .../algorithms/evolvers/evolution_problem.py | 12 +- .../evolvers/trotterization/trotter_qrte.py | 2 +- .../evolvers/variational/__init__.py | 139 ++++++++ .../evolvers/variational/solvers/__init__.py | 44 +++ .../variational/solvers/ode/__init__.py | 13 + .../solvers/ode/abstract_ode_function.py | 52 +++ .../solvers/ode/forward_euler_solver.py | 73 +++++ .../variational/solvers/ode/ode_function.py | 43 +++ .../solvers/ode/ode_function_factory.py | 83 +++++ .../solvers/ode/var_qte_ode_solver.py | 83 +++++ .../solvers/var_qte_linear_solver.py | 160 +++++++++ .../evolvers/variational/var_qite.py | 125 ++++++++ .../evolvers/variational/var_qrte.py | 126 ++++++++ .../evolvers/variational/var_qte.py | 303 ++++++++++++++++++ .../variational_principles/__init__.py | 25 ++ .../imaginary_mc_lachlan_principle.py | 76 +++++ .../imaginary_variational_principle.py | 24 ++ .../real_mc_lachlan_principle.py | 150 +++++++++ .../real_variational_principle.py | 42 +++ .../variational_principle.py | 129 ++++++++ qiskit/opflow/gradients/derivative_base.py | 11 +- ...antum-time-evolution-112ffeaf62782fea.yaml | 50 +++ ...fix-gradient-wrapper-2f9ab45941739044.yaml | 7 + .../evolvers/test_evolution_problem.py | 24 +- .../trotterization/test_trotter_qrte.py | 6 +- .../evolvers/variational/__init__.py | 11 + .../evolvers/variational/solvers/__init__.py | 11 + .../solvers/expected_results/__init__.py | 12 + .../test_varqte_linear_solver_expected_1.py | 182 +++++++++++ .../variational/solvers/ode/__init__.py | 11 + .../solvers/ode/test_forward_euler_solver.py | 47 +++ .../solvers/ode/test_ode_function.py | 165 ++++++++++ .../solvers/ode/test_var_qte_ode_solver.py | 136 ++++++++ .../solvers/test_varqte_linear_solver.py | 115 +++++++ .../evolvers/variational/test_var_qite.py | 287 +++++++++++++++++ .../evolvers/variational/test_var_qrte.py | 234 ++++++++++++++ .../evolvers/variational/test_var_qte.py | 78 +++++ .../variational_principles/__init__.py | 11 + .../expected_results/__init__.py | 12 + ...lachlan_variational_principle_expected1.py | 182 +++++++++++ ...lachlan_variational_principle_expected2.py | 182 +++++++++++ ...lachlan_variational_principle_expected3.py | 182 +++++++++++ .../imaginary/__init__.py | 11 + .../test_imaginary_mc_lachlan_principle.py | 111 +++++++ .../variational_principles/real/__init__.py | 11 + .../real/test_real_mc_lachlan_principle.py | 114 +++++++ 47 files changed, 3896 insertions(+), 29 deletions(-) create mode 100644 qiskit/algorithms/evolvers/variational/__init__.py create mode 100644 qiskit/algorithms/evolvers/variational/solvers/__init__.py create mode 100644 qiskit/algorithms/evolvers/variational/solvers/ode/__init__.py create mode 100644 qiskit/algorithms/evolvers/variational/solvers/ode/abstract_ode_function.py create mode 100644 qiskit/algorithms/evolvers/variational/solvers/ode/forward_euler_solver.py create mode 100644 qiskit/algorithms/evolvers/variational/solvers/ode/ode_function.py create mode 100644 qiskit/algorithms/evolvers/variational/solvers/ode/ode_function_factory.py create mode 100644 qiskit/algorithms/evolvers/variational/solvers/ode/var_qte_ode_solver.py create mode 100644 qiskit/algorithms/evolvers/variational/solvers/var_qte_linear_solver.py create mode 100644 qiskit/algorithms/evolvers/variational/var_qite.py create mode 100644 qiskit/algorithms/evolvers/variational/var_qrte.py create mode 100644 qiskit/algorithms/evolvers/variational/var_qte.py create mode 100644 qiskit/algorithms/evolvers/variational/variational_principles/__init__.py create mode 100644 qiskit/algorithms/evolvers/variational/variational_principles/imaginary_mc_lachlan_principle.py create mode 100644 qiskit/algorithms/evolvers/variational/variational_principles/imaginary_variational_principle.py create mode 100644 qiskit/algorithms/evolvers/variational/variational_principles/real_mc_lachlan_principle.py create mode 100644 qiskit/algorithms/evolvers/variational/variational_principles/real_variational_principle.py create mode 100644 qiskit/algorithms/evolvers/variational/variational_principles/variational_principle.py create mode 100644 releasenotes/notes/add-variational-quantum-time-evolution-112ffeaf62782fea.yaml create mode 100644 releasenotes/notes/fix-gradient-wrapper-2f9ab45941739044.yaml create mode 100644 test/python/algorithms/evolvers/variational/__init__.py create mode 100644 test/python/algorithms/evolvers/variational/solvers/__init__.py create mode 100644 test/python/algorithms/evolvers/variational/solvers/expected_results/__init__.py create mode 100644 test/python/algorithms/evolvers/variational/solvers/expected_results/test_varqte_linear_solver_expected_1.py create mode 100644 test/python/algorithms/evolvers/variational/solvers/ode/__init__.py create mode 100644 test/python/algorithms/evolvers/variational/solvers/ode/test_forward_euler_solver.py create mode 100644 test/python/algorithms/evolvers/variational/solvers/ode/test_ode_function.py create mode 100644 test/python/algorithms/evolvers/variational/solvers/ode/test_var_qte_ode_solver.py create mode 100644 test/python/algorithms/evolvers/variational/solvers/test_varqte_linear_solver.py create mode 100644 test/python/algorithms/evolvers/variational/test_var_qite.py create mode 100644 test/python/algorithms/evolvers/variational/test_var_qrte.py create mode 100644 test/python/algorithms/evolvers/variational/test_var_qte.py create mode 100644 test/python/algorithms/evolvers/variational/variational_principles/__init__.py create mode 100644 test/python/algorithms/evolvers/variational/variational_principles/expected_results/__init__.py create mode 100644 test/python/algorithms/evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected1.py create mode 100644 test/python/algorithms/evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected2.py create mode 100644 test/python/algorithms/evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected3.py create mode 100644 test/python/algorithms/evolvers/variational/variational_principles/imaginary/__init__.py create mode 100644 test/python/algorithms/evolvers/variational/variational_principles/imaginary/test_imaginary_mc_lachlan_principle.py create mode 100644 test/python/algorithms/evolvers/variational/variational_principles/real/__init__.py create mode 100644 test/python/algorithms/evolvers/variational/variational_principles/real/test_real_mc_lachlan_principle.py diff --git a/qiskit/algorithms/__init__.py b/qiskit/algorithms/__init__.py index 63e44798a710..30d71a59e59c 100644 --- a/qiskit/algorithms/__init__.py +++ b/qiskit/algorithms/__init__.py @@ -94,6 +94,17 @@ VQD +Variational Quantum Time Evolution +---------------------------------- + +Classes used by variational quantum time evolution algorithms - VarQITE and VarQRTE. + +.. autosummary:: + :toctree: ../stubs/ + + evolvers.variational + + Evolvers -------- @@ -108,11 +119,14 @@ RealEvolver ImaginaryEvolver TrotterQRTE + VarQITE + VarQRTE PVQD PVQDResult EvolutionResult EvolutionProblem + Factorizers ----------- @@ -248,6 +262,8 @@ from .exceptions import AlgorithmError from .aux_ops_evaluator import eval_observables from .evolvers.trotterization import TrotterQRTE +from .evolvers.variational.var_qite import VarQITE +from .evolvers.variational.var_qrte import VarQRTE from .evolvers.pvqd import PVQD, PVQDResult __all__ = [ @@ -273,6 +289,8 @@ "RealEvolver", "ImaginaryEvolver", "TrotterQRTE", + "VarQITE", + "VarQRTE", "EvolutionResult", "EvolutionProblem", "LinearSolverResult", diff --git a/qiskit/algorithms/evolvers/evolution_problem.py b/qiskit/algorithms/evolvers/evolution_problem.py index e0f9fe3063c6..175beecd6bf6 100644 --- a/qiskit/algorithms/evolvers/evolution_problem.py +++ b/qiskit/algorithms/evolvers/evolution_problem.py @@ -35,7 +35,7 @@ def __init__( aux_operators: Optional[ListOrDict[OperatorBase]] = None, truncation_threshold: float = 1e-12, t_param: Optional[Parameter] = None, - hamiltonian_value_dict: Optional[Dict[Parameter, complex]] = None, + param_value_dict: Optional[Dict[Parameter, complex]] = None, ): """ Args: @@ -50,15 +50,15 @@ def __init__( Used when ``aux_operators`` is provided. t_param: Time parameter in case of a time-dependent Hamiltonian. This free parameter must be within the ``hamiltonian``. - hamiltonian_value_dict: If the Hamiltonian contains free parameters, this - dictionary maps all these parameters to values. + param_value_dict: Maps free parameters in the problem to values. Depending on the + algorithm, it might refer to e.g. a Hamiltonian or an initial state. Raises: ValueError: If non-positive time of evolution is provided. """ self.t_param = t_param - self.hamiltonian_value_dict = hamiltonian_value_dict + self.param_value_dict = param_value_dict self.hamiltonian = hamiltonian self.time = time self.initial_state = initial_state @@ -95,9 +95,9 @@ def validate_params(self) -> None: if self.t_param is not None: t_param_set.add(self.t_param) hamiltonian_dict_param_set = set() - if self.hamiltonian_value_dict is not None: + if self.param_value_dict is not None: hamiltonian_dict_param_set = hamiltonian_dict_param_set.union( - set(self.hamiltonian_value_dict.keys()) + set(self.param_value_dict.keys()) ) params_set = t_param_set.union(hamiltonian_dict_param_set) hamiltonian_param_set = set(self.hamiltonian.parameters) diff --git a/qiskit/algorithms/evolvers/trotterization/trotter_qrte.py b/qiskit/algorithms/evolvers/trotterization/trotter_qrte.py index abe02e95c156..05b8266605b7 100644 --- a/qiskit/algorithms/evolvers/trotterization/trotter_qrte.py +++ b/qiskit/algorithms/evolvers/trotterization/trotter_qrte.py @@ -187,7 +187,7 @@ def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: f"PauliSumOp | SummedOp, {type(hamiltonian)} provided." ) if isinstance(hamiltonian, OperatorBase): - hamiltonian = hamiltonian.bind_parameters(evolution_problem.hamiltonian_value_dict) + hamiltonian = hamiltonian.bind_parameters(evolution_problem.param_value_dict) if isinstance(hamiltonian, SummedOp): hamiltonian = self._summed_op_to_pauli_sum_op(hamiltonian) # the evolution gate diff --git a/qiskit/algorithms/evolvers/variational/__init__.py b/qiskit/algorithms/evolvers/variational/__init__.py new file mode 100644 index 000000000000..8936d9030853 --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/__init__.py @@ -0,0 +1,139 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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. +""" +Variational Quantum Time Evolutions (:mod:`qiskit.algorithms.evolvers.variational`) +=================================================================================== + +Algorithms for performing Variational Quantum Time Evolution of quantum states, +which can be tailored to near-term devices. +:class:`~qiskit.algorithms.evolvers.variational.VarQTE` base class exposes an interface, compliant +with the Quantum Time Evolution Framework in Qiskit Terra, that is implemented by +:class:`~qiskit.algorithms.VarQRTE` and :class:`~qiskit.algorithms.VarQITE` classes for real and +imaginary time evolution respectively. The variational approach is taken according to a variational +principle chosen by a user. + +Examples: + +.. code-block:: + + from qiskit import BasicAer + from qiskit.circuit.library import EfficientSU2 + from qiskit.opflow import SummedOp, I, Z, Y, X + from qiskit.algorithms.evolvers.variational import ( + ImaginaryMcLachlanPrinciple, + ) + from qiskit.algorithms import EvolutionProblem + from qiskit.algorithms import VarQITE + + # define a Hamiltonian + observable = SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ).reduce() + + # define a parametrized initial state to be evolved + + ansatz = EfficientSU2(observable.num_qubits, reps=1) + parameters = ansatz.parameters + + # define values of initial parameters + init_param_values = np.zeros(len(ansatz.parameters)) + for i in range(len(ansatz.parameters)): + init_param_values[i] = np.pi / 2 + param_dict = dict(zip(parameters, init_param_values)) + + # define a variational principle + var_principle = ImaginaryMcLachlanPrinciple() + + # optionally define a backend + backend = BasicAer.get_backend("statevector_simulator") + + # define evolution time + time = 1 + + # define evolution problem + evolution_problem = EvolutionProblem(observable, time) + + # instantiate the algorithm + var_qite = VarQITE(ansatz, var_principle, param_dict, quantum_instance=backend) + + # run the algorithm/evolve the state + evolution_result = var_qite.evolve(evolution_problem) + +.. currentmodule:: qiskit.algorithms.evolvers.variational + +Variational Principles +---------------------- + +Variational principles can be used to simulate quantum time evolution by propagating the parameters +of a parameterized quantum circuit. + +They can be divided into two categories: + + 1) Variational Quantum Imaginary Time Evolution + Given a Hamiltonian, a time and a variational ansatz, the variational principle describes a + variational principle according to the normalized Wick-rotated Schroedinger equation. + + 2) Variational Quantum Real Time Evolution + Given a Hamiltonian, a time and a variational ansatz, the variational principle describes a + variational principle according to the Schroedinger equation. + +.. autosummary:: + :toctree: ../stubs/ + :template: autosummary/class_no_inherited_members.rst + + VariationalPrinciple + RealVariationalPrinciple + ImaginaryVariationalPrinciple + RealMcLachlanPrinciple + ImaginaryMcLachlanPrinciple + +ODE solvers +----------- +ODE solvers that implement the SciPy ODE Solver interface. The Forward Euler Solver is +a preferred choice in the presence of noise. One might also use solvers provided by SciPy directly, +e.g. RK45. + +.. autosummary:: + :toctree: ../stubs/ + :template: autosummary/class_no_inherited_members.rst + + ForwardEulerSolver + +""" +from .solvers.ode.forward_euler_solver import ForwardEulerSolver +from .var_qte import VarQTE +from .variational_principles.variational_principle import VariationalPrinciple +from .variational_principles import RealVariationalPrinciple, ImaginaryVariationalPrinciple +from .variational_principles.imaginary_mc_lachlan_principle import ( + ImaginaryMcLachlanPrinciple, +) +from .variational_principles.real_mc_lachlan_principle import ( + RealMcLachlanPrinciple, +) + + +__all__ = [ + "ForwardEulerSolver", + "VarQTE", + "VariationalPrinciple", + "RealVariationalPrinciple", + "ImaginaryVariationalPrinciple", + "RealMcLachlanPrinciple", + "ImaginaryMcLachlanPrinciple", +] diff --git a/qiskit/algorithms/evolvers/variational/solvers/__init__.py b/qiskit/algorithms/evolvers/variational/solvers/__init__.py new file mode 100644 index 000000000000..6d273c0618c9 --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/solvers/__init__.py @@ -0,0 +1,44 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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. + +""" +Solvers (:mod:`qiskit.algorithms.evolvers.variational.solvers`) +=============================================================== + +This package contains the necessary classes to solve systems of equations arising in the +Variational Quantum Time Evolution. They include ordinary differential equations (ODE) which +describe ansatz parameter propagation and systems of linear equations. + + +Systems of Linear Equations Solver +---------------------------------- + +.. autosummary:: + :toctree: ../stubs/ + :template: autosummary/class_no_inherited_members.rst + + VarQTELinearSolver + + +ODE Solver +---------- +.. autosummary:: + :toctree: ../stubs/ + :template: autosummary/class_no_inherited_members.rst + + VarQTEOdeSolver +""" + +from qiskit.algorithms.evolvers.variational.solvers.ode.var_qte_ode_solver import VarQTEOdeSolver +from qiskit.algorithms.evolvers.variational.solvers.var_qte_linear_solver import VarQTELinearSolver + +__all__ = ["VarQTELinearSolver", "VarQTEOdeSolver"] diff --git a/qiskit/algorithms/evolvers/variational/solvers/ode/__init__.py b/qiskit/algorithms/evolvers/variational/solvers/ode/__init__.py new file mode 100644 index 000000000000..8fbaef6f85d1 --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/solvers/ode/__init__.py @@ -0,0 +1,13 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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. + +"""ODE Solvers""" diff --git a/qiskit/algorithms/evolvers/variational/solvers/ode/abstract_ode_function.py b/qiskit/algorithms/evolvers/variational/solvers/ode/abstract_ode_function.py new file mode 100644 index 000000000000..a443a26d1888 --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/solvers/ode/abstract_ode_function.py @@ -0,0 +1,52 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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. + +"""Abstract class for generating ODE functions.""" + +from abc import ABC, abstractmethod +from typing import Iterable, Dict, Optional +from qiskit.circuit import Parameter +from ..var_qte_linear_solver import ( + VarQTELinearSolver, +) + + +class AbstractOdeFunction(ABC): + """Abstract class for generating ODE functions.""" + + def __init__( + self, + varqte_linear_solver: VarQTELinearSolver, + error_calculator, + param_dict: Dict[Parameter, complex], + t_param: Optional[Parameter] = None, + ) -> None: + + self._varqte_linear_solver = varqte_linear_solver + self._error_calculator = error_calculator + self._param_dict = param_dict + self._t_param = t_param + + @abstractmethod + def var_qte_ode_function(self, time: float, parameters_values: Iterable) -> Iterable: + """ + Evaluates an ODE function for a given time and parameter values. It is used by an ODE + solver. + + Args: + time: Current time of evolution. + parameters_values: Current values of parameters. + + Returns: + ODE gradient arising from solving a system of linear equations. + """ + pass diff --git a/qiskit/algorithms/evolvers/variational/solvers/ode/forward_euler_solver.py b/qiskit/algorithms/evolvers/variational/solvers/ode/forward_euler_solver.py new file mode 100644 index 000000000000..284b3106fa8a --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/solvers/ode/forward_euler_solver.py @@ -0,0 +1,73 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. +"""Forward Euler ODE solver.""" + +from typing import Sequence + +import numpy as np +from scipy.integrate import OdeSolver +from scipy.integrate._ivp.base import ConstantDenseOutput + + +class ForwardEulerSolver(OdeSolver): + """Forward Euler ODE solver.""" + + def __init__( + self, + function: callable, + t0: float, + y0: Sequence, + t_bound: float, + vectorized: bool = False, + support_complex: bool = False, + num_t_steps: int = 15, + ): + """ + Forward Euler ODE solver that implements an interface from SciPy. + + Args: + function: Right-hand side of the system. The calling signature is ``fun(t, y)``. Here + ``t`` is a scalar, and there are two options for the ndarray ``y``: + It can either have shape (n,); then ``fun`` must return array_like with + shape (n,). Alternatively it can have shape (n, k); then ``fun`` + must return an array_like with shape (n, k), i.e., each column + corresponds to a single column in ``y``. The choice between the two + options is determined by `vectorized` argument (see below). The + vectorized implementation allows a faster approximation of the Jacobian + by finite differences (required for this solver). + t0: Initial time. + y0: Initial state. + t_bound: Boundary time - the integration won't continue beyond it. It also determines + the direction of the integration. + vectorized: Whether ``fun`` is implemented in a vectorized fashion. Default is False. + support_complex: Whether integration in a complex domain should be supported. + Generally determined by a derived solver class capabilities. Default is False. + num_t_steps: Number of time steps for the forward Euler method. + """ + self.y_old = None + self.step_length = (t_bound - t0) / num_t_steps + super().__init__(function, t0, y0, t_bound, vectorized, support_complex) + + def _step_impl(self): + """ + Takes an Euler step. + """ + try: + self.y_old = self.y + self.y = list(np.add(self.y, self.step_length * self.fun(self.t, self.y))) + self.t += self.step_length + return True, None + except Exception as ex: # pylint: disable=broad-except + return False, f"Unknown ODE solver error: {str(ex)}." + + def _dense_output_impl(self): + return ConstantDenseOutput(self.t_old, self.t, self.y_old) diff --git a/qiskit/algorithms/evolvers/variational/solvers/ode/ode_function.py b/qiskit/algorithms/evolvers/variational/solvers/ode/ode_function.py new file mode 100644 index 000000000000..0d142262868c --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/solvers/ode/ode_function.py @@ -0,0 +1,43 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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. + +"""Class for generating ODE functions based on ODE gradients.""" +from typing import Iterable + +from ..ode.abstract_ode_function import ( + AbstractOdeFunction, +) + + +class OdeFunction(AbstractOdeFunction): + """Class for generating ODE functions based on ODE gradients.""" + + def var_qte_ode_function(self, time: float, parameters_values: Iterable) -> Iterable: + """ + Evaluates an ODE function for a given time and parameter values. It is used by an ODE + solver. + + Args: + time: Current time of evolution. + parameters_values: Current values of parameters. + + Returns: + ODE gradient arising from solving a system of linear equations. + """ + current_param_dict = dict(zip(self._param_dict.keys(), parameters_values)) + + ode_grad_res, _, _ = self._varqte_linear_solver.solve_lse( + current_param_dict, + time, + ) + + return ode_grad_res diff --git a/qiskit/algorithms/evolvers/variational/solvers/ode/ode_function_factory.py b/qiskit/algorithms/evolvers/variational/solvers/ode/ode_function_factory.py new file mode 100644 index 000000000000..2dce88a817b1 --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/solvers/ode/ode_function_factory.py @@ -0,0 +1,83 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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. + +"""Abstract class for generating ODE functions.""" + +from abc import ABC +from enum import Enum +from typing import Dict, Any, Optional, Callable + +import numpy as np + +from qiskit.circuit import Parameter +from .abstract_ode_function import AbstractOdeFunction +from .ode_function import OdeFunction +from ..var_qte_linear_solver import ( + VarQTELinearSolver, +) + + +class OdeFunctionType(Enum): + """Types of ODE functions for VatQTE algorithms.""" + + # more will be supported in the near future + STANDARD_ODE = "STANDARD_ODE" + + +class OdeFunctionFactory(ABC): + """Factory for building ODE functions.""" + + def __init__( + self, + ode_function_type: OdeFunctionType = OdeFunctionType.STANDARD_ODE, + lse_solver: Optional[Callable[[np.ndarray, np.ndarray], np.ndarray]] = None, + ) -> None: + """ + Args: + ode_function_type: An Enum that defines a type of an ODE function to be built. If + not provided, a default ``STANDARD_ODE`` is used. + lse_solver: Linear system of equations solver callable. It accepts ``A`` and ``b`` to + solve ``Ax=b`` and returns ``x``. If ``None``, the default ``np.linalg.lstsq`` + solver is used. + """ + self.ode_function_type = ode_function_type + self.lse_solver = lse_solver + + def _build( + self, + varqte_linear_solver: VarQTELinearSolver, + error_calculator: Any, + param_dict: Dict[Parameter, complex], + t_param: Optional[Parameter] = None, + ) -> AbstractOdeFunction: + """ + Initializes an ODE function specified in the class. + + Args: + varqte_linear_solver: Solver of LSE for the VarQTE algorithm. + error_calculator: Calculator of errors for error-based ODE functions. + param_dict: Dictionary which relates parameter values to the parameters in the ansatz. + t_param: Time parameter in case of a time-dependent Hamiltonian. + + Returns: + An ODE function. + + Raises: + ValueError: If unsupported ODE function provided. + + """ + if self.ode_function_type == OdeFunctionType.STANDARD_ODE: + return OdeFunction(varqte_linear_solver, error_calculator, param_dict, t_param) + raise ValueError( + f"Unsupported ODE function provided: {self.ode_function_type}." + f" Only {[tp.value for tp in OdeFunctionType]} are supported." + ) diff --git a/qiskit/algorithms/evolvers/variational/solvers/ode/var_qte_ode_solver.py b/qiskit/algorithms/evolvers/variational/solvers/ode/var_qte_ode_solver.py new file mode 100644 index 000000000000..525769ddc96c --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/solvers/ode/var_qte_ode_solver.py @@ -0,0 +1,83 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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. + +"""Class for solving ODEs for Quantum Time Evolution.""" +from functools import partial +from typing import List, Union, Type, Optional + +import numpy as np +from scipy.integrate import OdeSolver, solve_ivp + +from .abstract_ode_function import ( + AbstractOdeFunction, +) +from .forward_euler_solver import ForwardEulerSolver + + +class VarQTEOdeSolver: + """Class for solving ODEs for Quantum Time Evolution.""" + + def __init__( + self, + init_params: List[complex], + ode_function: AbstractOdeFunction, + ode_solver: Union[Type[OdeSolver], str] = ForwardEulerSolver, + num_timesteps: Optional[int] = None, + ) -> None: + """ + Initialize ODE Solver. + + Args: + init_params: Set of initial parameters for time 0. + ode_function: Generates the ODE function. + ode_solver: ODE solver callable that implements a SciPy ``OdeSolver`` interface or a + string indicating a valid method offered by SciPy. + num_timesteps: The number of timesteps to take. If None, it is + automatically selected to achieve a timestep of approximately 0.01. Only + relevant in case of the ``ForwardEulerSolver``. + """ + self._init_params = init_params + self._ode_function = ode_function.var_qte_ode_function + self._ode_solver = ode_solver + self._num_timesteps = num_timesteps + + def run(self, evolution_time: float) -> List[complex]: + """ + Finds numerical solution with ODE Solver. + + Args: + evolution_time: Evolution time. + + Returns: + List of parameters found by an ODE solver for a given ODE function callable. + """ + # determine the number of timesteps and set the timestep + num_timesteps = ( + int(np.ceil(evolution_time / 0.01)) + if self._num_timesteps is None + else self._num_timesteps + ) + + if self._ode_solver == ForwardEulerSolver: + solve = partial(solve_ivp, num_t_steps=num_timesteps) + else: + solve = solve_ivp + + sol = solve( + self._ode_function, + (0, evolution_time), + self._init_params, + method=self._ode_solver, + ) + final_params_vals = [lst[-1] for lst in sol.y] + + return final_params_vals diff --git a/qiskit/algorithms/evolvers/variational/solvers/var_qte_linear_solver.py b/qiskit/algorithms/evolvers/variational/solvers/var_qte_linear_solver.py new file mode 100644 index 000000000000..1c4a61963374 --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/solvers/var_qte_linear_solver.py @@ -0,0 +1,160 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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. + +"""Class for solving linear equations for Quantum Time Evolution.""" + +from typing import Union, List, Dict, Optional, Callable + +import numpy as np + +from qiskit import QuantumCircuit +from qiskit.algorithms.evolvers.variational.variational_principles.variational_principle import ( + VariationalPrinciple, +) +from qiskit.circuit import Parameter +from qiskit.opflow import ( + CircuitSampler, + OperatorBase, + ExpectationBase, +) +from qiskit.providers import Backend +from qiskit.utils import QuantumInstance +from qiskit.utils.backend_utils import is_aer_provider + + +class VarQTELinearSolver: + """Class for solving linear equations for Quantum Time Evolution.""" + + def __init__( + self, + var_principle: VariationalPrinciple, + hamiltonian: OperatorBase, + ansatz: QuantumCircuit, + gradient_params: List[Parameter], + t_param: Optional[Parameter] = None, + lse_solver: Optional[Callable[[np.ndarray, np.ndarray], np.ndarray]] = None, + imag_part_tol: float = 1e-7, + expectation: Optional[ExpectationBase] = None, + quantum_instance: Optional[QuantumInstance] = None, + ) -> None: + """ + Args: + var_principle: Variational Principle to be used. + hamiltonian: + Operator used for Variational Quantum Time Evolution. + The operator may be given either as a composed op consisting of a Hermitian + observable and a ``CircuitStateFn`` or a ``ListOp`` of a ``CircuitStateFn`` with a + ``ComboFn``. + The latter case enables the evaluation of a Quantum Natural Gradient. + ansatz: Quantum state in the form of a parametrized quantum circuit. + gradient_params: List of parameters with respect to which gradients should be computed. + t_param: Time parameter in case of a time-dependent Hamiltonian. + lse_solver: Linear system of equations solver callable. It accepts ``A`` and ``b`` to + solve ``Ax=b`` and returns ``x``. If ``None``, the default ``np.linalg.lstsq`` + solver is used. + imag_part_tol: Allowed value of an imaginary part that can be neglected if no + imaginary part is expected. + expectation: An instance of ``ExpectationBase`` used for calculating a metric tensor + and an evolution gradient. If ``None`` provided, a ``PauliExpectation`` is used. + quantum_instance: Backend used to evaluate the quantum circuit outputs. If ``None`` + provided, everything will be evaluated based on matrix multiplication (which is + slow). + """ + self._var_principle = var_principle + self._hamiltonian = hamiltonian + self._ansatz = ansatz + self._gradient_params = gradient_params + self._bind_params = gradient_params + [t_param] if t_param else gradient_params + self._time_param = t_param + self.lse_solver = lse_solver + self._quantum_instance = None + self._circuit_sampler = None + self._imag_part_tol = imag_part_tol + self._expectation = expectation + if quantum_instance is not None: + self.quantum_instance = quantum_instance + + @property + def lse_solver(self) -> Callable[[np.ndarray, np.ndarray], np.ndarray]: + """Returns an LSE solver callable.""" + return self._lse_solver + + @lse_solver.setter + def lse_solver( + self, lse_solver: Optional[Callable[[np.ndarray, np.ndarray], np.ndarray]] + ) -> None: + """Sets an LSE solver. Uses a ``np.linalg.lstsq`` callable if ``None`` provided.""" + if lse_solver is None: + lse_solver = lambda a, b: np.linalg.lstsq(a, b, rcond=1e-2)[0] + + self._lse_solver = lse_solver + + @property + def quantum_instance(self) -> Optional[QuantumInstance]: + """Returns quantum instance.""" + return self._quantum_instance + + @quantum_instance.setter + def quantum_instance(self, quantum_instance: Union[QuantumInstance, Backend]) -> None: + """Sets quantum_instance""" + if not isinstance(quantum_instance, QuantumInstance): + quantum_instance = QuantumInstance(quantum_instance) + + self._quantum_instance = quantum_instance + self._circuit_sampler = CircuitSampler( + quantum_instance, param_qobj=is_aer_provider(quantum_instance.backend) + ) + + def solve_lse( + self, + param_dict: Dict[Parameter, complex], + time_value: Optional[float] = None, + ) -> (Union[List, np.ndarray], Union[List, np.ndarray], np.ndarray): + """ + Solve the system of linear equations underlying McLachlan's variational principle for the + calculation without error bounds. + + Args: + param_dict: Dictionary which relates parameter values to the parameters in the ansatz. + time_value: Time value that will be bound to ``t_param``. It is required if ``t_param`` + is not ``None``. + + Returns: + Solution to the LSE, A from Ax=b, b from Ax=b. + """ + param_values = list(param_dict.values()) + if self._time_param is not None: + param_values.append(time_value) + + metric_tensor_lse_lhs = self._var_principle.metric_tensor( + self._ansatz, + self._bind_params, + self._gradient_params, + param_values, + self._expectation, + self._quantum_instance, + ) + evolution_grad_lse_rhs = self._var_principle.evolution_grad( + self._hamiltonian, + self._ansatz, + self._circuit_sampler, + param_dict, + self._bind_params, + self._gradient_params, + param_values, + self._expectation, + self._quantum_instance, + ) + + x = self._lse_solver(metric_tensor_lse_lhs, evolution_grad_lse_rhs) + + return np.real(x), metric_tensor_lse_lhs, evolution_grad_lse_rhs diff --git a/qiskit/algorithms/evolvers/variational/var_qite.py b/qiskit/algorithms/evolvers/variational/var_qite.py new file mode 100644 index 000000000000..5d53cc1eef63 --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/var_qite.py @@ -0,0 +1,125 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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. + +"""Variational Quantum Imaginary Time Evolution algorithm.""" +from typing import Optional, Union, Type, Callable, List, Dict + +import numpy as np +from scipy.integrate import OdeSolver + +from qiskit import QuantumCircuit +from qiskit.circuit import Parameter +from qiskit.opflow import ExpectationBase, OperatorBase +from qiskit.algorithms.evolvers.imaginary_evolver import ImaginaryEvolver +from qiskit.utils import QuantumInstance +from . import ImaginaryMcLachlanPrinciple +from .solvers.ode.forward_euler_solver import ForwardEulerSolver +from .variational_principles import ImaginaryVariationalPrinciple +from .var_qte import VarQTE + + +class VarQITE(VarQTE, ImaginaryEvolver): + """Variational Quantum Imaginary Time Evolution algorithm. + + .. code-block::python + + from qiskit.algorithms import EvolutionProblem + from qiskit.algorithms import VarQITE + from qiskit import BasicAer + from qiskit.circuit.library import EfficientSU2 + from qiskit.opflow import SummedOp, I, Z, Y, X + from qiskit.algorithms.evolvers.variational import ( + ImaginaryMcLachlanPrinciple, + ) + from qiskit.algorithms import EvolutionProblem + import numpy as np + + observable = SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ).reduce() + + ansatz = EfficientSU2(observable.num_qubits, reps=1) + parameters = ansatz.parameters + init_param_values = np.zeros(len(ansatz.parameters)) + for i in range(len(ansatz.ordered_parameters)): + init_param_values[i] = np.pi / 2 + param_dict = dict(zip(parameters, init_param_values)) + var_principle = ImaginaryMcLachlanPrinciple() + backend = BasicAer.get_backend("statevector_simulator") + time = 1 + evolution_problem = EvolutionProblem(observable, time) + var_qite = VarQITE(ansatz, var_principle, param_dict, quantum_instance=backend) + evolution_result = var_qite.evolve(evolution_problem) + """ + + def __init__( + self, + ansatz: Union[OperatorBase, QuantumCircuit], + variational_principle: Optional[ImaginaryVariationalPrinciple] = None, + initial_parameters: Optional[ + Union[Dict[Parameter, complex], List[complex], np.ndarray] + ] = None, + ode_solver: Union[Type[OdeSolver], str] = ForwardEulerSolver, + lse_solver: Optional[Callable[[np.ndarray, np.ndarray], np.ndarray]] = None, + num_timesteps: Optional[int] = None, + expectation: Optional[ExpectationBase] = None, + imag_part_tol: float = 1e-7, + num_instability_tol: float = 1e-7, + quantum_instance: Optional[QuantumInstance] = None, + ) -> None: + r""" + Args: + ansatz: Ansatz to be used for variational time evolution. + variational_principle: Variational Principle to be used. Defaults to + ``ImaginaryMcLachlanPrinciple``. + initial_parameters: Initial parameter values for an ansatz. If ``None`` provided, + they are initialized uniformly at random. + ode_solver: ODE solver callable that implements a SciPy ``OdeSolver`` interface or a + string indicating a valid method offered by SciPy. + lse_solver: Linear system of equations solver callable. It accepts ``A`` and ``b`` to + solve ``Ax=b`` and returns ``x``. If ``None``, the default ``np.linalg.lstsq`` + solver is used. + num_timesteps: The number of timesteps to take. If None, it is + automatically selected to achieve a timestep of approximately 0.01. Only + relevant in case of the ``ForwardEulerSolver``. + expectation: An instance of ``ExpectationBase`` which defines a method for calculating + a metric tensor and an evolution gradient and, if provided, expectation values of + ``EvolutionProblem.aux_operators``. + imag_part_tol: Allowed value of an imaginary part that can be neglected if no + imaginary part is expected. + num_instability_tol: The amount of negative value that is allowed to be + rounded up to 0 for quantities that are expected to be non-negative. + quantum_instance: Backend used to evaluate the quantum circuit outputs. If ``None`` + provided, everything will be evaluated based on NumPy matrix multiplication + (which might be slow for larger numbers of qubits). + """ + if variational_principle is None: + variational_principle = ImaginaryMcLachlanPrinciple() + super().__init__( + ansatz, + variational_principle, + initial_parameters, + ode_solver, + lse_solver=lse_solver, + num_timesteps=num_timesteps, + expectation=expectation, + imag_part_tol=imag_part_tol, + num_instability_tol=num_instability_tol, + quantum_instance=quantum_instance, + ) diff --git a/qiskit/algorithms/evolvers/variational/var_qrte.py b/qiskit/algorithms/evolvers/variational/var_qrte.py new file mode 100644 index 000000000000..c0846a7159b7 --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/var_qrte.py @@ -0,0 +1,126 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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. + +"""Variational Quantum Real Time Evolution algorithm.""" +from typing import Optional, Union, Type, Callable, List, Dict + +import numpy as np +from scipy.integrate import OdeSolver + +from qiskit import QuantumCircuit +from qiskit.algorithms.evolvers.real_evolver import RealEvolver +from qiskit.circuit import Parameter +from qiskit.opflow import ExpectationBase, OperatorBase +from qiskit.utils import QuantumInstance +from . import RealMcLachlanPrinciple +from .solvers.ode.forward_euler_solver import ForwardEulerSolver +from .variational_principles import RealVariationalPrinciple +from .var_qte import VarQTE + + +class VarQRTE(VarQTE, RealEvolver): + """Variational Quantum Real Time Evolution algorithm. + + .. code-block::python + + from qiskit.algorithms import EvolutionProblem + from qiskit.algorithms import VarQITE + from qiskit import BasicAer + from qiskit.circuit.library import EfficientSU2 + from qiskit.opflow import SummedOp, I, Z, Y, X + from qiskit.algorithms.evolvers.variational import ( + RealMcLachlanPrinciple, + ) + from qiskit.algorithms import EvolutionProblem + import numpy as np + + observable = SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ).reduce() + + ansatz = EfficientSU2(observable.num_qubits, reps=1) + parameters = ansatz.parameters + init_param_values = np.zeros(len(ansatz.parameters)) + for i in range(len(ansatz.parameters)): + init_param_values[i] = np.pi / 2 + param_dict = dict(zip(parameters, init_param_values)) + var_principle = RealMcLachlanPrinciple() + backend = BasicAer.get_backend("statevector_simulator") + time = 1 + evolution_problem = EvolutionProblem(observable, time) + var_qrte = VarQRTE(ansatz, var_principle, param_dict, quantum_instance=backend) + evolution_result = var_qite.evolve(evolution_problem) + """ + + def __init__( + self, + ansatz: Union[OperatorBase, QuantumCircuit], + variational_principle: Optional[RealVariationalPrinciple] = None, + initial_parameters: Optional[ + Union[Dict[Parameter, complex], List[complex], np.ndarray] + ] = None, + ode_solver: Union[Type[OdeSolver], str] = ForwardEulerSolver, + lse_solver: Optional[Callable[[np.ndarray, np.ndarray], np.ndarray]] = None, + num_timesteps: Optional[int] = None, + expectation: Optional[ExpectationBase] = None, + imag_part_tol: float = 1e-7, + num_instability_tol: float = 1e-7, + quantum_instance: Optional[QuantumInstance] = None, + ) -> None: + r""" + Args: + ansatz: Ansatz to be used for variational time evolution. + variational_principle: Variational Principle to be used. Defaults to + ``RealMcLachlanPrinciple``. + initial_parameters: Initial parameter values for an ansatz. If ``None`` provided, + they are initialized uniformly at random. + ode_solver: ODE solver callable that implements a SciPy ``OdeSolver`` interface or a + string indicating a valid method offered by SciPy. + lse_solver: Linear system of equations solver callable. It accepts ``A`` and ``b`` to + solve ``Ax=b`` and returns ``x``. If ``None``, the default ``np.linalg.lstsq`` + solver is used. + num_timesteps: The number of timesteps to take. If None, it is + automatically selected to achieve a timestep of approximately 0.01. Only + relevant in case of the ``ForwardEulerSolver``. + expectation: An instance of ``ExpectationBase`` which defines a method for calculating + a metric tensor and an evolution gradient and, if provided, expectation values of + ``EvolutionProblem.aux_operators``. + imag_part_tol: Allowed value of an imaginary part that can be neglected if no + imaginary part is expected. + num_instability_tol: The amount of negative value that is allowed to be + rounded up to 0 for quantities that are expected to be + non-negative. + quantum_instance: Backend used to evaluate the quantum circuit outputs. If ``None`` + provided, everything will be evaluated based on matrix multiplication (which is + slow). + """ + if variational_principle is None: + variational_principle = RealMcLachlanPrinciple() + super().__init__( + ansatz, + variational_principle, + initial_parameters, + ode_solver, + lse_solver=lse_solver, + num_timesteps=num_timesteps, + expectation=expectation, + imag_part_tol=imag_part_tol, + num_instability_tol=num_instability_tol, + quantum_instance=quantum_instance, + ) diff --git a/qiskit/algorithms/evolvers/variational/var_qte.py b/qiskit/algorithms/evolvers/variational/var_qte.py new file mode 100644 index 000000000000..7edc898037e0 --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/var_qte.py @@ -0,0 +1,303 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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. + +"""The Variational Quantum Time Evolution Interface""" +from abc import ABC +from typing import Optional, Union, Dict, List, Any, Type, Callable + +import numpy as np +from scipy.integrate import OdeSolver + +from qiskit import QuantumCircuit +from qiskit.algorithms.aux_ops_evaluator import eval_observables +from qiskit.algorithms.evolvers.evolution_problem import EvolutionProblem +from qiskit.algorithms.evolvers.evolution_result import EvolutionResult +from qiskit.circuit import Parameter +from qiskit.providers import Backend +from qiskit.utils import QuantumInstance +from qiskit.opflow import ( + CircuitSampler, + OperatorBase, + ExpectationBase, +) +from qiskit.utils.backend_utils import is_aer_provider +from .solvers.ode.forward_euler_solver import ForwardEulerSolver +from .solvers.ode.ode_function_factory import OdeFunctionFactory +from .solvers.var_qte_linear_solver import ( + VarQTELinearSolver, +) +from .variational_principles.variational_principle import ( + VariationalPrinciple, +) +from .solvers.ode.var_qte_ode_solver import ( + VarQTEOdeSolver, +) + + +class VarQTE(ABC): + """Variational Quantum Time Evolution. + + Algorithms that use variational principles to compute a time evolution for a given + Hermitian operator (Hamiltonian) and a quantum state prepared by a parameterized quantum circuit. + + References: + + [1] Benjamin, Simon C. et al. (2019). + Theory of variational quantum simulation. ``_ + """ + + def __init__( + self, + ansatz: Union[OperatorBase, QuantumCircuit], + variational_principle: VariationalPrinciple, + initial_parameters: Optional[ + Union[Dict[Parameter, complex], List[complex], np.ndarray] + ] = None, + ode_solver: Union[Type[OdeSolver], str] = ForwardEulerSolver, + lse_solver: Optional[Callable[[np.ndarray, np.ndarray], np.ndarray]] = None, + num_timesteps: Optional[int] = None, + expectation: Optional[ExpectationBase] = None, + imag_part_tol: float = 1e-7, + num_instability_tol: float = 1e-7, + quantum_instance: Optional[QuantumInstance] = None, + ) -> None: + r""" + Args: + ansatz: Ansatz to be used for variational time evolution. + variational_principle: Variational Principle to be used. + initial_parameters: Initial parameter values for an ansatz. If ``None`` provided, + they are initialized uniformly at random. + ode_solver: ODE solver callable that implements a SciPy ``OdeSolver`` interface or a + string indicating a valid method offered by SciPy. + lse_solver: Linear system of equations solver callable. It accepts ``A`` and ``b`` to + solve ``Ax=b`` and returns ``x``. If ``None``, the default ``np.linalg.lstsq`` + solver is used. + num_timesteps: The number of timesteps to take. If None, it is + automatically selected to achieve a timestep of approximately 0.01. Only + relevant in case of the ``ForwardEulerSolver``. + expectation: An instance of ``ExpectationBase`` which defines a method for calculating + a metric tensor and an evolution gradient and, if provided, expectation values of + ``EvolutionProblem.aux_operators``. + imag_part_tol: Allowed value of an imaginary part that can be neglected if no + imaginary part is expected. + num_instability_tol: The amount of negative value that is allowed to be + rounded up to 0 for quantities that are expected to be + non-negative. + quantum_instance: Backend used to evaluate the quantum circuit outputs. If ``None`` + provided, everything will be evaluated based on matrix multiplication (which is + slow). + """ + super().__init__() + self.ansatz = ansatz + self.variational_principle = variational_principle + self.initial_parameters = initial_parameters + self._quantum_instance = None + if quantum_instance is not None: + self.quantum_instance = quantum_instance + self.expectation = expectation + self.num_timesteps = num_timesteps + self.lse_solver = lse_solver + # OdeFunction abstraction kept for potential extensions - unclear at the moment; + # currently hidden from the user + self._ode_function_factory = OdeFunctionFactory(lse_solver=lse_solver) + self.ode_solver = ode_solver + self.imag_part_tol = imag_part_tol + self.num_instability_tol = num_instability_tol + + @property + def quantum_instance(self) -> Optional[QuantumInstance]: + """Returns quantum instance.""" + return self._quantum_instance + + @quantum_instance.setter + def quantum_instance(self, quantum_instance: Union[QuantumInstance, Backend]) -> None: + """Sets quantum_instance""" + if not isinstance(quantum_instance, QuantumInstance): + quantum_instance = QuantumInstance(quantum_instance) + + self._quantum_instance = quantum_instance + self._circuit_sampler = CircuitSampler( + quantum_instance, param_qobj=is_aer_provider(quantum_instance.backend) + ) + + def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: + """ + Apply Variational Quantum Imaginary Time Evolution (VarQITE) w.r.t. the given + operator. + + Args: + evolution_problem: Instance defining an evolution problem. + Returns: + Result of the evolution which includes a quantum circuit with bound parameters as an + evolved state and, if provided, observables evaluated on the evolved state using + a ``quantum_instance`` and ``expectation`` provided. + + Raises: + ValueError: If no ``initial_state`` is included in the ``evolution_problem``. + """ + self._validate_aux_ops(evolution_problem) + + if evolution_problem.initial_state is not None: + raise ValueError("initial_state provided but not applicable to VarQTE.") + + init_state_param_dict = self._create_init_state_param_dict( + self.initial_parameters, self.ansatz.parameters + ) + + error_calculator = None # TODO will be supported in another PR + + evolved_state = self._evolve( + init_state_param_dict, + evolution_problem.hamiltonian, + evolution_problem.time, + evolution_problem.t_param, + error_calculator, + ) + + evaluated_aux_ops = None + if evolution_problem.aux_operators is not None: + evaluated_aux_ops = eval_observables( + self.quantum_instance, + evolved_state, + evolution_problem.aux_operators, + self.expectation, + ) + + return EvolutionResult(evolved_state, evaluated_aux_ops) + + def _evolve( + self, + init_state_param_dict: Dict[Parameter, complex], + hamiltonian: OperatorBase, + time: float, + t_param: Optional[Parameter] = None, + error_calculator: Any = None, + ) -> OperatorBase: + r""" + Helper method for performing time evolution. Works both for imaginary and real case. + + Args: + init_state_param_dict: Parameter dictionary with initial values for a given + parametrized state/ansatz. If no initial parameter values are provided, they are + initialized uniformly at random. + hamiltonian: + Operator used for Variational Quantum Imaginary Time Evolution (VarQTE). + The operator may be given either as a composed op consisting of a Hermitian + observable and a ``CircuitStateFn`` or a ``ListOp`` of a ``CircuitStateFn`` with a + ``ComboFn``. + The latter case enables the evaluation of a Quantum Natural Gradient. + time: Total time of evolution. + t_param: Time parameter in case of a time-dependent Hamiltonian. + error_calculator: Not yet supported. Calculator of errors for error-based ODE functions. + + Returns: + Result of the evolution which is a quantum circuit with bound parameters as an + evolved state. + """ + + init_state_parameters = list(init_state_param_dict.keys()) + init_state_parameters_values = list(init_state_param_dict.values()) + + linear_solver = VarQTELinearSolver( + self.variational_principle, + hamiltonian, + self.ansatz, + init_state_parameters, + t_param, + self._ode_function_factory.lse_solver, + self.imag_part_tol, + self.expectation, + self._quantum_instance, + ) + + # Convert the operator that holds the Hamiltonian and ansatz into a NaturalGradient operator + ode_function = self._ode_function_factory._build( + linear_solver, error_calculator, init_state_param_dict, t_param + ) + + ode_solver = VarQTEOdeSolver( + init_state_parameters_values, ode_function, self.ode_solver, self.num_timesteps + ) + parameter_values = ode_solver.run(time) + param_dict_from_ode = dict(zip(init_state_parameters, parameter_values)) + + return self.ansatz.assign_parameters(param_dict_from_ode) + + @staticmethod + def _create_init_state_param_dict( + param_values: Union[Dict[Parameter, complex], List[complex], np.ndarray], + init_state_parameters: List[Parameter], + ) -> Dict[Parameter, complex]: + r""" + If ``param_values`` is a dictionary, it looks for parameters present in an initial state + (an ansatz) in a ``param_values``. Based on that, it creates a new dictionary containing + only parameters present in an initial state and their respective values. + If ``param_values`` is a list of values, it creates a new dictionary containing + parameters present in an initial state and their respective values. + If no ``param_values`` is provided, parameter values are chosen uniformly at random. + + Args: + param_values: Dictionary which relates parameter values to the parameters or a list of + values. + init_state_parameters: Parameters present in a quantum state. + + Returns: + Dictionary that maps parameters of an initial state to some values. + + Raises: + ValueError: If the dictionary with parameter values provided does not include all + parameters present in the initial state or if the list of values provided is not the + same length as the list of parameters. + TypeError: If an unsupported type of ``param_values`` provided. + """ + if param_values is None: + init_state_parameter_values = np.random.random(len(init_state_parameters)) + elif isinstance(param_values, dict): + init_state_parameter_values = [] + for param in init_state_parameters: + if param in param_values.keys(): + init_state_parameter_values.append(param_values[param]) + else: + raise ValueError( + f"The dictionary with parameter values provided does not " + f"include all parameters present in the initial state." + f"Parameters present in the state: {init_state_parameters}, " + f"parameters in the dictionary: " + f"{list(param_values.keys())}." + ) + elif isinstance(param_values, (list, np.ndarray)): + if len(init_state_parameters) != len(param_values): + raise ValueError( + f"Initial state has {len(init_state_parameters)} parameters and the" + f" list of values has {len(param_values)} elements. They should be" + f"equal in length." + ) + init_state_parameter_values = param_values + else: + raise TypeError(f"Unsupported type of param_values provided: {type(param_values)}.") + + init_state_param_dict = dict(zip(init_state_parameters, init_state_parameter_values)) + return init_state_param_dict + + def _validate_aux_ops(self, evolution_problem: EvolutionProblem) -> None: + if evolution_problem.aux_operators is not None: + if self.quantum_instance is None: + raise ValueError( + "aux_operators where provided for evaluations but no ``quantum_instance`` " + "was provided." + ) + + if self.expectation is None: + raise ValueError( + "aux_operators where provided for evaluations but no ``expectation`` " + "was provided." + ) diff --git a/qiskit/algorithms/evolvers/variational/variational_principles/__init__.py b/qiskit/algorithms/evolvers/variational/variational_principles/__init__.py new file mode 100644 index 000000000000..7c508f3921de --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/variational_principles/__init__.py @@ -0,0 +1,25 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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. + +"""Variational Principles""" + +from .imaginary_mc_lachlan_principle import ImaginaryMcLachlanPrinciple +from .imaginary_variational_principle import ImaginaryVariationalPrinciple +from .real_mc_lachlan_principle import RealMcLachlanPrinciple +from .real_variational_principle import RealVariationalPrinciple + +__all__ = [ + "ImaginaryMcLachlanPrinciple", + "ImaginaryVariationalPrinciple", + "RealMcLachlanPrinciple", + "RealVariationalPrinciple", +] diff --git a/qiskit/algorithms/evolvers/variational/variational_principles/imaginary_mc_lachlan_principle.py b/qiskit/algorithms/evolvers/variational/variational_principles/imaginary_mc_lachlan_principle.py new file mode 100644 index 000000000000..7a0c46b794f2 --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/variational_principles/imaginary_mc_lachlan_principle.py @@ -0,0 +1,76 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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. + +"""Class for an Imaginary McLachlan's Variational Principle.""" +from typing import Dict, List, Optional + +import numpy as np + +from qiskit import QuantumCircuit +from qiskit.circuit import Parameter +from qiskit.opflow import StateFn, OperatorBase, CircuitSampler, ExpectationBase +from qiskit.utils import QuantumInstance +from .imaginary_variational_principle import ( + ImaginaryVariationalPrinciple, +) + + +class ImaginaryMcLachlanPrinciple(ImaginaryVariationalPrinciple): + """Class for an Imaginary McLachlan's Variational Principle. It aims to minimize the distance + between both sides of the Wick-rotated Schrödinger equation with a quantum state given as a + parametrized trial state. The principle leads to a system of linear equations handled by a + linear solver. The imaginary variant means that we consider imaginary time dynamics. + """ + + def evolution_grad( + self, + hamiltonian: OperatorBase, + ansatz: QuantumCircuit, + circuit_sampler: CircuitSampler, + param_dict: Dict[Parameter, complex], + bind_params: List[Parameter], + gradient_params: List[Parameter], + param_values: List[complex], + expectation: Optional[ExpectationBase] = None, + quantum_instance: Optional[QuantumInstance] = None, + ) -> np.ndarray: + """ + Calculates an evolution gradient according to the rules of this variational principle. + + Args: + hamiltonian: Operator used for Variational Quantum Time Evolution. The operator may be + given either as a composed op consisting of a Hermitian observable and a + ``CircuitStateFn`` or a ``ListOp`` of a ``CircuitStateFn`` with a ``ComboFn``. The + latter case enables the evaluation of a Quantum Natural Gradient. + ansatz: Quantum state in the form of a parametrized quantum circuit. + circuit_sampler: A circuit sampler. + param_dict: Dictionary which relates parameter values to the parameters in the ansatz. + bind_params: List of parameters that are supposed to be bound. + gradient_params: List of parameters with respect to which gradients should be computed. + param_values: Values of parameters to be bound. + expectation: An instance of ``ExpectationBase`` used for calculating an evolution + gradient. If ``None`` provided, a ``PauliExpectation`` is used. + quantum_instance: Backend used to evaluate the quantum circuit outputs. If ``None`` + provided, everything will be evaluated based on matrix multiplication (which is + slow). + + Returns: + An evolution gradient. + """ + if self._evolution_gradient_callable is None: + operator = StateFn(hamiltonian, is_measurement=True) @ StateFn(ansatz) + self._evolution_gradient_callable = self._evolution_gradient.gradient_wrapper( + operator, bind_params, gradient_params, quantum_instance, expectation + ) + evolution_grad_lse_rhs = -0.5 * self._evolution_gradient_callable(param_values) + + return evolution_grad_lse_rhs diff --git a/qiskit/algorithms/evolvers/variational/variational_principles/imaginary_variational_principle.py b/qiskit/algorithms/evolvers/variational/variational_principles/imaginary_variational_principle.py new file mode 100644 index 000000000000..bcd60241942a --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/variational_principles/imaginary_variational_principle.py @@ -0,0 +1,24 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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. + +"""Abstract class for an Imaginary Variational Principle.""" + +from abc import ABC + +from ..variational_principles.variational_principle import ( + VariationalPrinciple, +) + + +class ImaginaryVariationalPrinciple(VariationalPrinciple, ABC): + """Abstract class for an Imaginary Variational Principle. The imaginary variant means that we + consider imaginary time dynamics.""" diff --git a/qiskit/algorithms/evolvers/variational/variational_principles/real_mc_lachlan_principle.py b/qiskit/algorithms/evolvers/variational/variational_principles/real_mc_lachlan_principle.py new file mode 100644 index 000000000000..ddc6a17ed879 --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/variational_principles/real_mc_lachlan_principle.py @@ -0,0 +1,150 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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. + +"""Class for a Real McLachlan's Variational Principle.""" +from typing import Union, Dict, List, Optional + +import numpy as np +from numpy import real + +from qiskit import QuantumCircuit +from qiskit.circuit import Parameter +from qiskit.opflow import ( + StateFn, + SummedOp, + Y, + I, + PauliExpectation, + CircuitQFI, + CircuitSampler, + OperatorBase, + ExpectationBase, +) +from qiskit.opflow.gradients.circuit_gradients import LinComb +from qiskit.utils import QuantumInstance +from .real_variational_principle import ( + RealVariationalPrinciple, +) + + +class RealMcLachlanPrinciple(RealVariationalPrinciple): + """Class for a Real McLachlan's Variational Principle. It aims to minimize the distance + between both sides of the Schrödinger equation with a quantum state given as a parametrized + trial state. The principle leads to a system of linear equations handled by a linear solver. + The real variant means that we consider real time dynamics. + """ + + def __init__( + self, + qfi_method: Union[str, CircuitQFI] = "lin_comb_full", + ) -> None: + """ + Args: + qfi_method: The method used to compute the QFI. Can be either + ``'lin_comb_full'`` or ``'overlap_block_diag'`` or ``'overlap_diag'`` or + ``CircuitQFI``. + """ + self._grad_method = LinComb(aux_meas_op=-Y) + self._energy_param = None + self._energy = None + + super().__init__(qfi_method) + + def evolution_grad( + self, + hamiltonian: OperatorBase, + ansatz: QuantumCircuit, + circuit_sampler: CircuitSampler, + param_dict: Dict[Parameter, complex], + bind_params: List[Parameter], + gradient_params: List[Parameter], + param_values: List[complex], + expectation: Optional[ExpectationBase] = None, + quantum_instance: Optional[QuantumInstance] = None, + ) -> np.ndarray: + """ + Calculates an evolution gradient according to the rules of this variational principle. + + Args: + hamiltonian: Operator used for Variational Quantum Time Evolution. The operator may be + given either as a composed op consisting of a Hermitian observable and a + ``CircuitStateFn`` or a ``ListOp`` of a ``CircuitStateFn`` with a ``ComboFn``. The + latter case enables the evaluation of a Quantum Natural Gradient. + ansatz: Quantum state in the form of a parametrized quantum circuit. + circuit_sampler: A circuit sampler. + param_dict: Dictionary which relates parameter values to the parameters in the ansatz. + bind_params: List of parameters that are supposed to be bound. + gradient_params: List of parameters with respect to which gradients should be computed. + param_values: Values of parameters to be bound. + expectation: An instance of ``ExpectationBase`` used for calculating an evolution + gradient. If ``None`` provided, a ``PauliExpectation`` is used. + quantum_instance: Backend used to evaluate the quantum circuit outputs. If ``None`` + provided, everything will be evaluated based on matrix multiplication (which is + slow). + + Returns: + An evolution gradient. + """ + if self._evolution_gradient_callable is None: + self._energy_param = Parameter("alpha") + modified_hamiltonian = self._construct_expectation( + hamiltonian, ansatz, self._energy_param + ) + + self._evolution_gradient_callable = self._evolution_gradient.gradient_wrapper( + modified_hamiltonian, + bind_params + [self._energy_param], + gradient_params, + quantum_instance, + expectation, + ) + + energy = StateFn(hamiltonian, is_measurement=True) @ StateFn(ansatz) + if expectation is None: + expectation = PauliExpectation() + self._energy = expectation.convert(energy) + + if circuit_sampler is not None: + energy = circuit_sampler.convert(self._energy, param_dict).eval() + else: + energy = self._energy.assign_parameters(param_dict).eval() + + param_values.append(real(energy)) + evolution_grad = 0.5 * self._evolution_gradient_callable(param_values) + + # quick fix due to an error on opflow; to be addressed in a separate PR + evolution_grad = (-1) * evolution_grad + return evolution_grad + + @staticmethod + def _construct_expectation( + hamiltonian: OperatorBase, ansatz: QuantumCircuit, energy_param: Parameter + ) -> OperatorBase: + """ + Modifies a Hamiltonian according to the rules of this variational principle. + + Args: + hamiltonian: Operator used for Variational Quantum Time Evolution. The operator may be + given either as a composed op consisting of a Hermitian observable and a + ``CircuitStateFn`` or a ``ListOp`` of a ``CircuitStateFn`` with a ``ComboFn``. The + latter case enables the evaluation of a Quantum Natural Gradient. + ansatz: Quantum state in the form of a parametrized quantum circuit. + energy_param: Parameter for energy correction. + + Returns: + An modified Hamiltonian composed with an ansatz. + """ + energy_term = I ^ hamiltonian.num_qubits + energy_term *= -1 + energy_term *= energy_param + modified_hamiltonian = SummedOp([hamiltonian, energy_term]).reduce() + return StateFn(modified_hamiltonian, is_measurement=True) @ StateFn(ansatz) diff --git a/qiskit/algorithms/evolvers/variational/variational_principles/real_variational_principle.py b/qiskit/algorithms/evolvers/variational/variational_principles/real_variational_principle.py new file mode 100644 index 000000000000..881e1f3827c7 --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/variational_principles/real_variational_principle.py @@ -0,0 +1,42 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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. + +"""Class for a Real Variational Principle.""" + +from abc import ABC +from typing import Union + +from qiskit.opflow import ( + CircuitQFI, +) +from .variational_principle import ( + VariationalPrinciple, +) + + +class RealVariationalPrinciple(VariationalPrinciple, ABC): + """Class for a Real Variational Principle. The real variant means that we consider real time + dynamics.""" + + def __init__( + self, + qfi_method: Union[str, CircuitQFI] = "lin_comb_full", + ) -> None: + """ + Args: + qfi_method: The method used to compute the QFI. Can be either ``'lin_comb_full'`` or + ``'overlap_block_diag'`` or ``'overlap_diag'`` or ``CircuitQFI``. + """ + super().__init__( + qfi_method, + self._grad_method, + ) diff --git a/qiskit/algorithms/evolvers/variational/variational_principles/variational_principle.py b/qiskit/algorithms/evolvers/variational/variational_principles/variational_principle.py new file mode 100644 index 000000000000..d3d6cbc20b67 --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/variational_principles/variational_principle.py @@ -0,0 +1,129 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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. + +"""Class for a Variational Principle.""" + +from abc import ABC, abstractmethod +from typing import Union, List, Optional, Dict + +import numpy as np + +from qiskit import QuantumCircuit +from qiskit.circuit import Parameter +from qiskit.opflow import ( + CircuitQFI, + CircuitGradient, + QFI, + Gradient, + CircuitStateFn, + CircuitSampler, + OperatorBase, + ExpectationBase, +) +from qiskit.utils import QuantumInstance + + +class VariationalPrinciple(ABC): + """A Variational Principle class. It determines the time propagation of parameters in a + quantum state provided as a parametrized quantum circuit (ansatz).""" + + def __init__( + self, + qfi_method: Union[str, CircuitQFI] = "lin_comb_full", + grad_method: Union[str, CircuitGradient] = "lin_comb", + ) -> None: + """ + Args: + grad_method: The method used to compute the state gradient. Can be either + ``'param_shift'`` or ``'lin_comb'`` or ``'fin_diff'`` or ``CircuitGradient``. + qfi_method: The method used to compute the QFI. Can be either + ``'lin_comb_full'`` or ``'overlap_block_diag'`` or ``'overlap_diag'`` or + ``CircuitQFI``. + """ + self._qfi_method = qfi_method + self.qfi = QFI(qfi_method) + self._grad_method = grad_method + self._evolution_gradient = Gradient(self._grad_method) + self._qfi_gradient_callable = None + self._evolution_gradient_callable = None + + def metric_tensor( + self, + ansatz: QuantumCircuit, + bind_params: List[Parameter], + gradient_params: List[Parameter], + param_values: List[complex], + expectation: Optional[ExpectationBase] = None, + quantum_instance: Optional[QuantumInstance] = None, + ) -> np.ndarray: + """ + Calculates a metric tensor according to the rules of this variational principle. + + Args: + ansatz: Quantum state in the form of a parametrized quantum circuit. + bind_params: List of parameters that are supposed to be bound. + gradient_params: List of parameters with respect to which gradients should be computed. + param_values: Values of parameters to be bound. + expectation: An instance of ``ExpectationBase`` used for calculating a metric tensor. + If ``None`` provided, a ``PauliExpectation`` is used. + quantum_instance: Backend used to evaluate the quantum circuit outputs. If ``None`` + provided, everything will be evaluated based on matrix multiplication (which is + slow). + + Returns: + Metric tensor. + """ + if self._qfi_gradient_callable is None: + self._qfi_gradient_callable = self.qfi.gradient_wrapper( + CircuitStateFn(ansatz), bind_params, gradient_params, quantum_instance, expectation + ) + metric_tensor = 0.25 * self._qfi_gradient_callable(param_values) + + return metric_tensor + + @abstractmethod + def evolution_grad( + self, + hamiltonian: OperatorBase, + ansatz: QuantumCircuit, + circuit_sampler: CircuitSampler, + param_dict: Dict[Parameter, complex], + bind_params: List[Parameter], + gradient_params: List[Parameter], + param_values: List[complex], + expectation: Optional[ExpectationBase] = None, + quantum_instance: Optional[QuantumInstance] = None, + ) -> np.ndarray: + """ + Calculates an evolution gradient according to the rules of this variational principle. + + Args: + hamiltonian: Operator used for Variational Quantum Time Evolution. The operator may be + given either as a composed op consisting of a Hermitian observable and a + ``CircuitStateFn`` or a ``ListOp`` of a ``CircuitStateFn`` with a ``ComboFn``. The + latter case enables the evaluation of a Quantum Natural Gradient. + ansatz: Quantum state in the form of a parametrized quantum circuit. + circuit_sampler: A circuit sampler. + param_dict: Dictionary which relates parameter values to the parameters in the ansatz. + bind_params: List of parameters that are supposed to be bound. + gradient_params: List of parameters with respect to which gradients should be computed. + param_values: Values of parameters to be bound. + expectation: An instance of ``ExpectationBase`` used for calculating an evolution + gradient. If ``None`` provided, a ``PauliExpectation`` is used. + quantum_instance: Backend used to evaluate the quantum circuit outputs. If ``None`` + provided, everything will be evaluated based on matrix multiplication (which is + slow). + + Returns: + An evolution gradient. + """ + pass diff --git a/qiskit/opflow/gradients/derivative_base.py b/qiskit/opflow/gradients/derivative_base.py index 7eff1e4e57c8..1e1b3fadb5b7 100644 --- a/qiskit/opflow/gradients/derivative_base.py +++ b/qiskit/opflow/gradients/derivative_base.py @@ -105,7 +105,7 @@ def gradient_wrapper( """ from ..converters import CircuitSampler - if not grad_params: + if grad_params is None: grad_params = bind_params grad = self.convert(operator, grad_params) @@ -113,15 +113,18 @@ def gradient_wrapper( expectation = PauliExpectation() grad = expectation.convert(grad) + sampler = CircuitSampler(backend=backend) if backend is not None else None + def gradient_fn(p_values): p_values_dict = dict(zip(bind_params, p_values)) if not backend: converter = grad.assign_parameters(p_values_dict) return np.real(converter.eval()) else: - p_values_dict = {k: [v] for k, v in p_values_dict.items()} - converter = CircuitSampler(backend=backend).convert(grad, p_values_dict) - return np.real(converter.eval()[0]) + p_values_list = {k: [v] for k, v in p_values_dict.items()} + sampled = sampler.convert(grad, p_values_list) + fully_bound = sampled.bind_parameters(p_values_dict) + return np.real(fully_bound.eval()[0]) return gradient_fn diff --git a/releasenotes/notes/add-variational-quantum-time-evolution-112ffeaf62782fea.yaml b/releasenotes/notes/add-variational-quantum-time-evolution-112ffeaf62782fea.yaml new file mode 100644 index 000000000000..fc4d0fb891d3 --- /dev/null +++ b/releasenotes/notes/add-variational-quantum-time-evolution-112ffeaf62782fea.yaml @@ -0,0 +1,50 @@ +--- +features: + - | + Add algorithms for Variational Quantum Time Evolution that implement a new interface for + Quantum Time Evolution. The feature supports real (:class:`qiskit.algorithms.VarQRTE`.) and + imaginary (:class:`qiskit.algorithms.VarQITE`.) quantum time evolution according to a + variational principle passed. Each algorithm accepts a variational principle and the following + are provided: + :class:`qiskit.algorithms.evolvers.variational.ImaginaryMcLachlanPrinciple`, + :class:`qiskit.algorithms.evolvers.variational.RealMcLachlanPrinciple`, + :class:`qiskit.algorithms.evolvers.variational.RealTimeDependentPrinciple`. + Both algorithms require solving ODE equations and linear equations which is handled by classes + implemented in `qiskit.algorithms.evolvers.variational.solvers` module. + + .. code-block:: python + + from qiskit.algorithms import EvolutionProblem + from qiskit.algorithms import VarQITE + from qiskit import BasicAer + from qiskit.circuit.library import EfficientSU2 + from qiskit.opflow import SummedOp, I, Z, Y, X + from qiskit.algorithms.evolvers.variational import ( + ImaginaryMcLachlanPrinciple, + ) + from qiskit.algorithms import EvolutionProblem + import numpy as np + + observable = SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ).reduce() + + ansatz = EfficientSU2(observable.num_qubits, reps=1) + parameters = ansatz.parameters + init_param_values = np.zeros(len(ansatz.parameters)) + for i in range(len(ansatz.parameters)): + init_param_values[i] = np.pi / 2 + param_dict = dict(zip(parameters, init_param_values)) + var_principle = ImaginaryMcLachlanPrinciple() + backend = BasicAer.get_backend("statevector_simulator") + time = 1 + evolution_problem = EvolutionProblem(observable, time) + var_qite = VarQITE(ansatz, var_principle, param_dict, quantum_instance=backend) + evolution_result = var_qite.evolve(evolution_problem) diff --git a/releasenotes/notes/fix-gradient-wrapper-2f9ab45941739044.yaml b/releasenotes/notes/fix-gradient-wrapper-2f9ab45941739044.yaml new file mode 100644 index 000000000000..07de4aa4eb0b --- /dev/null +++ b/releasenotes/notes/fix-gradient-wrapper-2f9ab45941739044.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixed the following issues with the function + :func:`~qiskit.opflow.gradients.derivative_base.gradient_wrapper`: + - reusing a circuit sampler between the calls, + - binding nested parameters. diff --git a/test/python/algorithms/evolvers/test_evolution_problem.py b/test/python/algorithms/evolvers/test_evolution_problem.py index 0d1d18951039..d3dd2f7bc9b2 100644 --- a/test/python/algorithms/evolvers/test_evolution_problem.py +++ b/test/python/algorithms/evolvers/test_evolution_problem.py @@ -38,14 +38,14 @@ def test_init_default(self): expected_initial_state = One expected_aux_operators = None expected_t_param = None - expected_hamiltonian_value_dict = None + expected_param_value_dict = None self.assertEqual(evo_problem.hamiltonian, expected_hamiltonian) self.assertEqual(evo_problem.time, expected_time) self.assertEqual(evo_problem.initial_state, expected_initial_state) self.assertEqual(evo_problem.aux_operators, expected_aux_operators) self.assertEqual(evo_problem.t_param, expected_t_param) - self.assertEqual(evo_problem.hamiltonian_value_dict, expected_hamiltonian_value_dict) + self.assertEqual(evo_problem.param_value_dict, expected_param_value_dict) def test_init_all(self): """Tests that all fields are initialized correctly.""" @@ -54,7 +54,7 @@ def test_init_all(self): time = 2 initial_state = One aux_operators = [X, Y] - hamiltonian_value_dict = {t_parameter: 3.2} + param_value_dict = {t_parameter: 3.2} evo_problem = EvolutionProblem( hamiltonian, @@ -62,7 +62,7 @@ def test_init_all(self): initial_state, aux_operators, t_param=t_parameter, - hamiltonian_value_dict=hamiltonian_value_dict, + param_value_dict=param_value_dict, ) expected_hamiltonian = Y + t_parameter * Z @@ -70,14 +70,14 @@ def test_init_all(self): expected_initial_state = One expected_aux_operators = [X, Y] expected_t_param = t_parameter - expected_hamiltonian_value_dict = {t_parameter: 3.2} + expected_param_value_dict = {t_parameter: 3.2} self.assertEqual(evo_problem.hamiltonian, expected_hamiltonian) self.assertEqual(evo_problem.time, expected_time) self.assertEqual(evo_problem.initial_state, expected_initial_state) self.assertEqual(evo_problem.aux_operators, expected_aux_operators) self.assertEqual(evo_problem.t_param, expected_t_param) - self.assertEqual(evo_problem.hamiltonian_value_dict, expected_hamiltonian_value_dict) + self.assertEqual(evo_problem.param_value_dict, expected_param_value_dict) @data([Y, -1, One], [Y, -1.2, One], [Y, 0, One]) @unpack @@ -93,27 +93,21 @@ def test_validate_params(self): with self.subTest(msg="Parameter missing in dict."): hamiltonian = param_x * X + param_y * Y param_dict = {param_y: 2} - evolution_problem = EvolutionProblem( - hamiltonian, 2, Zero, hamiltonian_value_dict=param_dict - ) + evolution_problem = EvolutionProblem(hamiltonian, 2, Zero, param_value_dict=param_dict) with assert_raises(ValueError): evolution_problem.validate_params() with self.subTest(msg="Empty dict."): hamiltonian = param_x * X + param_y * Y param_dict = {} - evolution_problem = EvolutionProblem( - hamiltonian, 2, Zero, hamiltonian_value_dict=param_dict - ) + evolution_problem = EvolutionProblem(hamiltonian, 2, Zero, param_value_dict=param_dict) with assert_raises(ValueError): evolution_problem.validate_params() with self.subTest(msg="Extra parameter in dict."): hamiltonian = param_x * X + param_y * Y param_dict = {param_y: 2, param_x: 1, Parameter("z"): 1} - evolution_problem = EvolutionProblem( - hamiltonian, 2, Zero, hamiltonian_value_dict=param_dict - ) + evolution_problem = EvolutionProblem(hamiltonian, 2, Zero, param_value_dict=param_dict) with assert_raises(ValueError): evolution_problem.validate_params() diff --git a/test/python/algorithms/evolvers/trotterization/test_trotter_qrte.py b/test/python/algorithms/evolvers/trotterization/test_trotter_qrte.py index 7baad84fe59b..fe53f929f5f6 100644 --- a/test/python/algorithms/evolvers/trotterization/test_trotter_qrte.py +++ b/test/python/algorithms/evolvers/trotterization/test_trotter_qrte.py @@ -185,7 +185,7 @@ def test_trotter_qrte_trotter_two_qubits_with_params(self): operator = w_param * (Z ^ Z) / 2.0 + (Z ^ I) + u_param * (I ^ Z) / 3.0 time = 1 evolution_problem = EvolutionProblem( - operator, time, initial_state, hamiltonian_value_dict=params_dict + operator, time, initial_state, param_value_dict=params_dict ) expected_state = VectorStateFn( Statevector([-0.9899925 - 0.14112001j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], dims=(2, 2)) @@ -222,7 +222,7 @@ def test_trotter_qrte_qdrift(self, initial_state, expected_state): @data((Parameter("t"), {}), (None, {Parameter("x"): 2}), (None, None)) @unpack - def test_trotter_qrte_trotter_errors(self, t_param, hamiltonian_value_dict): + def test_trotter_qrte_trotter_errors(self, t_param, param_value_dict): """Test TrotterQRTE with raising errors.""" operator = X * Parameter("t") + Z initial_state = Zero @@ -235,7 +235,7 @@ def test_trotter_qrte_trotter_errors(self, t_param, hamiltonian_value_dict): time, initial_state, t_param=t_param, - hamiltonian_value_dict=hamiltonian_value_dict, + param_value_dict=param_value_dict, ) _ = trotter_qrte.evolve(evolution_problem) diff --git a/test/python/algorithms/evolvers/variational/__init__.py b/test/python/algorithms/evolvers/variational/__init__.py new file mode 100644 index 000000000000..b3ac36d2a6d9 --- /dev/null +++ b/test/python/algorithms/evolvers/variational/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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. diff --git a/test/python/algorithms/evolvers/variational/solvers/__init__.py b/test/python/algorithms/evolvers/variational/solvers/__init__.py new file mode 100644 index 000000000000..b3ac36d2a6d9 --- /dev/null +++ b/test/python/algorithms/evolvers/variational/solvers/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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. diff --git a/test/python/algorithms/evolvers/variational/solvers/expected_results/__init__.py b/test/python/algorithms/evolvers/variational/solvers/expected_results/__init__.py new file mode 100644 index 000000000000..9c3165f57a2a --- /dev/null +++ b/test/python/algorithms/evolvers/variational/solvers/expected_results/__init__.py @@ -0,0 +1,12 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. +"""Stores expected results that are lengthy.""" diff --git a/test/python/algorithms/evolvers/variational/solvers/expected_results/test_varqte_linear_solver_expected_1.py b/test/python/algorithms/evolvers/variational/solvers/expected_results/test_varqte_linear_solver_expected_1.py new file mode 100644 index 000000000000..c6dcf903673f --- /dev/null +++ b/test/python/algorithms/evolvers/variational/solvers/expected_results/test_varqte_linear_solver_expected_1.py @@ -0,0 +1,182 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. +"""Stores expected results that are lengthy.""" +expected_metric_res_1 = [ + [ + 2.50000000e-01 + 0.0j, + -3.85185989e-33 + 0.0j, + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + -3.85185989e-33 + 0.0j, + -3.85185989e-33 + 0.0j, + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + 2.50000000e-01 + 0.0j, + -2.77500000e-17 + 0.0j, + 4.85000000e-17 + 0.0j, + 4.77630626e-32 + 0.0j, + ], + [ + -3.85185989e-33 + 0.0j, + 2.50000000e-01 + 0.0j, + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + -3.85185989e-33 + 0.0j, + 2.50000000e-01 + 0.0j, + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + -3.85185989e-33 + 0.0j, + 2.50000000e-01 + 0.0j, + 4.85334346e-32 + 0.0j, + 4.17500000e-17 + 0.0j, + ], + [ + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + 0.00000000e00 + 0.0j, + 0.00000000e00 + 0.0j, + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + 0.00000000e00 + 0.0j, + 0.00000000e00 + 0.0j, + -1.38777878e-17 + 0.0j, + -7.00000000e-18 + 0.0j, + 1.38006319e-17 + 0.0j, + -1.39493681e-17 + 0.0j, + ], + [ + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + 0.00000000e00 + 0.0j, + 0.00000000e00 + 0.0j, + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + 0.00000000e00 + 0.0j, + 0.00000000e00 + 0.0j, + -1.38777878e-17 + 0.0j, + -7.00000000e-18 + 0.0j, + 1.38006319e-17 + 0.0j, + -1.39493681e-17 + 0.0j, + ], + [ + -3.85185989e-33 + 0.0j, + -3.85185989e-33 + 0.0j, + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + 2.50000000e-01 + 0.0j, + -3.85185989e-33 + 0.0j, + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + -3.85185989e-33 + 0.0j, + 0.00000000e00 + 0.0j, + 4.85334346e-32 + 0.0j, + -7.00000000e-18 + 0.0j, + ], + [ + -3.85185989e-33 + 0.0j, + 2.50000000e-01 + 0.0j, + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + -3.85185989e-33 + 0.0j, + 2.50000000e-01 + 0.0j, + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + -3.85185989e-33 + 0.0j, + 2.50000000e-01 + 0.0j, + 4.85334346e-32 + 0.0j, + 4.17500000e-17 + 0.0j, + ], + [ + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + 0.00000000e00 + 0.0j, + 0.00000000e00 + 0.0j, + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + 0.00000000e00 + 0.0j, + 0.00000000e00 + 0.0j, + -1.38777878e-17 + 0.0j, + -7.00000000e-18 + 0.0j, + 1.38006319e-17 + 0.0j, + -1.39493681e-17 + 0.0j, + ], + [ + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + 0.00000000e00 + 0.0j, + 0.00000000e00 + 0.0j, + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + 0.00000000e00 + 0.0j, + 0.00000000e00 + 0.0j, + -1.38777878e-17 + 0.0j, + -7.00000000e-18 + 0.0j, + 1.38006319e-17 + 0.0j, + -1.39493681e-17 + 0.0j, + ], + [ + 2.50000000e-01 + 0.0j, + -3.85185989e-33 + 0.0j, + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + -3.85185989e-33 + 0.0j, + -3.85185989e-33 + 0.0j, + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + 2.50000000e-01 + 0.0j, + -2.77500000e-17 + 0.0j, + 4.85000000e-17 + 0.0j, + -7.00000000e-18 + 0.0j, + ], + [ + -2.77500000e-17 + 0.0j, + 2.50000000e-01 + 0.0j, + -7.00000000e-18 + 0.0j, + -7.00000000e-18 + 0.0j, + 0.00000000e00 + 0.0j, + 2.50000000e-01 + 0.0j, + -7.00000000e-18 + 0.0j, + -7.00000000e-18 + 0.0j, + -2.77500000e-17 + 0.0j, + 2.50000000e-01 + 0.0j, + 0.00000000e00 + 0.0j, + 4.17500000e-17 + 0.0j, + ], + [ + 4.85000000e-17 + 0.0j, + 4.85334346e-32 + 0.0j, + 1.38006319e-17 + 0.0j, + 1.38006319e-17 + 0.0j, + 4.85334346e-32 + 0.0j, + 4.85334346e-32 + 0.0j, + 1.38006319e-17 + 0.0j, + 1.38006319e-17 + 0.0j, + 4.85000000e-17 + 0.0j, + 0.00000000e00 + 0.0j, + 2.50000000e-01 + 0.0j, + -2.77500000e-17 + 0.0j, + ], + [ + 4.77630626e-32 + 0.0j, + 4.17500000e-17 + 0.0j, + -1.39493681e-17 + 0.0j, + -1.39493681e-17 + 0.0j, + -7.00000000e-18 + 0.0j, + 4.17500000e-17 + 0.0j, + -1.39493681e-17 + 0.0j, + -1.39493681e-17 + 0.0j, + -7.00000000e-18 + 0.0j, + 4.17500000e-17 + 0.0j, + -2.77500000e-17 + 0.0j, + 2.50000000e-01 + 0.0j, + ], +] diff --git a/test/python/algorithms/evolvers/variational/solvers/ode/__init__.py b/test/python/algorithms/evolvers/variational/solvers/ode/__init__.py new file mode 100644 index 000000000000..b3ac36d2a6d9 --- /dev/null +++ b/test/python/algorithms/evolvers/variational/solvers/ode/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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. diff --git a/test/python/algorithms/evolvers/variational/solvers/ode/test_forward_euler_solver.py b/test/python/algorithms/evolvers/variational/solvers/ode/test_forward_euler_solver.py new file mode 100644 index 000000000000..08d13233c76e --- /dev/null +++ b/test/python/algorithms/evolvers/variational/solvers/ode/test_forward_euler_solver.py @@ -0,0 +1,47 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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 Forward Euler solver.""" + +import unittest +from test.python.algorithms import QiskitAlgorithmsTestCase +import numpy as np +from ddt import ddt, data, unpack +from scipy.integrate import solve_ivp + +from qiskit.algorithms.evolvers.variational.solvers.ode.forward_euler_solver import ( + ForwardEulerSolver, +) + + +@ddt +class TestForwardEulerSolver(QiskitAlgorithmsTestCase): + """Test Forward Euler solver.""" + + @unpack + @data((4, 16), (16, 35.52713678800501), (320, 53.261108839604795)) + def test_solve(self, timesteps, expected_result): + """Test Forward Euler solver for a simple ODE.""" + + y0 = [1] + + # pylint: disable=unused-argument + def func(time, y): + return y + + t_span = [0.0, 4.0] + sol1 = solve_ivp(func, t_span, y0, method=ForwardEulerSolver, num_t_steps=timesteps) + np.testing.assert_equal(sol1.y[-1][-1], expected_result) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/algorithms/evolvers/variational/solvers/ode/test_ode_function.py b/test/python/algorithms/evolvers/variational/solvers/ode/test_ode_function.py new file mode 100644 index 000000000000..1ef84204f2cc --- /dev/null +++ b/test/python/algorithms/evolvers/variational/solvers/ode/test_ode_function.py @@ -0,0 +1,165 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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 ODE function generator.""" + +import unittest + +from test.python.algorithms import QiskitAlgorithmsTestCase +import numpy as np +from qiskit.algorithms.evolvers.variational.solvers.var_qte_linear_solver import ( + VarQTELinearSolver, +) +from qiskit.algorithms.evolvers.variational.solvers.ode.ode_function import ( + OdeFunction, +) +from qiskit import BasicAer +from qiskit.algorithms.evolvers.variational import ( + ImaginaryMcLachlanPrinciple, +) +from qiskit.circuit import Parameter +from qiskit.circuit.library import EfficientSU2 +from qiskit.opflow import ( + SummedOp, + X, + Y, + I, + Z, +) + + +class TestOdeFunctionGenerator(QiskitAlgorithmsTestCase): + """Test ODE function generator.""" + + def test_var_qte_ode_function(self): + """Test ODE function generator.""" + observable = SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ) + + d = 2 + ansatz = EfficientSU2(observable.num_qubits, reps=d) + + # Define a set of initial parameters + parameters = list(ansatz.parameters) + + param_dict = {param: np.pi / 4 for param in parameters} + backend = BasicAer.get_backend("statevector_simulator") + + var_principle = ImaginaryMcLachlanPrinciple() + + t_param = None + linear_solver = None + linear_solver = VarQTELinearSolver( + var_principle, + observable, + ansatz, + parameters, + t_param, + linear_solver, + quantum_instance=backend, + ) + + time = 2 + ode_function_generator = OdeFunction( + linear_solver, error_calculator=None, t_param=None, param_dict=param_dict + ) + + qte_ode_function = ode_function_generator.var_qte_ode_function(time, param_dict.values()) + + expected_qte_ode_function = [ + 0.442145, + -0.022081, + 0.106223, + -0.117468, + 0.251233, + 0.321256, + -0.062728, + -0.036209, + -0.509219, + -0.183459, + -0.050739, + -0.093163, + ] + + np.testing.assert_array_almost_equal(expected_qte_ode_function, qte_ode_function) + + def test_var_qte_ode_function_time_param(self): + """Test ODE function generator with time param.""" + t_param = Parameter("t") + observable = SummedOp( + [ + 0.2252 * t_param * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ) + + d = 2 + ansatz = EfficientSU2(observable.num_qubits, reps=d) + + # Define a set of initial parameters + parameters = list(ansatz.parameters) + + param_dict = {param: np.pi / 4 for param in parameters} + backend = BasicAer.get_backend("statevector_simulator") + + var_principle = ImaginaryMcLachlanPrinciple() + + time = 2 + + linear_solver = None + linear_solver = VarQTELinearSolver( + var_principle, + observable, + ansatz, + parameters, + t_param, + linear_solver, + quantum_instance=backend, + ) + ode_function_generator = OdeFunction( + linear_solver, error_calculator=None, t_param=t_param, param_dict=param_dict + ) + + qte_ode_function = ode_function_generator.var_qte_ode_function(time, param_dict.values()) + + expected_qte_ode_function = [ + 0.442145, + -0.022081, + 0.106223, + -0.117468, + 0.251233, + 0.321256, + -0.062728, + -0.036209, + -0.509219, + -0.183459, + -0.050739, + -0.093163, + ] + + np.testing.assert_array_almost_equal(expected_qte_ode_function, qte_ode_function, decimal=5) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/algorithms/evolvers/variational/solvers/ode/test_var_qte_ode_solver.py b/test/python/algorithms/evolvers/variational/solvers/ode/test_var_qte_ode_solver.py new file mode 100644 index 000000000000..5b39229ba502 --- /dev/null +++ b/test/python/algorithms/evolvers/variational/solvers/ode/test_var_qte_ode_solver.py @@ -0,0 +1,136 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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 solver of ODEs.""" + +import unittest +from test.python.algorithms import QiskitAlgorithmsTestCase +from ddt import ddt, data, unpack +import numpy as np +from qiskit.algorithms.evolvers.variational.solvers.ode.forward_euler_solver import ( + ForwardEulerSolver, +) +from qiskit.algorithms.evolvers.variational.solvers.var_qte_linear_solver import ( + VarQTELinearSolver, +) +from qiskit.algorithms.evolvers.variational.solvers.ode.var_qte_ode_solver import ( + VarQTEOdeSolver, +) +from qiskit.algorithms.evolvers.variational.solvers.ode.ode_function import ( + OdeFunction, +) +from qiskit import BasicAer +from qiskit.algorithms.evolvers.variational import ( + ImaginaryMcLachlanPrinciple, +) +from qiskit.circuit.library import EfficientSU2 +from qiskit.opflow import ( + SummedOp, + X, + Y, + I, + Z, +) + + +@ddt +class TestVarQTEOdeSolver(QiskitAlgorithmsTestCase): + """Test solver of ODEs.""" + + @data( + ( + "RK45", + [ + -0.30076755873631345, + -0.8032811383782005, + 1.1674108371914734e-15, + 3.2293849116821145e-16, + 2.541585055586039, + 1.155475184255733, + -2.966331417968169e-16, + 9.604292449638343e-17, + ], + ), + ( + ForwardEulerSolver, + [ + -3.2707e-01, + -8.0960e-01, + 3.4323e-16, + 8.9034e-17, + 2.5290e00, + 1.1563e00, + 3.0227e-16, + -2.2769e-16, + ], + ), + ) + @unpack + def test_run_no_backend(self, ode_solver, expected_result): + """Test ODE solver with no backend.""" + observable = SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ) + + d = 1 + ansatz = EfficientSU2(observable.num_qubits, reps=d) + + # Define a set of initial parameters + parameters = list(ansatz.parameters) + + init_param_values = np.zeros(len(parameters)) + for i in range(ansatz.num_qubits): + init_param_values[-(ansatz.num_qubits + i + 1)] = np.pi / 2 + + param_dict = dict(zip(parameters, init_param_values)) + + backend = BasicAer.get_backend("statevector_simulator") + + var_principle = ImaginaryMcLachlanPrinciple() + + time = 1 + + t_param = None + + linear_solver = None + linear_solver = VarQTELinearSolver( + var_principle, + observable, + ansatz, + parameters, + t_param, + linear_solver, + quantum_instance=backend, + ) + ode_function_generator = OdeFunction(linear_solver, None, param_dict, t_param) + + var_qte_ode_solver = VarQTEOdeSolver( + list(param_dict.values()), + ode_function_generator, + ode_solver=ode_solver, + num_timesteps=25, + ) + + result = var_qte_ode_solver.run(time) + + np.testing.assert_array_almost_equal(result, expected_result, decimal=4) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/algorithms/evolvers/variational/solvers/test_varqte_linear_solver.py b/test/python/algorithms/evolvers/variational/solvers/test_varqte_linear_solver.py new file mode 100644 index 000000000000..c5442447bf9c --- /dev/null +++ b/test/python/algorithms/evolvers/variational/solvers/test_varqte_linear_solver.py @@ -0,0 +1,115 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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 solver of linear equations.""" + +import unittest + +from test.python.algorithms import QiskitAlgorithmsTestCase +from ddt import ddt, data +import numpy as np + +from qiskit import BasicAer +from qiskit.algorithms.evolvers.variational import ( + ImaginaryMcLachlanPrinciple, +) +from qiskit.algorithms.evolvers.variational.solvers.var_qte_linear_solver import ( + VarQTELinearSolver, +) +from qiskit.circuit.library import EfficientSU2 +from qiskit.opflow import SummedOp, X, Y, I, Z +from .expected_results.test_varqte_linear_solver_expected_1 import ( + expected_metric_res_1, +) + + +@ddt +class TestVarQTELinearSolver(QiskitAlgorithmsTestCase): + """Test solver of linear equations.""" + + @data(BasicAer.get_backend("statevector_simulator"), None) + def test_solve_lse(self, backend): + """Test SLE solver.""" + + observable = SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ) + + d = 2 + ansatz = EfficientSU2(observable.num_qubits, reps=d) + + parameters = list(ansatz.parameters) + init_param_values = np.zeros(len(parameters)) + for i in range(ansatz.num_qubits): + init_param_values[-(ansatz.num_qubits + i + 1)] = np.pi / 2 + + param_dict = dict(zip(parameters, init_param_values)) + + var_principle = ImaginaryMcLachlanPrinciple() + t_param = None + linear_solver = None + linear_solver = VarQTELinearSolver( + var_principle, + observable, + ansatz, + parameters, + t_param, + linear_solver, + quantum_instance=backend, + ) + + nat_grad_res, metric_res, grad_res = linear_solver.solve_lse(param_dict) + + expected_nat_grad_res = [ + 3.43500000e-01, + -2.89800000e-01, + 2.43575264e-16, + 1.31792695e-16, + -9.61200000e-01, + -2.89800000e-01, + 1.27493709e-17, + 1.12587456e-16, + 3.43500000e-01, + -2.89800000e-01, + 3.69914720e-17, + 1.95052083e-17, + ] + + expected_grad_res = [ + (0.17174999999999926 - 0j), + (-0.21735000000000085 + 0j), + (4.114902862895087e-17 - 0j), + (4.114902862895087e-17 - 0j), + (-0.24030000000000012 + 0j), + (-0.21735000000000085 + 0j), + (4.114902862895087e-17 - 0j), + (4.114902862895087e-17 - 0j), + (0.17174999999999918 - 0j), + (-0.21735000000000076 + 0j), + (1.7789936190837538e-17 - 0j), + (-8.319872568662832e-17 + 0j), + ] + + np.testing.assert_array_almost_equal(nat_grad_res, expected_nat_grad_res, decimal=4) + np.testing.assert_array_almost_equal(grad_res, expected_grad_res, decimal=4) + np.testing.assert_array_almost_equal(metric_res, expected_metric_res_1, decimal=4) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/algorithms/evolvers/variational/test_var_qite.py b/test/python/algorithms/evolvers/variational/test_var_qite.py new file mode 100644 index 000000000000..6c4a26e13f8c --- /dev/null +++ b/test/python/algorithms/evolvers/variational/test_var_qite.py @@ -0,0 +1,287 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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 Variational Quantum Imaginary Time Evolution algorithm.""" + +import unittest + +from test.python.algorithms import QiskitAlgorithmsTestCase +from ddt import data, ddt +import numpy as np +from qiskit.test import slow_test +from qiskit.utils import algorithm_globals, QuantumInstance +from qiskit import BasicAer +from qiskit.algorithms import EvolutionProblem, VarQITE +from qiskit.algorithms.evolvers.variational import ( + ImaginaryMcLachlanPrinciple, +) +from qiskit.circuit.library import EfficientSU2 +from qiskit.opflow import ( + SummedOp, + X, + Y, + I, + Z, + ExpectationFactory, +) + + +@ddt +class TestVarQITE(QiskitAlgorithmsTestCase): + """Test Variational Quantum Imaginary Time Evolution algorithm.""" + + def setUp(self): + super().setUp() + self.seed = 11 + np.random.seed(self.seed) + backend_statevector = BasicAer.get_backend("statevector_simulator") + backend_qasm = BasicAer.get_backend("qasm_simulator") + self.quantum_instance = QuantumInstance( + backend=backend_statevector, + shots=1, + seed_simulator=self.seed, + seed_transpiler=self.seed, + ) + self.quantum_instance_qasm = QuantumInstance( + backend=backend_qasm, + shots=4000, + seed_simulator=self.seed, + seed_transpiler=self.seed, + ) + self.backends_dict = { + "qi_sv": self.quantum_instance, + "qi_qasm": self.quantum_instance_qasm, + "b_sv": backend_statevector, + } + + self.backends_names = ["qi_qasm", "b_sv", "qi_sv"] + + @slow_test + def test_run_d_1_with_aux_ops(self): + """Test VarQITE for d = 1 and t = 1 with evaluating auxiliary operator and the Forward + Euler solver..""" + observable = SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ) + aux_ops = [X ^ X, Y ^ Z] + d = 1 + ansatz = EfficientSU2(observable.num_qubits, reps=d) + + parameters = list(ansatz.parameters) + init_param_values = np.zeros(len(parameters)) + for i in range(len(parameters)): + init_param_values[i] = np.pi / 2 + init_param_values[0] = 1 + var_principle = ImaginaryMcLachlanPrinciple() + + param_dict = dict(zip(parameters, init_param_values)) + + time = 1 + + evolution_problem = EvolutionProblem(observable, time, aux_operators=aux_ops) + + thetas_expected_sv = [ + 1.03612467538419, + 1.91891042963193, + 2.81129500883365, + 2.78938736703301, + 2.2215151699331, + 1.61953721158502, + 2.23490753161058, + 1.97145113701782, + ] + + thetas_expected_qasm = [ + 1.03612467538419, + 1.91891042963193, + 2.81129500883365, + 2.78938736703301, + 2.2215151699331, + 1.61953721158502, + 2.23490753161058, + 1.97145113701782, + ] + + expected_aux_ops_evaluated_sv = [(-0.160899, 0.0), (0.26207, 0.0)] + expected_aux_ops_evaluated_qasm = [ + (-0.1765, 0.015563), + (0.2555, 0.015287), + ] + + for backend_name in self.backends_names: + with self.subTest(msg=f"Test {backend_name} backend."): + algorithm_globals.random_seed = self.seed + backend = self.backends_dict[backend_name] + expectation = ExpectationFactory.build( + operator=observable, + backend=backend, + ) + var_qite = VarQITE( + ansatz, + var_principle, + param_dict, + expectation=expectation, + num_timesteps=25, + quantum_instance=backend, + ) + evolution_result = var_qite.evolve(evolution_problem) + + evolved_state = evolution_result.evolved_state + aux_ops = evolution_result.aux_ops_evaluated + + parameter_values = evolved_state.data[0][0].params + + if backend_name == "qi_qasm": + thetas_expected = thetas_expected_qasm + expected_aux_ops = expected_aux_ops_evaluated_qasm + else: + thetas_expected = thetas_expected_sv + expected_aux_ops = expected_aux_ops_evaluated_sv + + for i, parameter_value in enumerate(parameter_values): + np.testing.assert_almost_equal( + float(parameter_value), thetas_expected[i], decimal=3 + ) + + np.testing.assert_array_almost_equal(aux_ops, expected_aux_ops) + + def test_run_d_1_t_7(self): + """Test VarQITE for d = 1 and t = 7 with RK45 ODE solver.""" + observable = SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ) + + d = 1 + ansatz = EfficientSU2(observable.num_qubits, reps=d) + + parameters = list(ansatz.parameters) + init_param_values = np.zeros(len(parameters)) + for i in range(len(parameters)): + init_param_values[i] = np.pi / 2 + init_param_values[0] = 1 + var_principle = ImaginaryMcLachlanPrinciple() + + backend = BasicAer.get_backend("statevector_simulator") + + time = 7 + var_qite = VarQITE( + ansatz, + var_principle, + init_param_values, + ode_solver="RK45", + num_timesteps=25, + quantum_instance=backend, + ) + + thetas_expected = [ + 0.828917365718767, + 1.88481074798033, + 3.14111335991238, + 3.14125849601269, + 2.33768562678401, + 1.78670990729437, + 2.04214275514208, + 2.04009918594422, + ] + + self._test_helper(observable, thetas_expected, time, var_qite, 2) + + @slow_test + @data( + SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ), + 0.2252 * (I ^ I) + + 0.5716 * (Z ^ Z) + + 0.3435 * (I ^ Z) + + -0.4347 * (Z ^ I) + + 0.091 * (Y ^ Y) + + 0.091 * (X ^ X), + ) + def test_run_d_2(self, observable): + """Test VarQITE for d = 2 and t = 1 with RK45 ODE solver.""" + d = 2 + ansatz = EfficientSU2(observable.num_qubits, reps=d) + + parameters = list(ansatz.parameters) + init_param_values = np.zeros(len(parameters)) + for i in range(len(parameters)): + init_param_values[i] = np.pi / 4 + + var_principle = ImaginaryMcLachlanPrinciple() + + param_dict = dict(zip(parameters, init_param_values)) + + backend = BasicAer.get_backend("statevector_simulator") + + time = 1 + var_qite = VarQITE( + ansatz, + var_principle, + param_dict, + ode_solver="RK45", + num_timesteps=25, + quantum_instance=backend, + ) + + thetas_expected = [ + 1.29495364023786, + 1.08970061333559, + 0.667488228710748, + 0.500122687902944, + 1.4377736672043, + 1.22881086103085, + 0.729773048146251, + 1.01698854755226, + 0.050807780587492, + 0.294828474947149, + 0.839305697704923, + 0.663689581255428, + ] + + self._test_helper(observable, thetas_expected, time, var_qite, 4) + + def _test_helper(self, observable, thetas_expected, time, var_qite, decimal): + evolution_problem = EvolutionProblem(observable, time) + evolution_result = var_qite.evolve(evolution_problem) + evolved_state = evolution_result.evolved_state + + parameter_values = evolved_state.data[0][0].params + for i, parameter_value in enumerate(parameter_values): + np.testing.assert_almost_equal( + float(parameter_value), thetas_expected[i], decimal=decimal + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/algorithms/evolvers/variational/test_var_qrte.py b/test/python/algorithms/evolvers/variational/test_var_qrte.py new file mode 100644 index 000000000000..22ab25394e14 --- /dev/null +++ b/test/python/algorithms/evolvers/variational/test_var_qrte.py @@ -0,0 +1,234 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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 Variational Quantum Real Time Evolution algorithm.""" + +import unittest + +from test.python.algorithms import QiskitAlgorithmsTestCase +from ddt import data, ddt +import numpy as np +from qiskit.test import slow_test +from qiskit.utils import QuantumInstance, algorithm_globals +from qiskit.algorithms import EvolutionProblem, VarQRTE +from qiskit.algorithms.evolvers.variational import ( + RealMcLachlanPrinciple, +) +from qiskit import BasicAer +from qiskit.circuit.library import EfficientSU2 +from qiskit.opflow import ( + SummedOp, + X, + Y, + I, + Z, + ExpectationFactory, +) + + +@ddt +class TestVarQRTE(QiskitAlgorithmsTestCase): + """Test Variational Quantum Real Time Evolution algorithm.""" + + def setUp(self): + super().setUp() + self.seed = 11 + np.random.seed(self.seed) + backend_statevector = BasicAer.get_backend("statevector_simulator") + backend_qasm = BasicAer.get_backend("qasm_simulator") + self.quantum_instance = QuantumInstance( + backend=backend_statevector, + shots=1, + seed_simulator=self.seed, + seed_transpiler=self.seed, + ) + self.quantum_instance_qasm = QuantumInstance( + backend=backend_qasm, + shots=4000, + seed_simulator=self.seed, + seed_transpiler=self.seed, + ) + self.backends_dict = { + "qi_sv": self.quantum_instance, + "qi_qasm": self.quantum_instance_qasm, + "b_sv": backend_statevector, + } + + self.backends_names = ["qi_qasm", "b_sv", "qi_sv"] + + @slow_test + def test_run_d_1_with_aux_ops(self): + """Test VarQRTE for d = 1 and t = 0.1 with evaluating auxiliary operators and the Forward + Euler solver.""" + observable = SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ) + aux_ops = [X ^ X, Y ^ Z] + d = 1 + ansatz = EfficientSU2(observable.num_qubits, reps=d) + + parameters = list(ansatz.parameters) + init_param_values = np.zeros(len(parameters)) + for i in range(len(parameters)): + init_param_values[i] = np.pi / 2 + init_param_values[0] = 1 + var_principle = RealMcLachlanPrinciple() + + time = 0.1 + + evolution_problem = EvolutionProblem(observable, time, aux_operators=aux_ops) + + thetas_expected_sv = [ + 0.88967020378258, + 1.53740751016451, + 1.57076759018861, + 1.58893301221363, + 1.60100970594142, + 1.57008242207638, + 1.63791241090936, + 1.53741371076912, + ] + + thetas_expected_qasm = [ + 0.88967811203145, + 1.53745130248168, + 1.57206794045495, + 1.58901347342829, + 1.60101431615503, + 1.57138020823337, + 1.63796000651177, + 1.53742227084076, + ] + + expected_aux_ops_evaluated_sv = [(0.06675, 0.0), (0.772636, 0.0)] + + expected_aux_ops_evaluated_qasm = [ + (0.06450000000000006, 0.01577846435810532), + (0.7895000000000001, 0.009704248425303218), + ] + + for backend_name in self.backends_names: + with self.subTest(msg=f"Test {backend_name} backend."): + algorithm_globals.random_seed = self.seed + backend = self.backends_dict[backend_name] + expectation = ExpectationFactory.build( + operator=observable, + backend=backend, + ) + var_qrte = VarQRTE( + ansatz, + var_principle, + init_param_values, + expectation=expectation, + num_timesteps=25, + quantum_instance=backend, + ) + evolution_result = var_qrte.evolve(evolution_problem) + + evolved_state = evolution_result.evolved_state + aux_ops = evolution_result.aux_ops_evaluated + + parameter_values = evolved_state.data[0][0].params + if backend_name == "qi_qasm": + thetas_expected = thetas_expected_qasm + expected_aux_ops = expected_aux_ops_evaluated_qasm + else: + thetas_expected = thetas_expected_sv + expected_aux_ops = expected_aux_ops_evaluated_sv + + for i, parameter_value in enumerate(parameter_values): + np.testing.assert_almost_equal( + float(parameter_value), thetas_expected[i], decimal=3 + ) + np.testing.assert_array_almost_equal(aux_ops, expected_aux_ops) + + @slow_test + @data( + SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ), + 0.2252 * (I ^ I) + + 0.5716 * (Z ^ Z) + + 0.3435 * (I ^ Z) + + -0.4347 * (Z ^ I) + + 0.091 * (Y ^ Y) + + 0.091 * (X ^ X), + ) + def test_run_d_2(self, observable): + """Test VarQRTE for d = 2 and t = 1 with RK45 ODE solver.""" + d = 2 + ansatz = EfficientSU2(observable.num_qubits, reps=d) + + parameters = list(ansatz.parameters) + init_param_values = np.zeros(len(parameters)) + for i in range(len(parameters)): + init_param_values[i] = np.pi / 4 + + var_principle = RealMcLachlanPrinciple() + + param_dict = dict(zip(parameters, init_param_values)) + + backend = BasicAer.get_backend("statevector_simulator") + + time = 1 + var_qrte = VarQRTE( + ansatz, + var_principle, + param_dict, + ode_solver="RK45", + num_timesteps=25, + quantum_instance=backend, + ) + + thetas_expected = [ + 0.348407744196573, + 0.919404626262464, + 1.18189219371626, + 0.771011177789998, + 0.734384256533924, + 0.965289520781899, + 1.14441687204195, + 1.17231927568571, + 1.03014771379412, + 0.867266309056347, + 0.699606368428206, + 0.610788576398685, + ] + + self._test_helper(observable, thetas_expected, time, var_qrte) + + def _test_helper(self, observable, thetas_expected, time, var_qrte): + evolution_problem = EvolutionProblem(observable, time) + evolution_result = var_qrte.evolve(evolution_problem) + evolved_state = evolution_result.evolved_state + + parameter_values = evolved_state.data[0][0].params + for i, parameter_value in enumerate(parameter_values): + np.testing.assert_almost_equal(float(parameter_value), thetas_expected[i], decimal=4) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/algorithms/evolvers/variational/test_var_qte.py b/test/python/algorithms/evolvers/variational/test_var_qte.py new file mode 100644 index 000000000000..1083a1564f68 --- /dev/null +++ b/test/python/algorithms/evolvers/variational/test_var_qte.py @@ -0,0 +1,78 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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 Variational Quantum Real Time Evolution algorithm.""" + +import unittest + +from test.python.algorithms import QiskitAlgorithmsTestCase +from numpy.testing import assert_raises +from ddt import data, ddt +import numpy as np + +from qiskit.algorithms.evolvers.variational.var_qte import VarQTE +from qiskit.circuit import Parameter + + +@ddt +class TestVarQTE(QiskitAlgorithmsTestCase): + """Test Variational Quantum Time Evolution class methods.""" + + def setUp(self): + super().setUp() + self._parameters1 = [Parameter("a"), Parameter("b"), Parameter("c")] + + @data([1.4, 2, 3], np.asarray([1.4, 2, 3])) + def test_create_init_state_param_dict(self, param_values): + """Tests if a correct dictionary is created.""" + expected = dict(zip(self._parameters1, param_values)) + with self.subTest("Parameters values given as a list test."): + result = VarQTE._create_init_state_param_dict(param_values, self._parameters1) + np.testing.assert_equal(result, expected) + with self.subTest("Parameters values given as a dictionary test."): + result = VarQTE._create_init_state_param_dict( + dict(zip(self._parameters1, param_values)), self._parameters1 + ) + np.testing.assert_equal(result, expected) + with self.subTest("Parameters values given as a superset dictionary test."): + expected = dict( + zip( + [self._parameters1[0], self._parameters1[2]], [param_values[0], param_values[2]] + ) + ) + result = VarQTE._create_init_state_param_dict( + dict(zip(self._parameters1, param_values)), + [self._parameters1[0], self._parameters1[2]], + ) + np.testing.assert_equal(result, expected) + + @data([1.4, 2], np.asarray([1.4, 3]), {}, []) + def test_create_init_state_param_dict_errors_list(self, param_values): + """Tests if an error is raised.""" + with assert_raises(ValueError): + _ = VarQTE._create_init_state_param_dict(param_values, self._parameters1) + + @data([1.4, 2], np.asarray([1.4, 3])) + def test_create_init_state_param_dict_errors_subset(self, param_values): + """Tests if an error is raised if subset of parameters provided.""" + param_values_dict = dict(zip([self._parameters1[0], self._parameters1[2]], param_values)) + with assert_raises(ValueError): + _ = VarQTE._create_init_state_param_dict(param_values_dict, self._parameters1) + + @data(5, "s", Parameter("x")) + def test_create_init_state_param_dict_errors_type(self, param_values): + """Tests if an error is raised if wrong input type.""" + with assert_raises(TypeError): + _ = VarQTE._create_init_state_param_dict(param_values, self._parameters1) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/algorithms/evolvers/variational/variational_principles/__init__.py b/test/python/algorithms/evolvers/variational/variational_principles/__init__.py new file mode 100644 index 000000000000..b3ac36d2a6d9 --- /dev/null +++ b/test/python/algorithms/evolvers/variational/variational_principles/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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. diff --git a/test/python/algorithms/evolvers/variational/variational_principles/expected_results/__init__.py b/test/python/algorithms/evolvers/variational/variational_principles/expected_results/__init__.py new file mode 100644 index 000000000000..9c3165f57a2a --- /dev/null +++ b/test/python/algorithms/evolvers/variational/variational_principles/expected_results/__init__.py @@ -0,0 +1,12 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. +"""Stores expected results that are lengthy.""" diff --git a/test/python/algorithms/evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected1.py b/test/python/algorithms/evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected1.py new file mode 100644 index 000000000000..231cbac4dba4 --- /dev/null +++ b/test/python/algorithms/evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected1.py @@ -0,0 +1,182 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. +"""Stores expected results that are lengthy.""" +expected_bound_metric_tensor_1 = [ + [ + 2.50000000e-01 + 0.0j, + 1.59600000e-33 + 0.0j, + 5.90075760e-18 + 0.0j, + -8.49242405e-19 + 0.0j, + 8.83883476e-02 + 0.0j, + 1.33253788e-17 + 0.0j, + 6.25000000e-02 + 0.0j, + 1.40000000e-17 + 0.0j, + -1.41735435e-01 + 0.0j, + 3.12500000e-02 + 0.0j, + 1.00222087e-01 + 0.0j, + -3.12500000e-02 + 0.0j, + ], + [ + 1.59600000e-33 + 0.0j, + 2.50000000e-01 + 0.0j, + 1.34350288e-17 + 0.0j, + 6.43502884e-18 + 0.0j, + -8.83883476e-02 + 0.0j, + 1.25000000e-01 + 0.0j, + 6.25000000e-02 + 0.0j, + 1.25000000e-01 + 0.0j, + -8.45970869e-02 + 0.0j, + 7.54441738e-02 + 0.0j, + 1.48207521e-01 + 0.0j, + 2.00444174e-01 + 0.0j, + ], + [ + 5.90075760e-18 + 0.0j, + 1.34350288e-17 + 0.0j, + 1.25000000e-01 + 0.0j, + -1.38777878e-17 + 0.0j, + -4.41941738e-02 + 0.0j, + 6.25000000e-02 + 0.0j, + 1.19638348e-01 + 0.0j, + 6.25000000e-02 + 0.0j, + -5.14514565e-02 + 0.0j, + 6.89720869e-02 + 0.0j, + 1.04933262e-02 + 0.0j, + -6.89720869e-02 + 0.0j, + ], + [ + -8.49242405e-19 + 0.0j, + 6.43502884e-18 + 0.0j, + -1.38777878e-17 + 0.0j, + 1.25000000e-01 + 0.0j, + -4.41941738e-02 + 0.0j, + -6.25000000e-02 + 0.0j, + 3.12500000e-02 + 0.0j, + 1.25000000e-01 + 0.0j, + 5.14514565e-02 + 0.0j, + -6.89720869e-02 + 0.0j, + 7.81250000e-03 + 0.0j, + 1.94162607e-02 + 0.0j, + ], + [ + 8.83883476e-02 + 0.0j, + -8.83883476e-02 + 0.0j, + -4.41941738e-02 + 0.0j, + -4.41941738e-02 + 0.0j, + 2.34375000e-01 + 0.0j, + -1.10485435e-01 + 0.0j, + -2.02014565e-02 + 0.0j, + -4.41941738e-02 + 0.0j, + 1.49547935e-02 + 0.0j, + -2.24896848e-02 + 0.0j, + -1.42172278e-03 + 0.0j, + -1.23822206e-01 + 0.0j, + ], + [ + 1.33253788e-17 + 0.0j, + 1.25000000e-01 + 0.0j, + 6.25000000e-02 + 0.0j, + -6.25000000e-02 + 0.0j, + -1.10485435e-01 + 0.0j, + 2.18750000e-01 + 0.0j, + -2.68082618e-03 + 0.0j, + -1.59099026e-17 + 0.0j, + -1.57197815e-01 + 0.0j, + 2.53331304e-02 + 0.0j, + 9.82311963e-03 + 0.0j, + 1.06138957e-01 + 0.0j, + ], + [ + 6.25000000e-02 + 0.0j, + 6.25000000e-02 + 0.0j, + 1.19638348e-01 + 0.0j, + 3.12500000e-02 + 0.0j, + -2.02014565e-02 + 0.0j, + -2.68082618e-03 + 0.0j, + 2.23881674e-01 + 0.0j, + 1.37944174e-01 + 0.0j, + -3.78033966e-02 + 0.0j, + 1.58423239e-01 + 0.0j, + 1.34535646e-01 + 0.0j, + -5.49651086e-02 + 0.0j, + ], + [ + 1.40000000e-17 + 0.0j, + 1.25000000e-01 + 0.0j, + 6.25000000e-02 + 0.0j, + 1.25000000e-01 + 0.0j, + -4.41941738e-02 + 0.0j, + -1.59099026e-17 + 0.0j, + 1.37944174e-01 + 0.0j, + 2.50000000e-01 + 0.0j, + -2.10523539e-17 + 0.0j, + 1.15574269e-17 + 0.0j, + 9.75412607e-02 + 0.0j, + 5.71383476e-02 + 0.0j, + ], + [ + -1.41735435e-01 + 0.0j, + -8.45970869e-02 + 0.0j, + -5.14514565e-02 + 0.0j, + 5.14514565e-02 + 0.0j, + 1.49547935e-02 + 0.0j, + -1.57197815e-01 + 0.0j, + -3.78033966e-02 + 0.0j, + -2.10523539e-17 + 0.0j, + 1.95283753e-01 + 0.0j, + -3.82941440e-02 + 0.0j, + -6.11392595e-02 + 0.0j, + -4.51588288e-02 + 0.0j, + ], + [ + 3.12500000e-02 + 0.0j, + 7.54441738e-02 + 0.0j, + 6.89720869e-02 + 0.0j, + -6.89720869e-02 + 0.0j, + -2.24896848e-02 + 0.0j, + 2.53331304e-02 + 0.0j, + 1.58423239e-01 + 0.0j, + 1.15574269e-17 + 0.0j, + -3.82941440e-02 + 0.0j, + 2.17629701e-01 + 0.0j, + 1.32431810e-01 + 0.0j, + -1.91961467e-02 + 0.0j, + ], + [ + 1.00222087e-01 + 0.0j, + 1.48207521e-01 + 0.0j, + 1.04933262e-02 + 0.0j, + 7.81250000e-03 + 0.0j, + -1.42172278e-03 + 0.0j, + 9.82311963e-03 + 0.0j, + 1.34535646e-01 + 0.0j, + 9.75412607e-02 + 0.0j, + -6.11392595e-02 + 0.0j, + 1.32431810e-01 + 0.0j, + 1.81683746e-01 + 0.0j, + 7.28902444e-02 + 0.0j, + ], + [ + -3.12500000e-02 + 0.0j, + 2.00444174e-01 + 0.0j, + -6.89720869e-02 + 0.0j, + 1.94162607e-02 + 0.0j, + -1.23822206e-01 + 0.0j, + 1.06138957e-01 + 0.0j, + -5.49651086e-02 + 0.0j, + 5.71383476e-02 + 0.0j, + -4.51588288e-02 + 0.0j, + -1.91961467e-02 + 0.0j, + 7.28902444e-02 + 0.0j, + 2.38616353e-01 + 0.0j, + ], +] diff --git a/test/python/algorithms/evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected2.py b/test/python/algorithms/evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected2.py new file mode 100644 index 000000000000..386e3196ea4e --- /dev/null +++ b/test/python/algorithms/evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected2.py @@ -0,0 +1,182 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. +"""Stores expected results that are lengthy.""" +expected_bound_metric_tensor_2 = [ + [ + 2.50000000e-01 + 0.0j, + 1.59600000e-33 + 0.0j, + 5.90075760e-18 + 0.0j, + -8.49242405e-19 + 0.0j, + 8.83883476e-02 + 0.0j, + 1.33253788e-17 + 0.0j, + 6.25000000e-02 + 0.0j, + 1.40000000e-17 + 0.0j, + -1.41735435e-01 + 0.0j, + 3.12500000e-02 + 0.0j, + 1.00222087e-01 + 0.0j, + -3.12500000e-02 + 0.0j, + ], + [ + 1.59600000e-33 + 0.0j, + 2.50000000e-01 + 0.0j, + 1.34350288e-17 + 0.0j, + 6.43502884e-18 + 0.0j, + -8.83883476e-02 + 0.0j, + 1.25000000e-01 + 0.0j, + 6.25000000e-02 + 0.0j, + 1.25000000e-01 + 0.0j, + -8.45970869e-02 + 0.0j, + 7.54441738e-02 + 0.0j, + 1.48207521e-01 + 0.0j, + 2.00444174e-01 + 0.0j, + ], + [ + 5.90075760e-18 + 0.0j, + 1.34350288e-17 + 0.0j, + 1.25000000e-01 + 0.0j, + -1.38777878e-17 + 0.0j, + -4.41941738e-02 + 0.0j, + 6.25000000e-02 + 0.0j, + 1.19638348e-01 + 0.0j, + 6.25000000e-02 + 0.0j, + -5.14514565e-02 + 0.0j, + 6.89720869e-02 + 0.0j, + 1.04933262e-02 + 0.0j, + -6.89720869e-02 + 0.0j, + ], + [ + -8.49242405e-19 + 0.0j, + 6.43502884e-18 + 0.0j, + -1.38777878e-17 + 0.0j, + 1.25000000e-01 + 0.0j, + -4.41941738e-02 + 0.0j, + -6.25000000e-02 + 0.0j, + 3.12500000e-02 + 0.0j, + 1.25000000e-01 + 0.0j, + 5.14514565e-02 + 0.0j, + -6.89720869e-02 + 0.0j, + 7.81250000e-03 + 0.0j, + 1.94162607e-02 + 0.0j, + ], + [ + 8.83883476e-02 + 0.0j, + -8.83883476e-02 + 0.0j, + -4.41941738e-02 + 0.0j, + -4.41941738e-02 + 0.0j, + 2.34375000e-01 + 0.0j, + -1.10485435e-01 + 0.0j, + -2.02014565e-02 + 0.0j, + -4.41941738e-02 + 0.0j, + 1.49547935e-02 + 0.0j, + -2.24896848e-02 + 0.0j, + -1.42172278e-03 + 0.0j, + -1.23822206e-01 + 0.0j, + ], + [ + 1.33253788e-17 + 0.0j, + 1.25000000e-01 + 0.0j, + 6.25000000e-02 + 0.0j, + -6.25000000e-02 + 0.0j, + -1.10485435e-01 + 0.0j, + 2.18750000e-01 + 0.0j, + -2.68082618e-03 + 0.0j, + -1.59099026e-17 + 0.0j, + -1.57197815e-01 + 0.0j, + 2.53331304e-02 + 0.0j, + 9.82311963e-03 + 0.0j, + 1.06138957e-01 + 0.0j, + ], + [ + 6.25000000e-02 + 0.0j, + 6.25000000e-02 + 0.0j, + 1.19638348e-01 + 0.0j, + 3.12500000e-02 + 0.0j, + -2.02014565e-02 + 0.0j, + -2.68082618e-03 + 0.0j, + 2.23881674e-01 + 0.0j, + 1.37944174e-01 + 0.0j, + -3.78033966e-02 + 0.0j, + 1.58423239e-01 + 0.0j, + 1.34535646e-01 + 0.0j, + -5.49651086e-02 + 0.0j, + ], + [ + 1.40000000e-17 + 0.0j, + 1.25000000e-01 + 0.0j, + 6.25000000e-02 + 0.0j, + 1.25000000e-01 + 0.0j, + -4.41941738e-02 + 0.0j, + -1.59099026e-17 + 0.0j, + 1.37944174e-01 + 0.0j, + 2.50000000e-01 + 0.0j, + -2.10523539e-17 + 0.0j, + 1.15574269e-17 + 0.0j, + 9.75412607e-02 + 0.0j, + 5.71383476e-02 + 0.0j, + ], + [ + -1.41735435e-01 + 0.0j, + -8.45970869e-02 + 0.0j, + -5.14514565e-02 + 0.0j, + 5.14514565e-02 + 0.0j, + 1.49547935e-02 + 0.0j, + -1.57197815e-01 + 0.0j, + -3.78033966e-02 + 0.0j, + -2.10523539e-17 + 0.0j, + 1.95283753e-01 + 0.0j, + -3.82941440e-02 + 0.0j, + -6.11392595e-02 + 0.0j, + -4.51588288e-02 + 0.0j, + ], + [ + 3.12500000e-02 + 0.0j, + 7.54441738e-02 + 0.0j, + 6.89720869e-02 + 0.0j, + -6.89720869e-02 + 0.0j, + -2.24896848e-02 + 0.0j, + 2.53331304e-02 + 0.0j, + 1.58423239e-01 + 0.0j, + 1.15574269e-17 + 0.0j, + -3.82941440e-02 + 0.0j, + 2.17629701e-01 + 0.0j, + 1.32431810e-01 + 0.0j, + -1.91961467e-02 + 0.0j, + ], + [ + 1.00222087e-01 + 0.0j, + 1.48207521e-01 + 0.0j, + 1.04933262e-02 + 0.0j, + 7.81250000e-03 + 0.0j, + -1.42172278e-03 + 0.0j, + 9.82311963e-03 + 0.0j, + 1.34535646e-01 + 0.0j, + 9.75412607e-02 + 0.0j, + -6.11392595e-02 + 0.0j, + 1.32431810e-01 + 0.0j, + 1.81683746e-01 + 0.0j, + 7.28902444e-02 + 0.0j, + ], + [ + -3.12500000e-02 + 0.0j, + 2.00444174e-01 + 0.0j, + -6.89720869e-02 + 0.0j, + 1.94162607e-02 + 0.0j, + -1.23822206e-01 + 0.0j, + 1.06138957e-01 + 0.0j, + -5.49651086e-02 + 0.0j, + 5.71383476e-02 + 0.0j, + -4.51588288e-02 + 0.0j, + -1.91961467e-02 + 0.0j, + 7.28902444e-02 + 0.0j, + 2.38616353e-01 + 0.0j, + ], +] diff --git a/test/python/algorithms/evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected3.py b/test/python/algorithms/evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected3.py new file mode 100644 index 000000000000..5c295c0c6f2a --- /dev/null +++ b/test/python/algorithms/evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected3.py @@ -0,0 +1,182 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. +"""Stores expected results that are lengthy.""" +expected_bound_metric_tensor_3 = [ + [ + -1.21000000e-34 + 0.00e00j, + 1.21000000e-34 + 2.50e-19j, + 1.76776695e-01 - 1.00e-18j, + -1.40000000e-17 + 0.00e00j, + -6.25000000e-02 + 0.00e00j, + 8.83883476e-02 - 1.25e-18j, + 1.69194174e-01 + 2.25e-18j, + 8.83883476e-02 - 2.50e-19j, + -7.27633476e-02 + 0.00e00j, + 9.75412607e-02 + 7.50e-19j, + 1.48398042e-02 - 1.75e-18j, + -9.75412607e-02 + 3.75e-18j, + ], + [ + 1.21000000e-34 + 2.50e-19j, + -1.21000000e-34 + 0.00e00j, + 1.10000000e-34 + 2.75e-18j, + 1.76776695e-01 - 2.25e-18j, + -6.25000000e-02 + 0.00e00j, + -8.83883476e-02 + 4.00e-18j, + 4.41941738e-02 - 1.25e-18j, + 1.76776695e-01 - 2.50e-19j, + 7.27633476e-02 - 7.50e-19j, + -9.75412607e-02 - 7.50e-19j, + 1.10485435e-02 - 7.50e-19j, + 2.74587393e-02 + 2.50e-19j, + ], + [ + 1.76776695e-01 - 1.00e-18j, + 1.10000000e-34 + 2.75e-18j, + -1.25000000e-01 + 0.00e00j, + -1.25000000e-01 + 0.00e00j, + -1.06694174e-01 + 1.25e-18j, + -6.25000000e-02 + 1.75e-18j, + -1.01332521e-01 + 7.50e-19j, + 4.67500000e-17 - 7.50e-19j, + 1.75206304e-02 + 5.00e-19j, + -8.57075215e-02 - 1.00e-18j, + -1.63277304e-01 + 1.00e-18j, + -1.56250000e-02 + 0.00e00j, + ], + [ + -1.40000000e-17 + 0.00e00j, + 1.76776695e-01 - 2.25e-18j, + -1.25000000e-01 + 0.00e00j, + -1.25000000e-01 + 0.00e00j, + 1.83058262e-02 - 1.50e-18j, + -1.50888348e-01 - 1.50e-18j, + -1.01332521e-01 + 2.50e-19j, + -8.83883476e-02 - 1.00e-18j, + -2.28822827e-02 - 1.00e-18j, + -1.16957521e-01 + 1.00e-18j, + -1.97208130e-01 + 0.00e00j, + -1.79457521e-01 + 1.25e-18j, + ], + [ + -6.25000000e-02 + 0.00e00j, + -6.25000000e-02 + 0.00e00j, + -1.06694174e-01 + 1.25e-18j, + 1.83058262e-02 - 1.50e-18j, + -1.56250000e-02 + 0.00e00j, + -2.20970869e-02 - 2.00e-18j, + 1.48992717e-01 - 1.00e-18j, + 2.60000000e-17 - 1.50e-18j, + -6.69614673e-02 - 5.00e-19j, + 2.00051576e-01 + 5.00e-19j, + 1.13640168e-01 + 1.25e-18j, + -4.83780325e-02 - 1.00e-18j, + ], + [ + 8.83883476e-02 - 1.25e-18j, + -8.83883476e-02 + 4.00e-18j, + -6.25000000e-02 + 1.75e-18j, + -1.50888348e-01 - 1.50e-18j, + -2.20970869e-02 - 2.00e-18j, + -3.12500000e-02 + 0.00e00j, + -2.85691738e-02 + 4.25e-18j, + 1.76776695e-01 + 0.00e00j, + 5.52427173e-03 + 1.00e-18j, + -1.29346478e-01 + 5.00e-19j, + -4.81004238e-02 + 4.25e-18j, + 5.27918696e-02 + 2.50e-19j, + ], + [ + 1.69194174e-01 + 2.25e-18j, + 4.41941738e-02 - 1.25e-18j, + -1.01332521e-01 + 7.50e-19j, + -1.01332521e-01 + 2.50e-19j, + 1.48992717e-01 - 1.00e-18j, + -2.85691738e-02 + 4.25e-18j, + -2.61183262e-02 + 0.00e00j, + -6.88900000e-33 + 0.00e00j, + 6.62099510e-02 - 1.00e-18j, + -2.90767610e-02 + 1.75e-18j, + -1.24942505e-01 + 0.00e00j, + -1.72430217e-02 + 2.50e-19j, + ], + [ + 8.83883476e-02 - 2.50e-19j, + 1.76776695e-01 - 2.50e-19j, + 4.67500000e-17 - 7.50e-19j, + -8.83883476e-02 - 1.00e-18j, + 2.60000000e-17 - 1.50e-18j, + 1.76776695e-01 + 0.00e00j, + -6.88900000e-33 + 0.00e00j, + -6.88900000e-33 + 0.00e00j, + 1.79457521e-01 - 1.75e-18j, + -5.33470869e-02 + 2.00e-18j, + -9.56456304e-02 + 3.00e-18j, + -1.32582521e-01 + 2.50e-19j, + ], + [ + -7.27633476e-02 + 0.00e00j, + 7.27633476e-02 - 7.50e-19j, + 1.75206304e-02 + 5.00e-19j, + -2.28822827e-02 - 1.00e-18j, + -6.69614673e-02 - 5.00e-19j, + 5.52427173e-03 + 1.00e-18j, + 6.62099510e-02 - 1.00e-18j, + 1.79457521e-01 - 1.75e-18j, + -5.47162473e-02 + 0.00e00j, + -4.20854047e-02 + 4.00e-18j, + -7.75494553e-02 - 2.50e-18j, + -2.49573723e-02 + 7.50e-19j, + ], + [ + 9.75412607e-02 + 7.50e-19j, + -9.75412607e-02 - 7.50e-19j, + -8.57075215e-02 - 1.00e-18j, + -1.16957521e-01 + 1.00e-18j, + 2.00051576e-01 + 5.00e-19j, + -1.29346478e-01 + 5.00e-19j, + -2.90767610e-02 + 1.75e-18j, + -5.33470869e-02 + 2.00e-18j, + -4.20854047e-02 + 4.00e-18j, + -3.23702991e-02 + 0.00e00j, + -4.70257118e-02 + 0.00e00j, + 1.22539288e-01 - 2.25e-18j, + ], + [ + 1.48398042e-02 - 1.75e-18j, + 1.10485435e-02 - 7.50e-19j, + -1.63277304e-01 + 1.00e-18j, + -1.97208130e-01 + 0.00e00j, + 1.13640168e-01 + 1.25e-18j, + -4.81004238e-02 + 4.25e-18j, + -1.24942505e-01 + 0.00e00j, + -9.56456304e-02 + 3.00e-18j, + -7.75494553e-02 - 2.50e-18j, + -4.70257118e-02 + 0.00e00j, + -6.83162540e-02 + 0.00e00j, + -2.78870598e-02 + 0.00e00j, + ], + [ + -9.75412607e-02 + 3.75e-18j, + 2.74587393e-02 + 2.50e-19j, + -1.56250000e-02 + 0.00e00j, + -1.79457521e-01 + 1.25e-18j, + -4.83780325e-02 - 1.00e-18j, + 5.27918696e-02 + 2.50e-19j, + -1.72430217e-02 + 2.50e-19j, + -1.32582521e-01 + 2.50e-19j, + -2.49573723e-02 + 7.50e-19j, + 1.22539288e-01 - 2.25e-18j, + -2.78870598e-02 + 0.00e00j, + -1.13836467e-02 + 0.00e00j, + ], +] diff --git a/test/python/algorithms/evolvers/variational/variational_principles/imaginary/__init__.py b/test/python/algorithms/evolvers/variational/variational_principles/imaginary/__init__.py new file mode 100644 index 000000000000..b3ac36d2a6d9 --- /dev/null +++ b/test/python/algorithms/evolvers/variational/variational_principles/imaginary/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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. diff --git a/test/python/algorithms/evolvers/variational/variational_principles/imaginary/test_imaginary_mc_lachlan_principle.py b/test/python/algorithms/evolvers/variational/variational_principles/imaginary/test_imaginary_mc_lachlan_principle.py new file mode 100644 index 000000000000..5118a9a699a4 --- /dev/null +++ b/test/python/algorithms/evolvers/variational/variational_principles/imaginary/test_imaginary_mc_lachlan_principle.py @@ -0,0 +1,111 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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 imaginary McLachlan's variational principle.""" + +import unittest +from test.python.algorithms import QiskitAlgorithmsTestCase +import numpy as np + +from qiskit.algorithms.evolvers.variational import ( + ImaginaryMcLachlanPrinciple, +) +from qiskit.circuit.library import EfficientSU2 +from qiskit.opflow import SummedOp, X, Y, I, Z +from ..expected_results.test_imaginary_mc_lachlan_variational_principle_expected1 import ( + expected_bound_metric_tensor_1, +) + + +class TestImaginaryMcLachlanPrinciple(QiskitAlgorithmsTestCase): + """Test imaginary McLachlan's variational principle.""" + + def test_calc_metric_tensor(self): + """Test calculating a metric tensor.""" + observable = SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ) + + d = 2 + ansatz = EfficientSU2(observable.num_qubits, reps=d) + + # Define a set of initial parameters + parameters = list(ansatz.parameters) + param_dict = {param: np.pi / 4 for param in parameters} + var_principle = ImaginaryMcLachlanPrinciple() + + bound_metric_tensor = var_principle.metric_tensor( + ansatz, parameters, parameters, param_dict.values(), None, None + ) + + np.testing.assert_almost_equal(bound_metric_tensor, expected_bound_metric_tensor_1) + + def test_calc_calc_evolution_grad(self): + """Test calculating evolution gradient.""" + observable = SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ) + + d = 2 + ansatz = EfficientSU2(observable.num_qubits, reps=d) + + # Define a set of initial parameters + parameters = list(ansatz.parameters) + param_dict = {param: np.pi / 4 for param in parameters} + var_principle = ImaginaryMcLachlanPrinciple() + + bound_evolution_grad = var_principle.evolution_grad( + observable, + ansatz, + None, + param_dict, + parameters, + parameters, + param_dict.values(), + None, + None, + ) + + expected_bound_evolution_grad = [ + (0.19308934095957098 - 1.4e-17j), + (0.007027674650099142 - 0j), + (0.03192524520091862 - 0j), + (-0.06810314606309673 - 1e-18j), + (0.07590371669521798 - 7e-18j), + (0.11891968269385343 + 1.5e-18j), + (-0.0012030273438232639 + 0j), + (-0.049885258804562266 + 1.8500000000000002e-17j), + (-0.20178860797540302 - 5e-19j), + (-0.0052269232310933195 + 1e-18j), + (0.022892905637005266 - 3e-18j), + (-0.022892905637005294 + 3.5e-18j), + ] + + np.testing.assert_almost_equal(bound_evolution_grad, expected_bound_evolution_grad) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/algorithms/evolvers/variational/variational_principles/real/__init__.py b/test/python/algorithms/evolvers/variational/variational_principles/real/__init__.py new file mode 100644 index 000000000000..b3ac36d2a6d9 --- /dev/null +++ b/test/python/algorithms/evolvers/variational/variational_principles/real/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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. diff --git a/test/python/algorithms/evolvers/variational/variational_principles/real/test_real_mc_lachlan_principle.py b/test/python/algorithms/evolvers/variational/variational_principles/real/test_real_mc_lachlan_principle.py new file mode 100644 index 000000000000..13c126928bdb --- /dev/null +++ b/test/python/algorithms/evolvers/variational/variational_principles/real/test_real_mc_lachlan_principle.py @@ -0,0 +1,114 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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 real McLachlan's variational principle.""" + +import unittest +from test.python.algorithms import QiskitAlgorithmsTestCase +import numpy as np +from qiskit.algorithms.evolvers.variational import ( + RealMcLachlanPrinciple, +) +from qiskit.circuit.library import EfficientSU2 +from qiskit.opflow import SummedOp, X, Y, I, Z +from ..expected_results.test_imaginary_mc_lachlan_variational_principle_expected2 import ( + expected_bound_metric_tensor_2, +) + + +class TestRealMcLachlanPrinciple(QiskitAlgorithmsTestCase): + """Test real McLachlan's variational principle.""" + + def test_calc_calc_metric_tensor(self): + """Test calculating a metric tensor.""" + observable = SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ) + + d = 2 + ansatz = EfficientSU2(observable.num_qubits, reps=d) + + # Define a set of initial parameters + parameters = list(ansatz.parameters) + param_dict = {param: np.pi / 4 for param in parameters} + var_principle = RealMcLachlanPrinciple() + + bound_metric_tensor = var_principle.metric_tensor( + ansatz, parameters, parameters, list(param_dict.values()), None, None + ) + + np.testing.assert_almost_equal( + bound_metric_tensor, expected_bound_metric_tensor_2, decimal=5 + ) + + def test_calc_evolution_grad(self): + """Test calculating evolution gradient.""" + observable = SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ) + + d = 2 + ansatz = EfficientSU2(observable.num_qubits, reps=d) + + # Define a set of initial parameters + parameters = list(ansatz.parameters) + param_dict = {param: np.pi / 4 for param in parameters} + var_principle = RealMcLachlanPrinciple() + + bound_evolution_grad = var_principle.evolution_grad( + observable, + ansatz, + None, + param_dict, + parameters, + parameters, + list(param_dict.values()), + None, + None, + ) + + expected_bound_evolution_grad = [ + (-0.04514911474522546 + 4e-18j), + (0.0963123928027075 - 1.5e-18j), + (0.1365347823673539 - 7e-18j), + (0.004969316401057883 - 4.9999999999999996e-18j), + (-0.003843833929692342 - 4.999999999999998e-19j), + (0.07036988622493834 - 7e-18j), + (0.16560609099860682 - 3.5e-18j), + (0.16674183768051887 + 1e-18j), + (-0.03843296670360974 - 6e-18j), + (0.08891074158680243 - 6e-18j), + (0.06425681697616654 + 7e-18j), + (-0.03172376682078948 - 7e-18j), + ] + + np.testing.assert_almost_equal( + bound_evolution_grad, expected_bound_evolution_grad, decimal=5 + ) + + +if __name__ == "__main__": + unittest.main() From 09429389e91e1325640eddd95cdb5d62aecc825c Mon Sep 17 00:00:00 2001 From: Guillermo-Mijares-Vilarino <106545082+Guillermo-Mijares-Vilarino@users.noreply.github.com> Date: Tue, 23 Aug 2022 19:25:50 +0200 Subject: [PATCH 61/82] Extended explanation from plot_bloch_multivector API reference (#8415) * Added multivector explanation * Remove unnecessary paragraph * remove matplotlib inline and use Statevector(qc) * changed bloch vector explanation * Use better-spaced LaTeX commands Co-authored-by: Junye Huang Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: Jake Lishman --- qiskit/visualization/state_visualization.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/qiskit/visualization/state_visualization.py b/qiskit/visualization/state_visualization.py index 215397633e86..40b38df245d2 100644 --- a/qiskit/visualization/state_visualization.py +++ b/qiskit/visualization/state_visualization.py @@ -186,7 +186,8 @@ def plot_state_hinton( def plot_bloch_vector(bloch, title="", ax=None, figsize=None, coord_type="cartesian"): """Plot the Bloch sphere. - Plot a sphere, axes, the Bloch vector, and its projections onto each axis. + Plot a Bloch sphere with the specified coordinates, that can be given in both + cartesian and spherical systems. Args: bloch (list[double]): array of three elements where [, , ] (Cartesian) @@ -247,9 +248,13 @@ def plot_bloch_vector(bloch, title="", ax=None, figsize=None, coord_type="cartes def plot_bloch_multivector( state, title="", figsize=None, *, rho=None, reverse_bits=False, filename=None ): - """Plot the Bloch sphere. + r"""Plot a Bloch sphere for each qubit. - Plot a sphere, axes, the Bloch vector, and its projections onto each axis. + Each component :math:`(x,y,z)` of the Bloch sphere labeled as 'qubit i' represents the expected + value of the corresponding Pauli operator acting only on that qubit, that is, the expected value + of :math:`I_{N-1} \otimes\dotsb\otimes I_{i+1}\otimes P_i \otimes I_{i-1}\otimes\dotsb\otimes + I_0`, where :math:`N` is the number of qubits, :math:`P\in \{X,Y,Z\}` and :math:`I` is the + identity operator. Args: state (Statevector or DensityMatrix or ndarray): an N-qubit quantum state. From 7c4bde5e8bf20b7eab6612d0393aa8c102f9fd6a Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Tue, 23 Aug 2022 20:49:38 +0200 Subject: [PATCH 62/82] Add equivalences between 2q Pauli rotations (#8376) * Add equivalences between 2q Pauli rotations * attempt fixing QPY tests * try fix QPY attempt 2 Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .../standard_gates/equivalence_library.py | 57 +++++++++++++++++++ test/qpy_compat/test_qpy.py | 10 ++-- 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/qiskit/circuit/library/standard_gates/equivalence_library.py b/qiskit/circuit/library/standard_gates/equivalence_library.py index 65184b6dfbd2..5ded739eb51f 100644 --- a/qiskit/circuit/library/standard_gates/equivalence_library.py +++ b/qiskit/circuit/library/standard_gates/equivalence_library.py @@ -252,6 +252,20 @@ def_rxx.append(inst, qargs, cargs) _sel.add_equivalence(RXXGate(theta), def_rxx) +# RXX to RZZ +q = QuantumRegister(2, "q") +theta = Parameter("theta") +rxx_to_rzz = QuantumCircuit(q) +for inst, qargs, cargs in [ + (HGate(), [q[0]], []), + (HGate(), [q[1]], []), + (RZZGate(theta), [q[0], q[1]], []), + (HGate(), [q[0]], []), + (HGate(), [q[1]], []), +]: + rxx_to_rzz.append(inst, qargs, cargs) +_sel.add_equivalence(RXXGate(theta), rxx_to_rzz) + # RZXGate # # ┌─────────┐ @@ -324,6 +338,20 @@ def_ryy.append(inst, qargs, cargs) _sel.add_equivalence(RYYGate(theta), def_ryy) +# RYY to RZZ +q = QuantumRegister(2, "q") +theta = Parameter("theta") +ryy_to_rzz = QuantumCircuit(q) +for inst, qargs, cargs in [ + (RXGate(pi / 2), [q[0]], []), + (RXGate(pi / 2), [q[1]], []), + (RZZGate(theta), [q[0], q[1]], []), + (RXGate(-pi / 2), [q[0]], []), + (RXGate(-pi / 2), [q[1]], []), +]: + ryy_to_rzz.append(inst, qargs, cargs) +_sel.add_equivalence(RYYGate(theta), ryy_to_rzz) + # RZGate # global phase: -ϴ/2 # ┌───────┐ ┌───────┐ @@ -382,6 +410,35 @@ def_rzz.append(inst, qargs, cargs) _sel.add_equivalence(RZZGate(theta), def_rzz) +# RZZ to RXX +q = QuantumRegister(2, "q") +theta = Parameter("theta") +rzz_to_rxx = QuantumCircuit(q) +for inst, qargs, cargs in [ + (HGate(), [q[0]], []), + (HGate(), [q[1]], []), + (RXXGate(theta), [q[0], q[1]], []), + (HGate(), [q[0]], []), + (HGate(), [q[1]], []), +]: + rzz_to_rxx.append(inst, qargs, cargs) +_sel.add_equivalence(RZZGate(theta), rzz_to_rxx) + +# RZZ to RYY +q = QuantumRegister(2, "q") +theta = Parameter("theta") +rzz_to_ryy = QuantumCircuit(q) +for inst, qargs, cargs in [ + (RXGate(-pi / 2), [q[0]], []), + (RXGate(-pi / 2), [q[1]], []), + (RYYGate(theta), [q[0], q[1]], []), + (RXGate(pi / 2), [q[0]], []), + (RXGate(pi / 2), [q[1]], []), +]: + rzz_to_ryy.append(inst, qargs, cargs) +_sel.add_equivalence(RZZGate(theta), rzz_to_ryy) + + # RZXGate # # ┌─────────┐ diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index 418202f59d49..801052269ed5 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -446,14 +446,14 @@ def generate_calibrated_circuits(): # custom gate mygate = Gate("mygate", 1, []) - qc = QuantumCircuit(1) + qc = QuantumCircuit(1, name="calibrated_circuit_1") qc.append(mygate, [0]) with builder.build() as caldef: builder.play(Constant(100, 0.1), DriveChannel(0)) qc.add_calibration(mygate, (0,), caldef) circuits.append(qc) # override instruction - qc = QuantumCircuit(1) + qc = QuantumCircuit(1, name="calibrated_circuit_2") qc.x(0) with builder.build() as caldef: builder.play(Constant(100, 0.1), DriveChannel(0)) @@ -466,7 +466,7 @@ def generate_calibrated_circuits(): def generate_controlled_gates(): """Test QPY serialization with custom ControlledGates.""" circuits = [] - qc = QuantumCircuit(3) + qc = QuantumCircuit(3, name="custom_controlled_gates") controlled_gate = DCXGate().control(1) qc.append(controlled_gate, [0, 1, 2]) circuits.append(qc) @@ -476,13 +476,13 @@ def generate_controlled_gates(): custom_definition.rz(1.5, 0) custom_definition.sdg(0) custom_gate.definition = custom_definition - nested_qc = QuantumCircuit(3) + nested_qc = QuantumCircuit(3, name="nested_qc") qc.append(custom_gate, [0]) controlled_gate = custom_gate.control(2) nested_qc.append(controlled_gate, [0, 1, 2]) nested_qc.measure_all() circuits.append(nested_qc) - qc_open = QuantumCircuit(2) + qc_open = QuantumCircuit(2, name="open_cx") qc_open.cx(0, 1, ctrl_state=0) circuits.append(qc_open) return circuits From 94131ec8ba450ecec126a82d255cbfa607eba58a Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 24 Aug 2022 13:54:35 +0100 Subject: [PATCH 63/82] Ensure QPY test circuits have non-generated names (#8608) Circuits in QPY backwards compatibility should always have fixed names, to avoid changes in the number of circuits constructed during library set-up code affected the output names. --- test/qpy_compat/test_qpy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index 801052269ed5..310ba871a771 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -491,7 +491,7 @@ def generate_controlled_gates(): def generate_open_controlled_gates(): """Test QPY serialization with custom ControlledGates with open controls.""" circuits = [] - qc = QuantumCircuit(3) + qc = QuantumCircuit(3, name="open_controls_simple") controlled_gate = DCXGate().control(1, ctrl_state=0) qc.append(controlled_gate, [0, 1, 2]) circuits.append(qc) @@ -502,7 +502,7 @@ def generate_open_controlled_gates(): custom_definition.rz(1.5, 0) custom_definition.sdg(0) custom_gate.definition = custom_definition - nested_qc = QuantumCircuit(3) + nested_qc = QuantumCircuit(3, name="open_controls_nested") nested_qc.append(custom_gate, [0]) controlled_gate = custom_gate.control(2, ctrl_state=1) nested_qc.append(controlled_gate, [0, 1, 2]) From 0e9aea69df8ba3b076a0966d2b21abb116a41b1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Aug 2022 14:54:22 +0000 Subject: [PATCH 64/82] Bump pyo3 from 0.16.5 to 0.16.6 (#8609) Bumps [pyo3](https://github.com/pyo3/pyo3) from 0.16.5 to 0.16.6. - [Release notes](https://github.com/pyo3/pyo3/releases) - [Changelog](https://github.com/PyO3/pyo3/blob/main/CHANGELOG.md) - [Commits](https://github.com/pyo3/pyo3/compare/v0.16.5...v0.16.6) --- updated-dependencies: - dependency-name: pyo3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 20 ++++++++++---------- Cargo.toml | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0f821d94ffdb..eb2736a8a7d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -330,9 +330,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.16.5" +version = "0.16.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6302e85060011447471887705bb7838f14aba43fcb06957d823739a496b3dc" +checksum = "0220c44442c9b239dd4357aa856ac468a4f5e1f0df19ddb89b2522952eb4c6ca" dependencies = [ "cfg-if", "hashbrown 0.12.3", @@ -349,9 +349,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.16.5" +version = "0.16.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b65b546c35d8a3b1b2f0ddbac7c6a569d759f357f2b9df884f5d6b719152c8" +checksum = "9c819d397859445928609d0ec5afc2da5204e0d0f73d6bf9e153b04e83c9cdc2" dependencies = [ "once_cell", "target-lexicon", @@ -359,9 +359,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.16.5" +version = "0.16.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c275a07127c1aca33031a563e384ffdd485aee34ef131116fcd58e3430d1742b" +checksum = "ca882703ab55f54702d7bfe1189b41b0af10272389f04cae38fe4cd56c65f75f" dependencies = [ "libc", "pyo3-build-config", @@ -369,9 +369,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.16.5" +version = "0.16.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284fc4485bfbcc9850a6d661d627783f18d19c2ab55880b021671c4ba83e90f7" +checksum = "568749402955ad7be7bad9a09b8593851cd36e549ac90bfd44079cea500f3f21" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -381,9 +381,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.16.5" +version = "0.16.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53bda0f58f73f5c5429693c96ed57f7abdb38fdfc28ae06da4101a257adb7faf" +checksum = "611f64e82d98f447787e82b8e7b0ebc681e1eb78fc1252668b2c605ffb4e1eb8" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index d0d3882db611..ab8cbb9fcc6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ num-bigint = "0.4" retworkx-core = "0.11" [dependencies.pyo3] -version = "0.16.5" +version = "0.16.6" features = ["extension-module", "hashbrown", "num-complex", "num-bigint"] [dependencies.ndarray] From 4693463d27ca5e03c0bf8d724b4d11c1be9a9054 Mon Sep 17 00:00:00 2001 From: Ikko Hamamura Date: Thu, 25 Aug 2022 02:34:36 +0900 Subject: [PATCH 65/82] Skip binding when parameter is None (#8605) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/primitives/estimator.py | 4 +++- qiskit/primitives/sampler.py | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/qiskit/primitives/estimator.py b/qiskit/primitives/estimator.py index e0ae5c9924e3..b2e1b5215ba1 100644 --- a/qiskit/primitives/estimator.py +++ b/qiskit/primitives/estimator.py @@ -102,7 +102,9 @@ def _call( f"the number of parameters ({len(self._parameters[i])})." ) bound_circuits.append( - self._circuits[i].bind_parameters(dict(zip(self._parameters[i], value))) + self._circuits[i] + if len(value) == 0 + else self._circuits[i].bind_parameters(dict(zip(self._parameters[i], value))) ) sorted_observables = [self._observables[i] for i in observables] expectation_values = [] diff --git a/qiskit/primitives/sampler.py b/qiskit/primitives/sampler.py index e5cee820ffce..5626ec5416ba 100644 --- a/qiskit/primitives/sampler.py +++ b/qiskit/primitives/sampler.py @@ -106,12 +106,11 @@ def _call( f"The number of values ({len(value)}) does not match " f"the number of parameters ({len(self._parameters[i])})." ) - bound_circuit = ( + bound_circuits.append( self._circuits[i] if len(value) == 0 else self._circuits[i].bind_parameters(dict(zip(self._parameters[i], value))) ) - bound_circuits.append(bound_circuit) qargs_list.append(self._qargs_list[i]) probabilities = [ Statevector(circ).probabilities(qargs=qargs) From 58f1162fbe11e94ed34f5370e1bbc89bee989d09 Mon Sep 17 00:00:00 2001 From: Ikko Hamamura Date: Thu, 25 Aug 2022 04:15:08 +0900 Subject: [PATCH 66/82] Add setuptools_rust to tox.ini (#8606) * Add setuptools_rust to tox.ini * add setuptools_rust to docs * add comment Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- tox.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index d2bf7ad9f7a5..ef11d313352e 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,8 @@ setenv = QISKIT_TEST_CAPTURE_STREAMS=1 QISKIT_PARALLEL=FALSE passenv = RAYON_NUM_THREADS OMP_NUM_THREADS QISKIT_PARALLEL RUST_BACKTRACE SETUPTOOLS_ENABLE_FEATURES -deps = -r{toxinidir}/requirements.txt +deps = setuptools_rust # This is work around for the bug of tox 3 (see #8606 for more details.) + -r{toxinidir}/requirements.txt -r{toxinidir}/requirements-dev.txt commands = stestr run {posargs} @@ -73,6 +74,7 @@ setenv = {[testenv]setenv} QISKIT_SUPPRESS_PACKAGING_WARNINGS=Y deps = + setuptools_rust # This is work around for the bug of tox 3 (see #8606 for more details.) -r{toxinidir}/requirements-dev.txt qiskit-aer commands = From d970c46b42c0a6a7800ba2e9ae5abe701679b2ac Mon Sep 17 00:00:00 2001 From: Anthony-Gandon Date: Wed, 24 Aug 2022 22:29:33 +0200 Subject: [PATCH 67/82] Fix VQD callback not being used (#8576) * Added support for non-hermitian operators in AerPauliExpectation Fixes #415 QEOM creates a dictionary of operators to evaluate on the groundstate. When using the noisy simulators (qasm_simulator or aer_simulator), one could either use PauliExpectation (with noise) or AerPauliExpectation (without noise). PauliExpectation works with non-hermitian operators but internal methods of AerPauliExpectation raised an Error. This is a workaround to this limitation. Note that using include_custom=True on qasm allows the VQE to use a local AerPauliExpectation without using the "expectation" input. This does not apply to QEOM and one should explicitly define the "expectation" input of the VQE for it to apply globally. * Added support for non-hermitian operators in AerPauliExpectation Fixes #415 QEOM creates a dictionary of operators to evaluate on the groundstate. When using the noisy simulators (qasm_simulator or aer_simulator), one could either use PauliExpectation (with noise) or AerPauliExpectation (without noise). PauliExpectation works with non-hermitian operators but internal methods of AerPauliExpectation raised an Error. This is a workaround to this limitation. Note that using include_custom=True on qasm allows the VQE to use a local AerPauliExpectation without using the "expectation" input. This does not apply to QEOM and one should explicitly define the "expectation" input of the VQE for it to apply globally. * Add a test case for non-hermitian operators. * Add a test case for non-hermitian operators. * Add a test case for non-hermitian operators. * Update test/python/opflow/test_aer_pauli_expectation.py Co-authored-by: Julien Gacon * Update aer_pauli_expectation.py Use a generator instead of list * Update qiskit/opflow/expectations/aer_pauli_expectation.py Co-authored-by: Julien Gacon * Update releasenotes/notes/add-support-non-hermitian-op-aerpauliexpectation-653d8e16de4eca07.yaml Co-authored-by: Julien Gacon * Add a test case for PauliOp * Change the test cases from using ~StateFn() to using StateFn(, is_measurement=True) * Fix the formatting * Working point for QEOM * Small changes + Release note * Undesired change * Undesired change * Indentation error * Parenthesis * Minor changes in docstring * Minor changes in docstring Co-authored-by: Julien Gacon Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/algorithms/eigen_solvers/vqd.py | 49 ++++++++++++------- ...e-of-callback-in-vqd-99e3c85f03181298.yaml | 9 ++++ test/python/algorithms/test_vqd.py | 16 +++++- 3 files changed, 53 insertions(+), 21 deletions(-) create mode 100644 releasenotes/notes/make-use-of-callback-in-vqd-99e3c85f03181298.yaml diff --git a/qiskit/algorithms/eigen_solvers/vqd.py b/qiskit/algorithms/eigen_solvers/vqd.py index d5070fad4367..29d237820273 100644 --- a/qiskit/algorithms/eigen_solvers/vqd.py +++ b/qiskit/algorithms/eigen_solvers/vqd.py @@ -108,10 +108,11 @@ def __init__( Args: ansatz: A parameterized circuit used as ansatz for the wave function. k: the number of eigenvalues to return. Returns the lowest k eigenvalues. - betas: beta parameter in the VQD paper. Should have size k -1, the number of excited states. - It is a hyperparameter that balances the contribution of the overlap - term to the cost function and has a default value computed as - mean square sum of coefficients of observable. + betas: beta parameters in the VQD paper. + Should have length k - 1, with k the number of excited states. + These hyperparameters balance the contribution of each overlap term to the cost + function and have a default value computed as the mean square sum of the + coefficients of the observable. optimizer: A classical optimizer. Can either be a Qiskit optimizer or a callable that takes an array as input and returns a Qiskit or SciPy optimization result. initial_point: An optional initial point (i.e. initial parameter values) @@ -141,8 +142,8 @@ def __init__( callback: a callback that can access the intermediate data during the optimization. Four parameter values are passed to the callback as follows during each evaluation by the optimizer for its current set of parameters as it works towards the minimum. - These are: the evaluation count, the optimizer parameters for the - ansatz, the evaluated mean and the evaluated standard deviation.` + These are: the evaluation count, the optimizer parameters for the ansatz, the + evaluated mean, the evaluated standard deviation, and the current step. quantum_instance: Quantum Instance or Backend """ @@ -264,12 +265,12 @@ def include_custom(self, include_custom: bool): self.expectation = None @property - def callback(self) -> Optional[Callable[[int, np.ndarray, float, float], None]]: + def callback(self) -> Optional[Callable[[int, np.ndarray, float, float, int], None]]: """Returns callback""" return self._callback @callback.setter - def callback(self, callback: Optional[Callable[[int, np.ndarray, float, float], None]]): + def callback(self, callback: Optional[Callable[[int, np.ndarray, float, float, int], None]]): """Sets callback""" self._callback = callback @@ -475,7 +476,8 @@ def _eval_aux_ops( aux_op_results = zip(aux_op_means, std_devs) # Return None eigenvalues for None operators if aux_operators is a list. - # None operators are already dropped in compute_minimum_eigenvalue if aux_operators is a dict. + # None operators are already dropped in compute_minimum_eigenvalue if aux_operators is a + # dict. if isinstance(aux_operators, list): aux_operator_eigenvalues = [None] * len(aux_operators) key_value_iterator = enumerate(aux_op_results) @@ -608,7 +610,8 @@ def compute_eigenvalues( if step == 1: logger.info( - "Ground state optimization complete in %s seconds.\nFound opt_params %s in %s evals", + "Ground state optimization complete in %s seconds.\n" + "Found opt_params %s in %s evals", eval_time, result.optimal_point, self._eval_count, @@ -616,7 +619,8 @@ def compute_eigenvalues( else: logger.info( ( - "%s excited state optimization complete in %s s.\nFound opt_parms %s in %s evals" + "%s excited state optimization complete in %s s.\n" + "Found opt_params %s in %s evals" ), str(step - 1), eval_time, @@ -624,7 +628,7 @@ def compute_eigenvalues( self._eval_count, ) - # To match the siignature of NumpyEigenSolver Result + # To match the signature of NumpyEigenSolver Result result.eigenstates = ListOp([StateFn(vec) for vec in result.eigenstates]) result.eigenvalues = np.array(result.eigenvalues) result.optimal_point = np.array(result.optimal_point) @@ -649,7 +653,7 @@ def get_energy_evaluation( This return value is the objective function to be passed to the optimizer for evaluation. Args: - step: level of enegy being calculated. 0 for ground, 1 for first excited state and so on. + step: level of energy being calculated. 0 for ground, 1 for first excited state... operator: The operator whose energy to evaluate. return_expectation: If True, return the ``ExpectationBase`` expectation converter used in the construction of the expectation value. Useful e.g. to evaluate other @@ -695,22 +699,29 @@ def get_energy_evaluation( def energy_evaluation(parameters): parameter_sets = np.reshape(parameters, (-1, num_parameters)) - # Create dict associating each parameter with the lists of parameterization values for it + # Dict associating each parameter with the lists of parameterization values for it param_bindings = dict(zip(ansatz_params, parameter_sets.transpose().tolist())) sampled_expect_op = self._circuit_sampler.convert(expect_op, params=param_bindings) - mean = np.real(sampled_expect_op.eval()) + means = np.real(sampled_expect_op.eval()) for state in range(step - 1): sampled_final_op = self._circuit_sampler.convert( overlap_op[state], params=param_bindings ) cost = sampled_final_op.eval() - mean += np.real(self.betas[state] * np.conj(cost) * cost) - - self._eval_count += len(mean) + means += np.real(self.betas[state] * np.conj(cost) * cost) + + if self._callback is not None: + variance = np.real(expectation.compute_variance(sampled_expect_op)) + estimator_error = np.sqrt(variance / self.quantum_instance.run_config.shots) + for i, param_set in enumerate(parameter_sets): + self._eval_count += 1 + self._callback(self._eval_count, param_set, means[i], estimator_error[i], step) + else: + self._eval_count += len(means) - return mean if len(mean) > 1 else mean[0] + return means if len(means) > 1 else means[0] if return_expectation: return energy_evaluation, expectation diff --git a/releasenotes/notes/make-use-of-callback-in-vqd-99e3c85f03181298.yaml b/releasenotes/notes/make-use-of-callback-in-vqd-99e3c85f03181298.yaml new file mode 100644 index 000000000000..7c39e26200f5 --- /dev/null +++ b/releasenotes/notes/make-use-of-callback-in-vqd-99e3c85f03181298.yaml @@ -0,0 +1,9 @@ +features: + - | + Add calls of the callback function during the :meth:`energy_evaluation` to track the progress of + the algorithm. Also adds a ``step`` argument to the callback to track which eigenstates of the + Hamiltonian is currently being optimized. +issues: + - | + The callback function in the :class:`VQD` was defined but never used. + diff --git a/test/python/algorithms/test_vqd.py b/test/python/algorithms/test_vqd.py index 665d73d687dd..286f9422daad 100644 --- a/test/python/algorithms/test_vqd.py +++ b/test/python/algorithms/test_vqd.py @@ -257,13 +257,14 @@ def test_with_aer_qasm_snapshot_mode(self): def test_callback(self): """Test the callback on VQD.""" - history = {"eval_count": [], "parameters": [], "mean": [], "std": []} + history = {"eval_count": [], "parameters": [], "mean": [], "std": [], "step": []} - def store_intermediate_result(eval_count, parameters, mean, std): + def store_intermediate_result(eval_count, parameters, mean, std, step): history["eval_count"].append(eval_count) history["parameters"].append(parameters) history["mean"].append(mean) history["std"].append(std) + history["step"].append(step) optimizer = COBYLA(maxiter=3) wavefunction = self.ry_wavefunction @@ -279,9 +280,20 @@ def store_intermediate_result(eval_count, parameters, mean, std): self.assertTrue(all(isinstance(count, int) for count in history["eval_count"])) self.assertTrue(all(isinstance(mean, float) for mean in history["mean"])) self.assertTrue(all(isinstance(std, float) for std in history["std"])) + self.assertTrue(all(isinstance(count, int) for count in history["step"])) for params in history["parameters"]: self.assertTrue(all(isinstance(param, float) for param in params)) + ref_eval_count = [1, 2, 3, 1, 2, 3] + ref_mean = [-1.063, -1.457, -1.360, 37.340, 48.543, 28.586] + ref_std = [0.011, 0.010, 0.014, 0.011, 0.010, 0.015] + ref_step = [1, 1, 1, 2, 2, 2] + + np.testing.assert_array_almost_equal(history["eval_count"], ref_eval_count, decimal=0) + np.testing.assert_array_almost_equal(history["mean"], ref_mean, decimal=2) + np.testing.assert_array_almost_equal(history["std"], ref_std, decimal=2) + np.testing.assert_array_almost_equal(history["step"], ref_step, decimal=0) + def test_reuse(self): """Test re-using a VQD algorithm instance.""" vqd = VQD(k=1) From c008008be283253ea9d0c8c7fb75bdab51e73c94 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Thu, 25 Aug 2022 16:54:48 +0200 Subject: [PATCH 68/82] Fix decomposition for a single qubit and clbit (#8614) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/transpiler/passes/basis/decompose.py | 6 +++++- .../notes/fix-decomp-1q-1c-84f369f9a897a5b7.yaml | 13 +++++++++++++ test/python/transpiler/test_decompose.py | 15 +++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/fix-decomp-1q-1c-84f369f9a897a5b7.yaml diff --git a/qiskit/transpiler/passes/basis/decompose.py b/qiskit/transpiler/passes/basis/decompose.py index 408a95271917..062e97558cfb 100644 --- a/qiskit/transpiler/passes/basis/decompose.py +++ b/qiskit/transpiler/passes/basis/decompose.py @@ -91,7 +91,11 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: continue # TODO: allow choosing among multiple decomposition rules rule = node.op.definition.data - if len(rule) == 1 and len(node.qargs) == len(rule[0].qubits) == 1: + if ( + len(rule) == 1 + and len(node.qargs) == len(rule[0].qubits) == 1 # to preserve gate order + and len(node.cargs) == len(rule[0].clbits) == 0 + ): if node.op.definition.global_phase: dag.global_phase += node.op.definition.global_phase dag.substitute_node(node, rule[0].operation, inplace=True) diff --git a/releasenotes/notes/fix-decomp-1q-1c-84f369f9a897a5b7.yaml b/releasenotes/notes/fix-decomp-1q-1c-84f369f9a897a5b7.yaml new file mode 100644 index 000000000000..74d8d1faa83b --- /dev/null +++ b/releasenotes/notes/fix-decomp-1q-1c-84f369f9a897a5b7.yaml @@ -0,0 +1,13 @@ +--- +fixes: + - | + Fixed a bug where decomposing an instruction with one qubit and one classical bit + containing a single quantum gate failed. Now the following decomposes as expected:: + + block = QuantumCircuit(1, 1) + block.h(0) + + circuit = QuantumCircuit(1, 1) + circuit.append(block, [0], [0]) + + decomposed = circuit.decompose() diff --git a/test/python/transpiler/test_decompose.py b/test/python/transpiler/test_decompose.py index 1a0aaf4a4b68..2664a00c7fbb 100644 --- a/test/python/transpiler/test_decompose.py +++ b/test/python/transpiler/test_decompose.py @@ -300,3 +300,18 @@ def test_decompose_reps(self): decom_circ = self.complex_circuit.decompose(reps=2) decomposed = self.complex_circuit.decompose().decompose() self.assertEqual(decom_circ, decomposed) + + def test_decompose_single_qubit_clbit(self): + """Test the decomposition of a block with a single qubit and clbit works. + + Regression test of Qiskit/qiskit-terra#8591. + """ + block = QuantumCircuit(1, 1) + block.h(0) + + circuit = QuantumCircuit(1, 1) + circuit.append(block, [0], [0]) + + decomposed = circuit.decompose() + + self.assertEqual(decomposed, block) From 9f5f8fba5145cdfb521a0ed650e2273b8830b158 Mon Sep 17 00:00:00 2001 From: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> Date: Fri, 26 Aug 2022 12:11:26 +0900 Subject: [PATCH 69/82] Improve performance of `Estimator` and `Sampler` by avoiding deep copy at `circuit_to_instruction`. (#8403) * add simplified circuit_to_instruction * add a safe guard * optimize sampler as well as estimator Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/primitives/estimator.py | 4 ++-- qiskit/primitives/sampler.py | 4 ++-- qiskit/primitives/utils.py | 34 +++++++++++++++++++++++++++++++++- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/qiskit/primitives/estimator.py b/qiskit/primitives/estimator.py index b2e1b5215ba1..0715abfcd859 100644 --- a/qiskit/primitives/estimator.py +++ b/qiskit/primitives/estimator.py @@ -30,7 +30,7 @@ from .base_estimator import BaseEstimator from .estimator_result import EstimatorResult from .primitive_job import PrimitiveJob -from .utils import init_circuit, init_observable +from .utils import bound_circuit_to_instruction, init_circuit, init_observable class Estimator(BaseEstimator): @@ -114,7 +114,7 @@ def _call( f"The number of qubits of a circuit ({circ.num_qubits}) does not match " f"the number of qubits of a observable ({obs.num_qubits})." ) - final_state = Statevector(circ) + final_state = Statevector(bound_circuit_to_instruction(circ)) expectation_value = final_state.expectation_value(obs) if shots is None: expectation_values.append(expectation_value) diff --git a/qiskit/primitives/sampler.py b/qiskit/primitives/sampler.py index 5626ec5416ba..f9c1c2910ecb 100644 --- a/qiskit/primitives/sampler.py +++ b/qiskit/primitives/sampler.py @@ -29,7 +29,7 @@ from .base_sampler import BaseSampler from .primitive_job import PrimitiveJob from .sampler_result import SamplerResult -from .utils import final_measurement_mapping, init_circuit +from .utils import bound_circuit_to_instruction, final_measurement_mapping, init_circuit class Sampler(BaseSampler): @@ -113,7 +113,7 @@ def _call( ) qargs_list.append(self._qargs_list[i]) probabilities = [ - Statevector(circ).probabilities(qargs=qargs) + Statevector(bound_circuit_to_instruction(circ)).probabilities(qargs=qargs) for circ, qargs in zip(bound_circuits, qargs_list) ] if shots is not None: diff --git a/qiskit/primitives/utils.py b/qiskit/primitives/utils.py index 1e2c16ccad3c..36fbae8f15f9 100644 --- a/qiskit/primitives/utils.py +++ b/qiskit/primitives/utils.py @@ -15,7 +15,7 @@ from __future__ import annotations -from qiskit.circuit import ParameterExpression, QuantumCircuit +from qiskit.circuit import ParameterExpression, QuantumCircuit, Instruction from qiskit.extensions.quantum_initializer.initializer import Initialize from qiskit.opflow import PauliSumOp from qiskit.quantum_info import SparsePauliOp, Statevector @@ -111,3 +111,35 @@ def final_measurement_mapping(circuit: QuantumCircuit) -> dict[int, int]: # Sort so that classical bits are in numeric order low->high. mapping = dict(sorted(mapping.items(), key=lambda item: item[1])) return mapping + + +def bound_circuit_to_instruction(circuit: QuantumCircuit) -> Instruction: + """Build an :class:`~qiskit.circuit.Instruction` object from + a :class:`~qiskit.circuit.QuantumCircuit` + + This is a specialized version of :func:`~qiskit.converters.circuit_to_instruction` + to avoid deep copy. This requires a quantum circuit whose parameters are all bound. + Because this does not take a copy of the input circuit, this assumes that the input + circuit won't be modified. + + If https://github.com/Qiskit/qiskit-terra/issues/7983 is resolved, + we can remove this function. + + Args: + circuit(QuantumCircuit): Input quantum circuit + + Returns: + An :class:`~qiskit.circuit.Instruction` object + """ + if len(circuit.qregs) > 1: + return circuit.to_instruction() + + # len(circuit.qregs) == 1 -> No need to flatten qregs + inst = Instruction( + name=circuit.name, + num_qubits=circuit.num_qubits, + num_clbits=circuit.num_clbits, + params=[], + ) + inst.definition = circuit + return inst From a511d99c1f55856d3ce7c84be7457db296af79f7 Mon Sep 17 00:00:00 2001 From: Emilio <63567458+epelaaez@users.noreply.github.com> Date: Mon, 29 Aug 2022 02:55:24 -0500 Subject: [PATCH 70/82] Add CS and CCZ gates (#8583) * add cs and ccz gates * add csdg gate * add gates to equivalence library * add release note * fix equivalences * commit review suggestions * simplify definitions and add ctrl_state to inverse * removed unused import --- .../library/standard_gates/__init__.py | 7 +- .../standard_gates/equivalence_library.py | 48 ++++++ qiskit/circuit/library/standard_gates/s.py | 157 +++++++++++++++++- qiskit/circuit/library/standard_gates/z.py | 79 ++++++++- qiskit/circuit/quantumcircuit.py | 95 +++++++++++ ...cz-cs-and-csdg-gates-4ad05e323f1dec4d.yaml | 5 + 6 files changed, 386 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/add-ccz-cs-and-csdg-gates-4ad05e323f1dec4d.yaml diff --git a/qiskit/circuit/library/standard_gates/__init__.py b/qiskit/circuit/library/standard_gates/__init__.py index c3d464d66879..d1345e13d8f2 100644 --- a/qiskit/circuit/library/standard_gates/__init__.py +++ b/qiskit/circuit/library/standard_gates/__init__.py @@ -36,6 +36,7 @@ CXGate CYGate CZGate + CCZGate HGate IGate MCPhaseGate @@ -54,6 +55,8 @@ ECRGate SGate SdgGate + CSGate + CSdgGate SwapGate iSwapGate SXGate @@ -84,7 +87,7 @@ from .xx_minus_yy import XXMinusYYGate from .xx_plus_yy import XXPlusYYGate from .ecr import ECRGate -from .s import SGate, SdgGate +from .s import SGate, SdgGate, CSGate, CSdgGate from .swap import SwapGate, CSwapGate from .iswap import iSwapGate from .sx import SXGate, SXdgGate, CSXGate @@ -97,6 +100,6 @@ from .x import XGate, CXGate, CCXGate, C3XGate, C3SXGate, C4XGate, RCCXGate, RC3XGate from .x import MCXGate, MCXGrayCode, MCXRecursive, MCXVChain from .y import YGate, CYGate -from .z import ZGate, CZGate +from .z import ZGate, CZGate, CCZGate from .multi_control_rotation_gates import mcrx, mcry, mcrz diff --git a/qiskit/circuit/library/standard_gates/equivalence_library.py b/qiskit/circuit/library/standard_gates/equivalence_library.py index 5ded739eb51f..f847d9a62eed 100644 --- a/qiskit/circuit/library/standard_gates/equivalence_library.py +++ b/qiskit/circuit/library/standard_gates/equivalence_library.py @@ -36,6 +36,8 @@ RZXGate, SGate, SdgGate, + CSGate, + CSdgGate, SwapGate, CSwapGate, iSwapGate, @@ -61,6 +63,7 @@ ECRGate, ZGate, CZGate, + CCZGate, ) @@ -496,6 +499,33 @@ def_sdg.append(U1Gate(-pi / 2), [q[0]], []) _sel.add_equivalence(SdgGate(), def_sdg) +# CSGate +# +# q_0: ──■── q_0: ───────■──────── +# ┌─┴─┐ ┌───┐┌─┴──┐┌───┐ +# q_1: ┤ S ├ = q_1: ┤ H ├┤ Sx ├┤ H ├ +# └───┘ └───┘└────┘└───┘ +q = QuantumRegister(2, "q") +def_cs = QuantumCircuit(q) +def_cs.append(HGate(), [q[1]], []) +def_cs.append(CSXGate(), [q[0], q[1]], []) +def_cs.append(HGate(), [q[1]], []) +_sel.add_equivalence(CSGate(), def_cs) + +# CSdgGate +# +# q_0: ───■─── q_0: ───────■────■──────── +# ┌──┴──┐ ┌───┐┌─┴─┐┌─┴──┐┌───┐ +# q_1: ┤ Sdg ├ = q_1: ┤ H ├┤ X ├┤ Sx ├┤ H ├ +# └─────┘ └───┘└───┘└────┘└───┘ +q = QuantumRegister(2, "q") +def_csdg = QuantumCircuit(q) +def_csdg.append(HGate(), [q[1]], []) +def_csdg.append(CXGate(), [q[0], q[1]], []) +def_csdg.append(CSXGate(), [q[0], q[1]], []) +def_csdg.append(HGate(), [q[1]], []) +_sel.add_equivalence(CSdgGate(), def_csdg) + # SdgGate # # ┌─────┐ ┌───┐┌───┐ @@ -1222,6 +1252,24 @@ def_cz.append(inst, qargs, cargs) _sel.add_equivalence(CZGate(), def_cz) +# CCZGate +# +# q_0: ─■─ q_0: ───────■─────── +# │ │ +# q_1: ─■─ = q_1: ───────■─────── +# │ ┌───┐┌─┴─┐┌───┐ +# q_2: ─■─ q_2: ┤ H ├┤ X ├┤ H ├ +# └───┘└───┘└───┘ +q = QuantumRegister(3, "q") +def_ccz = QuantumCircuit(q) +for inst, qargs, cargs in [ + (HGate(), [q[2]], []), + (CCXGate(), [q[0], q[1], q[2]], []), + (HGate(), [q[2]], []), +]: + def_ccz.append(inst, qargs, cargs) +_sel.add_equivalence(CCZGate(), def_ccz) + # XGate # global phase: π/2 # ┌───┐ ┌───────┐ diff --git a/qiskit/circuit/library/standard_gates/s.py b/qiskit/circuit/library/standard_gates/s.py index a9c2d96555a4..5bda08fa2a69 100644 --- a/qiskit/circuit/library/standard_gates/s.py +++ b/qiskit/circuit/library/standard_gates/s.py @@ -10,11 +10,12 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""The S and Sdg gate.""" +"""The S, Sdg, CS and CSdg gates.""" -from typing import Optional +from typing import Optional, Union import numpy from qiskit.qasm import pi +from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister @@ -135,3 +136,155 @@ def inverse(self): def __array__(self, dtype=None): """Return a numpy.array for the Sdg gate.""" return numpy.array([[1, 0], [0, -1j]], dtype=dtype) + + +class CSGate(ControlledGate): + r"""Controlled-S gate. + + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.cs` method. + + **Circuit symbol:** + + .. parsed-literal:: + + q_0: ──■── + ┌─┴─┐ + q_1: ┤ S ├ + └───┘ + + **Matrix representation:** + + .. math:: + + CS \ q_0, q_1 = + I \otimes |0 \rangle\langle 0| + S \otimes |1 \rangle\langle 1| = + \begin{pmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 1 & 0 \\ + 0 & 0 & 0 & i + \end{pmatrix} + """ + # Define class constants. This saves future allocation time. + _matrix1 = numpy.array( + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1j], + ] + ) + _matrix0 = numpy.array( + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1j, 0], + [0, 0, 0, 1], + ] + ) + + def __init__(self, label: Optional[str] = None, ctrl_state: Optional[Union[str, int]] = None): + """Create new CS gate.""" + super().__init__( + "cs", 2, [], label=label, num_ctrl_qubits=1, ctrl_state=ctrl_state, base_gate=SGate() + ) + + def _define(self): + """ + gate cs a,b { h b; cp(pi/2) a,b; h b; } + """ + # pylint: disable=cyclic-import + from .p import CPhaseGate + + self.definition = CPhaseGate(theta=pi / 2).definition + + def inverse(self): + """Return inverse of CSGate (CSdgGate).""" + return CSdgGate(ctrl_state=self.ctrl_state) + + def __array__(self, dtype=None): + """Return a numpy.array for the CS gate.""" + mat = self._matrix1 if self.ctrl_state == 1 else self._matrix0 + if dtype is not None: + return numpy.asarray(mat, dtype=dtype) + return mat + + +class CSdgGate(ControlledGate): + r"""Controlled-S^\dagger gate. + + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.csdg` method. + + **Circuit symbol:** + + .. parsed-literal:: + + q_0: ───■─── + ┌──┴──┐ + q_1: ┤ Sdg ├ + └─────┘ + + **Matrix representation:** + + .. math:: + + CS^\dagger \ q_0, q_1 = + I \otimes |0 \rangle\langle 0| + S^\dagger \otimes |1 \rangle\langle 1| = + \begin{pmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 1 & 0 \\ + 0 & 0 & 0 & -i + \end{pmatrix} + """ + # Define class constants. This saves future allocation time. + _matrix1 = numpy.array( + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, -1j], + ] + ) + _matrix0 = numpy.array( + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, -1j, 0], + [0, 0, 0, 1], + ] + ) + + def __init__(self, label: Optional[str] = None, ctrl_state: Optional[Union[str, int]] = None): + """Create new CSdg gate.""" + super().__init__( + "csdg", + 2, + [], + label=label, + num_ctrl_qubits=1, + ctrl_state=ctrl_state, + base_gate=SdgGate(), + ) + + def _define(self): + """ + gate csdg a,b { h b; cp(-pi/2) a,b; h b; } + """ + # pylint: disable=cyclic-import + from .p import CPhaseGate + + self.definition = CPhaseGate(theta=-pi / 2).definition + + def inverse(self): + """Return inverse of CSdgGate (CSGate).""" + return CSGate(ctrl_state=self.ctrl_state) + + def __array__(self, dtype=None): + """Return a numpy.array for the CSdg gate.""" + mat = self._matrix1 if self.ctrl_state == 1 else self._matrix0 + if dtype is not None: + return numpy.asarray(mat, dtype=dtype) + return mat diff --git a/qiskit/circuit/library/standard_gates/z.py b/qiskit/circuit/library/standard_gates/z.py index df57352e869a..9fad215878f9 100644 --- a/qiskit/circuit/library/standard_gates/z.py +++ b/qiskit/circuit/library/standard_gates/z.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Z and CZ gates.""" +"""Z, CZ and CCZ gates.""" from typing import Optional, Union import numpy @@ -18,6 +18,7 @@ from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister +from qiskit.circuit._utils import _compute_control_matrix class ZGate(Gate): @@ -187,3 +188,79 @@ def __array__(self, dtype=None): return numpy.array( [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]], dtype=dtype ) + + +class CCZGate(ControlledGate): + r"""CCZ gate. + + This is a symmetric gate. + + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.ccz` method. + + **Circuit symbol:** + + .. parsed-literal:: + + q_0: ─■─ + │ + q_1: ─■─ + │ + q_2: ─■─ + + **Matrix representation:** + + .. math:: + + CCZ\ q_0, q_1, q_2 = + I \otimes I \otimes |0\rangle\langle 0| + CZ \otimes |1\rangle\langle 1| = + \begin{pmatrix} + 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\ + 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ + 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\ + 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 \\ + 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\ + 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 \\ + 0 & 0 & 0 & 0 & 0 & 0 & 0 & -1 + \end{pmatrix} + + In the computational basis, this gate flips the phase of + the target qubit if the control qubits are in the :math:`|11\rangle` state. + """ + + def __init__(self, label: Optional[str] = None, ctrl_state: Optional[Union[str, int]] = None): + """Create new CCZ gate.""" + super().__init__( + "ccz", 3, [], label=label, num_ctrl_qubits=2, ctrl_state=ctrl_state, base_gate=ZGate() + ) + + def _define(self): + """ + gate ccz a,b,c { h c; ccx a,b,c; h c; } + """ + # pylint: disable=cyclic-import + from qiskit.circuit.quantumcircuit import QuantumCircuit + from .h import HGate + from .x import CCXGate + + q = QuantumRegister(3, "q") + qc = QuantumCircuit(q, name=self.name) + rules = [(HGate(), [q[2]], []), (CCXGate(), [q[0], q[1], q[2]], []), (HGate(), [q[2]], [])] + for instr, qargs, cargs in rules: + qc._append(instr, qargs, cargs) + + self.definition = qc + + def inverse(self): + """Return inverted CCZ gate (itself).""" + return CCZGate(ctrl_state=self.ctrl_state) # self-inverse + + def __array__(self, dtype=None): + """Return a numpy.array for the CCZ gate.""" + mat = _compute_control_matrix( + self.base_gate.to_matrix(), self.num_ctrl_qubits, ctrl_state=self.ctrl_state + ) + if dtype is not None: + return numpy.asarray(mat, dtype=dtype) + return mat diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 60ba8b80c6f3..a337102ea644 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1698,6 +1698,7 @@ def qasm( "sx", "sxdg", "cz", + "ccz", "cy", "swap", "ch", @@ -1710,6 +1711,8 @@ def qasm( "cp", "cu3", "csx", + "cs", + "csdg", "cu", "rxx", "rzz", @@ -3460,6 +3463,66 @@ def sdg(self, qubit: QubitSpecifier) -> InstructionSet: return self.append(SdgGate(), [qubit], []) + def cs( + self, + control_qubit: QubitSpecifier, + target_qubit: QubitSpecifier, + label: Optional[str] = None, + ctrl_state: Optional[Union[str, int]] = None, + ) -> InstructionSet: + """Apply :class:`~qiskit.circuit.library.CSGate`. + + For the full matrix form of this gate, see the underlying gate documentation. + + Args: + control_qubit: The qubit(s) used as the control. + target_qubit: The qubit(s) targeted by the gate. + label: The string label of the gate in the circuit. + ctrl_state: + The control state in decimal, or as a bitstring (e.g. '1'). Defaults to controlling + on the '1' state. + + Returns: + A handle to the instructions created. + """ + from .library.standard_gates.s import CSGate + + return self.append( + CSGate(label=label, ctrl_state=ctrl_state), + [control_qubit, target_qubit], + [], + ) + + def csdg( + self, + control_qubit: QubitSpecifier, + target_qubit: QubitSpecifier, + label: Optional[str] = None, + ctrl_state: Optional[Union[str, int]] = None, + ) -> InstructionSet: + """Apply :class:`~qiskit.circuit.library.CSdgGate`. + + For the full matrix form of this gate, see the underlying gate documentation. + + Args: + control_qubit: The qubit(s) used as the control. + target_qubit: The qubit(s) targeted by the gate. + label: The string label of the gate in the circuit. + ctrl_state: + The control state in decimal, or as a bitstring (e.g. '1'). Defaults to controlling + on the '1' state. + + Returns: + A handle to the instructions created. + """ + from .library.standard_gates.s import CSdgGate + + return self.append( + CSdgGate(label=label, ctrl_state=ctrl_state), + [control_qubit, target_qubit], + [], + ) + def swap(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.SwapGate`. @@ -4230,6 +4293,38 @@ def cz( CZGate(label=label, ctrl_state=ctrl_state), [control_qubit, target_qubit], [] ) + def ccz( + self, + control_qubit1: QubitSpecifier, + control_qubit2: QubitSpecifier, + target_qubit: QubitSpecifier, + label: Optional[str] = None, + ctrl_state: Optional[Union[str, int]] = None, + ) -> InstructionSet: + r"""Apply :class:`~qiskit.circuit.library.CCZGate`. + + For the full matrix form of this gate, see the underlying gate documentation. + + Args: + control_qubit1: The qubit(s) used as the first control. + control_qubit2: The qubit(s) used as the second control. + target_qubit: The qubit(s) targeted by the gate. + label: The string label of the gate in the circuit. + ctrl_state: + The control state in decimal, or as a bitstring (e.g. '10'). Defaults to controlling + on the '11' state. + + Returns: + A handle to the instructions created. + """ + from .library.standard_gates.z import CCZGate + + return self.append( + CCZGate(label=label, ctrl_state=ctrl_state), + [control_qubit1, control_qubit2, target_qubit], + [], + ) + def pauli( self, pauli_string: str, diff --git a/releasenotes/notes/add-ccz-cs-and-csdg-gates-4ad05e323f1dec4d.yaml b/releasenotes/notes/add-ccz-cs-and-csdg-gates-4ad05e323f1dec4d.yaml new file mode 100644 index 000000000000..c15c8f52ed5d --- /dev/null +++ b/releasenotes/notes/add-ccz-cs-and-csdg-gates-4ad05e323f1dec4d.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add new gates :class:`.CZZGate`, :class:`.CSGate`, and :class:`.CSdgGate`. + Added their equivalences into the standard :class:`EquivalenceLibrary`. From f2bf19173c65abe7e79f1d99a8735ea2af7bbe00 Mon Sep 17 00:00:00 2001 From: Ikko Hamamura Date: Mon, 29 Aug 2022 18:42:59 +0900 Subject: [PATCH 71/82] Support deprecate_arguments with new_arg None (#8613) * Support deprecate_arguments with None new_arg * Update qiskit/utils/deprecation.py Co-authored-by: Julien Gacon * fix lint * removed Co-authored-by: Julien Gacon Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/utils/deprecation.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/qiskit/utils/deprecation.py b/qiskit/utils/deprecation.py index 1a73803ab4f3..ccfa93615287 100644 --- a/qiskit/utils/deprecation.py +++ b/qiskit/utils/deprecation.py @@ -59,11 +59,19 @@ def _rename_kwargs(func_name, kwargs, kwarg_map): if new_arg in kwargs: raise TypeError(f"{func_name} received both {new_arg} and {old_arg} (deprecated).") - warnings.warn( - "{} keyword argument {} is deprecated and " - "replaced with {}.".format(func_name, old_arg, new_arg), - DeprecationWarning, - stacklevel=3, - ) - - kwargs[new_arg] = kwargs.pop(old_arg) + if new_arg is None: + warnings.warn( + f"{func_name} keyword argument {old_arg} is deprecated and " + "will in future be removed.", + DeprecationWarning, + stacklevel=3, + ) + else: + warnings.warn( + f"{func_name} keyword argument {old_arg} is deprecated and " + f"replaced with {new_arg}.", + DeprecationWarning, + stacklevel=3, + ) + + kwargs[new_arg] = kwargs.pop(old_arg) From 231961d6cfc3d944a8935c755f1d75ce68a311d1 Mon Sep 17 00:00:00 2001 From: Marc Sanz Drudis <93275620+MarcDrudis@users.noreply.github.com> Date: Mon, 29 Aug 2022 16:18:20 +0200 Subject: [PATCH 72/82] Steppable Optimizer (#8170) * All Changes so far * Revert "All Changes so far" This reverts commit 871f29ef540d3d5f93e3ffb5bee17295b3d4d65c. * All changes again * tests * Formated documentation Gradient Descent * Reviewed Documentation * Dealing with CALLBACK * Fixing documentation * Documentation * Apply suggestions from code review Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> * GD docs * Fixed format of documentation * Typo * Imported files CMAES * Testing * Forgot to add nit to result * Started stopping conditions * Fixed comments * Changes * merge and black * Fixed documentation * IDK why docs is missing optimize * make black * Updated Documentation * Updated Documentation * Remove CMAES and merge * Removed CMAES * Learning Rate Class * Lint * RENO and unittests * Removed attribute documentation * Fixed variable name * Added Learning Rate * Fixed unittestt * Fixes * Apply suggestions from code review Co-authored-by: Julien Gacon * Fixing utils? * lint * Fixed Reno? * Update releasenotes/notes/steppable-optimizers-9d9b48ba78bd58bb.yaml Co-authored-by: Julien Gacon * Apply suggestions from code review Co-authored-by: Julien Gacon * Fixed documentation typo * Removed properties * Apply suggestions from code review Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> * Fixed comments * Learning Rate init * LearningRate init * Update qiskit/algorithms/optimizers/utils/learning_rate.py Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> * changes * changes * I can't fix the documentation * Accidentaly merged with an other branch * remains Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> Co-authored-by: Julien Gacon Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/algorithms/__init__.py | 1 + qiskit/algorithms/optimizers/__init__.py | 30 +- .../algorithms/optimizers/gradient_descent.py | 346 +++++++++++++----- .../optimizers/optimizer_utils/__init__.py | 27 ++ .../optimizer_utils/learning_rate.py | 83 +++++ .../optimizers/steppable_optimizer.py | 302 +++++++++++++++ ...steppable-optimizers-9d9b48ba78bd58bb.yaml | 96 +++++ .../optimizers/test_gradient_descent.py | 125 +++++-- .../algorithms/optimizers/utils/__init__.py | 12 + .../optimizers/utils/test_learning_rate.py | 54 +++ 10 files changed, 967 insertions(+), 109 deletions(-) create mode 100644 qiskit/algorithms/optimizers/optimizer_utils/__init__.py create mode 100644 qiskit/algorithms/optimizers/optimizer_utils/learning_rate.py create mode 100644 qiskit/algorithms/optimizers/steppable_optimizer.py create mode 100644 releasenotes/notes/steppable-optimizers-9d9b48ba78bd58bb.yaml create mode 100644 test/python/algorithms/optimizers/utils/__init__.py create mode 100644 test/python/algorithms/optimizers/utils/test_learning_rate.py diff --git a/qiskit/algorithms/__init__.py b/qiskit/algorithms/__init__.py index 30d71a59e59c..f7929e337c4d 100644 --- a/qiskit/algorithms/__init__.py +++ b/qiskit/algorithms/__init__.py @@ -264,6 +264,7 @@ from .evolvers.trotterization import TrotterQRTE from .evolvers.variational.var_qite import VarQITE from .evolvers.variational.var_qrte import VarQRTE + from .evolvers.pvqd import PVQD, PVQDResult __all__ = [ diff --git a/qiskit/algorithms/optimizers/__init__.py b/qiskit/algorithms/optimizers/__init__.py index c0f30d86dd32..c1e6ef58bbd5 100644 --- a/qiskit/algorithms/optimizers/__init__.py +++ b/qiskit/algorithms/optimizers/__init__.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2020. +# (C) Copyright IBM 2018, 2022. # # 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 @@ -39,6 +39,25 @@ Optimizer Minimizer +Steppable Optimizer Base Class +============================== + +.. autosummary:: + :toctree: ../stubs/ + + optimizer_utils + +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + SteppableOptimizer + AskData + TellData + OptimizerState + + + Local Optimizers ================ @@ -53,6 +72,7 @@ L_BFGS_B GSLS GradientDescent + GradientDescentState NELDER_MEAD NFT P_BFGS @@ -110,7 +130,7 @@ from .cg import CG from .cobyla import COBYLA from .gsls import GSLS -from .gradient_descent import GradientDescent +from .gradient_descent import GradientDescent, GradientDescentState from .imfil import IMFIL from .l_bfgs_b import L_BFGS_B from .nelder_mead import NELDER_MEAD @@ -120,6 +140,7 @@ from .nlopts.direct_l_rand import DIRECT_L_RAND from .nlopts.esch import ESCH from .nlopts.isres import ISRES +from .steppable_optimizer import SteppableOptimizer, AskData, TellData, OptimizerState from .optimizer import Minimizer, Optimizer, OptimizerResult, OptimizerSupportLevel from .p_bfgs import P_BFGS from .powell import POWELL @@ -134,6 +155,10 @@ __all__ = [ "Optimizer", "OptimizerSupportLevel", + "SteppableOptimizer", + "AskData", + "TellData", + "OptimizerState", "OptimizerResult", "Minimizer", "ADAM", @@ -142,6 +167,7 @@ "COBYLA", "GSLS", "GradientDescent", + "GradientDescentState", "L_BFGS_B", "NELDER_MEAD", "NFT", diff --git a/qiskit/algorithms/optimizers/gradient_descent.py b/qiskit/algorithms/optimizers/gradient_descent.py index 38ed55048623..a354aa383a2b 100644 --- a/qiskit/algorithms/optimizers/gradient_descent.py +++ b/qiskit/algorithms/optimizers/gradient_descent.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -12,17 +12,36 @@ """A standard gradient descent optimizer.""" -from typing import Iterator, Optional, Union, Callable, Dict, Any, List, Tuple -from functools import partial - +from dataclasses import dataclass, field +from typing import Dict, Any, Union, Callable, Optional, Tuple, List, Iterator import numpy as np - from .optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT +from .steppable_optimizer import AskData, TellData, OptimizerState, SteppableOptimizer +from .optimizer_utils import LearningRate CALLBACK = Callable[[int, np.ndarray, float, float], None] -class GradientDescent(Optimizer): +@dataclass +class GradientDescentState(OptimizerState): + """State of :class:`~.GradientDescent`. + + Dataclass with all the information of an optimizer plus the learning_rate and the stepsize. + """ + + stepsize: Optional[float] + """Norm of the gradient on the last step.""" + + learning_rate: LearningRate = field(compare=False) + """Learning rate at the current step of the optimization process. + + It behaves like a generator, (use ``next(learning_rate)`` to get the learning rate for the + next step) but it can also return the current learning rate with ``learning_rate.current``. + + """ + + +class GradientDescent(SteppableOptimizer): r"""The gradient descent minimization routine. For a function :math:`f` and an initial point :math:`\vec\theta_0`, the standard (or "vanilla") @@ -31,14 +50,14 @@ class GradientDescent(Optimizer): .. math:: - \vec\theta_{n+1} = \vec\theta_{n} - \vec\eta\nabla f(\vec\theta_{n}), + \vec\theta_{n+1} = \vec\theta_{n} - \eta_n \vec\nabla f(\vec\theta_{n}), - for a small learning rate :math:`\eta > 0`. + for a small learning rate :math:`\eta_n > 0`. - You can either provide the analytic gradient :math:`\vec\nabla f` as ``gradient_function`` - in the ``optimize`` method, or, if you do not provide it, use a finite difference approximation - of the gradient. To adapt the size of the perturbation in the finite difference gradients, - set the ``perturbation`` property in the initializer. + You can either provide the analytic gradient :math:`\vec\nabla f` as ``jac`` + in the :meth:`~.minimize` method, or, if you do not provide it, use a finite difference + approximation of the gradient. To adapt the size of the perturbation in the finite difference + gradients, set the ``perturbation`` property in the initializer. This optimizer supports a callback function. If provided in the initializer, the optimizer will call the callback in each iteration with the following information in this order: @@ -60,14 +79,14 @@ def f(x): initial_point = np.array([1, 0.5, -0.2]) optimizer = GradientDescent(maxiter=100) - x_opt, fx_opt, nfevs = optimizer.optimize(initial_point.size, - f, - initial_point=initial_point) - print(f"Found minimum {x_opt} at a value of {fx_opt} using {nfevs} evaluations.") + result = optimizer.minimize(fun=fun, x0=initial_point) + + print(f"Found minimum {result.x} at a value" + "of {result.fun} using {result.nfev} evaluations.") An example where the learning rate is an iterator and we supply the analytic gradient. - Note how much faster this convergences (i.e. less ``nfevs``) compared to the previous + Note how much faster this convergences (i.e. less ``nfev``) compared to the previous example. .. code-block:: python @@ -77,7 +96,6 @@ def f(x): def learning_rate(): power = 0.6 constant_coeff = 0.1 - def powerlaw(): n = 0 while True: @@ -95,42 +113,151 @@ def grad_f(x): initial_point = np.array([1, 0.5, -0.2]) optimizer = GradientDescent(maxiter=100, learning_rate=learning_rate) - x_opt, fx_opt, nfevs = optimizer.optimize(initial_point.size, - f, - gradient_function=grad_f, - initial_point=initial_point) + result = optimizer.minimize(fun=fun, jac=grad_f, x0=initial_point) + + print(f"Found minimum {result.x} at a value" + "of {result.fun} using {result.nfev} evaluations.") + + + An other example where the evaluation of the function has a chance of failing. The user, with + specific knowledge about his function can catch this errors and handle them before passing the + result to the optimizer. + + .. code-block:: python + + import random + import numpy as np + from qiskit.algorithms.optimizers import GradientDescent + + def objective(x): + if random.choice([True, False]): + return None + else: + return (np.linalg.norm(x) - 1) ** 2 + + def grad(x): + if random.choice([True, False]): + return None + else: + return 2 * (np.linalg.norm(x) - 1) * x / np.linalg.norm(x) + - print(f"Found minimum {x_opt} at a value of {fx_opt} using {nfevs} evaluations.") + initial_point = np.random.normal(0, 1, size=(100,)) + + optimizer = GradientDescent(maxiter=20) + optimizer.start(x0=initial_point, fun=objective, jac=grad) + + while optimizer.continue_condition(): + ask_data = optimizer.ask() + evaluated_gradient = None + + while evaluated_gradient is None: + evaluated_gradient = grad(ask_data.x_center) + optimizer.state.njev += 1 + + optmizer.state.nit += 1 + + tell_data = TellData(eval_jac=evaluated_gradient) + optimizer.tell(ask_data=ask_data, tell_data=tell_data) + + result = optimizer.create_result() + + Users that aren't dealing with complicated functions and who are more familiar with step by step + optimization algorithms can use the :meth:`~.step` method which wraps the :meth:`~.ask` + and :meth:`~.tell` methods. In the same spirit the method :meth:`~.minimize` will optimize the + function and return the result. + + To see other libraries that use this interface one can visit: + https://optuna.readthedocs.io/en/stable/tutorial/20_recipes/009_ask_and_tell.html """ def __init__( self, maxiter: int = 100, - learning_rate: Union[float, Callable[[], Iterator]] = 0.01, + learning_rate: Union[float, List[float], np.ndarray, Callable[[], Iterator]] = 0.01, tol: float = 1e-7, callback: Optional[CALLBACK] = None, perturbation: Optional[float] = None, ) -> None: - r""" + """ Args: maxiter: The maximum number of iterations. - learning_rate: A constant or generator yielding learning rates for the parameter - updates. See the docstring for an example. + learning_rate: A constant, list, array or factory of generators yielding learning rates + for the parameter updates. See the docstring for an example. tol: If the norm of the parameter update is smaller than this threshold, the - optimizer is converged. - perturbation: If no gradient is passed to ``GradientDescent.optimize`` the gradient is - approximated with a symmetric finite difference scheme with ``perturbation`` + optimizer has converged. + perturbation: If no gradient is passed to :meth:`~.minimize` the gradient is + approximated with a forward finite difference scheme with ``perturbation`` perturbation in both directions (defaults to 1e-2 if required). - Ignored if a gradient callable is passed to ``GradientDescent.optimize``. + Ignored when we have an explicit function for the gradient. + Raises: + ValueError: If ``learning_rate`` is an array and its lenght is less than ``maxiter``. """ - super().__init__() - - self.maxiter = maxiter - self.learning_rate = learning_rate - self.perturbation = perturbation - self.tol = tol + super().__init__(maxiter=maxiter) self.callback = callback + self._state: Optional[GradientDescentState] = None + self._perturbation = perturbation + self._tol = tol + # if learning rate is an array, check it is sufficiently long. + if isinstance(learning_rate, (list, np.ndarray)): + if len(learning_rate) < maxiter: + raise ValueError( + f"Length of learning_rate ({len(learning_rate)}) " + f"is smaller than maxiter ({maxiter})." + ) + self.learning_rate = learning_rate + + @property + def state(self) -> GradientDescentState: + """Return the current state of the optimizer.""" + return self._state + + @state.setter + def state(self, state: GradientDescentState) -> None: + """Set the current state of the optimizer.""" + self._state = state + + @property + def tol(self) -> float: + """Returns the tolerance of the optimizer. + + Any step with smaller stepsize than this value will stop the optimization.""" + return self._tol + + @tol.setter + def tol(self, tol: float) -> None: + """Set the tolerance.""" + self._tol = tol + + @property + def perturbation(self) -> Optional[float]: + """Returns the perturbation. + + This is the perturbation used in the finite difference gradient approximation. + """ + return self._perturbation + + @perturbation.setter + def perturbation(self, perturbation: Optional[float]) -> None: + """Set the perturbation.""" + self._perturbation = perturbation + + def _callback_wrapper(self) -> None: + """ + Wraps the callback function to accomodate GradientDescent. + + Will call :attr:`~.callback` and pass the following arguments: + current number of function values, current parameters, current function value, + norm of current gradient. + """ + if self.callback is not None: + self.callback( + self.state.nfev, + self.state.x, + self.state.fun(self.state.x), + self.state.stepsize, + ) @property def settings(self) -> Dict[str, Any]: @@ -149,60 +276,114 @@ def settings(self) -> Dict[str, Any]: "callback": self.callback, } - def minimize( - self, - fun: Callable[[POINT], float], - x0: POINT, - jac: Optional[Callable[[POINT], POINT]] = None, - bounds: Optional[List[Tuple[float, float]]] = None, - ) -> OptimizerResult: - # set learning rate - if isinstance(self.learning_rate, float): - eta = constant(self.learning_rate) - else: - eta = self.learning_rate() + def ask(self) -> AskData: + """Returns an object with the data needed to evaluate the gradient. + + If this object contains a gradient function the gradient can be evaluated directly. Otherwise + approximate it with a finite difference scheme. + """ + return AskData( + x_jac=self.state.x, + ) + + def tell(self, ask_data: AskData, tell_data: TellData) -> None: + """ + Updates :attr:`.~GradientDescentState.x` by an ammount proportional to the learning + rate and value of the gradient at that point. + + Args: + ask_data: The data used to evaluate the function. + tell_data: The data from the function evaluation. + + Raises: + ValueError: If the gradient passed doesn't have the right dimension. + """ + if np.shape(self.state.x) != np.shape(tell_data.eval_jac): + raise ValueError("The gradient does not have the correct dimension") + self.state.x = self.state.x - next(self.state.learning_rate) * tell_data.eval_jac + self.state.stepsize = np.linalg.norm(tell_data.eval_jac) + self.state.nit += 1 + + def evaluate(self, ask_data: AskData) -> TellData: + """Evaluates the gradient. - if jac is None: - eps = 0.01 if self.perturbation is None else self.perturbation - jac = partial( - Optimizer.gradient_num_diff, - f=fun, + It does so either by evaluating an analytic gradient or by approximating it with a + finite difference scheme. It will either add ``1`` to the number of gradient evaluations or add + ``N+1`` to the number of function evaluations (Where N is the dimension of the gradient). + + Args: + ask_data: It contains the point where the gradient is to be evaluated and the gradient + function or, in its absence, the objective function to perform a finite difference + approximation. + + Returns: + The data containing the gradient evaluation. + """ + if self.state.jac is None: + eps = 0.01 if (self.perturbation is None) else self.perturbation + grad = Optimizer.gradient_num_diff( + x_center=ask_data.x_jac, + f=self.state.fun, epsilon=eps, max_evals_grouped=self._max_evals_grouped, ) + self.state.nfev += 1 + len(ask_data.x_jac) + else: + grad = self.state.jac(ask_data.x_jac) + self.state.njev += 1 - # prepare some initials - x = np.asarray(x0) - nfevs = 0 + return TellData(eval_jac=grad) - for _ in range(1, self.maxiter + 1): - # compute update -- gradient evaluation counts as one function evaluation - update = jac(x) - nfevs += 1 + def create_result(self) -> OptimizerResult: + """Creates a result of the optimization process. - # compute next parameter value - x_next = x - next(eta) * update + This result contains the best point, the best function value, the number of function/gradient + evaluations and the number of iterations. - # send information to callback - stepsize = np.linalg.norm(update) - if self.callback is not None: - self.callback(nfevs, x_next, fun(x_next), stepsize) + Returns: + The result of the optimization process. + """ + result = OptimizerResult() + result.x = self.state.x + result.fun = self.state.fun(self.state.x) + result.nfev = self.state.nfev + result.njev = self.state.njev + result.nit = self.state.nit + return result - # update parameters - x = x_next + def start( + self, + fun: Callable[[POINT], float], + x0: POINT, + jac: Optional[Callable[[POINT], POINT]] = None, + bounds: Optional[List[Tuple[float, float]]] = None, + ) -> None: - # check termination - if stepsize < self.tol: - break + self.state = GradientDescentState( + fun=fun, + jac=jac, + x=np.asarray(x0), + nit=0, + nfev=0, + njev=0, + learning_rate=LearningRate(learning_rate=self.learning_rate), + stepsize=None, + ) + + def continue_condition(self) -> bool: + """ + Condition that indicates the optimization process should come to an end. - # TODO the optimizer result should contain the number of gradient evaluations, - # if the gradient is passed - result = OptimizerResult() - result.x = x - result.fun = fun(x) - result.nfev = nfevs + When the stepsize is smaller than the tolerance, the optimization process is considered + finished. - return result + Returns: + ``True`` if the optimization process should continue, ``False`` otherwise. + """ + if self.state.stepsize is None: + return True + else: + return (self.state.stepsize > self.tol) and super().continue_condition() def get_support_level(self): """Get the support level dictionary.""" @@ -211,10 +392,3 @@ def get_support_level(self): "bounds": OptimizerSupportLevel.ignored, "initial_point": OptimizerSupportLevel.required, } - - -def constant(eta=0.01): - """Yield a constant.""" - - while True: - yield eta diff --git a/qiskit/algorithms/optimizers/optimizer_utils/__init__.py b/qiskit/algorithms/optimizers/optimizer_utils/__init__.py new file mode 100644 index 000000000000..33c5bc90b087 --- /dev/null +++ b/qiskit/algorithms/optimizers/optimizer_utils/__init__.py @@ -0,0 +1,27 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. +"""Utils for optimizers + +Optimizer Utils (:mod:`qiskit.algorithms.optimizers.optimizer_utils`) +===================================================================== + +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + LearningRate + +""" + +from .learning_rate import LearningRate + +__all__ = ["LearningRate"] diff --git a/qiskit/algorithms/optimizers/optimizer_utils/learning_rate.py b/qiskit/algorithms/optimizers/optimizer_utils/learning_rate.py new file mode 100644 index 000000000000..8ba8a0c69ca5 --- /dev/null +++ b/qiskit/algorithms/optimizers/optimizer_utils/learning_rate.py @@ -0,0 +1,83 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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. + +"""A class to represent the Learning Rate.""" + +from typing import Union, Callable, Optional, List, Iterator, Generator +from itertools import tee +import numpy as np + + +class LearningRate(Generator): + """Represents a Learning Rate. + Will be an attribute of :class:`~.GradientDescentState`. Note that :class:`~.GradientDescent` also + has a learning rate. That learning rate can be a float, a list, an array, a function returning + a generator and will be used to create a generator to be used during the + optimization process. + This class wraps ``Generator`` so that we can also access the last yielded value. + """ + + def __init__( + self, learning_rate: Union[float, List[float], np.ndarray, Callable[[], Iterator]] + ): + """ + Args: + learning_rate: Used to create a generator to iterate on. + """ + if isinstance(learning_rate, (float, int)): + self._gen = constant(learning_rate) + elif isinstance(learning_rate, Generator): + learning_rate, self._gen = tee(learning_rate) + elif isinstance(learning_rate, (list, np.ndarray)): + self._gen = (eta for eta in learning_rate) + else: + self._gen = learning_rate() + + self._current: Optional[float] = None + + def send(self, value): + """Send a value into the generator. + Return next yielded value or raise StopIteration. + """ + self._current = next(self._gen) + return self.current + + def throw(self, typ, val=None, tb=None): + """Raise an exception in the generator. + Return next yielded value or raise StopIteration. + """ + if val is None: + if tb is None: + raise typ + val = typ() + if tb is not None: + val = val.with_traceback(tb) + raise val + + @property + def current(self): + """Returns the current value of the learning rate.""" + return self._current + + +def constant(learning_rate: float = 0.01) -> Generator[float, None, None]: + """Returns a python generator that always yields the same value. + + Args: + learning_rate: The value to yield. + + Yields: + The learning rate for the next iteration. + """ + + while True: + yield learning_rate diff --git a/qiskit/algorithms/optimizers/steppable_optimizer.py b/qiskit/algorithms/optimizers/steppable_optimizer.py new file mode 100644 index 000000000000..928777133026 --- /dev/null +++ b/qiskit/algorithms/optimizers/steppable_optimizer.py @@ -0,0 +1,302 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""SteppableOptimizer interface""" + +from abc import abstractmethod, ABC +from dataclasses import dataclass +from typing import Union, Callable, Optional, Tuple, List +from .optimizer import Optimizer, POINT, OptimizerResult + + +@dataclass +class AskData(ABC): + """Base class for return type of :meth:`~.SteppableOptimizer.ask`. + + Args: + x_fun: Point or list of points where the function needs to be evaluated to compute the next + state of the optimizer. + x_jac: Point or list of points where the gradient/jacobian needs to be evaluated to compute + the next state of the optimizer. + + """ + + x_fun: Optional[Union[POINT, List[POINT]]] = None + x_jac: Optional[Union[POINT, List[POINT]]] = None + + +@dataclass +class TellData(ABC): + """Base class for argument type of :meth:`~.SteppableOptimizer.tell`. + + Args: + eval_fun: Image of the function at :attr:`~.ask_data.x_fun`. + eval_jac: Image of the gradient-jacobian at :attr:`~.ask_data.x_jac`. + + """ + + eval_fun: Union[float, List[float], None] = None + eval_jac: Union[POINT, List[POINT], None] = None + + +@dataclass +class OptimizerState: + """Base class representing the state of the optimizer. + + This class stores the current state of the optimizer, given by the current point and + (optionally) information like the function value, the gradient or the number of + function evaluations. This dataclass can also store any other individual variables that + change during the optimization. + + """ + + x: POINT # pylint: disable=invalid-name + """Current optimization parameters.""" + fun: Optional[Callable[[POINT], float]] + """Function being optimized.""" + jac: Optional[Callable[[POINT], POINT]] + """Jacobian of the function being optimized.""" + nfev: Optional[int] + """Number of function evaluations so far in the optimization.""" + njev: Optional[int] + """Number of jacobian evaluations so far in the opimization.""" + nit: Optional[int] + """Number of optmization steps performed so far in the optimization.""" + + +class SteppableOptimizer(Optimizer): + """ + Base class for a steppable optimizer. + + This family of optimizers uses the `ask and tell interface + `_. + When using this interface the user has to call :meth:`~.ask` to get information about + how to evaluate the fucntion (we are asking the optimizer about how to do the evaluation). + This information is typically the next points at which the function is evaluated, but depending + on the optimizer it can also determine whether to evaluate the function or its gradient. + Once the function has been evaluated, the user calls the method :meth:`~..tell` + to tell the optimizer what the result of the function evaluation(s) is. The optimizer then + updates its state accordingly and the user can decide whether to stop the optimization process + or to repeat a step. + + This interface is more customizable, and allows the user to have full control over the evaluation + of the function. + + Examples: + + An example where the evaluation of the function has a chance of failing. The user, with + specific knowledge about his function can catch this errors and handle them before passing + the result to the optimizer. + + .. code-block:: python + + import random + import numpy as np + from qiskit.algorithms.optimizers import GradientDescent + + def objective(x): + if random.choice([True, False]): + return None + else: + return (np.linalg.norm(x) - 1) ** 2 + + def grad(x): + if random.choice([True, False]): + return None + else: + return 2 * (np.linalg.norm(x) - 1) * x / np.linalg.norm(x) + + + initial_point = np.random.normal(0, 1, size=(100,)) + + optimizer = GradientDescent(maxiter=20) + optimizer.start(x0=initial_point, fun=objective, jac=grad) + + while optimizer.continue_condition(): + ask_data = optimizer.ask() + evaluated_gradient = None + + while evaluated_gradient is None: + evaluated_gradient = grad(ask_data.x_center) + optimizer.state.njev += 1 + + optmizer.state.nit += 1 + + cf = TellData(eval_jac=evaluated_gradient) + optimizer.tell(ask_data=ask_data, tell_data=tell_data) + + result = optimizer.create_result() + + + Users that aren't dealing with complicated functions and who are more familiar with step by step + optimization algorithms can use the :meth:`~.step` method which wraps the :meth:`~.ask` + and :meth:`~.tell` methods. In the same spirit the method :meth:`~.minimize` will optimize the + function and return the result. + + To see other libraries that use this interface one can visit: + https://optuna.readthedocs.io/en/stable/tutorial/20_recipes/009_ask_and_tell.html + + + """ + + def __init__( + self, + maxiter: int = 100, + ): + """ + Args: + maxiter: Number of steps in the optimization process before ending the loop. + """ + super().__init__() + self._state: Optional[OptimizerState] = None + self.maxiter = maxiter + + @property + def state(self) -> OptimizerState: + """Return the current state of the optimizer.""" + return self._state + + @state.setter + def state(self, state: OptimizerState) -> None: + """Set the current state of the optimizer.""" + self._state = state + + def ask(self) -> AskData: + """Ask the optimizer for a set of points to evaluate. + + This method asks the optimizer which are the next points to evaluate. + These points can, e.g., correspond to function values and/or its derivative. + It may also correspond to variables that let the user infer which points to evaluate. + It is the first method inside of a :meth:`~.step` in the optimization process. + + Returns: + An object containing the data needed to make the funciton evaluation to advance the + optimization process. + + """ + raise NotImplementedError + + def tell(self, ask_data: AskData, tell_data: TellData) -> None: + """Updates the optimization state using the results of the function evaluation. + + A canonical optimization example using :meth:`~.ask` and :meth:`~.tell` can be seen + in :meth:`~.step`. + + Args: + ask_data: Contains the information on how the evaluation was done. + tell_data: Contains all relevant information about the evaluation of the objective + function. + """ + raise NotImplementedError + + @abstractmethod + def evaluate(self, ask_data: AskData) -> TellData: + """Evaluates the function according to the instructions contained in :attr:`~.ask_data`. + + If the user decides to use :meth:`~.step` instead of :meth:`~.ask` and :meth:`~.tell` + this function will contain the logic on how to evaluate the function. + + Args: + ask_data: Contains the information on how to do the evaluation. + + Returns: + Data of all relevant information about the function evaluation. + + """ + raise NotImplementedError + + def _callback_wrapper(self) -> None: + """ + Wraps the callback function to accomodate each optimizer. + """ + pass + + def step(self) -> None: + """Performs one step in the optimization process. + + This method composes :meth:`~.ask`, :meth:`~.evaluate`, and :meth:`~.tell` to make a "step" + in the optimization process. + """ + ask_data = self.ask() + tell_data = self.evaluate(ask_data=ask_data) + self.tell(ask_data=ask_data, tell_data=tell_data) + + # pylint: disable=invalid-name + @abstractmethod + def start( + self, + fun: Callable[[POINT], float], + x0: POINT, + jac: Optional[Callable[[POINT], POINT]] = None, + bounds: Optional[List[Tuple[float, float]]] = None, + ) -> None: + """Populates the state of the optimizer with the data provided and sets all the counters to 0. + + Args: + fun: Function to minimize. + x0: Initial point. + jac: Function to compute the gradient. + bounds: Bounds of the search space. + + """ + raise NotImplementedError + + def minimize( + self, + fun: Callable[[POINT], float], + x0: POINT, + jac: Optional[Callable[[POINT], POINT]] = None, + bounds: Optional[List[Tuple[float, float]]] = None, + ) -> OptimizerResult: + """Minimizes the function. + + For well behaved functions the user can call this method to minimize a function. + If the user wants more control on how to evaluate the function a custom loop can be + created using :meth:`~.ask` and :meth:`~.tell` and evaluating the function manually. + + Args: + fun: Function to minimize. + x0: Initial point. + jac: Function to compute the gradient. + bounds: Bounds of the search space. + + Returns: + Object containing the result of the optimization. + + """ + self.start(x0=x0, fun=fun, jac=jac, bounds=bounds) + while self.continue_condition(): + self.step() + self._callback_wrapper() + return self.create_result() + + @abstractmethod + def create_result(self) -> OptimizerResult: + """Returns the result of the optimization. + + All the information needed to create such a result should be stored in the optimizer state + and will typically contain the best point found, the function value and gradient at that point, + the number of function and gradient evaluation and the number of iterations in the optimization. + + Returns: + The result of the optimization process. + + """ + raise NotImplementedError + + def continue_condition(self) -> bool: + """Condition that indicates the optimization process should continue. + + Returns: + ``True`` if the optimization process should continue, ``False`` otherwise. + """ + return self.state.nit < self.maxiter diff --git a/releasenotes/notes/steppable-optimizers-9d9b48ba78bd58bb.yaml b/releasenotes/notes/steppable-optimizers-9d9b48ba78bd58bb.yaml new file mode 100644 index 000000000000..6bde2fc01a40 --- /dev/null +++ b/releasenotes/notes/steppable-optimizers-9d9b48ba78bd58bb.yaml @@ -0,0 +1,96 @@ +--- +features: + - | + The :class:`~.SteppableOptimizer` class is added. It allows one to perfore classical + optimizations step-by-step using the :meth:`~.SteppableOptimizer.step` method. These + optimizers implement the "ask and tell" interface which (optionally) allows to manually compute + the required function or gradient evaluations and plug them back into the optimizer. + For more information about this interface see: `ask and tell interface + `_. + A very simple use case when the user might want to do the optimization step by step is for + readout: + + .. code-block:: python + + import random + import numpy as np + from qiskit.algorithms.optimizers import GradientDescent + + def objective(x): + if random.choice([True, False]): + return None + else: + return (np.linalg.norm(x) - 1) ** 2 + + def grad(x): + if random.choice([True, False]): + return None + else: + return 2 * (np.linalg.norm(x) - 1) * x / np.linalg.norm(x) + + + initial_point = np.random.normal(0, 1, size=(100,)) + + optimizer = GradientDescent(maxiter=20) + optimizer.start(x0=initial_point, fun=objective, jac=grad) + + for _ in range(maxiter): + state = optimizer.state + # Here you can manually read out anything from the optimizer state. + optimizer.step() + + result = optimizer.create_result() + + A more complex case would be error handling. Imagine that the funciton you are evaluating has + a random chance of failing. In this case you can catch the error and run the function again + until it yields the desired result before continuing the optimization process. In this case + one would use the ask and tell interface. + + .. code-block:: python + + import random + import numpy as np + from qiskit.algorithms.optimizers import GradientDescent + + def objective(x): + if random.choice([True, False]): + return None + else: + return (np.linalg.norm(x) - 1) ** 2 + + def grad(x): + if random.choice([True, False]): + return None + else: + return 2 * (np.linalg.norm(x) - 1) * x / np.linalg.norm(x) + + + initial_point = np.random.normal(0, 1, size=(100,)) + + optimizer = GradientDescent(maxiter=20) + optimizer.start(x0=initial_point, fun=objective, jac=grad) + + while optimizer.continue_condition(): + ask_data = optimizer.ask() + evaluated_gradient = None + + while evaluated_gradient is None: + evaluated_gradient = grad(ask_data.x_center) + optimizer.state.njev += 1 + + optmizer.state.nit += 1 + + cf = TellData(eval_jac=evaluated_gradient) + optimizer.tell(ask_data=ask_data, tell_data=tell_data) + + result = optimizer.create_result() + + Transitioned GradientDescent to be a subclass of SteppableOptimizer. + +fixes: + - | + :class:`.GradientDescent` will now correctly count the number of iterations, function evaluations and + gradient evaluations. Also the documentation now correctly states that the gradient is approximated + by a forward finite difference method. + + diff --git a/test/python/algorithms/optimizers/test_gradient_descent.py b/test/python/algorithms/optimizers/test_gradient_descent.py index 0391c3a714c6..abe15f4b5362 100644 --- a/test/python/algorithms/optimizers/test_gradient_descent.py +++ b/test/python/algorithms/optimizers/test_gradient_descent.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -13,10 +13,9 @@ """Tests for the Gradient Descent optimizer.""" from test.python.algorithms import QiskitAlgorithmsTestCase - import numpy as np - -from qiskit.algorithms.optimizers import GradientDescent +from qiskit.algorithms.optimizers import GradientDescent, GradientDescentState +from qiskit.algorithms.optimizers.steppable_optimizer import TellData, AskData from qiskit.circuit.library import PauliTwoDesign from qiskit.opflow import I, Z, StateFn from qiskit.test.decorators import slow_test @@ -28,6 +27,15 @@ class TestGradientDescent(QiskitAlgorithmsTestCase): def setUp(self): super().setUp() np.random.seed(12) + self.initial_point = np.array([1, 1, 1, 1, 0]) + + def objective(self, x): + """Objective Function for the tests""" + return (np.linalg.norm(x) - 1) ** 2 + + def grad(self, x): + """Gradient of the objective function""" + return 2 * (np.linalg.norm(x) - 1) * x / np.linalg.norm(x) @slow_test def test_pauli_two_design(self): @@ -54,15 +62,15 @@ def test_pauli_two_design(self): ] ) - def objective(x): + def objective_pauli(x): return expr.bind_parameters(dict(zip(parameters, x))).eval().real optimizer = GradientDescent(maxiter=100, learning_rate=0.1, perturbation=0.1) - result = optimizer.minimize(objective, x0=initial_point) + result = optimizer.minimize(objective_pauli, x0=initial_point) self.assertLess(result.fun, -0.95) # final loss - self.assertEqual(result.nfev, 100) # function evaluations + self.assertEqual(result.nfev, 1300) # function evaluations def test_callback(self): """Test the callback.""" @@ -74,10 +82,7 @@ def callback(*args): optimizer = GradientDescent(maxiter=1, callback=callback) - def objective(x): - return np.linalg.norm(x) - - _ = optimizer.minimize(objective, np.array([1, -1])) + _ = optimizer.minimize(self.objective, np.array([1, -1])) self.assertEqual(len(history), 1) self.assertIsInstance(history[0][0], int) # nfevs @@ -85,8 +90,8 @@ def objective(x): self.assertIsInstance(history[0][2], float) # function value self.assertIsInstance(history[0][3], float) # norm of the gradient - def test_iterator_learning_rate(self): - """Test setting the learning rate as iterator.""" + def test_minimize(self): + """Test setting the learning rate as iterator and minimizing the funciton.""" def learning_rate(): power = 0.6 @@ -100,15 +105,93 @@ def powerlaw(): return powerlaw() - def objective(x): - return (np.linalg.norm(x) - 1) ** 2 + optimizer = GradientDescent(maxiter=20, learning_rate=learning_rate) + result = optimizer.minimize(self.objective, self.initial_point, self.grad) - def grad(x): - return 2 * (np.linalg.norm(x) - 1) * x / np.linalg.norm(x) + self.assertLess(result.fun, 1e-5) - initial_point = np.array([1, 0.5, -2]) + def test_no_start(self): + """Tests that making a step without having started the optimizer raises an error.""" + optimizer = GradientDescent() + with self.assertRaises(AttributeError): + optimizer.step() + + def test_start(self): + """Tests if the start method initializes the state properly.""" + optimizer = GradientDescent() + self.assertIsNone(optimizer.state) + self.assertIsNone(optimizer.perturbation) + optimizer.start(x0=self.initial_point, fun=self.objective) + + test_state = GradientDescentState( + x=self.initial_point, + fun=self.objective, + jac=None, + nfev=0, + njev=0, + nit=0, + learning_rate=1, + stepsize=None, + ) - optimizer = GradientDescent(maxiter=20, learning_rate=learning_rate) - result = optimizer.minimize(objective, initial_point, grad) + self.assertEqual(test_state, optimizer.state) + + def test_ask(self): + """Test the ask method.""" + optimizer = GradientDescent() + optimizer.start(fun=self.objective, x0=self.initial_point) + + ask_data = optimizer.ask() + np.testing.assert_equal(ask_data.x_jac, self.initial_point) + self.assertIsNone(ask_data.x_fun) + + def test_evaluate(self): + """Test the evaluate method.""" + optimizer = GradientDescent(perturbation=1e-10) + optimizer.start(fun=self.objective, x0=self.initial_point) + ask_data = AskData(x_jac=self.initial_point) + tell_data = optimizer.evaluate(ask_data=ask_data) + np.testing.assert_almost_equal(tell_data.eval_jac, self.grad(self.initial_point), decimal=2) + + def test_tell(self): + """Test the tell method.""" + optimizer = GradientDescent(learning_rate=1.0) + optimizer.start(fun=self.objective, x0=self.initial_point) + ask_data = AskData(x_jac=self.initial_point) + tell_data = TellData(eval_jac=self.initial_point) + optimizer.tell(ask_data=ask_data, tell_data=tell_data) + np.testing.assert_equal(optimizer.state.x, np.zeros(optimizer.state.x.shape)) + + def test_continue_condition(self): + """Test if the continue condition is working properly.""" + optimizer = GradientDescent(tol=1) + optimizer.start(fun=self.objective, x0=self.initial_point) + self.assertTrue(optimizer.continue_condition()) + optimizer.state.stepsize = 0.1 + self.assertFalse(optimizer.continue_condition()) + optimizer.state.stepsize = 10 + optimizer.state.nit = 1000 + self.assertFalse(optimizer.continue_condition()) + + def test_step(self): + """Tests if performing one step yields the desired result.""" + optimizer = GradientDescent(learning_rate=1.0) + optimizer.start(fun=self.objective, jac=self.grad, x0=self.initial_point) + optimizer.step() + np.testing.assert_almost_equal( + optimizer.state.x, self.initial_point - self.grad(self.initial_point), 6 + ) - self.assertLess(result.fun, 1e-5) + def test_wrong_dimension_gradient(self): + """Tests if an error is raised when a gradient of the wrong dimension is passed.""" + + optimizer = GradientDescent(learning_rate=1.0) + optimizer.start(fun=self.objective, x0=self.initial_point) + ask_data = AskData(x_jac=self.initial_point) + tell_data = TellData(eval_jac=np.array([1.0, 5])) + with self.assertRaises(ValueError): + optimizer.tell(ask_data=ask_data, tell_data=tell_data) + + tell_data = TellData(eval_jac=np.array(1)) + with self.assertRaises(ValueError): + optimizer.tell(ask_data=ask_data, tell_data=tell_data) diff --git a/test/python/algorithms/optimizers/utils/__init__.py b/test/python/algorithms/optimizers/utils/__init__.py new file mode 100644 index 000000000000..f3adc3e3b4da --- /dev/null +++ b/test/python/algorithms/optimizers/utils/__init__.py @@ -0,0 +1,12 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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 Optimizer Utils.""" diff --git a/test/python/algorithms/optimizers/utils/test_learning_rate.py b/test/python/algorithms/optimizers/utils/test_learning_rate.py new file mode 100644 index 000000000000..52acdbf98aaa --- /dev/null +++ b/test/python/algorithms/optimizers/utils/test_learning_rate.py @@ -0,0 +1,54 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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 LearningRate.""" + +from test.python.algorithms import QiskitAlgorithmsTestCase +import numpy as np +from qiskit.algorithms.optimizers.optimizer_utils import LearningRate + + +class TestLearningRate(QiskitAlgorithmsTestCase): + """Tests for the LearningRate class.""" + + def setUp(self): + super().setUp() + np.random.seed(12) + self.initial_point = np.array([1, 1, 1, 1, 0]) + + def objective(self, x): + """Objective Function for the tests""" + return (np.linalg.norm(x) - 1) ** 2 + + def test_learning_rate(self): + """ + Tests if the learning rate is initialized properly for each kind of input: + float, list and iterator. + """ + constant_learning_rate_input = 0.01 + list_learning_rate_input = [0.01 * n for n in range(10)] + generator_learning_rate_input = lambda: (el for el in list_learning_rate_input) + + with self.subTest("Check constant learning rate."): + constant_learning_rate = LearningRate(learning_rate=constant_learning_rate_input) + for _ in range(5): + self.assertEqual(constant_learning_rate_input, next(constant_learning_rate)) + + with self.subTest("Check learning rate list."): + list_learning_rate = LearningRate(learning_rate=list_learning_rate_input) + for i in range(5): + self.assertEqual(list_learning_rate_input[i], next(list_learning_rate)) + + with self.subTest("Check learning rate generator."): + generator_learning_rate = LearningRate(generator_learning_rate_input) + for i in range(5): + self.assertEqual(list_learning_rate_input[i], next(generator_learning_rate)) From d163e89cd080fbfe4f21d964eaf58b0a0fb65ac7 Mon Sep 17 00:00:00 2001 From: Joseph McElroy <59029169+jmcelroy01@users.noreply.github.com> Date: Mon, 29 Aug 2022 12:54:15 -0400 Subject: [PATCH 73/82] Fix LaTeX drawer on split-filesystem systems (#8629) * Update circuit_visualization.py Corrects issue #8542 * Update circuit_visualization.py * Add release note Co-authored-by: Jake Lishman --- qiskit/visualization/circuit_visualization.py | 3 ++- .../notes/fix-latex-split-filesystem-0c38a1ade2f36e85.yaml | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/fix-latex-split-filesystem-0c38a1ade2f36e85.yaml diff --git a/qiskit/visualization/circuit_visualization.py b/qiskit/visualization/circuit_visualization.py index c25d94797417..65d9c5ffeb0b 100644 --- a/qiskit/visualization/circuit_visualization.py +++ b/qiskit/visualization/circuit_visualization.py @@ -27,6 +27,7 @@ import logging import os +import shutil import subprocess import tempfile from warnings import warn @@ -488,7 +489,7 @@ def _latex_circuit_drawer( image = utils._trim(image) if filename: if filename.endswith(".pdf"): - os.rename(base + ".pdf", filename) + shutil.move(base + ".pdf", filename) else: try: image.save(filename) diff --git a/releasenotes/notes/fix-latex-split-filesystem-0c38a1ade2f36e85.yaml b/releasenotes/notes/fix-latex-split-filesystem-0c38a1ade2f36e85.yaml new file mode 100644 index 000000000000..91fd099d3451 --- /dev/null +++ b/releasenotes/notes/fix-latex-split-filesystem-0c38a1ade2f36e85.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixed an ``OSError`` in the LaTeX circuit drawer on systems whose temporary + directories (*e.g* ``/tmp``) are on a different filesystem to the working + directory. See `#8542 `__ + for more detail. From bab9d4568334d572dd14ffef3b35567bb81fbc2c Mon Sep 17 00:00:00 2001 From: Ikko Hamamura Date: Tue, 30 Aug 2022 03:16:10 +0900 Subject: [PATCH 74/82] Fix deprecation messages in BaseSampler/BaseEstimator (#8631) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/primitives/base_estimator.py | 6 +++--- qiskit/primitives/base_sampler.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/qiskit/primitives/base_estimator.py b/qiskit/primitives/base_estimator.py index 830cb54c12fc..998e162e9497 100644 --- a/qiskit/primitives/base_estimator.py +++ b/qiskit/primitives/base_estimator.py @@ -212,7 +212,7 @@ def __new__( return self @deprecate_function( - "The BaseEstimator.__enter__ method is deprecated as of Qiskit Terra 0.21.0 " + "The BaseEstimator.__enter__ method is deprecated as of Qiskit Terra 0.22.0 " "and will be removed no sooner than 3 months after the releasedate. " "BaseEstimator should be initialized directly.", ) @@ -220,7 +220,7 @@ def __enter__(self): return self @deprecate_function( - "The BaseEstimator.__exit__ method is deprecated as of Qiskit Terra 0.21.0 " + "The BaseEstimator.__exit__ method is deprecated as of Qiskit Terra 0.22.0 " "and will be removed no sooner than 3 months after the releasedate. " "BaseEstimator should be initialized directly.", ) @@ -259,7 +259,7 @@ def parameters(self) -> tuple[ParameterView, ...]: return tuple(self._parameters) @deprecate_function( - "The BaseSampler.__call__ method is deprecated as of Qiskit Terra 0.21.0 " + "The BaseSampler.__call__ method is deprecated as of Qiskit Terra 0.22.0 " "and will be removed no sooner than 3 months after the releasedate. " "Use run method instead.", ) diff --git a/qiskit/primitives/base_sampler.py b/qiskit/primitives/base_sampler.py index 42591e582045..2b2f14b0d32f 100644 --- a/qiskit/primitives/base_sampler.py +++ b/qiskit/primitives/base_sampler.py @@ -172,7 +172,7 @@ def __new__( return self @deprecate_function( - "The BaseSampler.__enter__ method is deprecated as of Qiskit Terra 0.21.0 " + "The BaseSampler.__enter__ method is deprecated as of Qiskit Terra 0.22.0 " "and will be removed no sooner than 3 months after the releasedate. " "BaseSampler should be initialized directly.", ) @@ -180,7 +180,7 @@ def __enter__(self): return self @deprecate_function( - "The BaseSampler.__exit__ method is deprecated as of Qiskit Terra 0.21.0 " + "The BaseSampler.__exit__ method is deprecated as of Qiskit Terra 0.22.0 " "and will be removed no sooner than 3 months after the releasedate. " "BaseSampler should be initialized directly.", ) @@ -210,7 +210,7 @@ def parameters(self) -> tuple[ParameterView, ...]: return tuple(self._parameters) @deprecate_function( - "The BaseSampler.__call__ method is deprecated as of Qiskit Terra 0.21.0 " + "The BaseSampler.__call__ method is deprecated as of Qiskit Terra 0.22.0 " "and will be removed no sooner than 3 months after the releasedate. " "Use run method instead.", ) From 367ed4e34a3d662ef8ed8f2b91e8823bba1b8ca8 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 30 Aug 2022 10:09:13 -0400 Subject: [PATCH 75/82] Fix panic in SabreSwap with classical bits (#8636) * Fix panic in SabreSwap with classical bits This commit fixes a bug introduced in SabreSwap that was caught by the randomized testing in #8635. A copy paste error was causing the rusty sabre code to panic in cases where there were classical bits of a particular index assigned prior to the qubit with the same index. This commit fixes the typo so the behavior is corrected in general and the panic is also fixed. Fixes #8635 * Run black and fix formatting --- src/sabre_swap/sabre_dag.rs | 2 +- test/python/transpiler/test_sabre_layout.py | 41 +++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/sabre_swap/sabre_dag.rs b/src/sabre_swap/sabre_dag.rs index bb60b990b2f9..ae349d30020a 100644 --- a/src/sabre_swap/sabre_dag.rs +++ b/src/sabre_swap/sabre_dag.rs @@ -54,7 +54,7 @@ impl SabreDAG { } for x in cargs { if clbit_pos[*x] != usize::MAX { - dag.add_edge(NodeIndex::new(qubit_pos[*x]), gate_index, ()); + dag.add_edge(NodeIndex::new(clbit_pos[*x]), gate_index, ()); } clbit_pos[*x] = gate_index.index(); } diff --git a/test/python/transpiler/test_sabre_layout.py b/test/python/transpiler/test_sabre_layout.py index 85a975dd6a48..142a66f2f4d9 100644 --- a/test/python/transpiler/test_sabre_layout.py +++ b/test/python/transpiler/test_sabre_layout.py @@ -19,7 +19,9 @@ from qiskit.transpiler.passes import SabreLayout from qiskit.converters import circuit_to_dag from qiskit.test import QiskitTestCase +from qiskit.compiler.transpiler import transpile from qiskit.providers.fake_provider import FakeAlmaden +from qiskit.providers.fake_provider import FakeKolkata class TestSabreLayout(QiskitTestCase): @@ -99,6 +101,45 @@ def test_6q_circuit_20q_coupling(self): self.assertEqual(layout[qr1[1]], 12) self.assertEqual(layout[qr1[2]], 11) + def test_layout_with_classical_bits(self): + """Test sabre layout with classical bits recreate from issue #8635.""" + qc = QuantumCircuit.from_qasm_str( + """ +OPENQASM 2.0; +include "qelib1.inc"; +qreg q4833[1]; +qreg q4834[6]; +qreg q4835[7]; +creg c982[2]; +creg c983[2]; +creg c984[2]; +rzz(0) q4833[0],q4834[4]; +cu(0,-6.1035156e-05,0,1e-05) q4834[1],q4835[2]; +swap q4834[0],q4834[2]; +cu(-1.1920929e-07,0,-0.33333333,0) q4833[0],q4834[2]; +ccx q4835[2],q4834[5],q4835[4]; +measure q4835[4] -> c984[0]; +ccx q4835[2],q4835[5],q4833[0]; +measure q4835[5] -> c984[1]; +measure q4834[0] -> c982[1]; +u(10*pi,0,1.9) q4834[5]; +measure q4834[3] -> c984[1]; +measure q4835[0] -> c982[0]; +rz(0) q4835[1]; +""" + ) + res = transpile(qc, FakeKolkata(), layout_method="sabre", seed_transpiler=1234) + self.assertIsInstance(res, QuantumCircuit) + layout = res._layout + self.assertEqual(layout[qc.qubits[0]], 14) + self.assertEqual(layout[qc.qubits[1]], 19) + self.assertEqual(layout[qc.qubits[2]], 7) + self.assertEqual(layout[qc.qubits[3]], 13) + self.assertEqual(layout[qc.qubits[4]], 6) + self.assertEqual(layout[qc.qubits[5]], 16) + self.assertEqual(layout[qc.qubits[6]], 18) + self.assertEqual(layout[qc.qubits[7]], 26) + if __name__ == "__main__": unittest.main() From d85aec58a56aa4c6e8b9c481da9e255d6d1db904 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 30 Aug 2022 18:05:03 +0100 Subject: [PATCH 76/82] Enable Rust backtraces in CI (#8639) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .azure/test-linux.yml | 1 + .azure/test-macos.yml | 1 + .azure/test-windows.yml | 1 + .github/workflows/randomized_tests.yml | 2 ++ 4 files changed, 5 insertions(+) diff --git a/.azure/test-linux.yml b/.azure/test-linux.yml index 4ea728d46032..51a9afc90880 100644 --- a/.azure/test-linux.yml +++ b/.azure/test-linux.yml @@ -112,6 +112,7 @@ jobs: popd env: QISKIT_PARALLEL: FALSE + RUST_BACKTRACE: 1 displayName: 'Run tests' - bash: | diff --git a/.azure/test-macos.yml b/.azure/test-macos.yml index 69aca4e0c1e0..2219d853702a 100644 --- a/.azure/test-macos.yml +++ b/.azure/test-macos.yml @@ -60,6 +60,7 @@ jobs: python ./tools/verify_parallel_map.py env: QISKIT_PARALLEL: FALSE + RUST_BACKTRACE: 1 displayName: "Run tests" - bash: | diff --git a/.azure/test-windows.yml b/.azure/test-windows.yml index a1e4e85a1f9b..14753a78ed5d 100644 --- a/.azure/test-windows.yml +++ b/.azure/test-windows.yml @@ -53,6 +53,7 @@ jobs: LANG: 'C.UTF-8' PYTHONIOENCODING: 'utf-8:backslashreplace' QISKIT_PARALLEL: FALSE + RUST_BACKTRACE: 1 displayName: 'Run tests' - bash: | diff --git a/.github/workflows/randomized_tests.yml b/.github/workflows/randomized_tests.yml index f96f84e0ed78..7f526c1692b4 100644 --- a/.github/workflows/randomized_tests.yml +++ b/.github/workflows/randomized_tests.yml @@ -24,6 +24,8 @@ jobs: SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" - name: Run randomized tests run: make test_randomized + env: + RUST_BACKTRACE=1 - name: Create comment on failed test run if: ${{ failure() }} uses: peter-evans/create-or-update-comment@v1 From cef0a8cc43e19c70091c057f7ca2366802e2dfdc Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 30 Aug 2022 14:16:51 -0400 Subject: [PATCH 77/82] Add stage plugin interface for transpile (#8305) * Add stage plugin interface for transpile This commit adds a new plugin interface to qiskit for enabling external packages to write plugins that will replace a stage in the transpilation pipeline. For example, if an external package had a custom layout pass that they wanted to integrate into transpile() they could export a plugin using this new interface and then users would just need to run transpile(.., layout_method=foo) This adds long asked for extensibility to the transpiler so that to cleanly integrate new transpiler passes we're no longer required to merge the features into terra. This should hopefully make it easier for downstream pass authors to integrate their passes into terra and make it easier for the terra maintainers to evaluate new transpiler passes. * Fix docs builds * Fix doc warning * Make routing methods all plugins This commit converts all the built-in routing method options into separate plugins and also adds a default plugin for the default behavior at each optimization level. To support using plugins for routing method adding the optimization_level to the passmanager config was necessary so that the plugin has sufficient context on how to construct the routing pass used for the routing stage. As depending on the optimization level the settings for each pass differs. For example on stochastic swap the number of stochastic trials increases for level 3 to try and find a better solution at the cost of more runtime. * Add plugin usage to level 3 * Add plugin support to level 0 preset pass manager * Add default plugin to reserved routing methods list * Add release notes * Add tests * Apply suggestions from code review Co-authored-by: Alexander Ivrii * Apply suggestions from code review Co-authored-by: Alexander Ivrii * Remove raise on non-builtin layout method argument * Fix typo * Deduplicate code in built-in routing plugins This commit deduplicates the code in the built-in routing stage plugins. First it removes the default plugin which was duplicated with the stochastic and sabre plugins. There was no functional difference between just setting the implicit default method name and using a per method plugin and having a standalone default plugin. Secondly all the vf2 call limit code is abstracted into a helper function which reduces code duplication. * Make vf2 call limit function private * Deduplicate code in stochastic swap plugin * Expand example plugin documentation to show more complete use case * Update qiskit/transpiler/preset_passmanagers/level1.py Co-authored-by: Luciano Bello * Add missing TODO comment * Unify vf2 call limit handling * Remove default plugin from entry points * Simplify level 3 optimization stage logic * Update qiskit/transpiler/preset_passmanagers/plugin.py Co-authored-by: Luciano Bello * Prefer toqm plugin if one is available The qiskit-toqm project will be one of the first users of this plugin interface. Once this is released in qiskit, qiskit-toqm will likely publish their own plugin soon after and if they do we want that plugin to be used instead of the hardcoded stage in terra. This commit updates the logic for toqm handling to only use the built-in toqm if a version of qiskit-toqm is installed without a plugin present. * Apply suggestions from code review Co-authored-by: Kevin Hartman Co-authored-by: Toshinari Itoko <15028342+itoko@users.noreply.github.com> * Fix lint * Remove unnecessary elses in builtin plugins * Apply suggestions from code review Co-authored-by: Luciano Bello * Make optimization level a plugin argument * Add test coverage for all built-in routing plugins * Reorder stage variables to execution order Co-authored-by: Alexander Ivrii Co-authored-by: Luciano Bello Co-authored-by: Kevin Hartman Co-authored-by: Toshinari Itoko <15028342+itoko@users.noreply.github.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- docs/apidocs/terra.rst | 1 + docs/apidocs/transpiler_plugins.rst | 2 +- docs/apidocs/transpiler_synthesis_plugins.rst | 6 + qiskit/compiler/transpiler.py | 36 ++- qiskit/transpiler/passes/synthesis/plugin.py | 4 + qiskit/transpiler/passmanager_config.py | 22 +- .../preset_passmanagers/__init__.py | 36 ++- .../preset_passmanagers/builtin_plugins.py | 273 ++++++++++++++++ .../transpiler/preset_passmanagers/common.py | 20 ++ .../transpiler/preset_passmanagers/level0.py | 111 ++++--- .../transpiler/preset_passmanagers/level1.py | 146 +++++---- .../transpiler/preset_passmanagers/level2.py | 134 ++++---- .../transpiler/preset_passmanagers/level3.py | 200 +++++++----- .../transpiler/preset_passmanagers/plugin.py | 299 ++++++++++++++++++ ...age-plugin-interface-47daae40f7d0ad3c.yaml | 32 ++ setup.py | 9 +- test/python/transpiler/test_stage_plugin.py | 103 ++++++ 17 files changed, 1171 insertions(+), 263 deletions(-) create mode 100644 docs/apidocs/transpiler_synthesis_plugins.rst create mode 100644 qiskit/transpiler/preset_passmanagers/builtin_plugins.py create mode 100644 qiskit/transpiler/preset_passmanagers/plugin.py create mode 100644 releasenotes/notes/stage-plugin-interface-47daae40f7d0ad3c.yaml create mode 100644 test/python/transpiler/test_stage_plugin.py diff --git a/docs/apidocs/terra.rst b/docs/apidocs/terra.rst index 5b765c7c95e7..9154a9420fc6 100644 --- a/docs/apidocs/terra.rst +++ b/docs/apidocs/terra.rst @@ -36,6 +36,7 @@ Qiskit Terra API Reference transpiler_passes transpiler_preset transpiler_plugins + transpiler_synthesis_plugins transpiler_builtin_plugins utils utils_mitigation diff --git a/docs/apidocs/transpiler_plugins.rst b/docs/apidocs/transpiler_plugins.rst index 33646fe2d58f..b5de3efc8ff6 100644 --- a/docs/apidocs/transpiler_plugins.rst +++ b/docs/apidocs/transpiler_plugins.rst @@ -1,6 +1,6 @@ .. _qiskit-transpiler-plugins: -.. automodule:: qiskit.transpiler.passes.synthesis.plugin +.. automodule:: qiskit.transpiler.preset_passmanagers.plugin :no-members: :no-inherited-members: :no-special-members: diff --git a/docs/apidocs/transpiler_synthesis_plugins.rst b/docs/apidocs/transpiler_synthesis_plugins.rst new file mode 100644 index 000000000000..70bef1190f40 --- /dev/null +++ b/docs/apidocs/transpiler_synthesis_plugins.rst @@ -0,0 +1,6 @@ +.. _qiskit-transpiler-synthesis-plugins: + +.. automodule:: qiskit.transpiler.passes.synthesis.plugin + :no-members: + :no-inherited-members: + :no-special-members: diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index ac9ccc7c0db4..d5545e9fdda6 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -80,6 +80,8 @@ def transpile( unitary_synthesis_method: str = "default", unitary_synthesis_plugin_config: dict = None, target: Target = None, + init_method: str = None, + optimization_method: str = None, ) -> Union[QuantumCircuit, List[QuantumCircuit]]: """Transpile one or more circuits, according to some desired transpilation targets. @@ -150,17 +152,29 @@ def transpile( [qr[0], None, None, qr[1], None, qr[2]] - layout_method: Name of layout selection pass ('trivial', 'dense', 'noise_adaptive', 'sabre') + layout_method: Name of layout selection pass ('trivial', 'dense', 'noise_adaptive', 'sabre'). + This can also be the external plugin name to use for the ``layout`` stage. + You can see a list of installed plugins by using :func:`~.list_stage_plugins` with + ``"layout"`` for the ``stage_name`` argument. routing_method: Name of routing pass ('basic', 'lookahead', 'stochastic', 'sabre', 'toqm', 'none'). Note that to use method 'toqm', package 'qiskit-toqm' must be installed. + This can also be the external plugin name to use for the ``routing`` stage. + You can see a list of installed plugins by using :func:`~.list_stage_plugins` with + ``"routing"`` for the ``stage_name`` argument. translation_method: Name of translation pass ('unroller', 'translator', 'synthesis') + This can also be the external plugin name to use for the ``translation`` stage. + You can see a list of installed plugins by using :func:`~.list_stage_plugins` with + ``"translation"`` for the ``stage_name`` argument. scheduling_method: Name of scheduling pass. * ``'as_soon_as_possible'``: Schedule instructions greedily, as early as possible on a qubit resource. (alias: ``'asap'``) * ``'as_late_as_possible'``: Schedule instructions late, i.e. keeping qubits in the ground state when possible. (alias: ``'alap'``) - If ``None``, no scheduling will be done. + If ``None``, no scheduling will be done. This can also be the external plugin name + to use for the ``scheduling`` stage. You can see a list of installed plugins by + using :func:`~.list_stage_plugins` with ``"scheduling"`` for the ``stage_name`` + argument. instruction_durations: Durations of instructions. Applicable only if scheduling_method is specified. The gate lengths defined in ``backend.properties`` are used as default. @@ -246,6 +260,16 @@ def callback_func(**kwargs): the ``backend`` argument, but if you have manually constructed a :class:`~qiskit.transpiler.Target` object you can specify it manually here. This will override the target from ``backend``. + init_method: The plugin name to use for the ``init`` stage. By default an external + plugin is not used. You can see a list of installed plugins by + using :func:`~.list_stage_plugins` with ``"init"`` for the stage + name argument. + optimization_method: The plugin name to use for the + ``optimization`` stage. By default an external + plugin is not used. You can see a list of installed plugins by + using :func:`~.list_stage_plugins` with ``"optimization"`` for the + ``stage_name`` argument. + Returns: The transpiled circuit(s). @@ -310,6 +334,8 @@ def callback_func(**kwargs): unitary_synthesis_method, unitary_synthesis_plugin_config, target, + init_method, + optimization_method, ) # Get transpile_args to configure the circuit transpilation job(s) if coupling_map in unique_transpile_args: @@ -392,7 +418,7 @@ def _log_transpile_time(start_time, end_time): def _combine_args(shared_transpiler_args, unique_config): # Pop optimization_level to exclude it from the kwargs when building a # PassManagerConfig - level = shared_transpiler_args.pop("optimization_level") + level = shared_transpiler_args.get("optimization_level") pass_manager_config = shared_transpiler_args pass_manager_config.update(unique_config.pop("pass_manager_config")) pass_manager_config = PassManagerConfig(**pass_manager_config) @@ -560,6 +586,8 @@ def _parse_transpile_args( unitary_synthesis_method, unitary_synthesis_plugin_config, target, + init_method, + optimization_method, ) -> Tuple[List[Dict], Dict]: """Resolve the various types of args allowed to the transpile() function through duck typing, overriding args, etc. Refer to the transpile() docstring for details on @@ -627,6 +655,8 @@ def _parse_transpile_args( shared_dict = { "optimization_level": optimization_level, "basis_gates": basis_gates, + "init_method": init_method, + "optimization_method": optimization_method, } list_transpile_args = [] diff --git a/qiskit/transpiler/passes/synthesis/plugin.py b/qiskit/transpiler/passes/synthesis/plugin.py index 33f168319ced..b199e0ffdbe8 100644 --- a/qiskit/transpiler/passes/synthesis/plugin.py +++ b/qiskit/transpiler/passes/synthesis/plugin.py @@ -27,6 +27,10 @@ which enable packages external to qiskit to advertise they include a synthesis plugin. +See :mod:`qiskit.transpiler.preset_passmanagers.plugin` for details on how +to write plugins for transpiler stages. + + Writing Plugins =============== diff --git a/qiskit/transpiler/passmanager_config.py b/qiskit/transpiler/passmanager_config.py index 929d1aa13366..fc412dc4f286 100644 --- a/qiskit/transpiler/passmanager_config.py +++ b/qiskit/transpiler/passmanager_config.py @@ -39,6 +39,9 @@ def __init__( unitary_synthesis_method="default", unitary_synthesis_plugin_config=None, target=None, + init_method=None, + optimization_method=None, + optimization_level=None, ): """Initialize a PassManagerConfig object @@ -50,12 +53,16 @@ def __init__( coupling_map (CouplingMap): Directed graph represented a coupling map. layout_method (str): the pass to use for choosing initial qubit - placement. + placement. This will be the plugin name if an external layout stage + plugin is being used. routing_method (str): the pass to use for routing qubits on the - architecture. + architecture. This will be a plugin name if an external routing stage + plugin is being used. translation_method (str): the pass to use for translating gates to - basis_gates. - scheduling_method (str): the pass to use for scheduling instructions. + basis_gates. This will be a plugin name if an external translation stage + plugin is being used. + scheduling_method (str): the pass to use for scheduling instructions. This will + be a plugin name if an external scheduling stage plugin is being used. instruction_durations (InstructionDurations): Dictionary of duration (in dt) for each instruction. backend_properties (BackendProperties): Properties returned by a @@ -70,14 +77,20 @@ def __init__( :class:`~qiskit.transpiler.passes.UnitarySynthesis` pass. Will search installed plugins for a valid method. target (Target): The backend target + init_method (str): The plugin name for the init stage plugin to use + optimization_method (str): The plugin name for the optimization stage plugin + to use. + optimization_level (int): The optimization level being used for compilation. """ self.initial_layout = initial_layout self.basis_gates = basis_gates self.inst_map = inst_map self.coupling_map = coupling_map + self.init_method = init_method self.layout_method = layout_method self.routing_method = routing_method self.translation_method = translation_method + self.optimization_method = optimization_method self.scheduling_method = scheduling_method self.instruction_durations = instruction_durations self.backend_properties = backend_properties @@ -87,6 +100,7 @@ def __init__( self.unitary_synthesis_method = unitary_synthesis_method self.unitary_synthesis_plugin_config = unitary_synthesis_plugin_config self.target = target + self.optimization_level = optimization_level @classmethod def from_backend(cls, backend, **pass_manager_options): diff --git a/qiskit/transpiler/preset_passmanagers/__init__.py b/qiskit/transpiler/preset_passmanagers/__init__.py index eb2f0d2aed39..0af4e40dd031 100644 --- a/qiskit/transpiler/preset_passmanagers/__init__.py +++ b/qiskit/transpiler/preset_passmanagers/__init__.py @@ -55,6 +55,8 @@ def generate_preset_pass_manager( seed_transpiler=None, unitary_synthesis_method="default", unitary_synthesis_plugin_config=None, + init_method=None, + optimization_method=None, ): """Generate a preset :class:`~.PassManager` @@ -103,18 +105,30 @@ def generate_preset_pass_manager( layout_method (str): The :class:`~.Pass` to use for choosing initial qubit placement. Valid choices are ``'trivial'``, ``'dense'``, ``'noise_adaptive'``, and, ``'sabre'`` repsenting :class:`~.TrivialLayout`, :class:`~DenseLayout`, - :class:`~.NoiseAdaptiveLayout`, :class:`~.SabreLayout` respectively. + :class:`~.NoiseAdaptiveLayout`, :class:`~.SabreLayout` respectively. This can also + be the external plugin name to use for the ``layout`` stage of the output + :class:`~.StagedPassManager`. You can see a list of installed plugins by using + :func:`~.list_stage_plugins` with ``"layout"`` for the ``stage_name`` argument. routing_method (str): The pass to use for routing qubits on the architecture. Valid choices are ``'basic'``, ``'lookahead'``, ``'stochastic'``, ``'sabre'``, and ``'none'`` representing :class:`~.BasicSwap`, :class:`~.LookaheadSwap`, :class:`~.StochasticSwap`, :class:`~.SabreSwap`, and - erroring if routing is required respectively. + erroring if routing is required respectively. This can also be the external plugin + name to use for the ``routing`` stage of the output :class:`~.StagedPassManager`. + You can see a list of installed plugins by using :func:`~.list_stage_plugins` with + ``"routing"`` for the ``stage_name`` argument. translation_method (str): The method to use for translating gates to basis gates. Valid choices ``'unroller'``, ``'translator'``, ``'synthesis'`` representing :class:`~.Unroller`, :class:`~.BasisTranslator`, and - :class:`~.UnitarySynthesis` respectively. + :class:`~.UnitarySynthesis` respectively. This can also be the external plugin + name to use for the ``translation`` stage of the output :class:`~.StagedPassManager`. + You can see a list of installed plugins by using :func:`~.list_stage_plugins` with + ``"translation"`` for the ``stage_name`` argument. scheduling_method (str): The pass to use for scheduling instructions. Valid choices - are ``'alap'`` and ``'asap'``. + are ``'alap'`` and ``'asap'``. This can also be the external plugin name to use + for the ``scheduling`` stage of the output :class:`~.StagedPassManager`. You can + see a list of installed plugins by using :func:`~.list_stage_plugins` with + ``"scheduling"`` for the ``stage_name`` argument. backend_properties (BackendProperties): Properties returned by a backend, including information on gate errors, readout errors, qubit coherence times, etc. @@ -134,6 +148,17 @@ def generate_preset_pass_manager( the ``unitary_synthesis`` argument. As this is custom for each unitary synthesis plugin refer to the plugin documentation for how to use this option. + init_method (str): The plugin name to use for the ``init`` stage of + the output :class:`~.StagedPassManager`. By default an external + plugin is not used. You can see a list of installed plugins by + using :func:`~.list_stage_plugins` with ``"init"`` for the stage + name argument. + optimization_method (str): The plugin name to use for the + ``optimization`` stage of the output + :class:`~.StagedPassManager`. By default an external + plugin is not used. You can see a list of installed plugins by + using :func:`~.list_stage_plugins` with ``"optimization"`` for the + ``stage_name`` argument. Returns: StagedPassManager: The preset pass manager for the given options @@ -172,6 +197,9 @@ def generate_preset_pass_manager( unitary_synthesis_method=unitary_synthesis_method, unitary_synthesis_plugin_config=unitary_synthesis_plugin_config, initial_layout=initial_layout, + init_method=init_method, + optimization_method=optimization_method, + optimization_level=optimization_level, ) if backend is not None: diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py new file mode 100644 index 000000000000..d1fc2b5e67c7 --- /dev/null +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -0,0 +1,273 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""Built-in transpiler stage plugins for preset pass managers.""" + +from qiskit.transpiler.passmanager import PassManager +from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.passes import BasicSwap +from qiskit.transpiler.passes import LookaheadSwap +from qiskit.transpiler.passes import StochasticSwap +from qiskit.transpiler.passes import SabreSwap +from qiskit.transpiler.passes import Error +from qiskit.transpiler.preset_passmanagers import common +from qiskit.transpiler.preset_passmanagers.plugin import PassManagerStagePlugin + + +class BasicSwapPassManager(PassManagerStagePlugin): + """Plugin class for routing stage with :class:`~.BasicSwap`""" + + def pass_manager(self, pass_manager_config, optimization_level=None) -> PassManager: + """Build routing stage PassManager.""" + seed_transpiler = pass_manager_config.seed_transpiler + target = pass_manager_config.target + coupling_map = pass_manager_config.coupling_map + backend_properties = pass_manager_config.backend_properties + routing_pass = BasicSwap(coupling_map) + vf2_call_limit = common.get_vf2_call_limit( + optimization_level, + pass_manager_config.layout_method, + pass_manager_config.initial_layout, + ) + if optimization_level == 0: + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=True, + ) + if optimization_level == 1: + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + check_trivial=True, + use_barrier_before_measurement=True, + ) + if optimization_level == 2: + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=True, + ) + if optimization_level == 3: + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=True, + ) + raise TranspilerError(f"Invalid optimization level specified: {optimization_level}") + + +class StochasticSwapPassManager(PassManagerStagePlugin): + """Plugin class for routing stage with :class:`~.StochasticSwap`""" + + def pass_manager(self, pass_manager_config, optimization_level=None) -> PassManager: + """Build routing stage PassManager.""" + seed_transpiler = pass_manager_config.seed_transpiler + target = pass_manager_config.target + coupling_map = pass_manager_config.coupling_map + backend_properties = pass_manager_config.backend_properties + vf2_call_limit = common.get_vf2_call_limit( + optimization_level, + pass_manager_config.layout_method, + pass_manager_config.initial_layout, + ) + if optimization_level == 3: + routing_pass = StochasticSwap(coupling_map, trials=200, seed=seed_transpiler) + else: + routing_pass = StochasticSwap(coupling_map, trials=20, seed=seed_transpiler) + + if optimization_level == 0: + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=True, + ) + if optimization_level == 1: + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + check_trivial=True, + use_barrier_before_measurement=True, + ) + if optimization_level in {2, 3}: + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=True, + ) + raise TranspilerError(f"Invalid optimization level specified: {optimization_level}") + + +class LookaheadSwapPassManager(PassManagerStagePlugin): + """Plugin class for routing stage with :class:`~.LookaheadSwap`""" + + def pass_manager(self, pass_manager_config, optimization_level=None) -> PassManager: + """Build routing stage PassManager.""" + seed_transpiler = pass_manager_config.seed_transpiler + target = pass_manager_config.target + coupling_map = pass_manager_config.coupling_map + backend_properties = pass_manager_config.backend_properties + vf2_call_limit = common.get_vf2_call_limit( + optimization_level, + pass_manager_config.layout_method, + pass_manager_config.initial_layout, + ) + if optimization_level == 0: + routing_pass = LookaheadSwap(coupling_map, search_depth=2, search_width=2) + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=True, + ) + if optimization_level == 1: + routing_pass = LookaheadSwap(coupling_map, search_depth=4, search_width=4) + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + check_trivial=True, + use_barrier_before_measurement=True, + ) + if optimization_level == 2: + routing_pass = LookaheadSwap(coupling_map, search_depth=5, search_width=6) + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=True, + ) + if optimization_level == 3: + routing_pass = LookaheadSwap(coupling_map, search_depth=5, search_width=6) + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=True, + ) + raise TranspilerError(f"Invalid optimization level specified: {optimization_level}") + + +class SabreSwapPassManager(PassManagerStagePlugin): + """Plugin class for routing stage with :class:`~.SabreSwap`""" + + def pass_manager(self, pass_manager_config, optimization_level=None) -> PassManager: + """Build routing stage PassManager.""" + seed_transpiler = pass_manager_config.seed_transpiler + target = pass_manager_config.target + coupling_map = pass_manager_config.coupling_map + backend_properties = pass_manager_config.backend_properties + vf2_call_limit = common.get_vf2_call_limit( + optimization_level, + pass_manager_config.layout_method, + pass_manager_config.initial_layout, + ) + if optimization_level == 0: + routing_pass = SabreSwap(coupling_map, heuristic="basic", seed=seed_transpiler) + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=True, + ) + if optimization_level == 1: + routing_pass = SabreSwap(coupling_map, heuristic="lookahead", seed=seed_transpiler) + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + check_trivial=True, + use_barrier_before_measurement=True, + ) + if optimization_level == 2: + routing_pass = SabreSwap(coupling_map, heuristic="decay", seed=seed_transpiler) + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=True, + ) + if optimization_level == 3: + routing_pass = SabreSwap(coupling_map, heuristic="decay", seed=seed_transpiler) + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=True, + ) + raise TranspilerError(f"Invalid optimization level specified: {optimization_level}") + + +class NoneRoutingPassManager(PassManagerStagePlugin): + """Plugin class for routing stage with error on routing.""" + + def pass_manager(self, pass_manager_config, optimization_level=None) -> PassManager: + """Build routing stage PassManager.""" + seed_transpiler = pass_manager_config.seed_transpiler + target = pass_manager_config.target + coupling_map = pass_manager_config.coupling_map + routing_pass = Error( + msg="No routing method selected, but circuit is not routed to device. " + "CheckMap Error: {check_map_msg}", + action="raise", + ) + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=True, + ) diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index d66037e616b9..7e5513316042 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -14,6 +14,8 @@ """Common preset passmanager generators.""" +from typing import Optional + from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary as sel from qiskit.transpiler.passmanager import PassManager @@ -47,6 +49,7 @@ from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason from qiskit.transpiler.passes.layout.vf2_post_layout import VF2PostLayoutStopReason from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.layout import Layout def generate_unroll_3q( @@ -391,3 +394,20 @@ def _require_alignment(property_set): scheduling.append(PadDelay()) return scheduling + + +def get_vf2_call_limit( + optimization_level: int, + layout_method: Optional[str] = None, + initial_layout: Optional[Layout] = None, +) -> Optional[int]: + """Get the vf2 call limit for vf2 based layout passes.""" + vf2_call_limit = None + if layout_method is None and initial_layout is None: + if optimization_level == 1: + vf2_call_limit = int(5e4) # Set call limit to ~100ms with retworkx 0.10.2 + elif optimization_level == 2: + vf2_call_limit = int(5e6) # Set call limit to ~10 sec with retworkx 0.10.2 + elif optimization_level == 3: + vf2_call_limit = int(3e7) # Set call limit to ~60 sec with retworkx 0.10.2 + return vf2_call_limit diff --git a/qiskit/transpiler/preset_passmanagers/level0.py b/qiskit/transpiler/preset_passmanagers/level0.py index 37aadc1c4452..c02c517a23e1 100644 --- a/qiskit/transpiler/preset_passmanagers/level0.py +++ b/qiskit/transpiler/preset_passmanagers/level0.py @@ -25,12 +25,11 @@ from qiskit.transpiler.passes import DenseLayout from qiskit.transpiler.passes import NoiseAdaptiveLayout from qiskit.transpiler.passes import SabreLayout -from qiskit.transpiler.passes import BasicSwap -from qiskit.transpiler.passes import LookaheadSwap -from qiskit.transpiler.passes import StochasticSwap -from qiskit.transpiler.passes import SabreSwap -from qiskit.transpiler.passes import Error from qiskit.transpiler.preset_passmanagers import common +from qiskit.transpiler.preset_passmanagers.plugin import ( + PassManagerStagePluginManager, + list_stage_plugins, +) from qiskit.transpiler import TranspilerError from qiskit.utils.optionals import HAS_TOQM @@ -54,13 +53,16 @@ def level_0_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa Raises: TranspilerError: if the passmanager config is invalid. """ + plugin_manager = PassManagerStagePluginManager() basis_gates = pass_manager_config.basis_gates inst_map = pass_manager_config.inst_map coupling_map = pass_manager_config.coupling_map initial_layout = pass_manager_config.initial_layout + init_method = pass_manager_config.init_method layout_method = pass_manager_config.layout_method or "trivial" routing_method = pass_manager_config.routing_method or "stochastic" translation_method = pass_manager_config.translation_method or "translator" + optimization_method = pass_manager_config.optimization_method scheduling_method = pass_manager_config.scheduling_method instruction_durations = pass_manager_config.instruction_durations seed_transpiler = pass_manager_config.seed_transpiler @@ -85,20 +87,11 @@ def _choose_layout_condition(property_set): _choose_layout = NoiseAdaptiveLayout(backend_properties) elif layout_method == "sabre": _choose_layout = SabreLayout(coupling_map, max_iterations=1, seed=seed_transpiler) - else: - raise TranspilerError("Invalid layout method %s." % layout_method) toqm_pass = False # Choose routing pass - if routing_method == "basic": - routing_pass = BasicSwap(coupling_map) - elif routing_method == "stochastic": - routing_pass = StochasticSwap(coupling_map, trials=20, seed=seed_transpiler) - elif routing_method == "lookahead": - routing_pass = LookaheadSwap(coupling_map, search_depth=2, search_width=2) - elif routing_method == "sabre": - routing_pass = SabreSwap(coupling_map, heuristic="basic", seed=seed_transpiler) - elif routing_method == "toqm": + # TODO: Remove when qiskit-toqm has it's own plugin and we can rely on just the plugin interface + if routing_method == "toqm" and "toqm" not in list_stage_plugins("routing"): HAS_TOQM.require_now("TOQM-based routing") from qiskit_toqm import ToqmSwap, ToqmStrategyO0, latencies_from_target @@ -116,14 +109,17 @@ def _choose_layout_condition(property_set): ) ), ) - elif routing_method == "none": - routing_pass = Error( - msg="No routing method selected, but circuit is not routed to device. " - "CheckMap Error: {check_map_msg}", - action="raise", + routing_pm = common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=not toqm_pass, ) else: - raise TranspilerError("Invalid routing method %s." % routing_method) + routing_pm = plugin_manager.get_passmanager_stage( + "routing", routing_method, pass_manager_config, optimization_level=0 + ) unroll_3q = None # Build pass manager @@ -135,30 +131,34 @@ def _choose_layout_condition(property_set): unitary_synthesis_method, unitary_synthesis_plugin_config, ) - layout = PassManager() - layout.append(_given_layout) - layout.append(_choose_layout, condition=_choose_layout_condition) - layout += common.generate_embed_passmanager(coupling_map) - routing = common.generate_routing_passmanager( - routing_pass, - target, - coupling_map=coupling_map, - seed_transpiler=seed_transpiler, - use_barrier_before_measurement=not toqm_pass, - ) + if layout_method not in {"trivial", "dense", "noise_adaptive", "sabre"}: + layout = plugin_manager.get_passmanager_stage( + "layout", layout_method, pass_manager_config, optimization_level=0 + ) + else: + layout = PassManager() + layout.append(_given_layout) + layout.append(_choose_layout, condition=_choose_layout_condition) + layout += common.generate_embed_passmanager(coupling_map) + routing = routing_pm else: layout = None routing = None - translation = common.generate_translation_passmanager( - target, - basis_gates, - translation_method, - approximation_degree, - coupling_map, - backend_properties, - unitary_synthesis_method, - unitary_synthesis_plugin_config, - ) + if translation_method not in {"translator", "synthesis", "unroller"}: + translation = plugin_manager.get_passmanager_stage( + "translation", translation_method, pass_manager_config, optimization_level=0 + ) + else: + translation = common.generate_translation_passmanager( + target, + basis_gates, + translation_method, + approximation_degree, + coupling_map, + backend_properties, + unitary_synthesis_method, + unitary_synthesis_plugin_config, + ) pre_routing = None if toqm_pass: pre_routing = translation @@ -170,16 +170,33 @@ def _choose_layout_condition(property_set): pre_opt += translation else: pre_opt = None - sched = common.generate_scheduling( - instruction_durations, scheduling_method, timing_constraints, inst_map - ) + if scheduling_method is None or scheduling_method in {"alap", "asap"}: + sched = common.generate_scheduling( + instruction_durations, scheduling_method, timing_constraints, inst_map + ) + else: + sched = plugin_manager.get_passmanager_stage( + "scheduling", scheduling_method, pass_manager_config, optimization_level=0 + ) + if init_method is not None: + init = plugin_manager.get_passmanager_stage( + "init", init_method, pass_manager_config, optimization_level=0 + ) + else: + init = unroll_3q + optimization = None + if optimization_method is not None: + optimization = plugin_manager.get_passmanager_stage( + "optimization", optimization_method, pass_manager_config, optimization_level=0 + ) return StagedPassManager( - init=unroll_3q, + init=init, layout=layout, pre_routing=pre_routing, routing=routing, translation=translation, pre_optimization=pre_opt, + optimization=optimization, scheduling=sched, ) diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index 5da52fb23052..05e7d56a736e 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -27,21 +27,20 @@ from qiskit.transpiler.passes import DenseLayout from qiskit.transpiler.passes import NoiseAdaptiveLayout from qiskit.transpiler.passes import SabreLayout -from qiskit.transpiler.passes import BasicSwap -from qiskit.transpiler.passes import LookaheadSwap -from qiskit.transpiler.passes import StochasticSwap -from qiskit.transpiler.passes import SabreSwap from qiskit.transpiler.passes import FixedPoint from qiskit.transpiler.passes import Depth from qiskit.transpiler.passes import Size from qiskit.transpiler.passes import Optimize1qGatesDecomposition from qiskit.transpiler.passes import Layout2qDistance -from qiskit.transpiler.passes import Error from qiskit.transpiler.preset_passmanagers import common from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason from qiskit.transpiler import TranspilerError from qiskit.utils.optionals import HAS_TOQM +from qiskit.transpiler.preset_passmanagers.plugin import ( + PassManagerStagePluginManager, + list_stage_plugins, +) def level_1_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassManager: @@ -65,13 +64,16 @@ def level_1_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa Raises: TranspilerError: if the passmanager config is invalid. """ + plugin_manager = PassManagerStagePluginManager() basis_gates = pass_manager_config.basis_gates inst_map = pass_manager_config.inst_map coupling_map = pass_manager_config.coupling_map initial_layout = pass_manager_config.initial_layout layout_method = pass_manager_config.layout_method or "dense" + init_method = pass_manager_config.init_method routing_method = pass_manager_config.routing_method or "stochastic" translation_method = pass_manager_config.translation_method or "translator" + optimization_method = pass_manager_config.optimization_method scheduling_method = pass_manager_config.scheduling_method instruction_durations = pass_manager_config.instruction_durations seed_transpiler = pass_manager_config.seed_transpiler @@ -143,19 +145,11 @@ def _vf2_match_not_found(property_set): _improve_layout = NoiseAdaptiveLayout(backend_properties) elif layout_method == "sabre": _improve_layout = SabreLayout(coupling_map, max_iterations=2, seed=seed_transpiler) - else: - raise TranspilerError("Invalid layout method %s." % layout_method) toqm_pass = False - if routing_method == "basic": - routing_pass = BasicSwap(coupling_map) - elif routing_method == "stochastic": - routing_pass = StochasticSwap(coupling_map, trials=20, seed=seed_transpiler) - elif routing_method == "lookahead": - routing_pass = LookaheadSwap(coupling_map, search_depth=4, search_width=4) - elif routing_method == "sabre": - routing_pass = SabreSwap(coupling_map, heuristic="lookahead", seed=seed_transpiler) - elif routing_method == "toqm": + routing_pm = None + # TODO: Remove when qiskit-toqm has it's own plugin and we can rely on just the plugin interface + if routing_method == "toqm" and "toqm" not in list_stage_plugins("routing"): HAS_TOQM.require_now("TOQM-based routing") from qiskit_toqm import ToqmSwap, ToqmStrategyO1, latencies_from_target @@ -173,14 +167,26 @@ def _vf2_match_not_found(property_set): ) ), ) - elif routing_method == "none": - routing_pass = Error( - msg="No routing method selected, but circuit is not routed to device. " - "CheckMap Error: {check_map_msg}", - action="raise", + vf2_call_limit = common.get_vf2_call_limit( + 1, pass_manager_config.layout_method, pass_manager_config.initial_layout + ) + routing_pm = common.generate_routing_passmanager( + routing_pass, + target, + coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + check_trivial=True, + use_barrier_before_measurement=not toqm_pass, ) else: - raise TranspilerError("Invalid routing method %s." % routing_method) + routing_pm = plugin_manager.get_passmanager_stage( + "routing", + routing_method, + pass_manager_config, + optimization_level=1, + ) # Build optimization loop: merge 1q rotations and cancel CNOT gates iteratively # until no more change in depth @@ -202,38 +208,38 @@ def _opt_control(property_set): unitary_synthesis_method, unitary_synthesis_plugin_config, ) - layout = PassManager() - layout.append(_given_layout) - layout.append(_choose_layout_0, condition=_choose_layout_condition) - layout.append(_choose_layout_1, condition=_trivial_not_perfect) - layout.append(_improve_layout, condition=_vf2_match_not_found) - layout += common.generate_embed_passmanager(coupling_map) - vf2_call_limit = None - if pass_manager_config.layout_method is None and pass_manager_config.initial_layout is None: - vf2_call_limit = int(5e4) # Set call limit to ~100ms with retworkx 0.10.2 - routing = common.generate_routing_passmanager( - routing_pass, - target, - coupling_map, - vf2_call_limit=vf2_call_limit, - backend_properties=backend_properties, - seed_transpiler=seed_transpiler, - check_trivial=True, - use_barrier_before_measurement=not toqm_pass, - ) + if layout_method not in {"trivial", "dense", "noise_adaptive", "sabre"}: + layout = plugin_manager.get_passmanager_stage( + "layout", layout_method, pass_manager_config, optimization_level=1 + ) + else: + layout = PassManager() + layout.append(_given_layout) + layout.append(_choose_layout_0, condition=_choose_layout_condition) + layout.append(_choose_layout_1, condition=_trivial_not_perfect) + layout.append(_improve_layout, condition=_vf2_match_not_found) + layout += common.generate_embed_passmanager(coupling_map) + + routing = routing_pm + else: layout = None routing = None - translation = common.generate_translation_passmanager( - target, - basis_gates, - translation_method, - approximation_degree, - coupling_map, - backend_properties, - unitary_synthesis_method, - unitary_synthesis_plugin_config, - ) + if translation_method not in {"translator", "synthesis", "unroller"}: + translation = plugin_manager.get_passmanager_stage( + "translation", translation_method, pass_manager_config, optimization_level=1 + ) + else: + translation = common.generate_translation_passmanager( + target, + basis_gates, + translation_method, + approximation_degree, + coupling_map, + backend_properties, + unitary_synthesis_method, + unitary_synthesis_plugin_config, + ) pre_routing = None if toqm_pass: pre_routing = translation @@ -241,20 +247,38 @@ def _opt_control(property_set): if (coupling_map and not coupling_map.is_symmetric) or ( target is not None and target.get_non_global_operation_names(strict_direction=True) ): - pre_optimization = common.generate_pre_op_passmanager(target, coupling_map, True) + pre_optimization = common.generate_pre_op_passmanager( + target, coupling_map, remove_reset_in_zero=True + ) else: pre_optimization = common.generate_pre_op_passmanager(remove_reset_in_zero=True) - optimization = PassManager() - unroll = [pass_ for x in translation.passes() for pass_ in x["passes"]] - optimization.append(_depth_check + _size_check) - opt_loop = _opt + unroll + _depth_check + _size_check - optimization.append(opt_loop, do_while=_opt_control) - sched = common.generate_scheduling( - instruction_durations, scheduling_method, timing_constraints, inst_map - ) + if optimization_method is None: + optimization = PassManager() + unroll = [pass_ for x in translation.passes() for pass_ in x["passes"]] + optimization.append(_depth_check + _size_check) + opt_loop = _opt + unroll + _depth_check + _size_check + optimization.append(opt_loop, do_while=_opt_control) + else: + optimization = plugin_manager.get_passmanager_stage( + "optimization", optimization_method, pass_manager_config, optimization_level=1 + ) + if scheduling_method is None or scheduling_method in {"alap", "asap"}: + sched = common.generate_scheduling( + instruction_durations, scheduling_method, timing_constraints, inst_map + ) + else: + sched = plugin_manager.get_passmanager_stage( + "scheduling", scheduling_method, pass_manager_config, optimization_level=1 + ) + if init_method is not None: + init = plugin_manager.get_passmanager_stage( + "init", init_method, pass_manager_config, optimization_level=1 + ) + else: + init = unroll_3q return StagedPassManager( - init=unroll_3q, + init=init, layout=layout, pre_routing=pre_routing, routing=routing, diff --git a/qiskit/transpiler/preset_passmanagers/level2.py b/qiskit/transpiler/preset_passmanagers/level2.py index c2ee13ad8500..5a8dcde691e8 100644 --- a/qiskit/transpiler/preset_passmanagers/level2.py +++ b/qiskit/transpiler/preset_passmanagers/level2.py @@ -27,21 +27,20 @@ from qiskit.transpiler.passes import DenseLayout from qiskit.transpiler.passes import NoiseAdaptiveLayout from qiskit.transpiler.passes import SabreLayout -from qiskit.transpiler.passes import BasicSwap -from qiskit.transpiler.passes import LookaheadSwap -from qiskit.transpiler.passes import StochasticSwap -from qiskit.transpiler.passes import SabreSwap from qiskit.transpiler.passes import FixedPoint from qiskit.transpiler.passes import Depth from qiskit.transpiler.passes import Size from qiskit.transpiler.passes import Optimize1qGatesDecomposition from qiskit.transpiler.passes import CommutativeCancellation -from qiskit.transpiler.passes import Error from qiskit.transpiler.preset_passmanagers import common from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason from qiskit.transpiler import TranspilerError from qiskit.utils.optionals import HAS_TOQM +from qiskit.transpiler.preset_passmanagers.plugin import ( + PassManagerStagePluginManager, + list_stage_plugins, +) def level_2_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassManager: @@ -67,13 +66,16 @@ def level_2_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa Raises: TranspilerError: if the passmanager config is invalid. """ + plugin_manager = PassManagerStagePluginManager() basis_gates = pass_manager_config.basis_gates inst_map = pass_manager_config.inst_map coupling_map = pass_manager_config.coupling_map initial_layout = pass_manager_config.initial_layout + init_method = pass_manager_config.init_method layout_method = pass_manager_config.layout_method or "dense" routing_method = pass_manager_config.routing_method or "stochastic" translation_method = pass_manager_config.translation_method or "translator" + optimization_method = pass_manager_config.optimization_method scheduling_method = pass_manager_config.scheduling_method instruction_durations = pass_manager_config.instruction_durations seed_transpiler = pass_manager_config.seed_transpiler @@ -126,19 +128,11 @@ def _vf2_match_not_found(property_set): _choose_layout_1 = NoiseAdaptiveLayout(backend_properties) elif layout_method == "sabre": _choose_layout_1 = SabreLayout(coupling_map, max_iterations=2, seed=seed_transpiler) - else: - raise TranspilerError("Invalid layout method %s." % layout_method) toqm_pass = False - if routing_method == "basic": - routing_pass = BasicSwap(coupling_map) - elif routing_method == "stochastic": - routing_pass = StochasticSwap(coupling_map, trials=20, seed=seed_transpiler) - elif routing_method == "lookahead": - routing_pass = LookaheadSwap(coupling_map, search_depth=5, search_width=5) - elif routing_method == "sabre": - routing_pass = SabreSwap(coupling_map, heuristic="decay", seed=seed_transpiler) - elif routing_method == "toqm": + routing_pm = None + # TODO: Remove when qiskit-toqm has it's own plugin and we can rely on just the plugin interface + if routing_method == "toqm" and "toqm" not in list_stage_plugins("routing"): HAS_TOQM.require_now("TOQM-based routing") from qiskit_toqm import ToqmSwap, ToqmStrategyO2, latencies_from_target @@ -156,14 +150,22 @@ def _vf2_match_not_found(property_set): ) ), ) - elif routing_method == "none": - routing_pass = Error( - msg="No routing method selected, but circuit is not routed to device. " - "CheckMap Error: {check_map_msg}", - action="raise", + vf2_call_limit = common.get_vf2_call_limit( + 2, pass_manager_config.layout_method, pass_manager_config.initial_layout + ) + routing_pm = common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=not toqm_pass, ) else: - raise TranspilerError("Invalid routing method %s." % routing_method) + routing_pm = plugin_manager.get_passmanager_stage( + "routing", routing_method, pass_manager_config, optimization_level=2 + ) # Build optimization loop: 1q rotation merge and commutative cancellation iteratively until # no more change in depth @@ -188,36 +190,35 @@ def _opt_control(property_set): unitary_synthesis_method, unitary_synthesis_plugin_config, ) - layout = PassManager() - layout.append(_given_layout) - layout.append(_choose_layout_0, condition=_choose_layout_condition) - layout.append(_choose_layout_1, condition=_vf2_match_not_found) - layout += common.generate_embed_passmanager(coupling_map) - vf2_call_limit = None - if pass_manager_config.layout_method is None and pass_manager_config.initial_layout is None: - vf2_call_limit = int(5e6) # Set call limit to ~10 sec with retworkx 0.10.2 - routing = common.generate_routing_passmanager( - routing_pass, - target, - coupling_map=coupling_map, - vf2_call_limit=vf2_call_limit, - backend_properties=backend_properties, - seed_transpiler=seed_transpiler, - use_barrier_before_measurement=not toqm_pass, - ) + if layout_method not in {"trivial", "dense", "noise_adaptive", "sabre"}: + layout = plugin_manager.get_passmanager_stage( + "layout", layout_method, pass_manager_config, optimization_level=2 + ) + else: + layout = PassManager() + layout.append(_given_layout) + layout.append(_choose_layout_0, condition=_choose_layout_condition) + layout.append(_choose_layout_1, condition=_vf2_match_not_found) + layout += common.generate_embed_passmanager(coupling_map) + routing = routing_pm else: layout = None routing = None - translation = common.generate_translation_passmanager( - target, - basis_gates, - translation_method, - approximation_degree, - coupling_map, - backend_properties, - unitary_synthesis_method, - unitary_synthesis_plugin_config, - ) + if translation_method not in {"translator", "synthesis", "unroller"}: + translation = plugin_manager.get_passmanager_stage( + "translation", translation_method, pass_manager_config, optimization_level=2 + ) + else: + translation = common.generate_translation_passmanager( + target, + basis_gates, + translation_method, + approximation_degree, + coupling_map, + backend_properties, + unitary_synthesis_method, + unitary_synthesis_plugin_config, + ) pre_routing = None if toqm_pass: pre_routing = translation @@ -227,16 +228,33 @@ def _opt_control(property_set): pre_optimization = common.generate_pre_op_passmanager(target, coupling_map, True) else: pre_optimization = common.generate_pre_op_passmanager(remove_reset_in_zero=True) - optimization = PassManager() - unroll = [pass_ for x in translation.passes() for pass_ in x["passes"]] - optimization.append(_depth_check + _size_check) - opt_loop = _opt + unroll + _depth_check + _size_check - optimization.append(opt_loop, do_while=_opt_control) - sched = common.generate_scheduling( - instruction_durations, scheduling_method, timing_constraints, inst_map - ) + if optimization_method is None: + optimization = PassManager() + unroll = [pass_ for x in translation.passes() for pass_ in x["passes"]] + optimization.append(_depth_check + _size_check) + opt_loop = _opt + unroll + _depth_check + _size_check + optimization.append(opt_loop, do_while=_opt_control) + else: + optimization = plugin_manager.get_passmanager_stage( + "optimization", optimization_method, pass_manager_config, optimization_level=2 + ) + if scheduling_method is None or scheduling_method in {"alap", "asap"}: + sched = common.generate_scheduling( + instruction_durations, scheduling_method, timing_constraints, inst_map + ) + else: + sched = plugin_manager.get_passmanager_stage( + "scheduling", scheduling_method, pass_manager_config, optimization_level=2 + ) + if init_method is not None: + init = plugin_manager.get_passmanager_stage( + "init", init_method, pass_manager_config, optimization_level=2 + ) + else: + init = unroll_3q + return StagedPassManager( - init=unroll_3q, + init=init, layout=layout, pre_routing=pre_routing, routing=routing, diff --git a/qiskit/transpiler/preset_passmanagers/level3.py b/qiskit/transpiler/preset_passmanagers/level3.py index 7a0f22319ecb..0d664f2172a0 100644 --- a/qiskit/transpiler/preset_passmanagers/level3.py +++ b/qiskit/transpiler/preset_passmanagers/level3.py @@ -28,10 +28,6 @@ from qiskit.transpiler.passes import DenseLayout from qiskit.transpiler.passes import NoiseAdaptiveLayout from qiskit.transpiler.passes import SabreLayout -from qiskit.transpiler.passes import BasicSwap -from qiskit.transpiler.passes import LookaheadSwap -from qiskit.transpiler.passes import StochasticSwap -from qiskit.transpiler.passes import SabreSwap from qiskit.transpiler.passes import FixedPoint from qiskit.transpiler.passes import Depth from qiskit.transpiler.passes import Size @@ -43,10 +39,12 @@ from qiskit.transpiler.passes import Collect2qBlocks from qiskit.transpiler.passes import ConsolidateBlocks from qiskit.transpiler.passes import UnitarySynthesis -from qiskit.transpiler.passes import Error from qiskit.transpiler.preset_passmanagers import common from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason - +from qiskit.transpiler.preset_passmanagers.plugin import ( + PassManagerStagePluginManager, + list_stage_plugins, +) from qiskit.transpiler import TranspilerError from qiskit.utils.optionals import HAS_TOQM @@ -74,13 +72,16 @@ def level_3_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa Raises: TranspilerError: if the passmanager config is invalid. """ + plugin_manager = PassManagerStagePluginManager() basis_gates = pass_manager_config.basis_gates inst_map = pass_manager_config.inst_map coupling_map = pass_manager_config.coupling_map initial_layout = pass_manager_config.initial_layout + init_method = pass_manager_config.init_method layout_method = pass_manager_config.layout_method or "sabre" routing_method = pass_manager_config.routing_method or "sabre" translation_method = pass_manager_config.translation_method or "translator" + optimization_method = pass_manager_config.optimization_method scheduling_method = pass_manager_config.scheduling_method instruction_durations = pass_manager_config.instruction_durations seed_transpiler = pass_manager_config.seed_transpiler @@ -90,6 +91,11 @@ def level_3_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa timing_constraints = pass_manager_config.timing_constraints or TimingConstraints() unitary_synthesis_plugin_config = pass_manager_config.unitary_synthesis_plugin_config target = pass_manager_config.target + # Override an unset optimization_level for stage plugin use. + # it will be restored to None before this is returned + optimization_level = pass_manager_config.optimization_level + if optimization_level is None: + pass_manager_config.optimization_level = 3 # Layout on good qubits if calibration info available, otherwise on dense links _given_layout = SetLayout(initial_layout) @@ -133,19 +139,10 @@ def _vf2_match_not_found(property_set): _choose_layout_1 = NoiseAdaptiveLayout(backend_properties) elif layout_method == "sabre": _choose_layout_1 = SabreLayout(coupling_map, max_iterations=4, seed=seed_transpiler) - else: - raise TranspilerError("Invalid layout method %s." % layout_method) toqm_pass = False - if routing_method == "basic": - routing_pass = BasicSwap(coupling_map) - elif routing_method == "stochastic": - routing_pass = StochasticSwap(coupling_map, trials=200, seed=seed_transpiler) - elif routing_method == "lookahead": - routing_pass = LookaheadSwap(coupling_map, search_depth=5, search_width=6) - elif routing_method == "sabre": - routing_pass = SabreSwap(coupling_map, heuristic="decay", seed=seed_transpiler) - elif routing_method == "toqm": + # TODO: Remove when qiskit-toqm has it's own plugin and we can rely on just the plugin interface + if routing_method == "toqm" and "toqm" not in list_stage_plugins("routing"): HAS_TOQM.require_now("TOQM-based routing") from qiskit_toqm import ToqmSwap, ToqmStrategyO3, latencies_from_target @@ -163,14 +160,22 @@ def _vf2_match_not_found(property_set): ) ), ) - elif routing_method == "none": - routing_pass = Error( - msg="No routing method selected, but circuit is not routed to device. " - "CheckMap Error: {check_map_msg}", - action="raise", + vf2_call_limit = common.get_vf2_call_limit( + 3, pass_manager_config.layout_method, pass_manager_config.initial_layout + ) + routing_pm = common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=not toqm_pass, ) else: - raise TranspilerError("Invalid routing method %s." % routing_method) + routing_pm = plugin_manager.get_passmanager_stage( + "routing", routing_method, pass_manager_config, optimization_level=3 + ) # 8. Optimize iteratively until no more change in depth. Removes useless gates # after reset and before measure, commutes gates and optimizes contiguous blocks. @@ -197,80 +202,107 @@ def _opt_control(property_set): ] # Build pass manager - init = common.generate_unroll_3q( - target, - basis_gates, - approximation_degree, - unitary_synthesis_method, - unitary_synthesis_plugin_config, - ) + if init_method is not None: + init = plugin_manager.get_passmanager_stage( + "init", init_method, pass_manager_config, optimization_level=3 + ) + else: + init = common.generate_unroll_3q( + target, + basis_gates, + approximation_degree, + unitary_synthesis_method, + unitary_synthesis_plugin_config, + ) init.append(RemoveResetInZeroState()) init.append(OptimizeSwapBeforeMeasure()) init.append(RemoveDiagonalGatesBeforeMeasure()) if coupling_map or initial_layout: - layout = PassManager() - layout.append(_given_layout) - layout.append(_choose_layout_0, condition=_choose_layout_condition) - layout.append(_choose_layout_1, condition=_vf2_match_not_found) - layout += common.generate_embed_passmanager(coupling_map) - vf2_call_limit = None - if pass_manager_config.layout_method is None and pass_manager_config.initial_layout is None: - vf2_call_limit = int(3e7) # Set call limit to ~60 sec with retworkx 0.10.2 - routing = common.generate_routing_passmanager( - routing_pass, - target, - coupling_map=coupling_map, - vf2_call_limit=vf2_call_limit, - backend_properties=backend_properties, - seed_transpiler=seed_transpiler, - use_barrier_before_measurement=not toqm_pass, - ) + if layout_method not in {"trivial", "dense", "noise_adaptive", "sabre"}: + layout = plugin_manager.get_passmanager_stage( + "layout", layout_method, pass_manager_config, optimization_level=3 + ) + else: + layout = PassManager() + layout.append(_given_layout) + layout.append(_choose_layout_0, condition=_choose_layout_condition) + layout.append(_choose_layout_1, condition=_vf2_match_not_found) + layout += common.generate_embed_passmanager(coupling_map) + routing = routing_pm else: layout = None routing = None - translation = common.generate_translation_passmanager( - target, - basis_gates, - translation_method, - approximation_degree, - coupling_map, - backend_properties, - unitary_synthesis_method, - unitary_synthesis_plugin_config, - ) + if translation_method not in {"translator", "synthesis", "unroller"}: + translation = plugin_manager.get_passmanager_stage( + "translation", translation_method, pass_manager_config, optimization_level=3 + ) + else: + translation = common.generate_translation_passmanager( + target, + basis_gates, + translation_method, + approximation_degree, + coupling_map, + backend_properties, + unitary_synthesis_method, + unitary_synthesis_plugin_config, + ) pre_routing = None if toqm_pass: pre_routing = translation - optimization = PassManager() - unroll = [pass_ for x in translation.passes() for pass_ in x["passes"]] - optimization.append(_depth_check + _size_check) - if (coupling_map and not coupling_map.is_symmetric) or ( - target is not None and target.get_non_global_operation_names(strict_direction=True) - ): - pre_optimization = common.generate_pre_op_passmanager(target, coupling_map, True) - _direction = [ - pass_ - for x in common.generate_pre_op_passmanager(target, coupling_map).passes() - for pass_ in x["passes"] - ] - # For transpiling to a target we need to run GateDirection in the - # optimization loop to correct for incorrect directions that might be - # inserted by UnitarySynthesis which is direction aware but only via - # the coupling map which with a target doesn't give a full picture - if target is not None: - optimization.append( - _opt + unroll + _depth_check + _size_check + _direction, do_while=_opt_control - ) + if optimization_method is None: + optimization = PassManager() + unroll = [pass_ for x in translation.passes() for pass_ in x["passes"]] + optimization.append(_depth_check + _size_check) + if (coupling_map and not coupling_map.is_symmetric) or ( + target is not None and target.get_non_global_operation_names(strict_direction=True) + ): + pre_optimization = common.generate_pre_op_passmanager(target, coupling_map, True) + _direction = [ + pass_ + for x in common.generate_pre_op_passmanager(target, coupling_map).passes() + for pass_ in x["passes"] + ] + # For transpiling to a target we need to run GateDirection in the + # optimization loop to correct for incorrect directions that might be + # inserted by UnitarySynthesis which is direction aware but only via + # the coupling map which with a target doesn't give a full picture + if target is not None and optimization is not None: + optimization.append( + _opt + unroll + _depth_check + _size_check + _direction, do_while=_opt_control + ) + elif optimization is not None: + optimization.append( + _opt + unroll + _depth_check + _size_check, do_while=_opt_control + ) else: + pre_optimization = common.generate_pre_op_passmanager(remove_reset_in_zero=True) optimization.append(_opt + unroll + _depth_check + _size_check, do_while=_opt_control) + opt_loop = _depth_check + _opt + unroll + optimization.append(opt_loop, do_while=_opt_control) else: - pre_optimization = common.generate_pre_op_passmanager(remove_reset_in_zero=True) - optimization.append(_opt + unroll + _depth_check + _size_check, do_while=_opt_control) - opt_loop = _depth_check + _opt + unroll - optimization.append(opt_loop, do_while=_opt_control) - sched = common.generate_scheduling( - instruction_durations, scheduling_method, timing_constraints, inst_map - ) + optimization = plugin_manager.get_passmanager_stage( + "optimization", optimization_method, pass_manager_config, optimization_level=3 + ) + if (coupling_map and not coupling_map.is_symmetric) or ( + target is not None and target.get_non_global_operation_names(strict_direction=True) + ): + pre_optimization = common.generate_pre_op_passmanager(target, coupling_map, True) + else: + pre_optimization = common.generate_pre_op_passmanager(remove_reset_in_zero=True) + + if scheduling_method is None or scheduling_method in {"alap", "asap"}: + sched = common.generate_scheduling( + instruction_durations, scheduling_method, timing_constraints, inst_map + ) + else: + sched = plugin_manager.get_passmanager_stage( + "scheduling", scheduling_method, pass_manager_config, optimization_level=3 + ) + + # Restore PassManagerConfig optimization_level override + pass_manager_config.optimization_level = optimization_level + return StagedPassManager( init=init, layout=layout, diff --git a/qiskit/transpiler/preset_passmanagers/plugin.py b/qiskit/transpiler/preset_passmanagers/plugin.py new file mode 100644 index 000000000000..a442bbb49383 --- /dev/null +++ b/qiskit/transpiler/preset_passmanagers/plugin.py @@ -0,0 +1,299 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +""" +======================================================================================= +Transpiler Stage Plugin Interface (:mod:`qiskit.transpiler.preset_passmanagers.plugin`) +======================================================================================= + +.. currentmodule:: qiskit.transpiler.preset_passmanagers.plugin + +This module defines the plugin interface for providing custom stage +implementations for the preset pass managers and the :func:`~.transpile` +function. This enables external Python packages to provide +:class:`~.PassManager` objects that can be used for each stage. + +The plugin interfaces are built using setuptools +`entry points `__ +which enable packages external to Qiskit to advertise they include a transpiler stage. + +See :mod:`qiskit.transpiler.passes.synthesis.plugin` for details on how to +write plugins for synthesis methods which are used by the transpiler. + +.. _stage_table: + +Plugin Stages +============= + +Currently there are 6 stages in the preset pass managers used by and corresponding entrypoints. + +.. list-table:: Stages + :header-rows: 1 + + * - Stage Name + - Entry Point + - Reserved Names + - Description and expectations + * - ``init`` + - ``qiskit.transpiler.init`` + - No reserved names + - This stage runs first and is typically used for any initial logical optimization. Because most + layout and routing algorithms are only designed to work with 1 and 2 qubit gates, this stage + is also used to translate any gates that operate on more than 2 qubits into gates that only + operate on 1 or 2 qubits. + * - ``layout`` + - ``qiskit.transpiler.layout`` + - ``trivial``, ``dense``, ``noise_adaptive``, ``sabre`` + - The output from this stage is expected to have the ``layout`` property + set field set with a :class:`~.Layout` object. Additionally, the circuit is + typically expected to be embedded so that it is expanded to include all + qubits and the :class:`~.ApplyLayout` pass is expected to be run to apply the + layout. The embedding of the :class:`~.Layout` can be generated with + :func:`~.generate_embed_passmanager`. + * - ``routing`` + - ``qiskit.transpiler.routing`` + - ``basic``, ``stochastic``, ``lookahead``, ``sabre``, ``toqm`` + - The output from this stage is expected to have the circuit match the + connectivity constraints of the target backend. This does not necessarily + need to match the directionality of the edges in the target as a later + stage typically will adjust directional gates to match that constraint + (but there is no penalty for doing that in the ``routing`` stage). + * - ``translation`` + - ``qiskit.transpiler.translation`` + - ``translator``, ``synthesis``, ``unroller`` + - The output of this stage is expected to have every operation be a native + instruction on the target backend. + * - ``optimization`` + - ``qiskit.transpiler.optimization`` + - There are no reserved plugin names + - This stage is expected to perform optimization and simplification. + The constraints from earlier stages still apply to the output of this + stage. After the ``optimization`` stage is run we expect the circuit + to still be executable on the target. + * - ``scheduling`` + - ``qiskit.transpiler.scheduling`` + - ``alap``, ``asap`` + - This is the last stage run and it is expected to output a scheduled + circuit such that all idle periods in the circuit are marked by explicit + :class:`~qiskit.circuit.Delay` instructions. + +Writing Plugins +=============== + +To write a pass manager stage plugin there are 2 main steps. The first step is +to create a subclass of the abstract plugin class +:class:`~.PassManagerStagePluginManager` which is used to define how the :class:`~.PassManager` +for the stage will be constructed. For example, to create a ``layout`` stage plugin that just +runs :class:`~.VF2Layout` and will fallback to use :class:`~.TrivialLayout` if +:class:`~VF2Layout` is unable to find a perfect layout:: + + from qiskit.transpiler.preset_passmanagers.plugin import PassManagerStagePlugin + from qiskit.transpiler.preset_passmanagers import common + from qiskit.transpiler import PassManager + from qiskit.transpiler.passes import VF2Layout, TrivialLayout + from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason + + + def _vf2_match_not_found(property_set): + return property_set["layout"] is None or ( + property_set["VF2Layout_stop_reason"] is not None + and property_set["VF2Layout_stop_reason"] is not VF2LayoutStopReason.SOLUTION_FOUND + + + class VF2LayoutPlugin(PassManagerStagePlugin): + + def pass_manager(self, pass_manager_config): + layout_pm = PassManager( + [ + VF2Layout( + coupling_map=pass_manager_config.coupling_map, + properties=pass_manager_config.backend_properties, + target=pass_manager_config.target + ) + ] + ) + layout_pm.append( + TrivialLayout(pass_manager_config.coupling_map), + condition=_vf2_match_not_found, + ) + layout_pm += common.generate_embed_passmanager(pass_manager_config.coupling_map) + return layout_pm + +The second step is to expose the :class:`~.PassManagerStagePluginManager` +subclass as a setuptools entry point in the package metadata. This can be done +by simply adding an ``entry_points`` entry to the ``setuptools.setup`` call in +the ``setup.py`` or the plugin package with the necessary entry points under the +appropriate namespace for the stage your plugin is for. You can see the list +of stages, entrypoints, and expectations from the stage in :ref:`stage_table`. +For example, continuing from the example plugin above:: + + entry_points = { + 'qiskit.transpiler.layout': [ + 'vf2 = qiskit_plugin_pkg.module.plugin:VF2LayoutPlugin', + ] + }, + +(note that the entry point ``name = path`` is a single string not a Python +expression). There isn't a limit to the number of plugins a single package can +include as long as each plugin has a unique name. So a single package can +expose multiple plugins if necessary. Refer to :ref:`stage_table` for a list +of reserved names for each stage. + +Plugin API +========== + +.. autosummary:: + :toctree: ../stubs/ + + PassManagerStagePlugin + PassManagerStagePluginManager + list_stage_plugins +""" + +import abc +from typing import List, Optional + +import stevedore + +from qiskit.transpiler.passmanager import PassManager +from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.passmanager_config import PassManagerConfig + + +class PassManagerStagePlugin(abc.ABC): + """A ``PassManagerStagePlugin`` is a plugin interface object for using custom + stages in :func:`~.transpile`. + + A ``PassManagerStagePlugin`` object can be added to an external package and + integrated into the :func:`~.transpile` function with an entrypoint. This + will enable users to use the output of :meth:`.pass_manager` to implement + a stage in the compilation process. + """ + + @abc.abstractmethod + def pass_manager( + self, pass_manager_config: PassManagerConfig, optimization_level: Optional[int] = None + ) -> PassManager: + """This method is designed to return a :class:`~.PassManager` for the stage this implements + + Args: + pass_manager_config: A configuration object that defines all the target device + specifications and any user specified options to :func:`~.transpile` or + :func:`~.generate_preset_pass_manager` + optimization_level: The optimization level of the transpilation, if set this + should be used to set values for any tunable parameters to trade off runtime + for potential optimization. Valid values should be ``0``, ``1``, ``2``, or ``3`` + and the higher the number the more optimization is expected. + """ + pass + + +class PassManagerStagePluginManager: + """Manager class for preset pass manager stage plugins.""" + + def __init__(self): + super().__init__() + self.init_plugins = stevedore.ExtensionManager( + "qiskit.transpiler.init", invoke_on_load=True, propagate_map_exceptions=True + ) + self.layout_plugins = stevedore.ExtensionManager( + "qiskit.transpiler.layout", invoke_on_load=True, propagate_map_exceptions=True + ) + self.routing_plugins = stevedore.ExtensionManager( + "qiskit.transpiler.routing", invoke_on_load=True, propagate_map_exceptions=True + ) + self.translation_plugins = stevedore.ExtensionManager( + "qiskit.transpiler.translation", invoke_on_load=True, propagate_map_exceptions=True + ) + self.optimization_plugins = stevedore.ExtensionManager( + "qiskit.transpiler.optimization", invoke_on_load=True, propagate_map_exceptions=True + ) + self.scheduling_plugins = stevedore.ExtensionManager( + "qiskit.transpiler.scheduling", invoke_on_load=True, propagate_map_exceptions=True + ) + + def get_passmanager_stage( + self, + stage_name: str, + plugin_name: str, + pm_config: PassManagerConfig, + optimization_level=None, + ) -> PassManager: + """Get a stage""" + if stage_name == "init": + return self._build_pm( + self.init_plugins, stage_name, plugin_name, pm_config, optimization_level + ) + elif stage_name == "layout": + return self._build_pm( + self.layout_plugins, stage_name, plugin_name, pm_config, optimization_level + ) + elif stage_name == "routing": + return self._build_pm( + self.routing_plugins, stage_name, plugin_name, pm_config, optimization_level + ) + elif stage_name == "translation": + return self._build_pm( + self.translation_plugins, stage_name, plugin_name, pm_config, optimization_level + ) + elif stage_name == "optimization": + return self._build_pm( + self.optimization_plugins, stage_name, plugin_name, pm_config, optimization_level + ) + elif stage_name == "scheduling": + return self._build_pm( + self.scheduling_plugins, stage_name, plugin_name, pm_config, optimization_level + ) + else: + raise TranspilerError(f"Invalid stage name: {stage_name}") + + def _build_pm( + self, + stage_obj: stevedore.ExtensionManager, + stage_name: str, + plugin_name: str, + pm_config: PassManagerConfig, + optimization_level: Optional[int] = None, + ): + if plugin_name not in stage_obj: + raise TranspilerError(f"Invalid plugin name {plugin_name} for stage {stage_name}") + plugin_obj = stage_obj[plugin_name] + return plugin_obj.obj.pass_manager(pm_config, optimization_level) + + +def list_stage_plugins(stage_name: str) -> List[str]: + """Get a list of installed plugins for a stage. + + Args: + stage_name: The stage name to get the plugin names for + + Returns: + plugins: The list of installed plugin names for the specified stages + + Raises: + TranspilerError: If an invalid stage name is specified. + """ + plugin_mgr = PassManagerStagePluginManager() + if stage_name == "init": + return plugin_mgr.init_plugins.names() + elif stage_name == "layout": + return plugin_mgr.layout_plugins.names() + elif stage_name == "routing": + return plugin_mgr.routing_plugins.names() + elif stage_name == "translation": + return plugin_mgr.translation_plugins.names() + elif stage_name == "optimization": + return plugin_mgr.optimization_plugins.names() + elif stage_name == "scheduling": + return plugin_mgr.scheduling_plugins.names() + else: + raise TranspilerError(f"Invalid stage name: {stage_name}") diff --git a/releasenotes/notes/stage-plugin-interface-47daae40f7d0ad3c.yaml b/releasenotes/notes/stage-plugin-interface-47daae40f7d0ad3c.yaml new file mode 100644 index 000000000000..ea4c88c097df --- /dev/null +++ b/releasenotes/notes/stage-plugin-interface-47daae40f7d0ad3c.yaml @@ -0,0 +1,32 @@ +--- +features: + - | + Introduced a new plugin interface for transpiler stages which is used to + enable alternative :class:`~.PassManager` objects from an external package + in a particular stage as part of :func:`~.transpile` or the + :class:`~.StagedPassManager` output from + :func:`~.generate_preset_pass_manager`, :func:`~.level_0_pass_manager`, + :func:`~.level_1_pass_manager`, :func:`~.level_2_pass_manager`, and + :func:`~.level_3_pass_manager`. Users can select a plugin to use for a + transpiler stage with the ``init_method``, ``layout_method``, + ``routing_method``, ``translation_method``, ``optimization_method``, and + ``scheduling_method`` keyword arguments on :func:`~.transpile` and + :func:`~.generate_preset_pass_manager`. A full list of plugin names + currently installed can be found with the :func:`.list_stage_plugins` + function. For creating plugins refer to the + :mod:`qiskit.transpiler.preset_passmanagers.plugin` module documentation + which includes a guide for writing stage plugins. + - | + The :func:`~.transpile` has two new keyword arguments, ``init_method`` and + ``optimization_method`` which are used to specify alternative plugins to + use for the ``init`` stage and ``optimization`` stages respectively. + - | + The :class:`~.PassManagerConfig` class has 3 new attributes, + :attr:`~.PassManagerConfig.init_method`, + :attr:`~.PassManagerConfig.optimization_method`, and + :attr:`~.PassManagerConfig.optimization_level` along with matching keyword + arguments on the constructor methods. The first two attributes represent + the user specified ``init`` and ``optimization`` plugins to use for + compilation. The :attr:`~.PassManagerConfig.optimization_level` attribute + represents the compilations optimization level if specified which can + be used to inform stage plugin behavior. diff --git a/setup.py b/setup.py index 5cdccc12ceea..ed7d0489f8d3 100755 --- a/setup.py +++ b/setup.py @@ -100,6 +100,13 @@ "qiskit.unitary_synthesis": [ "default = qiskit.transpiler.passes.synthesis.unitary_synthesis:DefaultUnitarySynthesis", "aqc = qiskit.transpiler.synthesis.aqc.aqc_plugin:AQCSynthesisPlugin", - ] + ], + "qiskit.transpiler.routing": [ + "basic = qiskit.transpiler.preset_passmanagers.builtin_plugins:BasicSwapPassManager", + "stochastic = qiskit.transpiler.preset_passmanagers.builtin_plugins:StochasticSwapPassManager", + "lookahead = qiskit.transpiler.preset_passmanagers.builtin_plugins:LookaheadSwapPassManager", + "sabre = qiskit.transpiler.preset_passmanagers.builtin_plugins:SabreSwapPassManager", + "none = qiskit.transpiler.preset_passmanagers.builtin_plugins:NoneRoutingPassManager", + ], }, ) diff --git a/test/python/transpiler/test_stage_plugin.py b/test/python/transpiler/test_stage_plugin.py new file mode 100644 index 000000000000..1242905e9f4e --- /dev/null +++ b/test/python/transpiler/test_stage_plugin.py @@ -0,0 +1,103 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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 the staged transpiler plugins. +""" + +from test import combine + +import ddt + +from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit.compiler.transpiler import transpile +from qiskit.test import QiskitTestCase +from qiskit.transpiler import PassManager, PassManagerConfig, CouplingMap +from qiskit.transpiler.preset_passmanagers.plugin import ( + PassManagerStagePluginManager, + list_stage_plugins, +) +from qiskit.transpiler.exceptions import TranspilerError +from qiskit.providers.basicaer import QasmSimulatorPy + + +class TestStagePassManagerPlugin(QiskitTestCase): + """Tests for the transpiler stage plugin interface.""" + + def test_list_stage_plugins(self): + """Test list stage plugin function.""" + routing_passes = list_stage_plugins("routing") + self.assertIn("basic", routing_passes) + self.assertIn("sabre", routing_passes) + self.assertIn("lookahead", routing_passes) + self.assertIn("stochastic", routing_passes) + self.assertIsInstance(list_stage_plugins("init"), list) + self.assertIsInstance(list_stage_plugins("layout"), list) + self.assertIsInstance(list_stage_plugins("translation"), list) + self.assertIsInstance(list_stage_plugins("optimization"), list) + self.assertIsInstance(list_stage_plugins("scheduling"), list) + + def test_list_stage_plugins_invalid_stage_name(self): + """Test list stage plugin function with invalid stage name.""" + with self.assertRaises(TranspilerError): + list_stage_plugins("not_a_stage") + + def test_build_pm_invalid_plugin_name_valid_stage(self): + """Test get pm from plugin with invalid plugin name and valid stage.""" + plugin_manager = PassManagerStagePluginManager() + with self.assertRaises(TranspilerError): + plugin_manager.get_passmanager_stage("init", "empty_plugin", PassManagerConfig()) + + def test_build_pm_invalid_stage(self): + """Test get pm from plugin with invalid stage.""" + plugin_manager = PassManagerStagePluginManager() + with self.assertRaises(TranspilerError): + plugin_manager.get_passmanager_stage( + "not_a_sage", "fake_plugin_not_real", PassManagerConfig() + ) + + def test_build_pm(self): + """Test get pm from plugin.""" + plugin_manager = PassManagerStagePluginManager() + pm_config = PassManagerConfig() + pm = plugin_manager.get_passmanager_stage( + "routing", "sabre", pm_config, optimization_level=3 + ) + self.assertIsInstance(pm, PassManager) + + +@ddt.ddt +class TestBuiltinPlugins(QiskitTestCase): + """Test that all built-in plugins work in transpile().""" + + @combine( + optimization_level=list(range(4)), + routing_method=["basic", "lookahead", "sabre", "stochastic"], + ) + def test_routing_plugins(self, optimization_level, routing_method): + """Test all routing plugins (excluding error).""" + qc = QuantumCircuit(4) + qc.h(0) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.measure_all() + tqc = transpile( + qc, + basis_gates=["cx", "sx", "x", "rz"], + coupling_map=CouplingMap.from_line(4), + optimization_level=optimization_level, + routing_method=routing_method, + ) + backend = QasmSimulatorPy() + counts = backend.run(tqc, shots=1000).result().get_counts() + self.assertDictAlmostEqual(counts, {"0000": 500, "1111": 500}, delta=100) From d2b7cc878a8ea8ad685c382c08a9e8c250079f45 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 30 Aug 2022 15:57:32 -0400 Subject: [PATCH 78/82] Fix potential overflow in sabre swap (#8644) * Fix potential overflow in sabre swap This commit fixes a potential overflow which was caught by the randomized testing around tracking the number of search steps for the purposes of reseting the decay rates used by the heuristic scoring. The number of search steps was using an unsigned 8bit integer because it's typically not a large number and it's used to track if we've performed 5 steps or more before resetting the decay rate for each qubit. However in some cases if there were more than 255 steps in a layer this value would overflow the way this detection was being done. While this actually wouldn't be fatal because the value would reset back to 0 and we'd still track the decay rate correctly this was a potential error in debug mode. This commit fixes this by detecting when we've reached DECAY_RESET_INTERVAL steps and resetting the counter to 0. This way the value of the overflowing variable can never exceed DECAY_RESET_INTERVAL. * Update src/sabre_swap/mod.rs --- src/sabre_swap/mod.rs | 3 +- test/python/transpiler/test_sabre_layout.py | 68 +++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/sabre_swap/mod.rs b/src/sabre_swap/mod.rs index a2301c56e31f..c038e13b673a 100644 --- a/src/sabre_swap/mod.rs +++ b/src/sabre_swap/mod.rs @@ -318,8 +318,9 @@ pub fn build_swap_map( run_in_parallel, ); num_search_steps += 1; - if num_search_steps % DECAY_RESET_INTERVAL == 0 { + if num_search_steps >= DECAY_RESET_INTERVAL { qubits_decay.fill_with(|| 1.); + num_search_steps = 0; } else { qubits_decay[best_swap[0]] += DECAY_RATE; qubits_decay[best_swap[1]] += DECAY_RATE; diff --git a/test/python/transpiler/test_sabre_layout.py b/test/python/transpiler/test_sabre_layout.py index 142a66f2f4d9..27205afe8a26 100644 --- a/test/python/transpiler/test_sabre_layout.py +++ b/test/python/transpiler/test_sabre_layout.py @@ -22,6 +22,7 @@ from qiskit.compiler.transpiler import transpile from qiskit.providers.fake_provider import FakeAlmaden from qiskit.providers.fake_provider import FakeKolkata +from qiskit.providers.fake_provider import FakeMontreal class TestSabreLayout(QiskitTestCase): @@ -140,6 +141,73 @@ def test_layout_with_classical_bits(self): self.assertEqual(layout[qc.qubits[6]], 18) self.assertEqual(layout[qc.qubits[7]], 26) + # pylint: disable=line-too-long + def test_layout_many_search_trials(self): + """Test recreate failure from randomized testing that overflowed.""" + qc = QuantumCircuit.from_qasm_str( + """ + OPENQASM 2.0; +include "qelib1.inc"; +qreg q18585[14]; +creg c1423[5]; +creg c1424[4]; +creg c1425[3]; +barrier q18585[4],q18585[5],q18585[12],q18585[1]; +cz q18585[11],q18585[3]; +cswap q18585[8],q18585[10],q18585[6]; +u(-2.00001,6.1035156e-05,-1.9) q18585[2]; +barrier q18585[3],q18585[6],q18585[5],q18585[8],q18585[10],q18585[9],q18585[11],q18585[2],q18585[12],q18585[7],q18585[13],q18585[4],q18585[0],q18585[1]; +cp(0) q18585[2],q18585[4]; +cu(-0.99999,0,0,0) q18585[7],q18585[1]; +cu(0,0,0,2.1507119) q18585[6],q18585[3]; +barrier q18585[13],q18585[0],q18585[12],q18585[3],q18585[2],q18585[10]; +ry(-1.1044662) q18585[13]; +barrier q18585[13]; +id q18585[12]; +barrier q18585[12],q18585[6]; +cu(-1.9,1.9,-1.5,0) q18585[10],q18585[0]; +barrier q18585[13]; +id q18585[8]; +barrier q18585[12]; +barrier q18585[12],q18585[1],q18585[9]; +sdg q18585[2]; +rz(-10*pi) q18585[6]; +u(0,27.566433,1.9) q18585[1]; +barrier q18585[12],q18585[11],q18585[9],q18585[4],q18585[7],q18585[0],q18585[13],q18585[3]; +cu(-0.99999,-5.9604645e-08,-0.5,2.00001) q18585[3],q18585[13]; +rx(-5.9604645e-08) q18585[7]; +p(1.1) q18585[13]; +barrier q18585[12],q18585[13],q18585[10],q18585[9],q18585[7],q18585[4]; +z q18585[10]; +measure q18585[7] -> c1423[2]; +barrier q18585[0],q18585[3],q18585[7],q18585[4],q18585[1],q18585[8],q18585[6],q18585[11],q18585[5]; +barrier q18585[5],q18585[2],q18585[8],q18585[3],q18585[6]; +""" + ) + res = transpile( + qc, + FakeMontreal(), + layout_method="sabre", + routing_method="stochastic", + seed_transpiler=12345, + ) + self.assertIsInstance(res, QuantumCircuit) + layout = res._layout + self.assertEqual(layout[qc.qubits[0]], 11) + self.assertEqual(layout[qc.qubits[1]], 22) + self.assertEqual(layout[qc.qubits[2]], 17) + self.assertEqual(layout[qc.qubits[3]], 12) + self.assertEqual(layout[qc.qubits[4]], 18) + self.assertEqual(layout[qc.qubits[5]], 9) + self.assertEqual(layout[qc.qubits[6]], 16) + self.assertEqual(layout[qc.qubits[7]], 25) + self.assertEqual(layout[qc.qubits[8]], 19) + self.assertEqual(layout[qc.qubits[9]], 3) + self.assertEqual(layout[qc.qubits[10]], 14) + self.assertEqual(layout[qc.qubits[11]], 15) + self.assertEqual(layout[qc.qubits[12]], 20) + self.assertEqual(layout[qc.qubits[13]], 8) + if __name__ == "__main__": unittest.main() From 94814ec30804bbd01b8ad7572afaf1f0b24f9041 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 31 Aug 2022 00:21:01 +0100 Subject: [PATCH 79/82] Generate transpiler seed in randomised testing (#8645) Hypothesis sometimes fails to shrink tests or provide a proper reproducer because there is also randomisation in the transpiler. This now outputs a `seed_transpiler` value as part of the draw, which should fix the output completely. The draw configuration is re-organised into a `kwargs` dictionary because the argument list was getting unwieldy. --- .../randomized/test_transpiler_equivalence.py | 59 +++++++++---------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/test/randomized/test_transpiler_equivalence.py b/test/randomized/test_transpiler_equivalence.py index bfb48d0068de..1c678ddda5fc 100644 --- a/test/randomized/test_transpiler_equivalence.py +++ b/test/randomized/test_transpiler_equivalence.py @@ -229,18 +229,22 @@ def _fully_supports_scheduling(backend): @st.composite def transpiler_conf(draw): """Composite search strategy to pick a valid transpiler config.""" - opt_level = draw(st.integers(min_value=0, max_value=3)) - layout_method = draw(st.sampled_from(layout_methods)) - routing_method = draw(st.sampled_from(routing_methods)) + all_backends = st.one_of(st.none(), st.sampled_from(mock_backends)) + scheduling_backends = st.sampled_from(mock_backends_with_scheduling) scheduling_method = draw(st.sampled_from(scheduling_methods)) - - compatible_backends = st.one_of(st.none(), st.sampled_from(mock_backends)) - if scheduling_method is not None or backend_needs_durations: - compatible_backends = st.sampled_from(mock_backends_with_scheduling) - - backend = draw(st.one_of(compatible_backends)) - - return (backend, opt_level, layout_method, routing_method, scheduling_method) + backend = ( + draw(scheduling_backends) + if scheduling_method or backend_needs_durations + else draw(all_backends) + ) + return { + "backend": backend, + "optimization_level": draw(st.integers(min_value=0, max_value=3)), + "layout_method": draw(st.sampled_from(layout_methods)), + "routing_method": draw(st.sampled_from(routing_methods)), + "scheduling_method": scheduling_method, + "seed_transpiler": draw(st.integers(min_value=0, max_value=1_000_000)), + } class QCircuitMachine(RuleBasedStateMachine): @@ -337,21 +341,23 @@ def qasm(self): self.qc.qasm() @precondition(lambda self: any(isinstance(d[0], Measure) for d in self.qc.data)) - @rule(conf=transpiler_conf()) - def equivalent_transpile(self, conf): + @rule(kwargs=transpiler_conf()) + def equivalent_transpile(self, kwargs): """Simulate, transpile and simulate the present circuit. Verify that the counts are not significantly different before and after transpilation. """ - backend, opt_level, layout_method, routing_method, scheduling_method = conf - - assume(backend is None or backend.configuration().n_qubits >= len(self.qc.qubits)) + assume( + kwargs["backend"] is None + or kwargs["backend"].configuration().n_qubits >= len(self.qc.qubits) + ) - print( - f"Evaluating circuit at level {opt_level} on {backend} " - f"using layout_method={layout_method} routing_method={routing_method} " - f"and scheduling_method={scheduling_method}:\n{self.qc.qasm()}" + call = ( + "transpile(qc, " + + ", ".join(f"{key:s}={value!r}" for key, value in kwargs.items() if value is not None) + + ")" ) + print(f"Evaluating {call} for:\n{self.qc.qasm()}") shots = 4096 @@ -360,18 +366,9 @@ def equivalent_transpile(self, conf): aer_counts = self.backend.run(self.qc, shots=shots).result().get_counts() try: - xpiled_qc = transpile( - self.qc, - backend=backend, - optimization_level=opt_level, - layout_method=layout_method, - routing_method=routing_method, - scheduling_method=scheduling_method, - ) + xpiled_qc = transpile(self.qc, **kwargs) except Exception as e: - failed_qasm = "Exception caught during transpilation of circuit: \n{}".format( - self.qc.qasm() - ) + failed_qasm = f"Exception caught during transpilation of circuit: \n{self.qc.qasm()}" raise RuntimeError(failed_qasm) from e xpiled_aer_counts = self.backend.run(xpiled_qc, shots=shots).result().get_counts() From ea1d2d3d6f221574bbb6e0d20ab00a2d000c58fe Mon Sep 17 00:00:00 2001 From: Manoel Marques Date: Tue, 30 Aug 2022 20:53:50 -0400 Subject: [PATCH 80/82] Add optional category parameter to deprecate decorators (#8646) --- qiskit/utils/deprecation.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/qiskit/utils/deprecation.py b/qiskit/utils/deprecation.py index ccfa93615287..633c2472288d 100644 --- a/qiskit/utils/deprecation.py +++ b/qiskit/utils/deprecation.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017. +# (C) Copyright IBM 2017, 2022. # # 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 @@ -14,16 +14,17 @@ import functools import warnings +from typing import Type -def deprecate_arguments(kwarg_map): +def deprecate_arguments(kwarg_map, category: Type[Warning] = DeprecationWarning): """Decorator to automatically alias deprecated argument names and warn upon use.""" def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): if kwargs: - _rename_kwargs(func.__name__, kwargs, kwarg_map) + _rename_kwargs(func.__name__, kwargs, kwarg_map, category) return func(*args, **kwargs) return wrapper @@ -31,12 +32,13 @@ def wrapper(*args, **kwargs): return decorator -def deprecate_function(msg, stacklevel=2): +def deprecate_function(msg: str, stacklevel: int = 2, category: Type[Warning] = DeprecationWarning): """Emit a warning prior to calling decorated function. Args: - msg (str): Warning message to emit. - stacklevel (int): The warning stackevel to use, defaults to 2. + msg: Warning message to emit. + stacklevel: The warning stackevel to use, defaults to 2. + category: warning category, defaults to DeprecationWarning Returns: Callable: The decorated, deprecated callable. @@ -45,7 +47,7 @@ def deprecate_function(msg, stacklevel=2): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): - warnings.warn(msg, DeprecationWarning, stacklevel=stacklevel) + warnings.warn(msg, category=category, stacklevel=stacklevel) return func(*args, **kwargs) return wrapper @@ -53,7 +55,7 @@ def wrapper(*args, **kwargs): return decorator -def _rename_kwargs(func_name, kwargs, kwarg_map): +def _rename_kwargs(func_name, kwargs, kwarg_map, category: Type[Warning] = DeprecationWarning): for old_arg, new_arg in kwarg_map.items(): if old_arg in kwargs: if new_arg in kwargs: @@ -63,14 +65,14 @@ def _rename_kwargs(func_name, kwargs, kwarg_map): warnings.warn( f"{func_name} keyword argument {old_arg} is deprecated and " "will in future be removed.", - DeprecationWarning, + category=category, stacklevel=3, ) else: warnings.warn( f"{func_name} keyword argument {old_arg} is deprecated and " f"replaced with {new_arg}.", - DeprecationWarning, + category=category, stacklevel=3, ) From 548dae2cb82df3de0ea247fe9f82a04c12f019b0 Mon Sep 17 00:00:00 2001 From: Anthony-Gandon Date: Wed, 31 Aug 2022 05:38:33 +0200 Subject: [PATCH 81/82] Implements two-step tapering (#8590) * Adds two new methods for the tapering: `convert_clifford` and `taper_clifford` * Adds two new methods for the tapering: `convert_clifford` and `taper_clifford`. Adds a corresponding test. * Make black * Make lint * Resolving the duplication and documenting the tapering. * Add release note. * Small changes * Double the OpflowError + Update docstring indent * Apply suggestions from code review Co-authored-by: Ikko Hamamura Co-authored-by: Ikko Hamamura Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .../primitive_ops/tapered_pauli_sum_op.py | 81 ++++++++++++++++--- ...ts_two_step_tapering-f481a8cac3990cd5.yaml | 5 ++ test/python/opflow/test_z2_symmetries.py | 18 +++++ 3 files changed, 95 insertions(+), 9 deletions(-) create mode 100644 releasenotes/notes/implements_two_step_tapering-f481a8cac3990cd5.yaml diff --git a/qiskit/opflow/primitive_ops/tapered_pauli_sum_op.py b/qiskit/opflow/primitive_ops/tapered_pauli_sum_op.py index 0b4abf1713f1..97cc441b5b49 100644 --- a/qiskit/opflow/primitive_ops/tapered_pauli_sum_op.py +++ b/qiskit/opflow/primitive_ops/tapered_pauli_sum_op.py @@ -356,32 +356,59 @@ def find_Z2_symmetries(cls, operator: PauliSumOp) -> "Z2Symmetries": return cls(pauli_symmetries, sq_paulis, sq_list, None) - def taper(self, operator: PauliSumOp) -> OperatorBase: - """ - Taper an operator based on the z2_symmetries info and sector defined by `tapering_values`. - The `tapering_values` will be stored into the resulted operator for a record. + def convert_clifford(self, operator: PauliSumOp) -> OperatorBase: + """This method operates the first part of the tapering. + It converts the operator by composing it with the clifford unitaries defined in the current + symmetry. Args: - operator: the to-be-tapered operator. + operator: to-be-tapered operator Returns: - If tapering_values is None: [:class`PauliSumOp`]; otherwise, :class:`PauliSumOp` + :class:`PauliSumOp` corresponding to the converted operator. + Raises: OpflowError: Z2 symmetries, single qubit pauli and single qubit list cannot be empty + """ + if not self._symmetries or not self._sq_paulis or not self._sq_list: raise OpflowError( "Z2 symmetries, single qubit pauli and single qubit list cannot be empty." ) - # If the operator is zero then we can skip the following. We still need to taper the - # operator to reduce its size i.e. the number of qubits so for example 0*"IIII" could - # taper to 0*"II" when symmetries remove two qubits. if not operator.is_zero(): for clifford in self.cliffords: operator = cast(PauliSumOp, clifford @ operator @ clifford) operator = operator.reduce(atol=0) + return operator + + def taper_clifford(self, operator: PauliSumOp) -> OperatorBase: + """This method operates the second part of the tapering. + This function assumes that the input operators have already been transformed using + :meth:`convert_clifford`. The redundant qubits due to the symmetries are dropped and + replaced by their two possible eigenvalues. + The `tapering_values` will be stored into the resulted operator for a record. + + Args: + operator: Partially tapered operator resulting from a call to :meth:`convert_clifford` + + Returns: + If tapering_values is None: [:class:`PauliSumOp`]; otherwise, :class:`PauliSumOp` + + Raises: + OpflowError: Z2 symmetries, single qubit pauli and single qubit list cannot be empty + + """ + + if not self._symmetries or not self._sq_paulis or not self._sq_list: + raise OpflowError( + "Z2 symmetries, single qubit pauli and single qubit list cannot be empty." + ) + # If the operator is zero then we can skip the following. We still need to taper the + # operator to reduce its size i.e. the number of qubits so for example 0*"IIII" could + # taper to 0*"II" when symmetries remove two qubits. if self._tapering_values is None: tapered_ops_list = [ self._taper(operator, list(coeff)) @@ -393,6 +420,42 @@ def taper(self, operator: PauliSumOp) -> OperatorBase: return tapered_ops + def taper(self, operator: PauliSumOp) -> OperatorBase: + """ + Taper an operator based on the z2_symmetries info and sector defined by `tapering_values`. + The `tapering_values` will be stored into the resulted operator for a record. + + The tapering is a two-step algorithm which first converts the operator into a + :class:`PauliSumOp` with same eigenvalues but where some qubits are only acted upon + with the Pauli operators I or X. + The number M of these redundant qubits is equal to the number M of identified symmetries. + + The second step of the reduction consists in replacing these qubits with the possible + eigenvalues of the corresponding Pauli X, giving 2^M new operators with M less qubits. + If an eigenvalue sector was previously identified for the solution, then this reduces to + 1 new operator with M less qubits. + + Args: + operator: the to-be-tapered operator + + Returns: + If tapering_values is None: [:class:`PauliSumOp`]; otherwise, :class:`PauliSumOp` + + Raises: + OpflowError: Z2 symmetries, single qubit pauli and single qubit list cannot be empty + + """ + + if not self._symmetries or not self._sq_paulis or not self._sq_list: + raise OpflowError( + "Z2 symmetries, single qubit pauli and single qubit list cannot be empty." + ) + + converted_ops = self.convert_clifford(operator) + tapered_ops = self.taper_clifford(converted_ops) + + return tapered_ops + def _taper(self, op: PauliSumOp, curr_tapering_values: List[int]) -> OperatorBase: pauli_list = [] for pauli_term in op: diff --git a/releasenotes/notes/implements_two_step_tapering-f481a8cac3990cd5.yaml b/releasenotes/notes/implements_two_step_tapering-f481a8cac3990cd5.yaml new file mode 100644 index 000000000000..ca23df6dffea --- /dev/null +++ b/releasenotes/notes/implements_two_step_tapering-f481a8cac3990cd5.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Splits the internal procedure in :meth:`taper()` into two methods :meth:`convert_clifford()` and :meth:`taper_clifford()`. The logic remains the same but the methods are now exposed in the public API. Also improves the documentation of the method `taper()`. + diff --git a/test/python/opflow/test_z2_symmetries.py b/test/python/opflow/test_z2_symmetries.py index c7335f6425bd..911cc71db080 100644 --- a/test/python/opflow/test_z2_symmetries.py +++ b/test/python/opflow/test_z2_symmetries.py @@ -92,3 +92,21 @@ def test_truncate_tapered_op(self): ) expected_op = TaperedPauliSumOp(primitive, z2_symmetries) self.assertEqual(tapered_op, expected_op) + + def test_twostep_tapering(self): + """Test the two-step tapering""" + qubit_op = PauliSumOp.from_list( + [ + ("II", -1.0537076071291125), + ("IZ", 0.393983679438514), + ("ZI", -0.39398367943851387), + ("ZZ", -0.01123658523318205), + ("XX", 0.1812888082114961), + ] + ) + z2_symmetries = Z2Symmetries.find_Z2_symmetries(qubit_op) + tapered_op = z2_symmetries.taper(qubit_op) + + tapered_op_firststep = z2_symmetries.convert_clifford(qubit_op) + tapered_op_secondstep = z2_symmetries.taper_clifford(tapered_op_firststep) + self.assertEqual(tapered_op, tapered_op_secondstep) From 9195ec183f723c027d876f10c1dc79509a4e4f40 Mon Sep 17 00:00:00 2001 From: Manoel Marques Date: Wed, 31 Aug 2022 09:42:09 -0400 Subject: [PATCH 82/82] Deprecate Algorithms Factorizers and Linear Solvers (#8617) * Deprecate HHL and Shor * Deprecate factorizers and linear solvers * Fix docstrings --- qiskit/algorithms/factorizers/shor.py | 16 +++- qiskit/algorithms/linear_solvers/__init__.py | 4 +- qiskit/algorithms/linear_solvers/hhl.py | 27 +++++-- .../linear_solvers/linear_solver.py | 18 ++++- .../matrices/linear_system_matrix.py | 9 ++- .../linear_solvers/matrices/numpy_matrix.py | 14 +++- .../matrices/tridiagonal_toeplitz.py | 14 +++- .../linear_solvers/numpy_linear_solver.py | 29 +++++-- .../observables/absolute_average.py | 17 ++++- .../observables/linear_system_observable.py | 12 ++- .../observables/matrix_functional.py | 18 ++++- ...-solvers-factorizers-bbf5302484cb6831.yaml | 9 +++ test/python/algorithms/test_backendv1.py | 12 ++- test/python/algorithms/test_backendv2.py | 12 ++- test/python/algorithms/test_linear_solvers.py | 75 ++++++++++++++----- test/python/algorithms/test_shor.py | 22 ++++-- 16 files changed, 238 insertions(+), 70 deletions(-) create mode 100644 releasenotes/notes/deprecate-linear-solvers-factorizers-bbf5302484cb6831.yaml diff --git a/qiskit/algorithms/factorizers/shor.py b/qiskit/algorithms/factorizers/shor.py index 1030747ae89f..06f9f1610e09 100644 --- a/qiskit/algorithms/factorizers/shor.py +++ b/qiskit/algorithms/factorizers/shor.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2019, 2020. +# (C) Copyright IBM 2019, 2022. # # 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 @@ -30,6 +30,7 @@ from qiskit.utils.arithmetic import is_power from qiskit.utils.quantum_instance import QuantumInstance from qiskit.utils.validation import validate_min +from qiskit.utils.deprecation import deprecate_function from ..algorithm_result import AlgorithmResult from ..exceptions import AlgorithmError @@ -40,7 +41,12 @@ class Shor: - """Shor's factoring algorithm. + """The deprecated Shor's factoring algorithm. + + The Shor class is deprecated as of Qiskit Terra 0.22.0 + and will be removed no sooner than 3 months after the release date. + It is replaced by the tutorial at + `Shor `_ Shor's Factoring algorithm is one of the most well-known quantum algorithms and finds the prime factors for input integer :math:`N` in polynomial time. @@ -50,6 +56,10 @@ class Shor: See also https://arxiv.org/abs/quant-ph/0205095 """ + @deprecate_function( + "The Shor class is deprecated as of Qiskit Terra 0.22.0 " + "and will be removed no sooner than 3 months after the release date. " + ) def __init__(self, quantum_instance: Optional[Union[QuantumInstance, Backend]] = None) -> None: """ Args: @@ -480,7 +490,7 @@ def factor( class ShorResult(AlgorithmResult): - """Shor Result.""" + """The deprecated Shor Result.""" def __init__(self) -> None: super().__init__() diff --git a/qiskit/algorithms/linear_solvers/__init__.py b/qiskit/algorithms/linear_solvers/__init__.py index 2b8214fb7cf2..746fb8e40496 100644 --- a/qiskit/algorithms/linear_solvers/__init__.py +++ b/qiskit/algorithms/linear_solvers/__init__.py @@ -11,8 +11,8 @@ # that they have been altered from the originals. """ -Linear solvers (:mod:`qiskit.algorithms.linear_solvers`) -========================================================= +The deprecated Linear solvers (:mod:`qiskit.algorithms.linear_solvers`) +======================================================================= It contains classical and quantum algorithms to solve systems of linear equations such as :class:`~qiskit.algorithms.HHL`. Although the quantum algorithm accepts a general Hermitian matrix as input, Qiskit's default diff --git a/qiskit/algorithms/linear_solvers/hhl.py b/qiskit/algorithms/linear_solvers/hhl.py index 6e59de4babcb..c5bf52a0380d 100644 --- a/qiskit/algorithms/linear_solvers/hhl.py +++ b/qiskit/algorithms/linear_solvers/hhl.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 2022. # # 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 @@ -32,6 +32,7 @@ from qiskit.providers import Backend from qiskit.quantum_info.operators.base_operator import BaseOperator from qiskit.utils import QuantumInstance +from qiskit.utils.deprecation import deprecate_function from .linear_solver import LinearSolver, LinearSolverResult from .matrices.numpy_matrix import NumPyMatrix @@ -39,7 +40,8 @@ class HHL(LinearSolver): - r"""Systems of linear equations arise naturally in many real-life applications in a wide range + r"""The deprecated systems of linear equations arise naturally in many real-life applications + in a wide range of areas, such as in the solution of Partial Differential Equations, the calibration of financial models, fluid simulation or numerical field calculation. The problem can be defined as, given a matrix :math:`A\in\mathbb{C}^{N\times N}` and a vector @@ -64,24 +66,29 @@ class HHL(LinearSolver): .. jupyter-execute:: + import warnings import numpy as np from qiskit import QuantumCircuit from qiskit.algorithms.linear_solvers.hhl import HHL from qiskit.algorithms.linear_solvers.matrices import TridiagonalToeplitz from qiskit.algorithms.linear_solvers.observables import MatrixFunctional - matrix = TridiagonalToeplitz(2, 1, 1 / 3, trotter_steps=2) - right_hand_side = [1.0, -2.1, 3.2, -4.3] - observable = MatrixFunctional(1, 1 / 2) - rhs = right_hand_side / np.linalg.norm(right_hand_side) + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + matrix = TridiagonalToeplitz(2, 1, 1 / 3, trotter_steps=2) + right_hand_side = [1.0, -2.1, 3.2, -4.3] + observable = MatrixFunctional(1, 1 / 2) + rhs = right_hand_side / np.linalg.norm(right_hand_side) # Initial state circuit num_qubits = matrix.num_state_qubits qc = QuantumCircuit(num_qubits) qc.isometry(rhs, list(range(num_qubits)), None) - hhl = HHL() - solution = hhl.solve(matrix, qc, observable) + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + hhl = HHL() + solution = hhl.solve(matrix, qc, observable) approx_result = solution.observable References: @@ -96,6 +103,10 @@ class HHL(LinearSolver): """ + @deprecate_function( + "The HHL class is deprecated as of Qiskit Terra 0.22.0 " + "and will be removed no sooner than 3 months after the release date. " + ) def __init__( self, epsilon: float = 1e-2, diff --git a/qiskit/algorithms/linear_solvers/linear_solver.py b/qiskit/algorithms/linear_solvers/linear_solver.py index 093d8cd30e93..97a68226ebb9 100644 --- a/qiskit/algorithms/linear_solvers/linear_solver.py +++ b/qiskit/algorithms/linear_solvers/linear_solver.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 2022. # # 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 @@ -19,18 +19,23 @@ from qiskit import QuantumCircuit from qiskit.result import Result from qiskit.quantum_info.operators.base_operator import BaseOperator +from qiskit.utils.deprecation import deprecate_function from .observables.linear_system_observable import LinearSystemObservable from ..algorithm_result import AlgorithmResult class LinearSolverResult(AlgorithmResult): - """A base class for linear systems results. + """The deprecated base class for linear systems results. The linear systems algorithms return an object of the type ``LinearSystemsResult`` with the information about the solution obtained. """ + @deprecate_function( + "The LinearSolverResult class is deprecated as of Qiskit Terra 0.22.0 " + "and will be removed no sooner than 3 months after the release date. " + ) def __init__(self) -> None: super().__init__() @@ -93,7 +98,14 @@ def circuit_results(self, results: Union[List[float], List[Result]]): class LinearSolver(ABC): - """An abstract class for linear system solvers in Qiskit.""" + """The deprecated abstract class for linear system solvers in Qiskit.""" + + @deprecate_function( + "The LinearSolver class is deprecated as of Qiskit Terra 0.22.0 " + "and will be removed no sooner than 3 months after the release date. " + ) + def __init__(self) -> None: + pass @abstractmethod def solve( diff --git a/qiskit/algorithms/linear_solvers/matrices/linear_system_matrix.py b/qiskit/algorithms/linear_solvers/matrices/linear_system_matrix.py index 745fdca2b4f2..26e53860dc1f 100644 --- a/qiskit/algorithms/linear_solvers/matrices/linear_system_matrix.py +++ b/qiskit/algorithms/linear_solvers/matrices/linear_system_matrix.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 2022. # # 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 @@ -17,11 +17,16 @@ from qiskit import QuantumCircuit from qiskit.circuit.library import BlueprintCircuit +from qiskit.utils.deprecation import deprecate_function class LinearSystemMatrix(BlueprintCircuit, ABC): - """Base class for linear system matrices.""" + """The deprecated base class for linear system matrices.""" + @deprecate_function( + "The LinearSystemMatrix class is deprecated as of Qiskit Terra 0.22.0 " + "and will be removed no sooner than 3 months after the release date. " + ) def __init__( self, num_state_qubits: int, diff --git a/qiskit/algorithms/linear_solvers/matrices/numpy_matrix.py b/qiskit/algorithms/linear_solvers/matrices/numpy_matrix.py index 1fc94893c6ab..fdf30017c925 100644 --- a/qiskit/algorithms/linear_solvers/matrices/numpy_matrix.py +++ b/qiskit/algorithms/linear_solvers/matrices/numpy_matrix.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 2022. # # 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 @@ -17,22 +17,26 @@ import scipy as sp from qiskit import QuantumCircuit, QuantumRegister +from qiskit.utils.deprecation import deprecate_function from .linear_system_matrix import LinearSystemMatrix class NumPyMatrix(LinearSystemMatrix): - """Class of matrices given as a numpy array. + """The deprecated class of matrices given as a numpy array. Examples: .. jupyter-execute:: + import warnings import numpy as np from qiskit import QuantumCircuit from qiskit.algorithms.linear_solvers.matrices.numpy_matrix import NumPyMatrix - matrix = NumPyMatrix(np.array([[1 / 2, 1 / 6, 0, 0], [1 / 6, 1 / 2, 1 / 6, 0], + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + matrix = NumPyMatrix(np.array([[1 / 2, 1 / 6, 0, 0], [1 / 6, 1 / 2, 1 / 6, 0], [0, 1 / 6, 1 / 2, 1 / 6], [0, 0, 1 / 6, 1 / 2]])) power = 2 @@ -44,6 +48,10 @@ class NumPyMatrix(LinearSystemMatrix): qc.append(matrix.power(power).control(), list(range(circ_qubits))) """ + @deprecate_function( + "The NumPyMatrix class is deprecated as of Qiskit Terra 0.22.0 " + "and will be removed no sooner than 3 months after the release date. " + ) def __init__( self, matrix: np.ndarray, diff --git a/qiskit/algorithms/linear_solvers/matrices/tridiagonal_toeplitz.py b/qiskit/algorithms/linear_solvers/matrices/tridiagonal_toeplitz.py index 6dfbd0ad3c8a..6dda2716b680 100644 --- a/qiskit/algorithms/linear_solvers/matrices/tridiagonal_toeplitz.py +++ b/qiskit/algorithms/linear_solvers/matrices/tridiagonal_toeplitz.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 2022. # # 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 @@ -18,12 +18,13 @@ from qiskit.circuit import QuantumCircuit, QuantumRegister, AncillaRegister from qiskit.circuit.library import UGate, MCMTVChain +from qiskit.utils.deprecation import deprecate_function from .linear_system_matrix import LinearSystemMatrix class TridiagonalToeplitz(LinearSystemMatrix): - r"""Class of tridiagonal Toeplitz symmetric matrices. + r"""The deprecated class of tridiagonal Toeplitz symmetric matrices. Given the main entry, :math:`a`, and the off diagonal entry, :math:`b`, the :math:`4\times 4` dimensional tridiagonal Toeplitz symmetric matrix is @@ -41,11 +42,14 @@ class TridiagonalToeplitz(LinearSystemMatrix): .. jupyter-execute:: + import warnings import numpy as np from qiskit import QuantumCircuit from qiskit.algorithms.linear_solvers.matrices import TridiagonalToeplitz - matrix = TridiagonalToeplitz(2, 1, -1 / 3) + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + matrix = TridiagonalToeplitz(2, 1, -1 / 3) power = 3 # Controlled power (as within QPE) @@ -56,6 +60,10 @@ class TridiagonalToeplitz(LinearSystemMatrix): qc.append(matrix.power(power).control(), list(range(circ_qubits))) """ + @deprecate_function( + "The TridiagonalToeplitz class is deprecated as of Qiskit Terra 0.22.0 " + "and will be removed no sooner than 3 months after the release date. " + ) def __init__( self, num_state_qubits: int, diff --git a/qiskit/algorithms/linear_solvers/numpy_linear_solver.py b/qiskit/algorithms/linear_solvers/numpy_linear_solver.py index bbbb3a35f50f..0a3cbbd57191 100644 --- a/qiskit/algorithms/linear_solvers/numpy_linear_solver.py +++ b/qiskit/algorithms/linear_solvers/numpy_linear_solver.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 2022. # # 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 @@ -17,13 +17,14 @@ from qiskit import QuantumCircuit from qiskit.quantum_info import Operator, Statevector from qiskit.quantum_info.operators.base_operator import BaseOperator +from qiskit.utils.deprecation import deprecate_function from .linear_solver import LinearSolverResult, LinearSolver from .observables.linear_system_observable import LinearSystemObservable class NumPyLinearSolver(LinearSolver): - """The Numpy Linear Solver algorithm (classical). + """The deprecated Numpy Linear Solver algorithm (classical). This linear system solver computes the exact value of the given observable(s) or the full solution vector if no observable is specified. @@ -32,21 +33,33 @@ class NumPyLinearSolver(LinearSolver): .. jupyter-execute:: + import warnings import numpy as np from qiskit.algorithms import NumPyLinearSolver from qiskit.algorithms.linear_solvers.matrices import TridiagonalToeplitz from qiskit.algorithms.linear_solvers.observables import MatrixFunctional - matrix = TridiagonalToeplitz(2, 1, 1 / 3, trotter_steps=2) - right_hand_side = [1.0, -2.1, 3.2, -4.3] - observable = MatrixFunctional(1, 1 / 2) - rhs = right_hand_side / np.linalg.norm(right_hand_side) + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + matrix = TridiagonalToeplitz(2, 1, 1 / 3, trotter_steps=2) + right_hand_side = [1.0, -2.1, 3.2, -4.3] + observable = MatrixFunctional(1, 1 / 2) + rhs = right_hand_side / np.linalg.norm(right_hand_side) - np_solver = NumPyLinearSolver() - solution = np_solver.solve(matrix, rhs, observable) + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + np_solver = NumPyLinearSolver() + solution = np_solver.solve(matrix, rhs, observable) result = solution.observable """ + @deprecate_function( + "The NumPyLinearSolver class is deprecated as of Qiskit Terra 0.22.0 " + "and will be removed no sooner than 3 months after the release date. " + ) + def __init__(self) -> None: + super().__init__() + def solve( self, matrix: Union[np.ndarray, QuantumCircuit], diff --git a/qiskit/algorithms/linear_solvers/observables/absolute_average.py b/qiskit/algorithms/linear_solvers/observables/absolute_average.py index 9c9f644dd571..300c31edaed9 100644 --- a/qiskit/algorithms/linear_solvers/observables/absolute_average.py +++ b/qiskit/algorithms/linear_solvers/observables/absolute_average.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 2022. # # 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 @@ -18,12 +18,13 @@ from qiskit import QuantumCircuit from qiskit.opflow import I, Z, TensoredOp from qiskit.quantum_info import Statevector +from qiskit.utils.deprecation import deprecate_function from .linear_system_observable import LinearSystemObservable class AbsoluteAverage(LinearSystemObservable): - r"""An observable for the absolute average of a linear system of equations solution. + r"""The deprecated observable for the absolute average of a linear system of equations solution. For a vector :math:`x=(x_1,...,x_N)`, the absolute average is defined as :math:`\abs{\frac{1}{N}\sum_{i=1}^{N}x_i}`. @@ -32,13 +33,16 @@ class AbsoluteAverage(LinearSystemObservable): .. jupyter-execute:: + import warnings import numpy as np from qiskit import QuantumCircuit from qiskit.algorithms.linear_solvers.observables.absolute_average import \ AbsoluteAverage from qiskit.opflow import StateFn - observable = AbsoluteAverage() + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + observable = AbsoluteAverage() vector = [1.0, -2.1, 3.2, -4.3] init_state = vector / np.linalg.norm(vector) @@ -59,6 +63,13 @@ class AbsoluteAverage(LinearSystemObservable): exact = observable.evaluate_classically(init_state) """ + @deprecate_function( + "The AbsoluteAverage class is deprecated as of Qiskit Terra 0.22.0 " + "and will be removed no sooner than 3 months after the release date. " + ) + def __init__(self) -> None: + super().__init__() + def observable(self, num_qubits: int) -> Union[TensoredOp, List[TensoredOp]]: """The observable operator. diff --git a/qiskit/algorithms/linear_solvers/observables/linear_system_observable.py b/qiskit/algorithms/linear_solvers/observables/linear_system_observable.py index 048291d3d96d..fd6ea2339738 100644 --- a/qiskit/algorithms/linear_solvers/observables/linear_system_observable.py +++ b/qiskit/algorithms/linear_solvers/observables/linear_system_observable.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 2022. # # 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 @@ -18,10 +18,18 @@ from qiskit import QuantumCircuit from qiskit.opflow import TensoredOp +from qiskit.utils.deprecation import deprecate_function class LinearSystemObservable(ABC): - """An abstract class for linear system observables in Qiskit.""" + """The deprecated abstract class for linear system observables in Qiskit.""" + + @deprecate_function( + "The LinearSystemObservable class is deprecated as of Qiskit Terra 0.22.0 " + "and will be removed no sooner than 3 months after the release date. " + ) + def __init__(self) -> None: + pass @abstractmethod def observable(self, num_qubits: int) -> Union[TensoredOp, List[TensoredOp]]: diff --git a/qiskit/algorithms/linear_solvers/observables/matrix_functional.py b/qiskit/algorithms/linear_solvers/observables/matrix_functional.py index 8611620377e2..ca0cfecf16bc 100644 --- a/qiskit/algorithms/linear_solvers/observables/matrix_functional.py +++ b/qiskit/algorithms/linear_solvers/observables/matrix_functional.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 2022. # # 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 @@ -13,23 +13,26 @@ """The matrix functional of the vector solution to the linear systems.""" from typing import Union, List +import warnings import numpy as np from scipy.sparse import diags from qiskit import QuantumCircuit from qiskit.quantum_info import Statevector from qiskit.opflow import I, Z, TensoredOp +from qiskit.utils.deprecation import deprecate_function from .linear_system_observable import LinearSystemObservable class MatrixFunctional(LinearSystemObservable): - """A class for the matrix functional of the vector solution to the linear systems. + """The deprecated class for the matrix functional of the vector solution to the linear systems. Examples: .. jupyter-execute:: + import warnings import numpy as np from qiskit import QuantumCircuit from qiskit.algorithms.linear_solvers.observables.matrix_functional import \ @@ -40,7 +43,9 @@ class MatrixFunctional(LinearSystemObservable): tpass = RemoveResetInZeroState() vector = [1.0, -2.1, 3.2, -4.3] - observable = MatrixFunctional(1, -1 / 3) + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + observable = MatrixFunctional(1, -1 / 3) init_state = vector / np.linalg.norm(vector) num_qubits = int(np.log2(len(vector))) @@ -70,6 +75,10 @@ class MatrixFunctional(LinearSystemObservable): exact = observable.evaluate_classically(init_state) """ + @deprecate_function( + "The MatrixFunctional class is deprecated as of Qiskit Terra 0.22.0 " + "and will be removed no sooner than 3 months after the release date. " + ) def __init__(self, main_diag: float, off_diag: int) -> None: """ Args: @@ -78,6 +87,9 @@ def __init__(self, main_diag: float, off_diag: int) -> None: off_diag: The off diagonal of the tridiagonal Toeplitz symmetric matrix to compute the functional. """ + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + super().__init__() self._main_diag = main_diag self._off_diag = off_diag diff --git a/releasenotes/notes/deprecate-linear-solvers-factorizers-bbf5302484cb6831.yaml b/releasenotes/notes/deprecate-linear-solvers-factorizers-bbf5302484cb6831.yaml new file mode 100644 index 000000000000..2d7034394f5d --- /dev/null +++ b/releasenotes/notes/deprecate-linear-solvers-factorizers-bbf5302484cb6831.yaml @@ -0,0 +1,9 @@ +--- +deprecations: + - | + Modules :mod:`qiskit.algorithms.factorizers` and + :mod:`qiskit.algorithms.linear_solvers` are deprecated and will + be removed in a future release. + They are replaced by tutorials in the Qiskit Textbook: + `Shor `__ + `HHL `__ diff --git a/test/python/algorithms/test_backendv1.py b/test/python/algorithms/test_backendv1.py index aa251841e744..21674d1c953e 100644 --- a/test/python/algorithms/test_backendv1.py +++ b/test/python/algorithms/test_backendv1.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -13,6 +13,7 @@ """ Test Providers that support BackendV1 interface """ import unittest +import warnings from test.python.algorithms import QiskitAlgorithmsTestCase from qiskit import QuantumCircuit from qiskit.providers.fake_provider import FakeProvider @@ -40,7 +41,14 @@ def test_shor_factoring(self): qasm_simulator = QuantumInstance( self._qasm, shots=1000, seed_simulator=self.seed, seed_transpiler=self.seed ) - shor = Shor(quantum_instance=qasm_simulator) + with warnings.catch_warnings(record=True) as caught_warnings: + warnings.filterwarnings( + "always", + category=DeprecationWarning, + ) + shor = Shor(quantum_instance=qasm_simulator) + self.assertTrue("Shor class is deprecated" in str(caught_warnings[0].message)) + result = shor.factor(N=n_v) self.assertListEqual(result.factors[0], factors) self.assertTrue(result.total_counts >= result.successful_counts) diff --git a/test/python/algorithms/test_backendv2.py b/test/python/algorithms/test_backendv2.py index 27cf0f7cfb86..4b86bfc35139 100644 --- a/test/python/algorithms/test_backendv2.py +++ b/test/python/algorithms/test_backendv2.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -13,6 +13,7 @@ """ Test Providers that support BackendV2 interface """ import unittest +import warnings from test.python.algorithms import QiskitAlgorithmsTestCase from qiskit import QuantumCircuit from qiskit.providers.fake_provider import FakeProvider @@ -40,7 +41,14 @@ def test_shor_factoring(self): qasm_simulator = QuantumInstance( self._qasm, shots=1000, seed_simulator=self.seed, seed_transpiler=self.seed ) - shor = Shor(quantum_instance=qasm_simulator) + with warnings.catch_warnings(record=True) as caught_warnings: + warnings.filterwarnings( + "always", + category=DeprecationWarning, + ) + shor = Shor(quantum_instance=qasm_simulator) + self.assertTrue("Shor class is deprecated" in str(caught_warnings[0].message)) + result = shor.factor(N=n_v) self.assertListEqual(result.factors[0], factors) self.assertTrue(result.total_counts >= result.successful_counts) diff --git a/test/python/algorithms/test_linear_solvers.py b/test/python/algorithms/test_linear_solvers.py index ea16bf5ee0e9..b013ed5c32e2 100644 --- a/test/python/algorithms/test_linear_solvers.py +++ b/test/python/algorithms/test_linear_solvers.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 2022. # # 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 @@ -13,6 +13,7 @@ """Test the quantum linear system solver algorithm.""" import unittest +import warnings from test.python.algorithms import QiskitAlgorithmsTestCase from scipy.linalg import expm import numpy as np @@ -30,6 +31,22 @@ from qiskit import quantum_info +def _factory_tridiagonal_toeplitz( + num_state_qubits: int, main_diag: float, off_diag: float, trotter_steps: int = 1 +): + with warnings.catch_warnings(record=True): + warnings.simplefilter("ignore") + return TridiagonalToeplitz( + num_state_qubits, main_diag, off_diag, trotter_steps=trotter_steps + ) + + +def _factory_numpy_matrix(matrix: np.ndarray): + with warnings.catch_warnings(record=True): + warnings.simplefilter("ignore") + return NumPyMatrix(matrix) + + @ddt class TestMatrices(QiskitAlgorithmsTestCase): """Tests based on the matrices classes. @@ -40,10 +57,10 @@ class TestMatrices(QiskitAlgorithmsTestCase): @idata( [ - [TridiagonalToeplitz(2, 1, -1 / 3)], - [TridiagonalToeplitz(3, 2, 1), 1.1, 3], + [_factory_tridiagonal_toeplitz(2, 1, -1 / 3)], + [_factory_tridiagonal_toeplitz(3, 2, 1), 1.1, 3], [ - NumPyMatrix( + _factory_numpy_matrix( np.array( [ [1 / 2, 1 / 6, 0, 0], @@ -80,8 +97,8 @@ def test_matrices(self, matrix, time=1.0, power=1): @idata( [ - [TridiagonalToeplitz(2, 1.5, 2.5)], - [TridiagonalToeplitz(4, -1, 1.6)], + [_factory_tridiagonal_toeplitz(2, 1.5, 2.5)], + [_factory_tridiagonal_toeplitz(4, -1, 1.6)], ] ) @unpack @@ -101,6 +118,18 @@ def test_eigs_bounds(self, matrix): np.testing.assert_almost_equal(matrix_lambda_max, exact_lambda_max, decimal=6) +def _factory_absolute_average(): + with warnings.catch_warnings(record=True): + warnings.simplefilter("ignore") + return AbsoluteAverage() + + +def _factory_matrix_functional(main_diag: float, off_diag: int): + with warnings.catch_warnings(record=True): + warnings.simplefilter("ignore") + return MatrixFunctional(main_diag, off_diag) + + @ddt class TestObservables(QiskitAlgorithmsTestCase): """Tests based on the observables classes. @@ -111,8 +140,8 @@ class TestObservables(QiskitAlgorithmsTestCase): @idata( [ - [AbsoluteAverage(), [1.0, -2.1, 3.2, -4.3]], - [AbsoluteAverage(), [-9 / 4, -0.3, 8 / 7, 10, -5, 11.1, 13 / 11, -27 / 12]], + [_factory_absolute_average(), [1.0, -2.1, 3.2, -4.3]], + [_factory_absolute_average(), [-9 / 4, -0.3, 8 / 7, 10, -5, 11.1, 13 / 11, -27 / 12]], ] ) @unpack @@ -139,9 +168,9 @@ def test_absolute_average(self, observable, vector): @idata( [ - [MatrixFunctional(1, -1 / 3), [1.0, -2.1, 3.2, -4.3]], + [_factory_matrix_functional(1, -1 / 3), [1.0, -2.1, 3.2, -4.3]], [ - MatrixFunctional(2 / 3, 11 / 7), + _factory_matrix_functional(2 / 3, 11 / 7), [-9 / 4, -0.3, 8 / 7, 10, -5, 11.1, 13 / 11, -27 / 12], ], ] @@ -237,16 +266,16 @@ class TestLinearSolver(QiskitAlgorithmsTestCase): @idata( [ [ - TridiagonalToeplitz(2, 1, 1 / 3, trotter_steps=2), + _factory_tridiagonal_toeplitz(2, 1, 1 / 3, trotter_steps=2), [1.0, -2.1, 3.2, -4.3], - MatrixFunctional(1, 1 / 2), + _factory_matrix_functional(1, 1 / 2), ], [ np.array( [[0, 0, 1.585, 0], [0, 0, -0.585, 1], [1.585, -0.585, 0, 0], [0, 1, 0, 0]] ), [1.0, 0, 0, 0], - MatrixFunctional(1, 1 / 2), + _factory_matrix_functional(1, 1 / 2), ], [ [ @@ -256,18 +285,18 @@ class TestLinearSolver(QiskitAlgorithmsTestCase): [0, 0, 1 / 6, 1 / 2], ], [1.0, -2.1, 3.2, -4.3], - MatrixFunctional(1, 1 / 2), + _factory_matrix_functional(1, 1 / 2), ], [ np.array([[82, 34], [34, 58]]), np.array([[1], [0]]), - AbsoluteAverage(), + _factory_absolute_average(), 3, ], [ - TridiagonalToeplitz(3, 1, -1 / 2, trotter_steps=2), + _factory_tridiagonal_toeplitz(3, 1, -1 / 2, trotter_steps=2), [-9 / 4, -0.3, 8 / 7, 10, -5, 11.1, 13 / 11, -27 / 12], - AbsoluteAverage(), + _factory_absolute_average(), ], ] ) @@ -287,8 +316,11 @@ def test_hhl(self, matrix, right_hand_side, observable, decimal=1): qc = QuantumCircuit(num_qubits) qc.isometry(rhs, list(range(num_qubits)), None) - hhl = HHL() - solution = hhl.solve(matrix, qc, observable) + with warnings.catch_warnings(record=True) as caught_warnings: + warnings.simplefilter("always") + hhl = HHL() + self.assertTrue("HHL class is deprecated" in str(caught_warnings[0].message)) + solution = hhl.solve(matrix, qc, observable) approx_result = solution.observable # Calculate analytical value @@ -304,7 +336,10 @@ def test_hhl(self, matrix, right_hand_side, observable, decimal=1): def test_hhl_qi(self): """Test the HHL quantum instance getter and setter.""" - hhl = HHL() + with warnings.catch_warnings(record=True) as caught_warnings: + warnings.simplefilter("always") + hhl = HHL() + self.assertTrue("HHL class is deprecated" in str(caught_warnings[0].message)) self.assertIsNone(hhl.quantum_instance) # Defaults to None # First set a valid quantum instance and check via getter diff --git a/test/python/algorithms/test_shor.py b/test/python/algorithms/test_shor.py index 0ecc5886fe54..811680b1799a 100644 --- a/test/python/algorithms/test_shor.py +++ b/test/python/algorithms/test_shor.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2020. +# (C) Copyright IBM 2018, 2022. # # 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 @@ -13,6 +13,7 @@ """ Test Shor """ import unittest +import warnings import math from test.python.algorithms import QiskitAlgorithmsTestCase from ddt import ddt, data, idata, unpack @@ -30,12 +31,18 @@ class TestShor(QiskitAlgorithmsTestCase): def setUp(self): super().setUp() - backend = Aer.get_backend("qasm_simulator") - self.instance = Shor(quantum_instance=QuantumInstance(backend, shots=1000)) + backend = Aer.get_backend("aer_simulator") + with warnings.catch_warnings(record=True) as caught_warnings: + warnings.filterwarnings( + "always", + category=DeprecationWarning, + ) + self.instance = Shor(quantum_instance=QuantumInstance(backend, shots=1000)) + self.assertTrue("Shor class is deprecated" in str(caught_warnings[0].message)) @idata( [ - [15, "qasm_simulator", [3, 5]], + [15, "aer_simulator", [3, 5]], ] ) @unpack @@ -46,7 +53,7 @@ def test_shor_factoring(self, n_v, backend, factors): @slow_test @idata( [ - [21, "qasm_simulator", [3, 7]], + [21, "aer_simulator", [3, 7]], ] ) @unpack @@ -56,7 +63,10 @@ def test_shor_factoring_5_bit_number(self, n_v, backend, factors): def _test_shor_factoring(self, backend, factors, n_v): """shor factoring test""" - shor = Shor(quantum_instance=QuantumInstance(Aer.get_backend(backend), shots=1000)) + with warnings.catch_warnings(record=True) as caught_warnings: + warnings.simplefilter("always") + shor = Shor(quantum_instance=QuantumInstance(Aer.get_backend(backend), shots=1000)) + self.assertTrue("Shor class is deprecated" in str(caught_warnings[0].message)) result = shor.factor(N=n_v) self.assertListEqual(result.factors[0], factors) self.assertTrue(result.total_counts >= result.successful_counts)