From cd507adc68a96a333bc62de7ce9799a245506f25 Mon Sep 17 00:00:00 2001 From: Jay Gambetta Date: Fri, 8 Sep 2023 11:15:41 -0400 Subject: [PATCH 01/19] Adding h, p and u to basic aer (#10673) * Adding h, u and p to the BasicAer * typo * reno * reno * Update releasenotes/notes/h_basic_aer-3fc5e6776f0de9c1.yaml --------- Co-authored-by: Luciano Bello --- qiskit/providers/basicaer/basicaertools.py | 8 +++++++- qiskit/providers/basicaer/qasm_simulator.py | 17 ++++++++++++++++- .../notes/h_basic_aer-3fc5e6776f0de9c1.yaml | 5 +++++ 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/h_basic_aer-3fc5e6776f0de9c1.yaml diff --git a/qiskit/providers/basicaer/basicaertools.py b/qiskit/providers/basicaer/basicaertools.py index dfba59e7c36f..768febaecc75 100644 --- a/qiskit/providers/basicaer/basicaertools.py +++ b/qiskit/providers/basicaer/basicaertools.py @@ -23,7 +23,7 @@ from qiskit.exceptions import QiskitError # Single qubit gates supported by ``single_gate_params``. -SINGLE_QUBIT_GATES = ("U", "u1", "u2", "u3", "rz", "sx", "x") +SINGLE_QUBIT_GATES = ("U", "u", "h", "p", "u1", "u2", "u3", "rz", "sx", "x") def single_gate_matrix(gate: str, params: Optional[List[float]] = None): @@ -45,6 +45,12 @@ def single_gate_matrix(gate: str, params: Optional[List[float]] = None): gc = gates.UGate elif gate == "u3": gc = gates.U3Gate + elif gate == "h": + gc = gates.HGate + elif gate == "u": + gc = gates.UGate + elif gate == "p": + gc = gates.PhaseGate elif gate == "u2": gc = gates.U2Gate elif gate == "u1": diff --git a/qiskit/providers/basicaer/qasm_simulator.py b/qiskit/providers/basicaer/qasm_simulator.py index fd5027e13cd7..127ea1c26768 100644 --- a/qiskit/providers/basicaer/qasm_simulator.py +++ b/qiskit/providers/basicaer/qasm_simulator.py @@ -70,8 +70,23 @@ class QasmSimulatorPy(BackendV1): "max_shots": 0, "coupling_map": None, "description": "A python simulator for qasm experiments", - "basis_gates": ["u1", "u2", "u3", "rz", "sx", "x", "cx", "id", "unitary"], + "basis_gates": ["h", "u", "p", "u1", "u2", "u3", "rz", "sx", "x", "cx", "id", "unitary"], "gates": [ + { + "name": "h", + "parameters": [], + "qasm_def": "gate h q { U(pi/2,0,pi) q; }", + }, + { + "name": "p", + "parameters": ["lambda"], + "qasm_def": "gate p(lambda) q { U(0,0,lambda) q; }", + }, + { + "name": "u", + "parameters": ["theta", "phi", "lambda"], + "qasm_def": "gate u(theta,phi,lambda) q { U(theta,phi,lambda) q; }", + }, { "name": "u1", "parameters": ["lambda"], diff --git a/releasenotes/notes/h_basic_aer-3fc5e6776f0de9c1.yaml b/releasenotes/notes/h_basic_aer-3fc5e6776f0de9c1.yaml new file mode 100644 index 000000000000..530836dbe597 --- /dev/null +++ b/releasenotes/notes/h_basic_aer-3fc5e6776f0de9c1.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + The `QasmSimulator` python-based simulator included in :class:`qiskit.providers.basicaer` + now includes `h` (:class:`.HGate`), `p` (:class:`.PhaseGate`), and `u` (:class:`.UGate`) in its basis gate set. From 25c24f6706053fbd385ba2eeec719ce541e89b00 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 12 Sep 2023 08:35:14 -0400 Subject: [PATCH 02/19] Add logging message with rust runtime to sabre passes (#10816) This commit adds two debug level log messages to SabreLayout and SabreSwap. For debugging purposes it's often useful to know how long the inner algorithm runtime in rust takes for the pass (especially as compared to the total pass runtime. This commit facilitates this by measuring the runtime of the pass's inner rust execution and adding the debug log message with those details. --- qiskit/transpiler/passes/layout/sabre_layout.py | 8 ++++++++ qiskit/transpiler/passes/routing/sabre_swap.py | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index 8dbefa51174d..6e521c05d7f2 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -16,6 +16,8 @@ import copy import dataclasses import logging +import time + import numpy as np import rustworkx as rx @@ -324,6 +326,7 @@ def _inner_run(self, dag, coupling_map): sabre_dag, circuit_to_dag_dict = _build_sabre_dag( dag, coupling_map.size(), {bit: index for index, bit in enumerate(dag.qubits)} ) + sabre_start = time.perf_counter() (initial_layout, final_permutation, sabre_result) = sabre_layout_and_routing( sabre_dag, neighbor_table, @@ -334,6 +337,11 @@ def _inner_run(self, dag, coupling_map): self.layout_trials, self.seed, ) + sabre_stop = time.perf_counter() + logger.debug( + "Sabre layout algorithm execution for a connected component complete in: %s sec.", + sabre_stop - sabre_start, + ) return _DisjointComponent( dag, coupling_map, diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py index d7298f5370d6..c62a468cf6f3 100644 --- a/qiskit/transpiler/passes/routing/sabre_swap.py +++ b/qiskit/transpiler/passes/routing/sabre_swap.py @@ -14,6 +14,7 @@ import logging from copy import deepcopy +import time import rustworkx @@ -237,6 +238,7 @@ def run(self, dag): self.coupling_map.size(), self._qubit_indices, ) + sabre_start = time.perf_counter() *sabre_result, final_permutation = build_swap_map( len(dag.qubits), sabre_dag, @@ -247,6 +249,8 @@ def run(self, dag): self.trials, self.seed, ) + sabre_stop = time.perf_counter() + logging.debug("Sabre swap algorithm execution complete in: %s", sabre_stop - sabre_start) self.property_set["final_layout"] = Layout(dict(zip(dag.qubits, final_permutation))) if self.fake_run: From 7d17e6875d9f06a3310ee3c64eb5ef96d8f9dadb Mon Sep 17 00:00:00 2001 From: Simone Gasperini Date: Tue, 12 Sep 2023 15:36:56 +0200 Subject: [PATCH 03/19] Allow empty list default initialization for `SparsePauliOp` (#10765) * Allow empty list default initialization of SparsePauliOp.from_list and SparsePauliOp.from_sparse_list * Fix empty iterable comparison in SparsePauliOp list initialization * Fix default arguments, internal checks, and error messages * Add unit tests for SparsePauliOp empty iterable initialization * Fix missing argument and linting errors * Resolve minor changes requested by @ikkoham review * Add release note with reference to issue #10159 * Edit release note as suggested by @jakelishman Co-authored-by: Jake Lishman --------- Co-authored-by: Jake Lishman --- .../operators/symplectic/sparse_pauli_op.py | 42 ++++++++++++------- ...-pauli-op-num-qubits-9c1a3f7dcc7949b9.yaml | 6 +++ .../symplectic/test_sparse_pauli_op.py | 17 ++++++++ 3 files changed, 51 insertions(+), 14 deletions(-) create mode 100644 releasenotes/notes/sparse-pauli-op-num-qubits-9c1a3f7dcc7949b9.yaml diff --git a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py index f42d730c837d..f7031276e6b5 100644 --- a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py +++ b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py @@ -770,7 +770,9 @@ def from_operator( return SparsePauliOp(paulis, coeffs, copy=False) @staticmethod - def from_list(obj: Iterable[tuple[str, complex]], dtype: type = complex) -> SparsePauliOp: + def from_list( + obj: Iterable[tuple[str, complex]], dtype: type = complex, *, num_qubits: int = None + ) -> SparsePauliOp: """Construct from a list of Pauli strings and coefficients. For example, the 5-qubit Hamiltonian @@ -787,23 +789,34 @@ def from_list(obj: Iterable[tuple[str, complex]], dtype: type = complex) -> Spar op = SparsePauliOp.from_list([("XIIZI", 1), ("IYIIY", 2)]) Args: - obj (Iterable[tuple[str, complex]]): The list of 2-tuples specifying the Pauli terms. - dtype (type): The dtype of coeffs (Default complex). + obj (Iterable[Tuple[str, complex]]): The list of 2-tuples specifying the Pauli terms. + dtype (type): The dtype of coeffs (Default: complex). + num_qubits (int): The number of qubits of the operator (Default: None). Returns: SparsePauliOp: The SparsePauliOp representation of the Pauli terms. Raises: - QiskitError: If the list of Paulis is empty. + QiskitError: If an empty list is passed and num_qubits is None. + QiskitError: If num_qubits and the objects in the input list do not match. """ obj = list(obj) # To convert zip or other iterable + size = len(obj) - size = len(obj) # number of Pauli terms + if size == 0 and num_qubits is None: + raise QiskitError( + "Could not determine the number of qubits from an empty list. Try passing num_qubits." + ) + if size > 0 and num_qubits is not None: + if len(obj[0][0]) != num_qubits: + raise QiskitError( + f"num_qubits ({num_qubits}) and the objects in the input list do not match." + ) + if num_qubits is None: + num_qubits = len(obj[0][0]) if size == 0: - raise QiskitError("Input Pauli list is empty.") - - # determine the number of qubits - num_qubits = len(obj[0][0]) + obj = [("I" * num_qubits, 0)] + size = len(obj) coeffs = np.zeros(size, dtype=dtype) labels = np.zeros(size, dtype=f"`__. diff --git a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py index 767153e23883..f44da980c95c 100644 --- a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py +++ b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py @@ -150,6 +150,7 @@ def test_sparse_pauli_op_init(self): self.assertEqual(spp_op, ref_op) +@ddt class TestSparsePauliOpConversions(QiskitTestCase): """Tests SparsePauliOp representation conversions.""" @@ -228,6 +229,22 @@ def test_from_zip(self): np.testing.assert_array_equal(spp_op.coeffs, coeffs) self.assertEqual(spp_op.paulis, PauliList(labels)) + @combine(iterable=[[], (), zip()], num_qubits=[1, 2, 3]) + def test_from_empty_iterable(self, iterable, num_qubits): + """Test from_list method for empty iterable input.""" + with self.assertRaises(QiskitError): + _ = SparsePauliOp.from_list(iterable) + spp_op = SparsePauliOp.from_list(iterable, num_qubits=num_qubits) + self.assertEqual(spp_op.paulis, PauliList("I" * num_qubits)) + np.testing.assert_array_equal(spp_op.coeffs, [0]) + + @combine(iterable=[[], (), zip()], num_qubits=[1, 2, 3]) + def test_from_sparse_empty_iterable(self, iterable, num_qubits): + """Test from_sparse_list method for empty iterable input.""" + spp_op = SparsePauliOp.from_sparse_list(iterable, num_qubits) + self.assertEqual(spp_op.paulis, PauliList("I" * num_qubits)) + np.testing.assert_array_equal(spp_op.coeffs, [0]) + def test_to_matrix(self): """Test to_matrix method.""" labels = ["XI", "YZ", "YY", "ZZ"] From 884cfe6c6673b39eeba6a4374e79444bfce5fc2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Tue, 12 Sep 2023 16:43:40 +0200 Subject: [PATCH 04/19] Deprecate `bind_parameters` in favor of `assign_parameters` (#10792) * Deprecate bind parameters * Replace bind in docstring * Test deprecation warning * Remove bind_parameters in algorithms * Update releasenotes/notes/deprecate-bind-parameters-283c94069e8a9142.yaml Co-authored-by: Jake Lishman * Apply suggestions from code review * suppress warnings in test Parameter Expressions * Add module to filter * Add filter to context manager * Fix black --------- Co-authored-by: Jake Lishman --- qiskit/algorithms/eigen_solvers/vqd.py | 14 +++++--- qiskit/algorithms/eigensolvers/vqd.py | 2 +- .../evolvers/trotterization/trotter_qrte.py | 2 +- qiskit/algorithms/gradients/reverse/bind.py | 2 +- .../algorithms/minimum_eigen_solvers/vqe.py | 4 +-- qiskit/algorithms/optimizers/qnspsa.py | 4 +-- qiskit/algorithms/optimizers/spsa.py | 2 +- qiskit/algorithms/time_evolvers/pvqd/pvqd.py | 4 +-- qiskit/circuit/parameter.py | 2 +- qiskit/circuit/quantumcircuit.py | 4 +++ qiskit/compiler/assembler.py | 2 +- qiskit/primitives/backend_estimator.py | 2 +- qiskit/primitives/backend_sampler.py | 2 +- qiskit/primitives/estimator.py | 2 +- qiskit/primitives/sampler.py | 2 +- .../passes/synthesis/unitary_synthesis.py | 2 +- .../passes/utils/unroll_forloops.py | 4 ++- ...cate-bind-parameters-283c94069e8a9142.yaml | 8 +++++ test/benchmarks/circuit_construction.py | 4 ++- .../algorithms/test_aux_ops_evaluator.py | 2 +- .../algorithms/test_observables_evaluator.py | 6 ++-- test/python/algorithms/test_vqe.py | 2 +- .../variational/test_var_qrte.py | 2 +- .../circuit/test_circuit_load_from_qpy.py | 4 ++- .../python/circuit/test_circuit_operations.py | 4 +-- test/python/circuit/test_hamiltonian_gate.py | 10 +++--- test/python/circuit/test_parameters.py | 33 ++++++++++++++++++- test/python/compiler/test_assembler.py | 2 +- .../primitives/test_backend_estimator.py | 2 +- test/python/primitives/test_estimator.py | 4 +-- test/python/qasm2/test_circuit_methods.py | 6 ++-- test/python/qasm2/test_legacy_importer.py | 6 ++-- test/python/qasm3/test_export.py | 8 ++--- .../test_optimize_1q_decomposition.py | 12 +++---- .../transpiler/test_template_matching.py | 8 ++--- .../visualization/test_circuit_latex.py | 2 +- .../visualization/test_circuit_text_drawer.py | 6 ++-- test/qpy_compat/test_qpy.py | 4 +-- .../circuit/test_circuit_matplotlib_drawer.py | 2 +- 39 files changed, 123 insertions(+), 70 deletions(-) create mode 100644 releasenotes/notes/deprecate-bind-parameters-283c94069e8a9142.yaml diff --git a/qiskit/algorithms/eigen_solvers/vqd.py b/qiskit/algorithms/eigen_solvers/vqd.py index 72e3dd306647..068cdd08cd14 100644 --- a/qiskit/algorithms/eigen_solvers/vqd.py +++ b/qiskit/algorithms/eigen_solvers/vqd.py @@ -473,7 +473,9 @@ def _eval_aux_ops( list_op = ListOp(aux_operators) aux_op_meas = expectation.convert(StateFn(list_op, is_measurement=True)) - aux_op_expect = aux_op_meas.compose(CircuitStateFn(self.ansatz.bind_parameters(parameters))) + aux_op_expect = aux_op_meas.compose( + CircuitStateFn(self.ansatz.assign_parameters(parameters)) + ) aux_op_expect_sampled = sampler.convert(aux_op_expect) # compute means @@ -611,7 +613,9 @@ def compute_eigenvalues( eigenvalue = ( StateFn(operator, is_measurement=True) - .compose(CircuitStateFn(self.ansatz.bind_parameters(result.optimal_parameters[-1]))) + .compose( + CircuitStateFn(self.ansatz.assign_parameters(result.optimal_parameters[-1])) + ) .reduce() .eval() ) @@ -620,7 +624,7 @@ def compute_eigenvalues( result.eigenstates.append(self._get_eigenstate(result.optimal_parameters[-1])) if aux_operators is not None: - bound_ansatz = self.ansatz.bind_parameters(result.optimal_point[-1]) + bound_ansatz = self.ansatz.assign_parameters(result.optimal_point[-1]) aux_value = eval_observables( self.quantum_instance, bound_ansatz, aux_operators, expectation=expectation ) @@ -715,7 +719,7 @@ def get_energy_evaluation( for state in range(step - 1): - prev_circ = self.ansatz.bind_parameters(prev_states[state]) + prev_circ = self.ansatz.assign_parameters(prev_states[state]) overlap_op.append(~CircuitStateFn(prev_circ) @ CircuitStateFn(self.ansatz)) def energy_evaluation(parameters): @@ -751,7 +755,7 @@ def energy_evaluation(parameters): def _get_eigenstate(self, optimal_parameters) -> list[float] | dict[str, int]: """Get the simulation outcome of the ansatz, provided with parameters.""" - optimal_circuit = self.ansatz.bind_parameters(optimal_parameters) + optimal_circuit = self.ansatz.assign_parameters(optimal_parameters) state_fn = self._circuit_sampler.convert(StateFn(optimal_circuit)).eval() if self.quantum_instance.is_statevector: state = state_fn.primitive.data # VectorStateFn -> Statevector -> np.array diff --git a/qiskit/algorithms/eigensolvers/vqd.py b/qiskit/algorithms/eigensolvers/vqd.py index cbb4d56af313..59f07d8a918b 100644 --- a/qiskit/algorithms/eigensolvers/vqd.py +++ b/qiskit/algorithms/eigensolvers/vqd.py @@ -271,7 +271,7 @@ def compute_eigenvalues( initial_point = validate_initial_point(initial_points[step - 1], self.ansatz) if step > 1: - prev_states.append(self.ansatz.bind_parameters(result.optimal_points[-1])) + prev_states.append(self.ansatz.assign_parameters(result.optimal_points[-1])) self._eval_count = 0 energy_evaluation = self._get_evaluate_energy( diff --git a/qiskit/algorithms/evolvers/trotterization/trotter_qrte.py b/qiskit/algorithms/evolvers/trotterization/trotter_qrte.py index 4c4af495e6ba..538635c67f42 100644 --- a/qiskit/algorithms/evolvers/trotterization/trotter_qrte.py +++ b/qiskit/algorithms/evolvers/trotterization/trotter_qrte.py @@ -204,7 +204,7 @@ def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: f"PauliSumOp | SummedOp, {type(hamiltonian)} provided." ) if isinstance(hamiltonian, OperatorBase): - hamiltonian = hamiltonian.bind_parameters(evolution_problem.param_value_dict) + hamiltonian = hamiltonian.assign_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/gradients/reverse/bind.py b/qiskit/algorithms/gradients/reverse/bind.py index 5380090b7425..7660f7c836d0 100644 --- a/qiskit/algorithms/gradients/reverse/bind.py +++ b/qiskit/algorithms/gradients/reverse/bind.py @@ -26,7 +26,7 @@ def bind( """Bind parameters in a circuit (or list of circuits). This method also allows passing parameter binds to parameters that are not in the circuit, - and thereby differs to :meth:`.QuantumCircuit.bind_parameters`. + and thereby differs to :meth:`.QuantumCircuit.assign_parameters`. Args: circuits: Input circuit(s). diff --git a/qiskit/algorithms/minimum_eigen_solvers/vqe.py b/qiskit/algorithms/minimum_eigen_solvers/vqe.py index bf9304e4d6b7..3ca342f9894d 100644 --- a/qiskit/algorithms/minimum_eigen_solvers/vqe.py +++ b/qiskit/algorithms/minimum_eigen_solvers/vqe.py @@ -569,7 +569,7 @@ def compute_minimum_eigenvalue( self._ret = result if aux_operators is not None: - bound_ansatz = self.ansatz.bind_parameters(result.optimal_point) + bound_ansatz = self.ansatz.assign_parameters(result.optimal_point) aux_values = eval_observables( self.quantum_instance, bound_ansatz, aux_operators, expectation=expectation @@ -648,7 +648,7 @@ def energy_evaluation(parameters): def _get_eigenstate(self, optimal_parameters) -> list[float] | dict[str, int]: """Get the simulation outcome of the ansatz, provided with parameters.""" - optimal_circuit = self.ansatz.bind_parameters(optimal_parameters) + optimal_circuit = self.ansatz.assign_parameters(optimal_parameters) state_fn = self._circuit_sampler.convert(StateFn(optimal_circuit)).eval() if self.quantum_instance.is_statevector: state = state_fn.primitive.data # VectorStateFn -> Statevector -> np.array diff --git a/qiskit/algorithms/optimizers/qnspsa.py b/qiskit/algorithms/optimizers/qnspsa.py index c9c195729a84..be5907afbf17 100644 --- a/qiskit/algorithms/optimizers/qnspsa.py +++ b/qiskit/algorithms/optimizers/qnspsa.py @@ -105,7 +105,7 @@ def loss(x): initial_point = np.random.random(ansatz.num_parameters) def loss(x): - bound = ansatz.bind_parameters(x) + bound = ansatz.assign_parameters(x) return np.real((StateFn(observable, is_measurement=True) @ StateFn(bound)).eval()) fidelity = QNSPSA.get_fidelity(ansatz) @@ -387,7 +387,7 @@ def fidelity(values_x, values_y): value_dict = dict( zip(params_x[:] + params_y[:], values_x.tolist() + values_y.tolist()) ) - return np.abs(expression.bind_parameters(value_dict).eval()) ** 2 + return np.abs(expression.assign_parameters(value_dict).eval()) ** 2 else: sampler = CircuitSampler(backend) diff --git a/qiskit/algorithms/optimizers/spsa.py b/qiskit/algorithms/optimizers/spsa.py index 0226f8a00a1e..fb39ad781f72 100644 --- a/qiskit/algorithms/optimizers/spsa.py +++ b/qiskit/algorithms/optimizers/spsa.py @@ -98,7 +98,7 @@ class SPSA(Optimizer): initial_point = np.random.random(ansatz.num_parameters) def loss(x): - bound = ansatz.bind_parameters(x) + bound = ansatz.assign_parameters(x) return np.real((StateFn(observable, is_measurement=True) @ StateFn(bound)).eval()) spsa = SPSA(maxiter=300) diff --git a/qiskit/algorithms/time_evolvers/pvqd/pvqd.py b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py index ea3d82dc4d74..dd560858cebd 100644 --- a/qiskit/algorithms/time_evolvers/pvqd/pvqd.py +++ b/qiskit/algorithms/time_evolvers/pvqd/pvqd.py @@ -232,7 +232,7 @@ def get_loss( self._validate_setup(skip={"optimizer"}) # use Trotterization to evolve the current state - trotterized = ansatz.bind_parameters(current_parameters) + trotterized = ansatz.assign_parameters(current_parameters) evolution_gate = PauliEvolutionGate(hamiltonian, time=dt, synthesis=self.evolution) @@ -388,7 +388,7 @@ def evolve(self, evolution_problem: TimeEvolutionProblem) -> TimeEvolutionResult if observables is not None: observable_values.append(evaluate_observables(next_parameters)) - evolved_state = self.ansatz.bind_parameters(parameters[-1]) + evolved_state = self.ansatz.assign_parameters(parameters[-1]) result = PVQDResult( evolved_state=evolved_state, diff --git a/qiskit/circuit/parameter.py b/qiskit/circuit/parameter.py index fae84a71852b..75d51ed592d1 100644 --- a/qiskit/circuit/parameter.py +++ b/qiskit/circuit/parameter.py @@ -45,7 +45,7 @@ class Parameter(ParameterExpression): qc.draw('mpl') # bind the parameters after circuit to create a bound circuit - bc = qc.bind_parameters({phi: 3.14}) + bc = qc.assign_parameters({phi: 3.14}) bc.measure_all() bc.draw('mpl') """ diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 651cde25678e..77660a55889a 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -2780,6 +2780,10 @@ def _unroll_param_dict( out[parameter] = value return out + @deprecate_func( + additional_msg=("Use assign_parameters() instead"), + since="0.45.0", + ) def bind_parameters( self, values: Union[Mapping[Parameter, float], Sequence[float]] ) -> "QuantumCircuit": diff --git a/qiskit/compiler/assembler.py b/qiskit/compiler/assembler.py index 5a9e15c33f0e..9ebcb0783526 100644 --- a/qiskit/compiler/assembler.py +++ b/qiskit/compiler/assembler.py @@ -587,7 +587,7 @@ def _expand_parameters(circuits, run_config): ) circuits = [ - circuit.bind_parameters(binds) for circuit in circuits for binds in parameter_binds + circuit.assign_parameters(binds) for circuit in circuits for binds in parameter_binds ] # All parameters have been expanded and bound, so remove from run_config diff --git a/qiskit/primitives/backend_estimator.py b/qiskit/primitives/backend_estimator.py index d4c733b5133d..740a870e0d65 100644 --- a/qiskit/primitives/backend_estimator.py +++ b/qiskit/primitives/backend_estimator.py @@ -245,7 +245,7 @@ def _call( bound_circuits = [ transpiled_circuits[circuit_index] if len(p) == 0 - else transpiled_circuits[circuit_index].bind_parameters(p) + else transpiled_circuits[circuit_index].assign_parameters(p) for i, (p, n) in enumerate(zip(parameter_dicts, num_observables)) for circuit_index in range(accum[i], accum[i] + n) ] diff --git a/qiskit/primitives/backend_sampler.py b/qiskit/primitives/backend_sampler.py index 6d7f16173980..6510b981b68a 100644 --- a/qiskit/primitives/backend_sampler.py +++ b/qiskit/primitives/backend_sampler.py @@ -140,7 +140,7 @@ def _call( bound_circuits = [ transpiled_circuits[i] if len(value) == 0 - else transpiled_circuits[i].bind_parameters((dict(zip(self._parameters[i], value)))) + else transpiled_circuits[i].assign_parameters((dict(zip(self._parameters[i], value)))) for i, value in zip(circuits, parameter_values) ] bound_circuits = self._bound_pass_manager_run(bound_circuits) diff --git a/qiskit/primitives/estimator.py b/qiskit/primitives/estimator.py index 8afa7783ee40..a63949542d06 100644 --- a/qiskit/primitives/estimator.py +++ b/qiskit/primitives/estimator.py @@ -96,7 +96,7 @@ def _call( bound_circuits.append( self._circuits[i] if len(value) == 0 - else self._circuits[i].bind_parameters(dict(zip(self._parameters[i], value))) + else self._circuits[i].assign_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 82c637b5d2ba..c96f7b886c1c 100644 --- a/qiskit/primitives/sampler.py +++ b/qiskit/primitives/sampler.py @@ -93,7 +93,7 @@ def _call( bound_circuits.append( self._circuits[i] if len(value) == 0 - else self._circuits[i].bind_parameters(dict(zip(self._parameters[i], value))) + else self._circuits[i].assign_parameters(dict(zip(self._parameters[i], value))) ) qargs_list.append(self._qargs_list[i]) probabilities = [ diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index cbab15b37a11..30a8ebfe4e30 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -786,7 +786,7 @@ def is_controlled(gate): # rewrite XX of the same strength in terms of it embodiment = XXEmbodiments[type(v)] if len(embodiment.parameters) == 1: - embodiments[strength] = embodiment.bind_parameters([strength]) + embodiments[strength] = embodiment.assign_parameters([strength]) else: embodiments[strength] = embodiment # basis equivalent to CX are well optimized so use for the pi/2 angle if available diff --git a/qiskit/transpiler/passes/utils/unroll_forloops.py b/qiskit/transpiler/passes/utils/unroll_forloops.py index c83a47f72eec..8cde3874adcb 100644 --- a/qiskit/transpiler/passes/utils/unroll_forloops.py +++ b/qiskit/transpiler/passes/utils/unroll_forloops.py @@ -59,7 +59,9 @@ def run(self, dag): unrolled_dag = circuit_to_dag(body).copy_empty_like() for index_value in indexset: - bound_body = body.bind_parameters({loop_param: index_value}) if loop_param else body + bound_body = ( + body.assign_parameters({loop_param: index_value}) if loop_param else body + ) unrolled_dag.compose(circuit_to_dag(bound_body), inplace=True) dag.substitute_node_with_dag(forloop_op, unrolled_dag) diff --git a/releasenotes/notes/deprecate-bind-parameters-283c94069e8a9142.yaml b/releasenotes/notes/deprecate-bind-parameters-283c94069e8a9142.yaml new file mode 100644 index 000000000000..62ed323a4a57 --- /dev/null +++ b/releasenotes/notes/deprecate-bind-parameters-283c94069e8a9142.yaml @@ -0,0 +1,8 @@ +--- +deprecations: + - | + The method :meth:`.QuantumCircuit.bind_parameters` is now deprecated and will be removed + from the codebase in no less than 3 months from the release date. Its functionality + overlapped highly with :meth:`.QuantumCircuit.assign_parameters`, and can be totally + replaced by it. Please use :meth:`.QuantumCircuit.assign_parameters` instead. + diff --git a/test/benchmarks/circuit_construction.py b/test/benchmarks/circuit_construction.py index 6e984823dff0..d4497ed3a04c 100644 --- a/test/benchmarks/circuit_construction.py +++ b/test/benchmarks/circuit_construction.py @@ -93,4 +93,6 @@ def setup(self, width, gates, params): self.circuit, self.params = build_parameterized_circuit(width, gates, params) def time_bind_params(self, _, __, ___): - self.circuit.bind_parameters({x: 3.14 for x in self.params}) + # TODO: write more complete benchmarks of assign_parameters + # that test more of the input formats / combinations + self.circuit.assign_parameters({x: 3.14 for x in self.params}) diff --git a/test/python/algorithms/test_aux_ops_evaluator.py b/test/python/algorithms/test_aux_ops_evaluator.py index 11e4c8e76fc5..af4e30faaf7f 100644 --- a/test/python/algorithms/test_aux_ops_evaluator.py +++ b/test/python/algorithms/test_aux_ops_evaluator.py @@ -125,7 +125,7 @@ def test_eval_observables(self, observables: ListOrDict[OperatorBase]): dtype=float, ) - bound_ansatz = ansatz.bind_parameters(parameters) + bound_ansatz = ansatz.assign_parameters(parameters) expected_result = self.get_exact_expectation(bound_ansatz, observables) for backend_name in self.backend_names: diff --git a/test/python/algorithms/test_observables_evaluator.py b/test/python/algorithms/test_observables_evaluator.py index f4df4ba4bdf5..045c05c36f2d 100644 --- a/test/python/algorithms/test_observables_evaluator.py +++ b/test/python/algorithms/test_observables_evaluator.py @@ -119,7 +119,7 @@ def test_estimate_observables(self, observables: ListOrDict[BaseOperator | Pauli dtype=float, ) - bound_ansatz = ansatz.bind_parameters(parameters) + bound_ansatz = ansatz.assign_parameters(parameters) states = bound_ansatz expected_result = self.get_exact_expectation(bound_ansatz, observables) estimator = Estimator() @@ -140,7 +140,7 @@ def test_estimate_observables_zero_op(self): dtype=float, ) - bound_ansatz = ansatz.bind_parameters(parameters) + bound_ansatz = ansatz.assign_parameters(parameters) state = bound_ansatz estimator = Estimator() observables = [SparsePauliOp(["XX", "YY"]), 0] @@ -162,7 +162,7 @@ def test_estimate_observables_shots(self): dtype=float, ) - bound_ansatz = ansatz.bind_parameters(parameters) + bound_ansatz = ansatz.assign_parameters(parameters) state = bound_ansatz estimator = Estimator(options={"shots": 2048}) with self.assertWarns(DeprecationWarning): diff --git a/test/python/algorithms/test_vqe.py b/test/python/algorithms/test_vqe.py index c9fee9547d19..4272227fa027 100644 --- a/test/python/algorithms/test_vqe.py +++ b/test/python/algorithms/test_vqe.py @@ -844,7 +844,7 @@ def test_construct_eigenstate_from_optpoint(self): vqe = VQE(optimizer=optimizer, quantum_instance=quantum_instance) result = vqe.compute_minimum_eigenvalue(hamiltonian) - optimal_circuit = vqe.ansatz.bind_parameters(result.optimal_point) + optimal_circuit = vqe.ansatz.assign_parameters(result.optimal_point) self.assertTrue(Statevector(result.eigenstate).equiv(optimal_circuit)) diff --git a/test/python/algorithms/time_evolvers/variational/test_var_qrte.py b/test/python/algorithms/time_evolvers/variational/test_var_qrte.py index b7dfaf59f12b..a2d9665417d8 100644 --- a/test/python/algorithms/time_evolvers/variational/test_var_qrte.py +++ b/test/python/algorithms/time_evolvers/variational/test_var_qrte.py @@ -65,7 +65,7 @@ def expected_state(time): result = varqrte.evolve(evolution_problem) final_parameters = result.parameter_values[-1] - final_state = Statevector(circuit.bind_parameters(final_parameters)).to_dict() + final_state = Statevector(circuit.assign_parameters(final_parameters)).to_dict() final_expected_state = expected_state(final_time) for key, expected_value in final_state.items(): diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 5057105cb5ca..1e2f5263ce34 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -251,7 +251,9 @@ def test_parameter(self): qpy_file.seek(0) new_circ = load(qpy_file)[0] self.assertEqual(qc, new_circ) - self.assertEqual(qc.bind_parameters({theta: 3.14}), new_circ.bind_parameters({theta: 3.14})) + self.assertEqual( + qc.assign_parameters({theta: 3.14}), new_circ.assign_parameters({theta: 3.14}) + ) self.assertDeprecatedBitProperties(qc, new_circ) def test_bound_parameter(self): diff --git a/test/python/circuit/test_circuit_operations.py b/test/python/circuit/test_circuit_operations.py index 81e9e0f214fd..43c60ae4c767 100644 --- a/test/python/circuit/test_circuit_operations.py +++ b/test/python/circuit/test_circuit_operations.py @@ -760,7 +760,7 @@ def test_bind_global_phase(self): circuit = QuantumCircuit(1, global_phase=x) self.assertEqual(circuit.parameters, {x}) - bound = circuit.bind_parameters({x: 2}) + bound = circuit.assign_parameters({x: 2}) self.assertEqual(bound.global_phase, 2) self.assertEqual(bound.parameters, set()) @@ -774,7 +774,7 @@ def test_bind_parameter_in_phase_and_gate(self): ref = QuantumCircuit(1, global_phase=2) ref.rx(2, 0) - bound = circuit.bind_parameters({x: 2}) + bound = circuit.assign_parameters({x: 2}) self.assertEqual(bound, ref) self.assertEqual(bound.parameters, set()) diff --git a/test/python/circuit/test_hamiltonian_gate.py b/test/python/circuit/test_hamiltonian_gate.py index 2ae7ec1c43f4..a27818489447 100644 --- a/test/python/circuit/test_hamiltonian_gate.py +++ b/test/python/circuit/test_hamiltonian_gate.py @@ -77,7 +77,7 @@ def test_1q_hamiltonian(self): qc.x(qr[0]) theta = Parameter("theta") qc.append(HamiltonianGate(matrix, theta), [qr[0]]) - qc = qc.bind_parameters({theta: 1}) + qc = qc.assign_parameters({theta: 1}) # test of text drawer self.log.info(qc) @@ -107,7 +107,7 @@ def test_2q_hamiltonian(self): theta = Parameter("theta") uni2q = HamiltonianGate(matrix, theta) qc.append(uni2q, [qr[0], qr[1]]) - qc2 = qc.bind_parameters({theta: -np.pi / 2}) + qc2 = qc.assign_parameters({theta: -np.pi / 2}) dag = circuit_to_dag(qc2) nodes = dag.two_qubit_ops() self.assertEqual(len(nodes), 1) @@ -131,7 +131,7 @@ def test_3q_hamiltonian(self): qc.cx(qr[3], qr[2]) # test of text drawer self.log.info(qc) - qc = qc.bind_parameters({theta: -np.pi / 2}) + qc = qc.assign_parameters({theta: -np.pi / 2}) dag = circuit_to_dag(qc) nodes = dag.multi_qubit_ops() self.assertEqual(len(nodes), 1) @@ -150,7 +150,7 @@ def test_qobj_with_hamiltonian(self): uni = HamiltonianGate(matrix, theta, label="XIZ") qc.append(uni, [qr[0], qr[1], qr[3]]) qc.cx(qr[3], qr[2]) - qc = qc.bind_parameters({theta: np.pi / 2}) + qc = qc.assign_parameters({theta: np.pi / 2}) qobj = qiskit.compiler.assemble(qc) instr = qobj.experiments[0].instructions[1] self.assertEqual(instr.name, "hamiltonian") @@ -167,6 +167,6 @@ def test_decomposes_into_correct_unitary(self): theta = Parameter("theta") uni2q = HamiltonianGate(matrix, theta) qc.append(uni2q, [0, 1]) - qc = qc.bind_parameters({theta: -np.pi / 2}).decompose() + qc = qc.assign_parameters({theta: -np.pi / 2}).decompose() decomposed_ham = qc.data[0].operation self.assertEqual(decomposed_ham, UnitaryGate(Operator.from_label("XY"))) diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index 31b73c55afca..caa3a148cd7f 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -12,6 +12,7 @@ """Test circuits with variable parameters.""" import unittest +import warnings import cmath import math import copy @@ -108,6 +109,14 @@ def raise_if_parameter_table_invalid(circuit): class TestParameters(QiskitTestCase): """Test Parameters.""" + def setUp(self): + super().setUp() + # TODO: delete once bind_parameters is removed from the codebase + # and related tests are also removed. + warnings.filterwarnings( + "ignore", category=DeprecationWarning, module=r"test\.python\.circuit\.test_parameters" + ) + def test_gate(self): """Test instantiating gate with variable parameters""" theta = Parameter("θ") @@ -486,12 +495,14 @@ def test_raise_if_assigning_params_not_in_circuit(self): y = Parameter("y") z = ParameterVector("z", 3) qr = QuantumRegister(1) - qc = QuantumCircuit(qr) # test for both `bind_parameters` and `assign_parameters` for assign_fun in ["bind_parameters", "assign_parameters"]: qc = QuantumCircuit(qr) with self.subTest(assign_fun=assign_fun): + # TODO: delete once bind_parameters is removed from the codebase + warnings.filterwarnings("ignore", category=DeprecationWarning) + qc.p(0.1, qr[0]) self.assertRaises(CircuitError, getattr(qc, assign_fun), {x: 1}) qc.p(x, qr[0]) @@ -1270,6 +1281,14 @@ class TestParameterExpressions(QiskitTestCase): supported_operations = [add, sub, mul, truediv] + def setUp(self): + super().setUp() + # TODO: delete once bind_parameters is removed from the codebase + # and related tests are also removed. + warnings.filterwarnings( + "ignore", category=DeprecationWarning, module=r"test\.python\.circuit\.test_parameters" + ) + def test_compare_to_value_when_bound(self): """Verify expression can be compared to a fixed value when fully bound.""" @@ -2174,5 +2193,17 @@ def test_ne(self): self.assertFalse(self.view3 != self.view3) +class TestBindParametersDeprecation(QiskitTestCase): + """Test deprecation of bind_parameters().""" + + def test_circuit_bind_parameters_raises(self): + """Test that the deprecated bind_parameters method raises a deprecation warning.""" + qc = QuantumCircuit(1) + qc.rx(Parameter("x"), 0) + + with self.assertWarns(DeprecationWarning): + _ = qc.bind_parameters([1]) + + if __name__ == "__main__": unittest.main() diff --git a/test/python/compiler/test_assembler.py b/test/python/compiler/test_assembler.py index debd47e3a48b..2641012857a6 100644 --- a/test/python/compiler/test_assembler.py +++ b/test/python/compiler/test_assembler.py @@ -244,7 +244,7 @@ def test_assemble_unroll_parametervector(self): qc.barrier() qc.measure([0, 1], [0, 1]) - qc.bind_parameters({pv1: [0.1, 0.2, 0.3], pv2: [0.4, 0.5, 0.6]}) + qc.assign_parameters({pv1: [0.1, 0.2, 0.3], pv2: [0.4, 0.5, 0.6]}) qobj = assemble(qc, parameter_binds=[{pv1: [0.1, 0.2, 0.3], pv2: [0.4, 0.5, 0.6]}]) diff --git a/test/python/primitives/test_backend_estimator.py b/test/python/primitives/test_backend_estimator.py index df9387ee070b..44af5791cddc 100644 --- a/test/python/primitives/test_backend_estimator.py +++ b/test/python/primitives/test_backend_estimator.py @@ -107,7 +107,7 @@ def test_estimator_run(self, backend): def test_estimator_run_no_params(self, backend): """test for estimator without parameters""" backend.set_options(seed_simulator=123) - circuit = self.ansatz.bind_parameters([0, 1, 1, 2, 3, 5]) + circuit = self.ansatz.assign_parameters([0, 1, 1, 2, 3, 5]) est = BackendEstimator(backend=backend) result = est.run([circuit], [self.observable]).result() self.assertIsInstance(result, EstimatorResult) diff --git a/test/python/primitives/test_estimator.py b/test/python/primitives/test_estimator.py index 4a9209e7fa8a..5b620dc7c0e5 100644 --- a/test/python/primitives/test_estimator.py +++ b/test/python/primitives/test_estimator.py @@ -95,7 +95,7 @@ def test_estimator_run(self): def test_estiamtor_run_no_params(self): """test for estimator without parameters""" - circuit = self.ansatz.bind_parameters([0, 1, 1, 2, 3, 5]) + circuit = self.ansatz.assign_parameters([0, 1, 1, 2, 3, 5]) est = Estimator() result = est.run([circuit], [self.observable]).result() self.assertIsInstance(result, EstimatorResult) @@ -259,7 +259,7 @@ def test_run_numpy_params(self): def test_run_with_operator(self): """test for run with Operator as an observable""" - circuit = self.ansatz.bind_parameters([0, 1, 1, 2, 3, 5]) + circuit = self.ansatz.assign_parameters([0, 1, 1, 2, 3, 5]) matrix = Operator( [ [-1.06365335, 0.0, 0.0, 0.1809312], diff --git a/test/python/qasm2/test_circuit_methods.py b/test/python/qasm2/test_circuit_methods.py index fdd30211b73f..ff1e5842715c 100644 --- a/test/python/qasm2/test_circuit_methods.py +++ b/test/python/qasm2/test_circuit_methods.py @@ -376,7 +376,7 @@ def test_from_qasm_str_custom_gate4(self): qr = QuantumRegister(1, name="qr") expected = QuantumCircuit(qr, name="circuit") expected.append(my_gate, [qr[0]]) - expected = expected.bind_parameters({phi: 3.141592653589793, lam: 3.141592653589793}) + expected = expected.assign_parameters({phi: 3.141592653589793, lam: 3.141592653589793}) self.assertEqualUnroll("u", circuit, expected) @@ -400,7 +400,7 @@ def test_from_qasm_str_custom_gate5(self): qr = QuantumRegister(1, name="qr") expected = QuantumCircuit(qr, name="circuit") expected.append(my_gate, [qr[0]]) - expected = expected.bind_parameters({phi: 3.141592653589793, lam: 3.141592653589793}) + expected = expected.assign_parameters({phi: 3.141592653589793, lam: 3.141592653589793}) self.assertEqualUnroll("u", circuit, expected) @@ -426,7 +426,7 @@ def test_from_qasm_str_custom_gate6(self): qr = QuantumRegister(1, name="qr") expected = QuantumCircuit(qr, name="circuit") expected.append(my_gate, [qr[0]]) - expected = expected.bind_parameters({phi: 3.141592653589793, lam: 3.141592653589793}) + expected = expected.assign_parameters({phi: 3.141592653589793, lam: 3.141592653589793}) self.assertEqualUnroll(["rx", "ry"], circuit, expected) diff --git a/test/python/qasm2/test_legacy_importer.py b/test/python/qasm2/test_legacy_importer.py index 070646b1d9ea..891c23233d35 100644 --- a/test/python/qasm2/test_legacy_importer.py +++ b/test/python/qasm2/test_legacy_importer.py @@ -389,7 +389,7 @@ def test_from_qasm_str_custom_gate4(self): qr = QuantumRegister(1, name="qr") expected = QuantumCircuit(qr, name="circuit") expected.append(my_gate, [qr[0]]) - expected = expected.bind_parameters({phi: 3.141592653589793, lam: 3.141592653589793}) + expected = expected.assign_parameters({phi: 3.141592653589793, lam: 3.141592653589793}) self.assertEqualUnroll("u", circuit, expected) @@ -413,7 +413,7 @@ def test_from_qasm_str_custom_gate5(self): qr = QuantumRegister(1, name="qr") expected = QuantumCircuit(qr, name="circuit") expected.append(my_gate, [qr[0]]) - expected = expected.bind_parameters({phi: 3.141592653589793, lam: 3.141592653589793}) + expected = expected.assign_parameters({phi: 3.141592653589793, lam: 3.141592653589793}) self.assertEqualUnroll("u", circuit, expected) @@ -439,7 +439,7 @@ def test_from_qasm_str_custom_gate6(self): qr = QuantumRegister(1, name="qr") expected = QuantumCircuit(qr, name="circuit") expected.append(my_gate, [qr[0]]) - expected = expected.bind_parameters({phi: 3.141592653589793, lam: 3.141592653589793}) + expected = expected.assign_parameters({phi: 3.141592653589793, lam: 3.141592653589793}) self.assertEqualUnroll(["rx", "ry"], circuit, expected) diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index bcec1699be68..26876667abc7 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -392,7 +392,7 @@ def test_custom_gate_with_bound_parameter(self): custom = QuantumCircuit(1) custom.rx(parameter_a, 0) - custom_gate = custom.bind_parameters({parameter_a: 0.5}).to_gate() + custom_gate = custom.assign_parameters({parameter_a: 0.5}).to_gate() custom_gate.name = "custom" circuit = QuantumCircuit(1) @@ -453,8 +453,8 @@ def test_reused_custom_parameter(self): custom.rx(parameter_a, 0) circuit = QuantumCircuit(1) - circuit.append(custom.bind_parameters({parameter_a: 0.5}).to_gate(), [0]) - circuit.append(custom.bind_parameters({parameter_a: 1}).to_gate(), [0]) + circuit.append(custom.assign_parameters({parameter_a: 0.5}).to_gate(), [0]) + circuit.append(custom.assign_parameters({parameter_a: 1}).to_gate(), [0]) circuit_name_0 = circuit.data[0].operation.definition.name circuit_name_1 = circuit.data[1].operation.definition.name @@ -1390,7 +1390,7 @@ def test_custom_gate_used_in_loop_scope(self): custom = QuantumCircuit(1) custom.rx(parameter_a, 0) - custom_gate = custom.bind_parameters({parameter_a: 0.5}).to_gate() + custom_gate = custom.assign_parameters({parameter_a: 0.5}).to_gate() custom_gate.name = "custom" loop_body = QuantumCircuit(1) diff --git a/test/python/transpiler/test_optimize_1q_decomposition.py b/test/python/transpiler/test_optimize_1q_decomposition.py index 9f3cd3669afa..5d3ebb3d7245 100644 --- a/test/python/transpiler/test_optimize_1q_decomposition.py +++ b/test/python/transpiler/test_optimize_1q_decomposition.py @@ -287,8 +287,8 @@ def test_single_parameterized_circuit(self, basis): result = passmanager.run(qc) self.assertTrue( - Operator(qc.bind_parameters({theta: 3.14})).equiv( - Operator(result.bind_parameters({theta: 3.14})) + Operator(qc.assign_parameters({theta: 3.14})).equiv( + Operator(result.assign_parameters({theta: 3.14})) ) ) @@ -324,8 +324,8 @@ def test_parameterized_circuits(self, basis): result = passmanager.run(qc) self.assertTrue( - Operator(qc.bind_parameters({theta: 3.14})).equiv( - Operator(result.bind_parameters({theta: 3.14})) + Operator(qc.assign_parameters({theta: 3.14})).equiv( + Operator(result.assign_parameters({theta: 3.14})) ) ) @@ -364,8 +364,8 @@ def test_parameterized_expressions_in_circuits(self, basis): result = passmanager.run(qc) self.assertTrue( - Operator(qc.bind_parameters({theta: 3.14, phi: 10})).equiv( - Operator(result.bind_parameters({theta: 3.14, phi: 10})) + Operator(qc.assign_parameters({theta: 3.14, phi: 10})).equiv( + Operator(result.assign_parameters({theta: 3.14, phi: 10})) ) ) diff --git a/test/python/transpiler/test_template_matching.py b/test/python/transpiler/test_template_matching.py index e6d8ed2cff7a..95294c7a2ab1 100644 --- a/test/python/transpiler/test_template_matching.py +++ b/test/python/transpiler/test_template_matching.py @@ -396,8 +396,8 @@ def test_unbound_parameters_in_rzx_template(self): # however these are equivalent if the operators are the same theta_set = 0.42 self.assertTrue( - Operator(circuit_in.bind_parameters({theta: theta_set})).equiv( - circuit_out.bind_parameters({theta: theta_set}) + Operator(circuit_in.assign_parameters({theta: theta_set})).equiv( + circuit_out.assign_parameters({theta: theta_set}) ) ) @@ -483,8 +483,8 @@ def test_two_parameter_template(self): alpha_set = 0.37 beta_set = 0.42 self.assertTrue( - Operator(circuit_in.bind_parameters({alpha: alpha_set, beta: beta_set})).equiv( - circuit_out.bind_parameters({alpha: alpha_set, beta: beta_set}) + Operator(circuit_in.assign_parameters({alpha: alpha_set, beta: beta_set})).equiv( + circuit_out.assign_parameters({alpha: alpha_set, beta: beta_set}) ) ) diff --git a/test/python/visualization/test_circuit_latex.py b/test/python/visualization/test_circuit_latex.py index e61a3b2f0bf3..b5f06529702a 100644 --- a/test/python/visualization/test_circuit_latex.py +++ b/test/python/visualization/test_circuit_latex.py @@ -305,7 +305,7 @@ def test_big_gates(self): matrix = np.zeros((4, 4)) theta = Parameter("theta") circuit.append(HamiltonianGate(matrix, theta), [qr[1], qr[2]]) - circuit = circuit.bind_parameters({theta: 1}) + circuit = circuit.assign_parameters({theta: 1}) circuit.isometry(np.eye(4, 4), list(range(3, 5)), []) circuit_drawer(circuit, filename=filename, output="latex_source") diff --git a/test/python/visualization/test_circuit_text_drawer.py b/test/python/visualization/test_circuit_text_drawer.py index 671756bf0d82..8ff1c1a8b3fe 100644 --- a/test/python/visualization/test_circuit_text_drawer.py +++ b/test/python/visualization/test_circuit_text_drawer.py @@ -1933,7 +1933,7 @@ def test_text_bound_parameters(self): qr = QuantumRegister(1, name="qr") circuit = QuantumCircuit(qr, name="circuit") circuit.append(my_u2, [qr[0]]) - circuit = circuit.bind_parameters({phi: 3.141592653589793, lam: 3.141592653589793}) + circuit = circuit.assign_parameters({phi: 3.141592653589793, lam: 3.141592653589793}) self.assertEqual(str(_text_circuit_drawer(circuit)), expected) @@ -5095,7 +5095,7 @@ def test_draw_hamiltonian_single(self): matrix = numpy.zeros((2, 2)) theta = Parameter("theta") circuit.append(HamiltonianGate(matrix, theta), [qr[0]]) - circuit = circuit.bind_parameters({theta: 1}) + circuit = circuit.assign_parameters({theta: 1}) self.assertEqual(circuit.draw(output="text").single_string(), expected) def test_draw_hamiltonian_multi(self): @@ -5115,7 +5115,7 @@ def test_draw_hamiltonian_multi(self): matrix = numpy.zeros((4, 4)) theta = Parameter("theta") circuit.append(HamiltonianGate(matrix, theta), [qr[0], qr[1]]) - circuit = circuit.bind_parameters({theta: 1}) + circuit = circuit.assign_parameters({theta: 1}) self.assertEqual(circuit.draw(output="text").single_string(), expected) diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index 7eb9b1a6dd99..4e9429f248fe 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -768,8 +768,8 @@ def assert_equal(reference, qpy, count, version_parts, bind=None): ) sys.stderr.write(msg) sys.exit(4) - reference = reference.bind_parameters(bind) - qpy = qpy.bind_parameters(bind) + reference = reference.assign_parameters(bind) + qpy = qpy.assign_parameters(bind) if reference != qpy: msg = ( f"Reference Circuit {count}:\n{reference}\nis not equivalent to " diff --git a/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py b/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py index 9a348a1957be..82a65b33e131 100644 --- a/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py +++ b/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py @@ -542,7 +542,7 @@ def test_big_gates(self): matrix = np.zeros((4, 4)) theta = Parameter("theta") circuit.append(HamiltonianGate(matrix, theta), [qr[1], qr[2]]) - circuit = circuit.bind_parameters({theta: 1}) + circuit = circuit.assign_parameters({theta: 1}) circuit.isometry(np.eye(4, 4), list(range(3, 5)), []) fname = "big_gates.png" From 2ab1e547e71b08592245970ec4a8dff90ac8842e Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 12 Sep 2023 11:44:25 -0400 Subject: [PATCH 05/19] Remove deprecated BIPMapping pass (#10526) * Remove deprecated BIPMapping pass This commit removes the deprecated BIPMapping pass. It was deprecated in qiskit-terra 0.24.0 and the minimum deprecation period has elapsed for the Qiskit 0.45.0 release. This pass has been moved to an external plugin that can be installed separately and integrated into the transpiler more cleanly than a single pass and also separates the optional dependency on the proprietary CPLEX into a separate package. * Apply suggestions from code review Co-authored-by: Kevin Hartman * Remove additional uses of BIPMapping There were still two locations where the BIPMapping pass was being used. First the dedicated unit tests, and then also import redirects for the docs and easier access. This commit removes these locations so nothing left is using the BIPMapping pass. * Update optionals documentation * Update install commands --------- Co-authored-by: Kevin Hartman --- qiskit/transpiler/passes/__init__.py | 2 - qiskit/transpiler/passes/routing/__init__.py | 1 - .../passes/routing/algorithms/bip_model.py | 497 ------------------ .../transpiler/passes/routing/bip_mapping.py | 273 ---------- qiskit/utils/optionals.py | 10 +- ...eprecated-bip-mapper-e1206c8f905502dd.yaml | 15 + test/python/transpiler/test_bip_mapping.py | 379 ------------- 7 files changed, 21 insertions(+), 1156 deletions(-) delete mode 100644 qiskit/transpiler/passes/routing/algorithms/bip_model.py delete mode 100644 qiskit/transpiler/passes/routing/bip_mapping.py create mode 100644 releasenotes/notes/remove-deprecated-bip-mapper-e1206c8f905502dd.yaml delete mode 100644 test/python/transpiler/test_bip_mapping.py diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index 73e6ea055165..ebf095b54edc 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -45,7 +45,6 @@ LookaheadSwap StochasticSwap SabreSwap - BIPMapping Commuting2qGateRouter Basis Change @@ -199,7 +198,6 @@ from .routing import LookaheadSwap from .routing import StochasticSwap from .routing import SabreSwap -from .routing import BIPMapping from .routing import Commuting2qGateRouter # basis change diff --git a/qiskit/transpiler/passes/routing/__init__.py b/qiskit/transpiler/passes/routing/__init__.py index 618b2dc6a9f1..2316705b4a1a 100644 --- a/qiskit/transpiler/passes/routing/__init__.py +++ b/qiskit/transpiler/passes/routing/__init__.py @@ -17,6 +17,5 @@ from .lookahead_swap import LookaheadSwap from .stochastic_swap import StochasticSwap from .sabre_swap import SabreSwap -from .bip_mapping import BIPMapping from .commuting_2q_gate_routing.commuting_2q_gate_router import Commuting2qGateRouter from .commuting_2q_gate_routing.swap_strategy import SwapStrategy diff --git a/qiskit/transpiler/passes/routing/algorithms/bip_model.py b/qiskit/transpiler/passes/routing/algorithms/bip_model.py deleted file mode 100644 index 972cf8242918..000000000000 --- a/qiskit/transpiler/passes/routing/algorithms/bip_model.py +++ /dev/null @@ -1,497 +0,0 @@ -# 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. -"""Integer programming model for quantum circuit compilation.""" -import copy -import logging -from functools import lru_cache - -import numpy as np - -from qiskit.transpiler.exceptions import TranspilerError, CouplingError -from qiskit.transpiler.layout import Layout -from qiskit.circuit.library.standard_gates import SwapGate -from qiskit.providers.models import BackendProperties -from qiskit.quantum_info import two_qubit_cnot_decompose -from qiskit.quantum_info.synthesis.two_qubit_decompose import ( - TwoQubitWeylDecomposition, - trace_to_fid, -) -from qiskit.utils import optionals as _optionals -from qiskit.utils.deprecation import deprecate_func - -logger = logging.getLogger(__name__) - - -@_optionals.HAS_DOCPLEX.require_in_instance -class BIPMappingModel: - """Internal model to create and solve a BIP problem for mapping. - - Attributes: - problem (Model): - A CPLEX problem model object, which is set by calling - :method:`create_cpx_problem`. After calling :method:`solve_cpx_problem`, - the solution will be stored in :attr:`solution`). None if it's not yet set. - """ - - @deprecate_func( - since="0.24.0", - additional_msg="This has been replaced by a new transpiler plugin package: " - "qiskit-bip-mapper. More details can be found here: " - "https://github.com/qiskit-community/qiskit-bip-mapper", - ) # pylint: disable=bad-docstring-quotes - def __init__(self, dag, coupling_map, qubit_subset, dummy_timesteps=None): - """ - Args: - dag (DAGCircuit): DAG circuit to be mapped - coupling_map (CouplingMap): Coupling map of the device on which the `dag` is mapped. - qubit_subset (list[int]): Sublist of physical qubits to be used in the mapping. - dummy_timesteps (int): - Number of dummy time steps, after each real layer of gates, to - allow arbitrary swaps between neighbors. - - Raises: - MissingOptionalLibraryError: If docplex is not installed - TranspilerError: If size of virtual qubits and physical qubits differ, or - if coupling_map is not symmetric (bidirectional). - """ - - self._dag = dag - self._coupling = copy.deepcopy(coupling_map) # reduced coupling map - try: - self._coupling = self._coupling.reduce(qubit_subset) - except CouplingError as err: - raise TranspilerError( - "The 'coupling_map' reduced by 'qubit_subset' must be connected." - ) from err - self._coupling.make_symmetric() - self.global_qubit = qubit_subset # the map from reduced qubit index to global qubit index - - self.problem = None - self.solution = None - self.num_vqubits = len(self._dag.qubits) - self.num_pqubits = self._coupling.size() - self._arcs = self._coupling.get_edges() - - if self.num_vqubits != self.num_pqubits: - raise TranspilerError( - "BIPMappingModel assumes the same size of virtual and physical qubits." - ) - - self._index_to_virtual = dict(enumerate(dag.qubits)) - - # Construct internal circuit model - # Extract layers with 2-qubit gates - self._to_su4layer = [] - self.su4layers = [] - for lay in dag.layers(): - laygates = [] - for node in lay["graph"].two_qubit_ops(): - i1 = self._dag.find_bit(node.qargs[0]).index - i2 = self._dag.find_bit(node.qargs[1]).index - laygates.append(((i1, i2), node)) - if laygates: - self._to_su4layer.append(len(self.su4layers)) - self.su4layers.append(laygates) - else: - self._to_su4layer.append(-1) - # Add dummy time steps inbetween su4layers. Dummy time steps can only contain SWAPs. - self.gates = [] # layered 2q-gates with dummy steps - for k, lay in enumerate(self.su4layers): - self.gates.append(lay) - if k == len(self.su4layers) - 1: # do not add dummy steps after the last layer - break - self.gates.extend([[]] * dummy_timesteps) - - self.bprop = None # Backend properties to compute cx fidelities (set later if necessary) - self.default_cx_error_rate = ( - None # Default cx error rate in case backend properties are not available - ) - - logger.info("Num virtual qubits: %d", self.num_vqubits) - logger.info("Num physical qubits: %d", self.num_pqubits) - logger.info("Model depth: %d", self.depth) - logger.info("Dummy steps: %d", dummy_timesteps) - - @property - def depth(self): - """Number of time-steps (including dummy steps).""" - return len(self.gates) - - def is_su4layer(self, depth: int) -> bool: - """Check if the depth-th layer is su4layer (layer containing 2q-gates) or not. - - Args: - depth: Depth of the ordinary layer - - Returns: - True if the depth-th layer is su4layer, otherwise False - """ - return self._to_su4layer[depth] >= 0 - - def to_su4layer_depth(self, depth: int) -> int: - """Return the depth as a su4layer. If the depth-th layer is not a su4layer, return -1. - - Args: - depth: Depth of the ordinary layer - - Returns: - su4layer depth if the depth-th layer is a su4layer, otherwise -1 - """ - return self._to_su4layer[depth] - - # pylint: disable=invalid-name - def _is_dummy_step(self, t: int): - """Check if the time-step t is a dummy step or not.""" - return len(self.gates[t]) == 0 - - @_optionals.HAS_DOCPLEX.require_in_call - def create_cpx_problem( - self, - objective: str, - backend_prop: BackendProperties = None, - line_symm: bool = False, - depth_obj_weight: float = 0.1, - default_cx_error_rate: float = 5e-3, - ): - """Create integer programming model to compile a circuit. - - Args: - objective: - Type of objective function to be minimized: - - * ``'gate_error'``: Approximate gate error of the circuit, which is given as the sum of - negative logarithm of CNOT gate fidelities in the circuit. It takes into account - only the CNOT gate errors reported in ``backend_prop``. - * ``'depth'``: Depth (number of timesteps) of the circuit - * ``'balanced'``: Weighted sum of gate_error and depth - - backend_prop: - Backend properties storing gate errors, which are required in computing certain - types of objective function such as ``'gate_error'`` or ``'balanced'``. - If this is not available, default_cx_error_rate is used instead. - - line_symm: - Use symmetry breaking constrainst for line topology. Should - only be True if the hardware graph is a chain/line/path. - - depth_obj_weight: - Weight of depth objective in ``'balanced'`` objective function. - - default_cx_error_rate: - Default CX error rate to be used if backend_prop is not available. - - Raises: - TranspilerError: if unknown objective type is specified or invalid options are specified. - MissingOptionalLibraryError: If docplex is not installed - """ - self.bprop = backend_prop - self.default_cx_error_rate = default_cx_error_rate - if self.bprop is None and self.default_cx_error_rate is None: - raise TranspilerError("BackendProperties or default_cx_error_rate must be specified") - from docplex.mp.model import Model - - mdl = Model() - - # *** Define main variables *** - # Add w variables - w = {} - for t in range(self.depth): - for q in range(self.num_vqubits): - for j in range(self.num_pqubits): - w[t, q, j] = mdl.binary_var(name=f"w_{t}_{q}_{j}") - # Add y variables - y = {} - for t in range(self.depth): - for ((p, q), _) in self.gates[t]: - for (i, j) in self._arcs: - y[t, p, q, i, j] = mdl.binary_var(name=f"y_{t}_{p}_{q}_{i}_{j}") - # Add x variables - x = {} - for t in range(self.depth - 1): - for q in range(self.num_vqubits): - for i in range(self.num_pqubits): - x[t, q, i, i] = mdl.binary_var(name=f"x_{t}_{q}_{i}_{i}") - for j in self._coupling.neighbors(i): - x[t, q, i, j] = mdl.binary_var(name=f"x_{t}_{q}_{i}_{j}") - - # *** Define main constraints *** - # Assignment constraints for w variables - for t in range(self.depth): - for q in range(self.num_vqubits): - mdl.add_constraint( - sum(w[t, q, j] for j in range(self.num_pqubits)) == 1, - ctname=f"assignment_vqubits_{q}_at_{t}", - ) - for t in range(self.depth): - for j in range(self.num_pqubits): - mdl.add_constraint( - sum(w[t, q, j] for q in range(self.num_vqubits)) == 1, - ctname=f"assignment_pqubits_{j}_at_{t}", - ) - # Each gate must be implemented - for t in range(self.depth): - for ((p, q), _) in self.gates[t]: - mdl.add_constraint( - sum(y[t, p, q, i, j] for (i, j) in self._arcs) == 1, - ctname=f"implement_gate_{p}_{q}_at_{t}", - ) - # Gate can be implemented iff both of its qubits are located at the associated nodes - for t in range(self.depth - 1): - for ((p, q), _) in self.gates[t]: - for (i, j) in self._arcs: - # Apply McCormick to y[t, p, q, i, j] == w[t, p, i] * w[t, q, j] - mdl.add_constraint( - y[t, p, q, i, j] >= w[t, p, i] + w[t, q, j] - 1, - ctname=f"McCormickLB_{p}_{q}_{i}_{j}_at_{t}", - ) - # Stronger version of McCormick: gate (p,q) is implemented at (i, j) - # if i moves to i or j, and j moves to i or j - mdl.add_constraint( - y[t, p, q, i, j] <= x[t, p, i, i] + x[t, p, i, j], - ctname=f"McCormickUB1_{p}_{q}_{i}_{j}_at_{t}", - ) - mdl.add_constraint( - y[t, p, q, i, j] <= x[t, q, j, i] + x[t, q, j, j], - ctname=f"McCormickUB2_{p}_{q}_{i}_{j}_at_{t}", - ) - # For last time step, use regular McCormick - for ((p, q), _) in self.gates[self.depth - 1]: - for (i, j) in self._arcs: - # Apply McCormick to y[self.depth - 1, p, q, i, j] - # == w[self.depth - 1, p, i] * w[self.depth - 1, q, j] - mdl.add_constraint( - y[self.depth - 1, p, q, i, j] - >= w[self.depth - 1, p, i] + w[self.depth - 1, q, j] - 1, - ctname=f"McCormickLB_{p}_{q}_{i}_{j}_at_last", - ) - mdl.add_constraint( - y[self.depth - 1, p, q, i, j] <= w[self.depth - 1, p, i], - ctname=f"McCormickUB1_{p}_{q}_{i}_{j}_at_last", - ) - mdl.add_constraint( - y[self.depth - 1, p, q, i, j] <= w[self.depth - 1, q, j], - ctname=f"McCormickUB2_{p}_{q}_{i}_{j}_at_last", - ) - # Logical qubit flow-out constraints - for t in range(self.depth - 1): # Flow out; skip last time step - for q in range(self.num_vqubits): - for i in range(self.num_pqubits): - mdl.add_constraint( - w[t, q, i] - == x[t, q, i, i] + sum(x[t, q, i, j] for j in self._coupling.neighbors(i)), - ctname=f"flow_out_{q}_{i}_at_{t}", - ) - # Logical qubit flow-in constraints - for t in range(1, self.depth): # Flow in; skip first time step - for q in range(self.num_vqubits): - for i in range(self.num_pqubits): - mdl.add_constraint( - w[t, q, i] - == x[t - 1, q, i, i] - + sum(x[t - 1, q, j, i] for j in self._coupling.neighbors(i)), - ctname=f"flow_in_{q}_{i}_at_{t}", - ) - # If a gate is implemented, involved qubits cannot swap with other positions - for t in range(self.depth - 1): - for ((p, q), _) in self.gates[t]: - for (i, j) in self._arcs: - mdl.add_constraint( - x[t, p, i, j] == x[t, q, j, i], ctname=f"swap_{p}_{q}_{i}_{j}_at_{t}" - ) - # Qubit not in gates can flip with their neighbors - for t in range(self.depth - 1): - q_no_gate = list(range(self.num_vqubits)) - for ((p, q), _) in self.gates[t]: - q_no_gate.remove(p) - q_no_gate.remove(q) - for (i, j) in self._arcs: - mdl.add_constraint( - sum(x[t, q, i, j] for q in q_no_gate) == sum(x[t, p, j, i] for p in q_no_gate), - ctname=f"swap_no_gate_{i}_{j}_at_{t}", - ) - - # *** Define supplemental variables *** - # Add z variables to count dummy steps (supplemental variables for symmetry breaking) - z = {} - for t in range(self.depth): - if self._is_dummy_step(t): - z[t] = mdl.binary_var(name=f"z_{t}") - - # *** Define supplemental constraints *** - # See if a dummy time step is needed - for t in range(self.depth): - if self._is_dummy_step(t): - for q in range(self.num_vqubits): - mdl.add_constraint( - sum(x[t, q, i, j] for (i, j) in self._arcs) <= z[t], - ctname=f"dummy_ts_needed_for_vqubit_{q}_at_{t}", - ) - # Symmetry breaking between dummy time steps - for t in range(self.depth - 1): - # This is a dummy time step and the next one is dummy too - if self._is_dummy_step(t) and self._is_dummy_step(t + 1): - # We cannot use the next time step unless this one is used too - mdl.add_constraint(z[t] >= z[t + 1], ctname=f"dummy_precedence_{t}") - # Symmetry breaking on the line -- only works on line topology! - if line_symm: - for h in range(1, self.num_vqubits): - mdl.add_constraint( - sum(w[0, p, 0] for p in range(h)) - + sum(w[0, q, self.num_pqubits - 1] for q in range(h, self.num_vqubits)) - >= 1, - ctname=f"sym_break_line_{h}", - ) - - # *** Define objevtive function *** - if objective == "depth": - objexr = sum(z[t] for t in range(self.depth) if self._is_dummy_step(t)) - for t in range(self.depth - 1): - for q in range(self.num_vqubits): - for (i, j) in self._arcs: - objexr += 0.01 * x[t, q, i, j] - mdl.minimize(objexr) - elif objective in ("gate_error", "balanced"): - # We add the depth objective with coefficient depth_obj_weight if balanced was selected. - objexr = 0 - for t in range(self.depth - 1): - for (p, q), node in self.gates[t]: - for (i, j) in self._arcs: - # We pay the cost for gate implementation. - pbest_fid = -np.log(self._max_expected_fidelity(node, i, j)) - objexr += y[t, p, q, i, j] * pbest_fid - # If a gate is mirrored (followed by a swap on the same qubit pair), - # its cost should be replaced with the cost of the combined (mirrored) gate. - pbest_fidm = -np.log(self._max_expected_mirrored_fidelity(node, i, j)) - objexr += x[t, q, i, j] * (pbest_fidm - pbest_fid) / 2 - # Cost of swaps on unused qubits - for q in range(self.num_vqubits): - used_qubits = {q for (pair, _) in self.gates[t] for q in pair} - if q not in used_qubits: - for i in range(self.num_pqubits): - for j in self._coupling.neighbors(i): - objexr += x[t, q, i, j] * -3 / 2 * np.log(self._cx_fidelity(i, j)) - # Cost for the last layer (x variables are not defined for depth-1) - for (p, q), node in self.gates[self.depth - 1]: - for (i, j) in self._arcs: - pbest_fid = -np.log(self._max_expected_fidelity(node, i, j)) - objexr += y[self.depth - 1, p, q, i, j] * pbest_fid - if objective == "balanced": - objexr += depth_obj_weight * sum( - z[t] for t in range(self.depth) if self._is_dummy_step(t) - ) - mdl.minimize(objexr) - else: - raise TranspilerError(f"Unknown objective type: {objective}") - - self.problem = mdl - logger.info("BIP problem stats: %s", self.problem.statistics) - - def _max_expected_fidelity(self, node, i, j): - return max( - gfid * self._cx_fidelity(i, j) ** k - for k, gfid in enumerate(self._gate_fidelities(node)) - ) - - def _max_expected_mirrored_fidelity(self, node, i, j): - return max( - gfid * self._cx_fidelity(i, j) ** k - for k, gfid in enumerate(self._mirrored_gate_fidelities(node)) - ) - - def _cx_fidelity(self, i, j) -> float: - # fidelity of cx on global physical qubits - if self.bprop is not None: - return 1.0 - self.bprop.gate_error("cx", [self.global_qubit[i], self.global_qubit[j]]) - else: - return 1.0 - self.default_cx_error_rate - - @staticmethod - @lru_cache() - def _gate_fidelities(node): - matrix = node.op.to_matrix() - target = TwoQubitWeylDecomposition(matrix) - traces = two_qubit_cnot_decompose.traces(target) - return [trace_to_fid(traces[i]) for i in range(4)] - - @staticmethod - @lru_cache() - def _mirrored_gate_fidelities(node): - matrix = node.op.to_matrix() - swap = SwapGate().to_matrix() - targetm = TwoQubitWeylDecomposition(matrix @ swap) - tracesm = two_qubit_cnot_decompose.traces(targetm) - return [trace_to_fid(tracesm[i]) for i in range(4)] - - @_optionals.HAS_CPLEX.require_in_call - def solve_cpx_problem(self, time_limit: float = 60, threads: int = None) -> str: - """Solve the BIP problem using CPLEX. - - Args: - time_limit: - Time limit (seconds) given to CPLEX. - - threads: - Number of threads to be allowed for CPLEX to use. - - Returns: - Status string that CPLEX returned after solving the BIP problem. - - Raises: - MissingOptionalLibraryError: If CPLEX is not installed - """ - self.problem.set_time_limit(time_limit) - if threads is not None: - self.problem.context.cplex_parameters.threads = threads - self.problem.context.cplex_parameters.randomseed = 777 - - self.solution = self.problem.solve() - - status = self.problem.solve_details.status - logger.info("BIP solution status: %s", status) - return status - - def get_layout(self, t: int) -> Layout: - """Get layout at time-step t. - - Args: - t: Time-step - - Returns: - Layout - """ - dic = {} - for q in range(self.num_vqubits): - for i in range(self.num_pqubits): - if self.solution.get_value(f"w_{t}_{q}_{i}") > 0.5: - dic[self._index_to_virtual[q]] = self.global_qubit[i] - layout = Layout(dic) - for reg in self._dag.qregs.values(): - layout.add_register(reg) - return layout - - def get_swaps(self, t: int) -> list: - """Get swaps (pairs of physical qubits) inserted at time-step ``t``. - - Args: - t: Time-step (<= depth - 1) - - Returns: - List of swaps (pairs of physical qubits (integers)) - """ - swaps = [] - for (i, j) in self._arcs: - if i >= j: - continue - for q in range(self.num_vqubits): - if self.solution.get_value(f"x_{t}_{q}_{i}_{j}") > 0.5: - swaps.append((self.global_qubit[i], self.global_qubit[j])) - return swaps diff --git a/qiskit/transpiler/passes/routing/bip_mapping.py b/qiskit/transpiler/passes/routing/bip_mapping.py deleted file mode 100644 index a658c1c98eaa..000000000000 --- a/qiskit/transpiler/passes/routing/bip_mapping.py +++ /dev/null @@ -1,273 +0,0 @@ -# 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. - -"""Map a DAGCircuit onto a given ``coupling_map``, allocating qubits and adding swap gates.""" -import copy -import logging -import math - -from qiskit.circuit import QuantumRegister -from qiskit.circuit.library.standard_gates import SwapGate -from qiskit.dagcircuit import DAGCircuit, DAGOpNode -from qiskit.utils import optionals as _optionals -from qiskit.transpiler import TransformationPass -from qiskit.transpiler.exceptions import TranspilerError -from qiskit.transpiler.passes.routing.algorithms.bip_model import BIPMappingModel -from qiskit.transpiler.target import target_to_backend_properties, Target -from qiskit.utils.deprecation import deprecate_func -from qiskit.transpiler.passes.layout import disjoint_utils - -logger = logging.getLogger(__name__) - - -@_optionals.HAS_CPLEX.require_in_instance("BIP-based mapping pass") -@_optionals.HAS_DOCPLEX.require_in_instance("BIP-based mapping pass") -class BIPMapping(TransformationPass): - r"""Map a DAGCircuit onto a given ``coupling_map``, allocating qubits and adding swap gates. - - The BIP mapper tries to find the best layout and routing at once by - solving a BIP (binary integer programming) problem as described in [1]. - - The BIP problem represents the layer-by-layer mapping of 2-qubit gates, assuming all the gates - in a layer can be run on the ``coupling_map``. In the problem, the variables :math:`w` represent - the layout of qubits for each layer and the variables :math:`x` represent which pair of qubits - should be swapped in between layers. Based on the values in the solution of the BIP problem, - the mapped circuit will be constructed. - - The BIP mapper depends on ``docplex`` to represent the BIP problem and CPLEX (``cplex``) - to solve it. Those packages can be installed with ``pip install qiskit-terra[bip-mapper]``. - Since the free version of CPLEX can solve only small BIP problems, i.e. mapping of circuits - with less than about 5 qubits, the paid version of CPLEX may be needed to map larger circuits. - - If you want to fix physical qubits to be used in the mapping (e.g. running Quantum Volume - circuits), you need to supply ``qubit_subset``, i.e. list of physical qubits to be used - within the ``coupling_map``. - Please do not use ``initial_layout`` for that purpose because the BIP mapper gracefully - ignores ``initial_layout`` (and tries to determines its best layout). - - .. warning:: - The BIP mapper does not scale very well with respect to the number of qubits or gates. - For example, it may not work with ``qubit_subset`` beyond 10 qubits because - the BIP solver (CPLEX) may not find any solution within the default time limit. - - **References:** - - [1] G. Nannicini et al. "Optimal qubit assignment and routing via integer programming." - `arXiv:2106.06446 `_ - """ - - @deprecate_func( - since="0.24.0", - additional_msg="This has been replaced by a new transpiler plugin package: " - "qiskit-bip-mapper. More details can be found here: " - "https://github.com/qiskit-community/qiskit-bip-mapper", - ) # pylint: disable=bad-docstring-quotes - def __init__( - self, - coupling_map, - qubit_subset=None, - objective="balanced", - backend_prop=None, - time_limit=30, - threads=None, - max_swaps_inbetween_layers=None, - depth_obj_weight=0.1, - default_cx_error_rate=5e-3, - ): - """BIPMapping initializer. - - Args: - coupling_map (Union[CouplingMap, Target]): Directed graph represented a coupling map. - qubit_subset (list[int]): Sublist of physical qubits to be used in the mapping. - If None, all qubits in the coupling_map will be considered. - objective (str): Type of objective function to be minimized: - - * ``'gate_error'``: Approximate gate error of the circuit, which is given as the sum of - negative logarithm of 2q-gate fidelities in the circuit. It takes into account only - the 2q-gate (CNOT) errors reported in ``backend_prop`` and ignores the other errors - in such as 1q-gates, SPAMs and idle times. - * ``'depth'``: Depth (number of 2q-gate layers) of the circuit. - * ``'balanced'``: [Default] Weighted sum of ``'gate_error'`` and ``'depth'`` - - backend_prop (BackendProperties): Backend properties object containing 2q-gate gate errors, - which are required in computing certain types of objective function - such as ``'gate_error'`` or ``'balanced'``. If this is not available, - default_cx_error_rate is used instead. - time_limit (float): Time limit for solving BIP in seconds - threads (int): Number of threads to be allowed for CPLEX to solve BIP - max_swaps_inbetween_layers (int): - Number of swaps allowed in between layers. If None, automatically set. - Large value could decrease the probability to build infeasible BIP problem but also - could reduce the chance of finding a feasible solution within the ``time_limit``. - - depth_obj_weight (float): - Weight of depth objective in ``'balanced'`` objective. The balanced objective is the - sum of error_rate + depth_obj_weight * depth. - - default_cx_error_rate (float): - Default CX error rate to be used if backend_prop is not available. - - Raises: - MissingOptionalLibraryError: if cplex or docplex are not installed. - TranspilerError: if invalid options are specified. - """ - super().__init__() - if isinstance(coupling_map, Target): - self.target = coupling_map - self.coupling_map = self.target.build_coupling_map() - self.backend_prop = target_to_backend_properties(self.target) - else: - self.target = None - self.coupling_map = coupling_map - self.backend_prop = None - self.qubit_subset = qubit_subset - self.objective = objective - if backend_prop is not None: - self.backend_prop = backend_prop - self.time_limit = time_limit - self.threads = threads - self.max_swaps_inbetween_layers = max_swaps_inbetween_layers - self.depth_obj_weight = depth_obj_weight - self.default_cx_error_rate = default_cx_error_rate - if self.coupling_map is not None and self.qubit_subset is None: - self.qubit_subset = list(range(self.coupling_map.size())) - - def run(self, dag): - """Run the BIPMapping pass on `dag`, assuming the number of virtual qubits (defined in - `dag`) and the number of physical qubits (defined in `coupling_map`) are the same. - - Args: - dag (DAGCircuit): DAG to map. - - Returns: - DAGCircuit: A mapped DAG. If there is no 2q-gate in DAG or it fails to map, - returns the original dag. - - Raises: - TranspilerError: if the number of virtual and physical qubits are not the same. - AssertionError: if the final layout is not valid. - """ - if self.coupling_map is None: - return dag - - if len(dag.qubits) > len(self.qubit_subset): - raise TranspilerError("More virtual qubits exist than physical qubits.") - - if len(dag.qubits) != len(self.qubit_subset): - raise TranspilerError( - "BIPMapping requires the number of virtual and physical qubits to be the same. " - "Supply 'qubit_subset' to specify physical qubits to use." - ) - disjoint_utils.require_layout_isolated_to_component( - dag, self.coupling_map if self.target is None else self.target - ) - - original_dag = dag - - dummy_steps = math.ceil(math.sqrt(dag.num_qubits())) - if self.max_swaps_inbetween_layers is not None: - dummy_steps = max(0, self.max_swaps_inbetween_layers - 1) - - model = BIPMappingModel( - dag=dag, - coupling_map=self.coupling_map, - qubit_subset=self.qubit_subset, - dummy_timesteps=dummy_steps, - ) - - if len(model.su4layers) == 0: - logger.info("BIPMapping is skipped due to no 2q-gates.") - return original_dag - - model.create_cpx_problem( - objective=self.objective, - backend_prop=self.backend_prop, - depth_obj_weight=self.depth_obj_weight, - default_cx_error_rate=self.default_cx_error_rate, - ) - - status = model.solve_cpx_problem(time_limit=self.time_limit, threads=self.threads) - if model.solution is None: - logger.warning("Failed to solve a BIP problem. Status: %s", status) - return original_dag - - # Get the optimized initial layout - optimized_layout = model.get_layout(0) - - # Create a layout to track changes in layout for each layer - layout = copy.deepcopy(optimized_layout) - - # Construct the mapped circuit - canonical_qreg = QuantumRegister(self.coupling_map.size(), "q") - mapped_dag = self._create_empty_dagcircuit(dag, canonical_qreg) - interval = dummy_steps + 1 - for k, layer in enumerate(dag.layers()): - if model.is_su4layer(k): - su4dep = model.to_su4layer_depth(k) - # add swaps between (su4dep-1)-th and su4dep-th su4layer - from_steps = max(interval * (su4dep - 1), 0) - to_steps = min(interval * su4dep, model.depth - 1) - for t in range(from_steps, to_steps): # pylint: disable=invalid-name - for (i, j) in model.get_swaps(t): - mapped_dag.apply_operation_back( - op=SwapGate(), - qargs=[canonical_qreg[i], canonical_qreg[j]], - ) - # update layout, swapping physical qubits (i, j) - layout.swap(i, j) - - # map gates in k-th layer - for node in layer["graph"].nodes(): - if isinstance(node, DAGOpNode): - mapped_dag.apply_operation_back( - op=copy.deepcopy(node.op), - qargs=[canonical_qreg[layout[q]] for q in node.qargs], - cargs=node.cargs, - ) - # TODO: double check with y values? - - # Check final layout - final_layout = model.get_layout(model.depth - 1) - if layout != final_layout: - raise AssertionError( - f"Bug: final layout {final_layout} != the layout computed from swaps {layout}" - ) - - self.property_set["layout"] = self._to_full_layout(optimized_layout) - self.property_set["final_layout"] = self._to_full_layout(final_layout) - - return mapped_dag - - @staticmethod - def _create_empty_dagcircuit(source_dag: DAGCircuit, canonical_qreg: QuantumRegister): - target_dag = DAGCircuit() - target_dag.name = source_dag.name - target_dag._global_phase = source_dag._global_phase - target_dag.metadata = source_dag.metadata - - target_dag.add_qreg(canonical_qreg) - for creg in source_dag.cregs.values(): - target_dag.add_creg(creg) - - return target_dag - - def _to_full_layout(self, layout): - # fill layout with ancilla qubits (required by drawers) - idle_physical_qubits = [ - q for q in range(self.coupling_map.size()) if q not in layout.get_physical_bits() - ] - if idle_physical_qubits: - qreg = QuantumRegister(len(idle_physical_qubits), name="ancilla") - for idx, idle_q in enumerate(idle_physical_qubits): - layout[idle_q] = qreg[idx] - layout.add_register(qreg) - return layout diff --git a/qiskit/utils/optionals.py b/qiskit/utils/optionals.py index 8655f4346a0c..0321a1cbc7e5 100644 --- a/qiskit/utils/optionals.py +++ b/qiskit/utils/optionals.py @@ -58,7 +58,8 @@ * - .. py:data:: HAS_CPLEX - The `IBM CPLEX Optimizer `__ is a high-performance mathematical programming solver for linear, mixed-integer and quadratic - programming. It is required by the :class:`.BIPMapping` transpiler pass. + programming. This is no longer by Qiskit, but it weas historically and the optional + remains for backwards compatibility. * - .. py:data:: HAS_CVXPY - `CVXPY `__ is a Python package for solving convex optimization @@ -68,7 +69,8 @@ * - .. py:data:: HAS_DOCPLEX - `IBM Decision Optimization CPLEX Modelling `__ is a library for prescriptive - analysis. Like CPLEX, it is required for the :class:`.BIPMapping` transpiler pass. + analysis. Like CPLEX, this is no longer by Qiskit, but it weas historically and the + optional remains for backwards compatibility. * - .. py:data:: HAS_FIXTURES - The test suite has additional features that are available if the optional `fixtures @@ -246,13 +248,13 @@ HAS_CPLEX = _LazyImportTester( "cplex", - install="pip install 'qiskit-terra[bip-mapper]'", + install="pip install cplex", msg="This may not be possible for all Python versions and OSes", ) HAS_CVXPY = _LazyImportTester("cvxpy", install="pip install cvxpy") HAS_DOCPLEX = _LazyImportTester( {"docplex": (), "docplex.mp.model": ("Model",)}, - install="pip install 'qiskit-terra[bip-mapper]'", + install="pip install docplex", msg="This may not be possible for all Python versions and OSes", ) HAS_FIXTURES = _LazyImportTester("fixtures", install="pip install fixtures") diff --git a/releasenotes/notes/remove-deprecated-bip-mapper-e1206c8f905502dd.yaml b/releasenotes/notes/remove-deprecated-bip-mapper-e1206c8f905502dd.yaml new file mode 100644 index 000000000000..223d931c7f2d --- /dev/null +++ b/releasenotes/notes/remove-deprecated-bip-mapper-e1206c8f905502dd.yaml @@ -0,0 +1,15 @@ +--- +upgrade: + - | + The deprecated transpiler routing pass, ``BIPMapping`` has been removed. + It was marked as deprecated in the Qiskit 0.43.0 release. It has been + replaced by an external plugin package: ``qiskit-bip-mapper``. Details for + this new package can be found at the package's github repository: + + https://github.com/qiskit-community/qiskit-bip-mapper + + The pass was made into a separate plugin package for two reasons, first + the dependency on CPLEX makes it harder to use and secondly the plugin + package more cleanly integrates with :func:`~.transpile`. The optional + extra ``bip-mapper`` to install the ``cplex`` and ``docplex`` to support + this pass has been removed as nothing in Qiskit optionally requires it anymore. diff --git a/test/python/transpiler/test_bip_mapping.py b/test/python/transpiler/test_bip_mapping.py deleted file mode 100644 index a95ca727f79a..000000000000 --- a/test/python/transpiler/test_bip_mapping.py +++ /dev/null @@ -1,379 +0,0 @@ -# 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. - -"""Test the BIPMapping pass""" - -import unittest - -from qiskit import QuantumRegister, QuantumCircuit, ClassicalRegister -from qiskit.circuit import Barrier -from qiskit.circuit.library.standard_gates import SwapGate, CXGate -from qiskit.converters import circuit_to_dag -from qiskit.test import QiskitTestCase -from qiskit.providers.fake_provider import FakeLima -from qiskit.transpiler import CouplingMap, Layout, PassManager, Target -from qiskit.transpiler.exceptions import TranspilerError -from qiskit.transpiler.passes import BIPMapping -from qiskit.transpiler.passes import CheckMap, Collect2qBlocks, ConsolidateBlocks, UnitarySynthesis -from qiskit.utils import optionals - - -@unittest.skipUnless(optionals.HAS_CPLEX, "cplex is required to run the BIPMapping tests") -@unittest.skipUnless(optionals.HAS_DOCPLEX, "docplex is required to run the BIPMapping tests") -class TestBIPMapping(QiskitTestCase): - """Tests the BIPMapping pass.""" - - def test_empty(self): - """Returns the original circuit if the circuit is empty.""" - coupling = CouplingMap([[0, 1]]) - circuit = QuantumCircuit(2) - with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): - actual = BIPMapping(coupling)(circuit) - self.assertEqual(circuit, actual) - - def test_no_two_qubit_gates(self): - """Returns the original circuit if the circuit has no 2q-gates - q0:--[H]-- - q1:------- - CouplingMap map: [0]--[1] - """ - coupling = CouplingMap([[0, 1]]) - - circuit = QuantumCircuit(2) - circuit.h(0) - with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): - actual = BIPMapping(coupling)(circuit) - - self.assertEqual(circuit, actual) - - def test_trivial_case(self): - """No need to have any swap, the CX are distance 1 to each other - q0:--(+)-[H]-(+)- - | | - q1:---.-------|-- - | - q2:-----------.-- - CouplingMap map: [1]--[0]--[2] - """ - coupling = CouplingMap([[0, 1], [0, 2]]) - - circuit = QuantumCircuit(3) - circuit.cx(1, 0) - circuit.h(0) - circuit.cx(2, 0) - - with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): - actual = BIPMapping(coupling)(circuit) - self.assertEqual(3, len(actual)) - for inst, _, _ in actual.data: # there are no swaps - self.assertFalse(isinstance(inst, SwapGate)) - - def test_no_swap(self): - """Adding no swap if not giving initial layout""" - coupling = CouplingMap([[0, 1], [0, 2]]) - - circuit = QuantumCircuit(3) - circuit.cx(1, 2) - - with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): - actual = BIPMapping(coupling)(circuit) - - q = QuantumRegister(3, name="q") - expected = QuantumCircuit(q) - expected.cx(q[0], q[1]) - - self.assertEqual(expected, actual) - - def test_ignore_initial_layout(self): - """Ignoring initial layout even when it is supplied""" - coupling = CouplingMap([[0, 1], [0, 2]]) - - circuit = QuantumCircuit(3) - circuit.cx(1, 2) - - property_set = {"layout": Layout.generate_trivial_layout(*circuit.qubits)} - with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): - actual = BIPMapping(coupling)(circuit, property_set) - - q = QuantumRegister(3, name="q") - expected = QuantumCircuit(q) - expected.cx(q[0], q[1]) - - self.assertEqual(expected, actual) - - def test_can_map_measurements_correctly(self): - """Verify measurement nodes are updated to map correct cregs to re-mapped qregs.""" - coupling = CouplingMap([[0, 1], [0, 2]]) - - qr = QuantumRegister(3, "qr") - cr = ClassicalRegister(2) - circuit = QuantumCircuit(qr, cr) - circuit.cx(qr[1], qr[2]) - circuit.measure(qr[1], cr[0]) - circuit.measure(qr[2], cr[1]) - - with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): - actual = BIPMapping(coupling)(circuit) - - q = QuantumRegister(3, "q") - expected = QuantumCircuit(q, cr) - expected.cx(q[0], q[1]) - expected.measure(q[0], cr[0]) # <- changed due to initial layout change - expected.measure(q[1], cr[1]) # <- changed due to initial layout change - - self.assertEqual(expected, actual) - - def test_can_map_measurements_correctly_with_target(self): - """Verify measurement nodes are updated to map correct cregs to re-mapped qregs.""" - target = Target() - target.add_instruction(CXGate(), {(0, 1): None, (0, 2): None}) - - qr = QuantumRegister(3, "qr") - cr = ClassicalRegister(2) - circuit = QuantumCircuit(qr, cr) - circuit.cx(qr[1], qr[2]) - circuit.measure(qr[1], cr[0]) - circuit.measure(qr[2], cr[1]) - - with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): - actual = BIPMapping(target)(circuit) - - q = QuantumRegister(3, "q") - expected = QuantumCircuit(q, cr) - expected.cx(q[0], q[1]) - expected.measure(q[0], cr[0]) # <- changed due to initial layout change - expected.measure(q[1], cr[1]) # <- changed due to initial layout change - - self.assertEqual(expected, actual) - - def test_never_modify_mapped_circuit(self): - """Test that the mapping is idempotent. - It should not modify a circuit which is already compatible with the - coupling map, and can be applied repeatedly without modifying the circuit. - """ - coupling = CouplingMap([[0, 1], [0, 2]]) - - circuit = QuantumCircuit(3, 2) - circuit.cx(1, 2) - circuit.measure(1, 0) - circuit.measure(2, 1) - dag = circuit_to_dag(circuit) - - with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): - mapped_dag = BIPMapping(coupling).run(dag) - remapped_dag = BIPMapping(coupling).run(mapped_dag) - - self.assertEqual(mapped_dag, remapped_dag) - - def test_no_swap_multi_layer(self): - """Can find the best layout for a circuit with multiple layers.""" - coupling = CouplingMap([[0, 1], [1, 2], [2, 3]]) - - qr = QuantumRegister(4, name="qr") - circuit = QuantumCircuit(qr) - circuit.cx(qr[1], qr[0]) - circuit.cx(qr[0], qr[3]) - - property_set = {} - with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): - actual = BIPMapping(coupling, objective="depth")(circuit, property_set) - self.assertEqual(2, actual.depth()) - - CheckMap(coupling)(actual, property_set) - self.assertTrue(property_set["is_swap_mapped"]) - - def test_unmappable_cnots_in_a_layer(self): - """Test mapping of a circuit with 2 cnots in a layer into T-shape coupling, - which BIPMapping cannot map.""" - qr = QuantumRegister(4, "q") - cr = ClassicalRegister(4, "c") - circuit = QuantumCircuit(qr, cr) - circuit.cx(qr[0], qr[1]) - circuit.cx(qr[2], qr[3]) - circuit.measure(qr, cr) - - coupling = CouplingMap([[0, 1], [1, 2], [1, 3]]) # {0: [1], 1: [2, 3]} - with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): - actual = BIPMapping(coupling)(circuit) - - # Fails to map and returns the original circuit - self.assertEqual(circuit, actual) - - def test_multi_cregs(self): - """Test for multiple ClassicalRegisters.""" - - # ┌───┐ ░ ┌─┐ - # qr_0: ──■────────────┤ X ├─░─┤M├───────── - # ┌─┴─┐ ┌───┐└─┬─┘ ░ └╥┘┌─┐ - # qr_1: ┤ X ├──■──┤ H ├──■───░──╫─┤M├────── - # └───┘┌─┴─┐└───┘ ░ ║ └╥┘┌─┐ - # qr_2: ──■──┤ X ├───────────░──╫──╫─┤M├─── - # ┌─┴─┐└───┘ ░ ║ ║ └╥┘┌─┐ - # qr_3: ┤ X ├────────────────░──╫──╫──╫─┤M├ - # └───┘ ░ ║ ║ ║ └╥┘ - # c: 2/════════════════════════╩══╬══╩══╬═ - # 0 ║ 1 ║ - # ║ ║ - # d: 2/═══════════════════════════╩═════╩═ - # 0 1 - qr = QuantumRegister(4, "qr") - cr1 = ClassicalRegister(2, "c") - cr2 = ClassicalRegister(2, "d") - circuit = QuantumCircuit(qr, cr1, cr2) - circuit.cx(qr[0], qr[1]) - circuit.cx(qr[2], qr[3]) - circuit.cx(qr[1], qr[2]) - circuit.h(qr[1]) - circuit.cx(qr[1], qr[0]) - circuit.barrier(qr) - circuit.measure(qr[0], cr1[0]) - circuit.measure(qr[1], cr2[0]) - circuit.measure(qr[2], cr1[1]) - circuit.measure(qr[3], cr2[1]) - - coupling = CouplingMap([[0, 1], [0, 2], [2, 3]]) # linear [1, 0, 2, 3] - property_set = {} - with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): - actual = BIPMapping(coupling, objective="depth")(circuit, property_set) - self.assertEqual(5, actual.depth()) - - CheckMap(coupling)(actual, property_set) - self.assertTrue(property_set["is_swap_mapped"]) - - def test_swaps_in_dummy_steps(self): - """Test the case when swaps are inserted in dummy steps.""" - - # ┌───┐ ░ ░ - # q_0: ──■──┤ H ├─░───■────────░───■─────── - # ┌─┴─┐├───┤ ░ │ ░ │ - # q_1: ┤ X ├┤ H ├─░───┼────■───░───┼────■── - # └───┘├───┤ ░ │ ┌─┴─┐ ░ ┌─┴─┐ │ - # q_2: ──■──┤ H ├─░───┼──┤ X ├─░─┤ X ├──┼── - # ┌─┴─┐├───┤ ░ ┌─┴─┐└───┘ ░ └───┘┌─┴─┐ - # q_3: ┤ X ├┤ H ├─░─┤ X ├──────░──────┤ X ├ - # └───┘└───┘ ░ └───┘ ░ └───┘ - circuit = QuantumCircuit(4) - circuit.cx(0, 1) - circuit.cx(2, 3) - circuit.h([0, 1, 2, 3]) - circuit.barrier() - circuit.cx(0, 3) - circuit.cx(1, 2) - circuit.barrier() - circuit.cx(0, 2) - circuit.cx(1, 3) - - coupling = CouplingMap.from_line(4) - property_set = {} - with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): - actual = BIPMapping(coupling, objective="depth")(circuit, property_set) - self.assertEqual(7, actual.depth()) - - CheckMap(coupling)(actual, property_set) - self.assertTrue(property_set["is_swap_mapped"]) - - # no swaps before the first barrier - for inst, _, _ in actual.data: - if isinstance(inst, Barrier): - break - self.assertFalse(isinstance(inst, SwapGate)) - - def test_different_number_of_virtual_and_physical_qubits(self): - """Test the case when number of virtual and physical qubits are different.""" - - # q_0: ──■────■─────── - # ┌─┴─┐ │ - # q_1: ┤ X ├──┼────■── - # └───┘ │ ┌─┴─┐ - # q_2: ──■────┼──┤ X ├ - # ┌─┴─┐┌─┴─┐└───┘ - # q_3: ┤ X ├┤ X ├───── - # └───┘└───┘ - circuit = QuantumCircuit(4) - circuit.cx(0, 1) - circuit.cx(2, 3) - circuit.cx(0, 3) - circuit.cx(1, 2) - - coupling = CouplingMap.from_line(5) - with self.assertRaises(TranspilerError): - with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): - BIPMapping(coupling)(circuit) - - def test_qubit_subset(self): - """Test if `qubit_subset` option works as expected.""" - circuit = QuantumCircuit(3) - circuit.cx(0, 1) - circuit.cx(1, 2) - circuit.cx(0, 2) - - coupling = CouplingMap([(0, 1), (1, 3), (3, 2)]) - qubit_subset = [0, 1, 3] - with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): - actual = BIPMapping(coupling, qubit_subset=qubit_subset)(circuit) - # all used qubits are in qubit_subset - bit_indices = {bit: index for index, bit in enumerate(actual.qubits)} - for _, qargs, _ in actual.data: - for q in qargs: - self.assertTrue(bit_indices[q] in qubit_subset) - # ancilla qubits are set in the resulting qubit - idle = QuantumRegister(1, name="ancilla") - self.assertEqual(idle[0], actual._layout.initial_layout[2]) - - def test_unconnected_qubit_subset(self): - """Fails if qubits in `qubit_subset` are not connected.""" - circuit = QuantumCircuit(3) - circuit.cx(0, 1) - - coupling = CouplingMap([(0, 1), (1, 3), (3, 2)]) - with self.assertRaises(TranspilerError): - with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): - BIPMapping(coupling, qubit_subset=[0, 1, 2])(circuit) - - def test_objective_function(self): - """Test if ``objective`` functions prioritize metrics correctly.""" - - # ┌──────┐┌──────┐ ┌──────┐ - # q_0: ┤0 ├┤0 ├─────┤0 ├ - # │ Dcx ││ │ │ Dcx │ - # q_1: ┤1 ├┤ Dcx ├──■──┤1 ├ - # └──────┘│ │ │ └──────┘ - # q_2: ───■────┤1 ├──┼─────■──── - # ┌─┴─┐ └──────┘┌─┴─┐ ┌─┴─┐ - # q_3: ─┤ X ├──────────┤ X ├─┤ X ├── - # └───┘ └───┘ └───┘ - qc = QuantumCircuit(4) - qc.dcx(0, 1) - qc.cx(2, 3) - qc.dcx(0, 2) - qc.cx(1, 3) - qc.dcx(0, 1) - qc.cx(2, 3) - coupling = CouplingMap(FakeLima().configuration().coupling_map) - with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): - dep_opt = BIPMapping(coupling, objective="depth", qubit_subset=[0, 1, 3, 4])(qc) - with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): - err_opt = BIPMapping( - coupling, - objective="gate_error", - qubit_subset=[0, 1, 3, 4], - backend_prop=FakeLima().properties(), - )(qc) - # depth = number of su4 layers (mirrored gates have to be consolidated as single su4 gates) - pm_ = PassManager([Collect2qBlocks(), ConsolidateBlocks(basis_gates=["cx", "u"])]) - dep_opt = pm_.run(dep_opt) - err_opt = pm_.run(err_opt) - self.assertLessEqual(dep_opt.depth(), err_opt.depth()) - # count CNOTs after synthesized - dep_opt = UnitarySynthesis(basis_gates=["cx", "u"])(dep_opt) - err_opt = UnitarySynthesis(basis_gates=["cx", "u"])(err_opt) - self.assertGreater(dep_opt.count_ops()["cx"], err_opt.count_ops()["cx"]) From 29dddabb6fb5fd2821b6169e0365d24d7d78b6b4 Mon Sep 17 00:00:00 2001 From: Raghav <83136390+Raghav-Bell@users.noreply.github.com> Date: Wed, 13 Sep 2023 02:10:03 +0530 Subject: [PATCH 06/19] Removed code deprecated in 0.20 (#10767) * Remove code deprecated in 0.20 * Removed deprated tests in 0.20 * Removed deprecated tests in 0.20. * Added release note and reviewed changes * Renamed test_copy_circuit_metadata and fixed syntax in release note. * Accommodated the suggested changes in release note. * Resolved conflict and linting --- qiskit/dagcircuit/dagcircuit.py | 9 ----- qiskit/synthesis/evolution/suzuki_trotter.py | 25 +++++------- qiskit/transpiler/coupling.py | 16 -------- qiskit/visualization/circuit/latex.py | 38 ++----------------- ...ved_deprecation_0.20-17b0ea6a3980eb4f.yaml | 18 +++++++++ test/python/dagcircuit/test_dagcircuit.py | 2 +- test/python/transpiler/test_coupling.py | 10 ----- 7 files changed, 32 insertions(+), 86 deletions(-) create mode 100644 releasenotes/notes/removed_deprecation_0.20-17b0ea6a3980eb4f.yaml diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index f5f33fd85d8a..f60223698e02 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -45,7 +45,6 @@ from qiskit.dagcircuit.exceptions import DAGCircuitError from qiskit.dagcircuit.dagnode import DAGNode, DAGOpNode, DAGInNode, DAGOutNode from qiskit.circuit.bit import Bit -from qiskit.utils.deprecation import deprecate_func BitLocations = namedtuple("BitLocations", ("index", "registers")) @@ -608,14 +607,6 @@ def _decrement_op(self, op): else: self._op_names[op.name] -= 1 - @deprecate_func( - additional_msg="Instead, use :meth:`~copy_empty_like()`, which acts identically.", - since="0.20.0", - ) - def _copy_circuit_metadata(self): - """DEPRECATED""" - return self.copy_empty_like() - def copy_empty_like(self): """Return a copy of self with the same structure but empty. diff --git a/qiskit/synthesis/evolution/suzuki_trotter.py b/qiskit/synthesis/evolution/suzuki_trotter.py index b8dd766c3bcd..43c7d0e799e3 100644 --- a/qiskit/synthesis/evolution/suzuki_trotter.py +++ b/qiskit/synthesis/evolution/suzuki_trotter.py @@ -18,7 +18,7 @@ from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.quantum_info.operators import SparsePauliOp, Pauli -from qiskit.utils.deprecation import deprecate_arg + from .product_formula import ProductFormula @@ -51,17 +51,6 @@ class SuzukiTrotter(ProductFormula): `arXiv:math-ph/0506007 `_ """ - @deprecate_arg( - "order", - deprecation_description=( - "Setting `order` to an odd number in the constructor of SuzukiTrotter" - ), - additional_msg=( - "Suzuki product formulae are symmetric and therefore only defined for even orders." - ), - since="0.20.0", - predicate=lambda order: order % 2 == 1, - ) def __init__( self, order: int = 2, @@ -83,11 +72,15 @@ def __init__( atomic_evolution: A function to construct the circuit for the evolution of single Pauli string. Per default, a single Pauli evolution is decomposed in a CX chain and a single qubit Z rotation. + Raises: + ValueError: If order is not even """ - # TODO replace deprecation warning by the following error and add unit test for odd - # if order % 2 == 1: - # raise ValueError("Suzuki product formulae are symmetric and therefore only defined " - # "for even orders.") + + if order % 2 == 1: + raise ValueError( + "Suzuki product formulae are symmetric and therefore only defined " + "for even orders." + ) super().__init__(order, reps, insert_barriers, cx_structure, atomic_evolution) def synthesize(self, evolution): diff --git a/qiskit/transpiler/coupling.py b/qiskit/transpiler/coupling.py index 903341d1fc93..774f79329ff3 100644 --- a/qiskit/transpiler/coupling.py +++ b/qiskit/transpiler/coupling.py @@ -27,7 +27,6 @@ from rustworkx.visualization import graphviz_draw from qiskit.transpiler.exceptions import CouplingError -from qiskit.utils.deprecation import deprecate_func class CouplingMap: @@ -125,21 +124,6 @@ def add_edge(self, src, dst): self._dist_matrix = None # invalidate self._is_symmetric = None # invalidate - @deprecate_func( - additional_msg=( - "Instead, use :meth:`~reduce`. It does the same thing, but preserves nodelist order." - ), - since="0.20.0", - ) - def subgraph(self, nodelist): - """Return a CouplingMap object for a subgraph of self. - - nodelist (list): list of integer node labels - """ - subcoupling = CouplingMap() - subcoupling.graph = self.graph.subgraph(nodelist) - return subcoupling - @property def physical_qubits(self): """Returns a sorted list of physical_qubits""" diff --git a/qiskit/visualization/circuit/latex.py b/qiskit/visualization/circuit/latex.py index 67a6afe387c8..66e172b1ef85 100644 --- a/qiskit/visualization/circuit/latex.py +++ b/qiskit/visualization/circuit/latex.py @@ -19,13 +19,13 @@ from warnings import warn import numpy as np -from qiskit.circuit import Clbit, Qubit, ClassicalRegister, QuantumRegister, QuantumCircuit +from qiskit.circuit import Clbit, Qubit, ClassicalRegister from qiskit.circuit.classical import expr from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.library.standard_gates import SwapGate, XGate, ZGate, RZZGate, U1Gate, PhaseGate from qiskit.circuit.measure import Measure from qiskit.circuit.tools.pi_check import pi_check -from qiskit.utils.deprecation import deprecate_arg + from .qcstyle import load_style from ._utils import ( @@ -49,10 +49,6 @@ class QCircuitImage: Thanks to Eric Sabo for the initial implementation for Qiskit. """ - @deprecate_arg("gregs", since="0.20.0") - @deprecate_arg("cregs", since="0.20.0") - @deprecate_arg("layout", since="0.20.0") - @deprecate_arg("global_phase", since="0.20.0") def __init__( # pylint: disable=bad-docstring-quotes self, qubits, @@ -62,12 +58,8 @@ def __init__( # pylint: disable=bad-docstring-quotes style=None, reverse_bits=False, plot_barriers=True, - layout=None, initial_state=False, cregbundle=None, - global_phase=None, - qregs=None, - cregs=None, with_layout=False, circuit=None, ): @@ -82,36 +74,14 @@ def __init__( # pylint: disable=bad-docstring-quotes reverse_bits (bool): when True, reverse the bit ordering of the registers plot_barriers (bool): Enable/disable drawing barriers in the output circuit. Defaults to True. - layout (Layout or None): If present, the layout information will be - included. initial_state (bool): Optional. Adds |0> in the beginning of the line. Default: `False`. cregbundle (bool): Optional. If set True bundle classical registers. - global_phase (float): Optional, the global phase for the circuit. circuit (QuantumCircuit): the circuit that's being displayed Raises: ImportError: If pylatexenc is not installed """ - del layout - del global_phase - # This check should be removed when the 4 deprecations above are removed - if circuit is None: - warn( - "The 'circuit' kwarg to the QCircuitImage class must be a valid " - "QuantumCircuit and not None. A new circuit is being created using " - "the qubits and clbits for rendering the drawing.", - DeprecationWarning, - 2, - ) - circ = QuantumCircuit(qubits, clbits) - for reg in qregs or []: - bits = [qubits[circ._qubit_indices[q].index] for q in reg] - circ.add_register(QuantumRegister(None, reg.name, list(bits))) - for reg in cregs or []: - bits = [clbits[circ._clbit_indices[q].index] for q in reg] - circ.add_register(ClassicalRegister(None, reg.name, list(bits))) - self._circuit = circ - else: - self._circuit = circuit + + self._circuit = circuit self._qubits = qubits self._clbits = clbits diff --git a/releasenotes/notes/removed_deprecation_0.20-17b0ea6a3980eb4f.yaml b/releasenotes/notes/removed_deprecation_0.20-17b0ea6a3980eb4f.yaml new file mode 100644 index 000000000000..e5a6faff0062 --- /dev/null +++ b/releasenotes/notes/removed_deprecation_0.20-17b0ea6a3980eb4f.yaml @@ -0,0 +1,18 @@ +--- +upgrade: + - | + The class constructor arguments `qregs`, `cregs`, + `layout` and `global_phase` for :class:`.visualization.QCircuitImage` + are removed, as they were deprecated in 0.20. + + - | + In :class:`.transpiler.CouplingMap` method `subgraph` is removed + as deprecated in 0.20. :meth:`~reduce` can be used in place of method `subgraph`. + + - | + The parameter `order` in :class:`.synthesis.SuzukiTrotter` + constructor raises an exception instead of deprecation warning when + set in an odd number. Suzuki product formulae are symmetric and + therefore only defined for even orders. + + \ No newline at end of file diff --git a/test/python/dagcircuit/test_dagcircuit.py b/test/python/dagcircuit/test_dagcircuit.py index 02df668c0452..7f7375178bb0 100644 --- a/test/python/dagcircuit/test_dagcircuit.py +++ b/test/python/dagcircuit/test_dagcircuit.py @@ -410,7 +410,7 @@ def test_remove_idle_clbit(self): self.assert_cregs_equal(self.original_cregs) self.assert_clbits_equal(self.original_clbits, excluding={self.individual_clbit}) - def test_copy_circuit_metadata(self): + def test_copy_empty_like(self): """Copy dag circuit metadata with copy_empty_like.""" result_dag = self.dag.copy_empty_like() self.assertEqual(self.dag.name, result_dag.name) diff --git a/test/python/transpiler/test_coupling.py b/test/python/transpiler/test_coupling.py index f573d9abc819..7c75e309cb95 100644 --- a/test/python/transpiler/test_coupling.py +++ b/test/python/transpiler/test_coupling.py @@ -442,16 +442,6 @@ def test_hexagonal_lattice_2_2_factory_bidirectional(self): ] self.assertEqual(set(edges), set(expected)) - def test_subgraph(self): - coupling = CouplingMap.from_line(6, bidirectional=False) - with self.assertWarns(DeprecationWarning): - subgraph = coupling.subgraph([4, 2, 3, 5]) - self.assertEqual(subgraph.size(), 4) - self.assertEqual([0, 1, 2, 3], subgraph.physical_qubits) - edge_list = subgraph.get_edges() - expected = [(0, 1), (1, 2), (2, 3)] - self.assertEqual(expected, edge_list, f"{edge_list} does not match {expected}") - def test_implements_iter(self): """Test that the object is implicitly iterable.""" coupling = CouplingMap.from_line(3) From 578e109a115629e2f84376a291ff51293d2b27ba Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 12 Sep 2023 18:42:58 -0400 Subject: [PATCH 07/19] Add option to SabreLayout to specify starting layouts (#10721) * Add option to SabreLayout to specify starting layouts The SabreLayout pass typically starts with each layout trial with a random starting layout. However, in some cases starting with a specific starting layout can result in better output quality than starting with a fully random layout. To use this feature an analysis pass can set a new `sabre_starting_layout` field in the property set before `SabreLayout` with a list of layout objects that will add additional trials using each layout as a starting layout. * Fix lint * Combine random layout and partial layouts * Expand property set docs Co-authored-by: Jake Lishman * Simplify branching logic for parallelism * Use a slice for starting_layout in a trial * Update releasenotes/notes/seed-sabre-with-layout-17d46e1a6f516b0e.yaml Co-authored-by: Jake Lishman * Make starting layout variable names plural The starting layout is a list and can have more than one entry. To make this clearer, this commit renames starting_layout -> starting_layouts and sabre_starting_layout -> sabre_starting_layouts. * Update releasenotes/notes/seed-sabre-with-layout-17d46e1a6f516b0e.yaml Co-authored-by: Jake Lishman * Update property set key in test * Update test to only use partial layout --------- Co-authored-by: Jake Lishman Co-authored-by: Jake Lishman --- crates/accelerate/src/sabre_layout.rs | 46 ++++++++++-- .../transpiler/passes/layout/sabre_layout.py | 60 ++++++++++++++- ...ed-sabre-with-layout-17d46e1a6f516b0e.yaml | 38 ++++++++++ test/python/transpiler/test_sabre_layout.py | 73 ++++++++++++++++++- 4 files changed, 204 insertions(+), 13 deletions(-) create mode 100644 releasenotes/notes/seed-sabre-with-layout-17d46e1a6f516b0e.yaml diff --git a/crates/accelerate/src/sabre_layout.rs b/crates/accelerate/src/sabre_layout.rs index ce49e9115aae..decfe590c050 100644 --- a/crates/accelerate/src/sabre_layout.rs +++ b/crates/accelerate/src/sabre_layout.rs @@ -11,6 +11,7 @@ // that they have been altered from the originals. #![allow(clippy::too_many_arguments)] +use hashbrown::HashSet; use ndarray::prelude::*; use numpy::{IntoPyArray, PyArray, PyReadonlyArray2}; use pyo3::prelude::*; @@ -28,6 +29,7 @@ use crate::sabre_swap::swap_map::SwapMap; use crate::sabre_swap::{build_swap_map_inner, Heuristic, NodeBlockResults, SabreResult}; #[pyfunction] +#[pyo3(signature = (dag, neighbor_table, distance_matrix, heuristic, max_iterations, num_swap_trials, num_random_trials, seed=None, partial_layouts=vec![]))] pub fn sabre_layout_and_routing( py: Python, dag: &SabreDAG, @@ -36,20 +38,24 @@ pub fn sabre_layout_and_routing( heuristic: &Heuristic, max_iterations: usize, num_swap_trials: usize, - num_layout_trials: usize, + num_random_trials: usize, seed: Option, + mut partial_layouts: Vec>>, ) -> (NLayout, PyObject, (SwapMap, PyObject, NodeBlockResults)) { let run_in_parallel = getenv_use_multiple_threads(); + let mut starting_layouts: Vec>> = + (0..num_random_trials).map(|_| vec![]).collect(); + starting_layouts.append(&mut partial_layouts); let outer_rng = match seed { Some(seed) => Pcg64Mcg::seed_from_u64(seed), None => Pcg64Mcg::from_entropy(), }; let seed_vec: Vec = outer_rng .sample_iter(&rand::distributions::Standard) - .take(num_layout_trials) + .take(starting_layouts.len()) .collect(); let dist = distance_matrix.as_array(); - let res = if run_in_parallel && num_layout_trials > 1 { + let res = if run_in_parallel && starting_layouts.len() > 1 { seed_vec .into_par_iter() .enumerate() @@ -65,6 +71,7 @@ pub fn sabre_layout_and_routing( max_iterations, num_swap_trials, run_in_parallel, + &starting_layouts[index], ), ) }) @@ -79,7 +86,8 @@ pub fn sabre_layout_and_routing( } else { seed_vec .into_iter() - .map(|seed_trial| { + .enumerate() + .map(|(index, seed_trial)| { layout_trial( dag, neighbor_table, @@ -89,6 +97,7 @@ pub fn sabre_layout_and_routing( max_iterations, num_swap_trials, run_in_parallel, + &starting_layouts[index], ) }) .min_by_key(|(_, _, result)| result.map.map.values().map(|x| x.len()).sum::()) @@ -114,15 +123,38 @@ fn layout_trial( max_iterations: usize, num_swap_trials: usize, run_swap_in_parallel: bool, + starting_layout: &[Option], ) -> (NLayout, Vec, SabreResult) { let num_physical_qubits: u32 = distance_matrix.shape()[0].try_into().unwrap(); let mut rng = Pcg64Mcg::seed_from_u64(seed); // Pick a random initial layout including a full ancilla allocation. let mut initial_layout = { - let mut physical_qubits: Vec = - (0..num_physical_qubits).map(PhysicalQubit::new).collect(); - physical_qubits.shuffle(&mut rng); + let physical_qubits: Vec = if !starting_layout.is_empty() { + let used_bits: HashSet = starting_layout + .iter() + .filter_map(|x| x.as_ref()) + .copied() + .collect(); + let mut free_bits: Vec = (0..num_physical_qubits) + .filter(|x| !used_bits.contains(x)) + .collect(); + free_bits.shuffle(&mut rng); + (0..num_physical_qubits) + .map(|x| { + let bit_index = match starting_layout.get(x as usize) { + Some(phys) => phys.unwrap_or_else(|| free_bits.pop().unwrap()), + None => free_bits.pop().unwrap(), + }; + PhysicalQubit::new(bit_index) + }) + .collect() + } else { + let mut physical_qubits: Vec = + (0..num_physical_qubits).map(PhysicalQubit::new).collect(); + physical_qubits.shuffle(&mut rng); + physical_qubits + }; NLayout::from_virtual_to_physical(physical_qubits).unwrap() }; diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index 6e521c05d7f2..62af8fd57a35 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -16,6 +16,7 @@ import copy import dataclasses import logging +import functools import time import numpy as np @@ -74,6 +75,34 @@ class SabreLayout(TransformationPass): layout pass. When specified this will use the specified routing pass to select an initial layout only and will not run multiple seed trials. + In addition to starting with a random initial `Layout` the pass can also take in + an additional list of starting layouts which will be used for additional + trials. If the ``sabre_starting_layouts`` is present in the property set + when this pass is run, that will be used for additional trials. There will still + be ``layout_trials`` of full random starting layouts run and the contents of + ``sabre_starting_layouts`` will be run in addition to those. The output which results + in the lowest amount of swap gates (whether from the random trials or the property + set starting point) will be used. The value for this property set field should be a + list of :class:`.Layout` objects representing the starting layouts to use. If a + virtual qubit is missing from an :class:`.Layout` object in the list a random qubit + will be selected. + + Property Set Fields Read + ------------------------ + + ``sabre_starting_layouts`` (``list[Layout]``) + An optional list of :class:`~.Layout` objects to use for additional layout trials. This is + in addition to the full random trials specified with the ``layout_trials`` argument. + + Property Set Values Written + --------------------------- + + ``layout`` (:class:`.Layout`) + The chosen initial mapping of virtual to physical qubits, including the ancilla allocation. + + ``final_layout`` (:class:`.Layout`) + A permutation of how swaps have been applied to the input qubits at the end of the circuit. + **References:** [1] Li, Gushu, Yufei Ding, and Yuan Xie. "Tackling the qubit mapping problem @@ -232,8 +261,12 @@ def run(self, dag): target.make_symmetric() else: target = self.coupling_map - - components = disjoint_utils.run_pass_over_connected_components(dag, target, self._inner_run) + inner_run = self._inner_run + if "sabre_starting_layouts" in self.property_set: + inner_run = functools.partial( + self._inner_run, starting_layouts=self.property_set["sabre_starting_layouts"] + ) + components = disjoint_utils.run_pass_over_connected_components(dag, target, inner_run) self.property_set["layout"] = Layout( { component.dag.qubits[logic]: component.coupling_map.graph[phys] @@ -314,7 +347,7 @@ def run(self, dag): disjoint_utils.combine_barriers(mapped_dag, retain_uuid=False) return mapped_dag - def _inner_run(self, dag, coupling_map): + def _inner_run(self, dag, coupling_map, starting_layouts=None): if not coupling_map.is_symmetric: # deepcopy is needed here to avoid modifications updating # shared references in passes which require directional @@ -323,8 +356,26 @@ def _inner_run(self, dag, coupling_map): coupling_map.make_symmetric() neighbor_table = NeighborTable(rx.adjacency_matrix(coupling_map.graph)) dist_matrix = coupling_map.distance_matrix + original_qubit_indices = {bit: index for index, bit in enumerate(dag.qubits)} + partial_layouts = [] + if starting_layouts is not None: + coupling_map_reverse_mapping = { + coupling_map.graph[x]: x for x in coupling_map.graph.node_indices() + } + for layout in starting_layouts: + virtual_bits = layout.get_virtual_bits() + out_layout = [None] * len(dag.qubits) + for bit, phys in virtual_bits.items(): + pos = original_qubit_indices.get(bit, None) + if pos is None: + continue + out_layout[pos] = coupling_map_reverse_mapping[phys] + partial_layouts.append(out_layout) + sabre_dag, circuit_to_dag_dict = _build_sabre_dag( - dag, coupling_map.size(), {bit: index for index, bit in enumerate(dag.qubits)} + dag, + coupling_map.size(), + original_qubit_indices, ) sabre_start = time.perf_counter() (initial_layout, final_permutation, sabre_result) = sabre_layout_and_routing( @@ -336,6 +387,7 @@ def _inner_run(self, dag, coupling_map): self.swap_trials, self.layout_trials, self.seed, + partial_layouts, ) sabre_stop = time.perf_counter() logger.debug( diff --git a/releasenotes/notes/seed-sabre-with-layout-17d46e1a6f516b0e.yaml b/releasenotes/notes/seed-sabre-with-layout-17d46e1a6f516b0e.yaml new file mode 100644 index 000000000000..fa6a12b9c1f3 --- /dev/null +++ b/releasenotes/notes/seed-sabre-with-layout-17d46e1a6f516b0e.yaml @@ -0,0 +1,38 @@ +--- +features: + - | + Added support to the :class:`.SabreLayout` pass to add trials with specified + starting layouts. The :class:`.SabreLayout` transpiler pass typically + runs multiple layout trials that all start with fully random layouts which + then use a routing pass to permute that layout instead of inserting swaps + to find a layout which will result in fewer swap gates. This new feature + enables running an :class:`.AnalysisPass` prior to :class:`.SabreLayout` + which sets the ``"sabre_starting_layout"`` field in the property set + to provide the :class:`.SabreLayout` with additional starting layouts + to use in its internal trials. For example, if you wanted to run + :class:`.DenseLayout` as the starting point for one trial in + :class:`.SabreLayout` you would do something like:: + + from qiskit.providers.fake_provider import FakeSherbrooke + from qiskit.transpiler import AnalysisPass, PassManager + from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager + from qiskit.transpiler.passes import DenseLayout + + class SabreDenseLayoutTrial(AnalysisPass): + + def __init__(self, target): + self.dense_pass = DenseLayout(target=target) + super().__init__() + + def run(self, dag): + self.dense_pass.run(dag) + self.property_set["sabre_starting_layouts"] = [self.dense_pass.property_set["layout"]] + + backend = FakeSherbrooke() + opt_level_1 = generate_preset_pass_manager(1, backend) + pre_layout = PassManager([SabreDenseLayoutTrial(backend.target)]) + opt_level_1.pre_layout = pre_layout + + Then when the ``opt_level_1`` :class:`.StagedPassManager` is run with a circuit the output + of the :class:`.DenseLayout` pass will be used for one of the :class:`.SabreLayout` trials + in addition to the 5 fully random trials that run by default in optimization level 1. diff --git a/test/python/transpiler/test_sabre_layout.py b/test/python/transpiler/test_sabre_layout.py index ff9788521c16..38c17b442964 100644 --- a/test/python/transpiler/test_sabre_layout.py +++ b/test/python/transpiler/test_sabre_layout.py @@ -15,8 +15,8 @@ import unittest from qiskit import QuantumRegister, QuantumCircuit -from qiskit.transpiler import CouplingMap -from qiskit.transpiler.passes import SabreLayout +from qiskit.transpiler import CouplingMap, AnalysisPass, PassManager +from qiskit.transpiler.passes import SabreLayout, DenseLayout from qiskit.transpiler.exceptions import TranspilerError from qiskit.converters import circuit_to_dag from qiskit.test import QiskitTestCase @@ -94,6 +94,41 @@ def test_6q_circuit_20q_coupling(self): layout = pass_.property_set["layout"] self.assertEqual([layout[q] for q in circuit.qubits], [7, 8, 12, 6, 11, 13]) + def test_6q_circuit_20q_coupling_with_partial(self): + """Test finds layout for 6q circuit on 20q device.""" + # ┌───┐┌───┐┌───┐┌───┐┌───┐ + # q0_0: ┤ X ├┤ X ├┤ X ├┤ X ├┤ X ├ + # └─┬─┘└─┬─┘└─┬─┘└─┬─┘└─┬─┘ + # q0_1: ──┼────■────┼────┼────┼── + # │ ┌───┐ │ │ │ + # q0_2: ──┼──┤ X ├──┼────■────┼── + # │ └───┘ │ │ + # q1_0: ──■─────────┼─────────┼── + # ┌───┐ │ │ + # q1_1: ─────┤ X ├──┼─────────■── + # └───┘ │ + # q1_2: ────────────■──────────── + qr0 = QuantumRegister(3, "q0") + qr1 = QuantumRegister(3, "q1") + circuit = QuantumCircuit(qr0, qr1) + circuit.cx(qr1[0], qr0[0]) + circuit.cx(qr0[1], qr0[0]) + circuit.cx(qr1[2], qr0[0]) + circuit.x(qr0[2]) + circuit.cx(qr0[2], qr0[0]) + circuit.x(qr1[1]) + circuit.cx(qr1[1], qr0[0]) + + pm = PassManager( + [ + DensePartialSabreTrial(CouplingMap(self.cmap20)), + SabreLayout(CouplingMap(self.cmap20), seed=0, swap_trials=32, layout_trials=0), + ] + ) + pm.run(circuit) + layout = pm.property_set["layout"] + self.assertEqual([layout[q] for q in circuit.qubits], [1, 3, 5, 2, 6, 0]) + def test_6q_circuit_20q_coupling_with_target(self): """Test finds layout for 6q circuit on 20q device.""" # ┌───┐┌───┐┌───┐┌───┐┌───┐ @@ -218,6 +253,18 @@ def test_layout_many_search_trials(self): ) +class DensePartialSabreTrial(AnalysisPass): + """Pass to run dense layout as a sabre trial.""" + + def __init__(self, cmap): + self.dense_pass = DenseLayout(cmap) + super().__init__() + + def run(self, dag): + self.dense_pass.run(dag) + self.property_set["sabre_starting_layouts"] = [self.dense_pass.property_set["layout"]] + + class TestDisjointDeviceSabreLayout(QiskitTestCase): """Test SabreLayout with a disjoint coupling map.""" @@ -319,6 +366,28 @@ def test_too_large_components(self): with self.assertRaises(TranspilerError): layout_routing_pass(qc) + def test_with_partial_layout(self): + """Test a partial layout with a disjoint connectivity graph.""" + qc = QuantumCircuit(8, name="double dhz") + qc.h(0) + qc.cz(0, 1) + qc.cz(0, 2) + qc.h(3) + qc.cx(3, 4) + qc.cx(3, 5) + qc.cx(3, 6) + qc.cx(3, 7) + qc.measure_all() + pm = PassManager( + [ + DensePartialSabreTrial(self.dual_grid_cmap), + SabreLayout(self.dual_grid_cmap, seed=123456, swap_trials=1, layout_trials=1), + ] + ) + pm.run(qc) + layout = pm.property_set["layout"] + self.assertEqual([layout[q] for q in qc.qubits], [3, 1, 2, 5, 4, 6, 7, 8]) + if __name__ == "__main__": unittest.main() From 579084e9ca446534d7abd32c5329b8615443ccf9 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 13 Sep 2023 12:34:42 +0100 Subject: [PATCH 08/19] Add missing `expr.bit_xor` documentation (#10828) --- qiskit/circuit/classical/expr/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qiskit/circuit/classical/expr/__init__.py b/qiskit/circuit/classical/expr/__init__.py index d0302aeb60bd..d2cd4bc5044e 100644 --- a/qiskit/circuit/classical/expr/__init__.py +++ b/qiskit/circuit/classical/expr/__init__.py @@ -104,6 +104,7 @@ .. autofunction:: bit_and .. autofunction:: bit_or +.. autofunction:: bit_xor .. autofunction:: logic_and .. autofunction:: logic_or .. autofunction:: equal From e5997133d6d8c89405c17e89117216e986a87462 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 13 Sep 2023 14:19:44 +0100 Subject: [PATCH 09/19] Do not emit warnings from `opflow` initialisation (#10830) Opflow is completely deprecated, and `import qiskit.opflow` should and does raise a warning blaming the importer. However, during the import, several opflow objects are initialised, which also trigger deprecation warnings. These will correctly be blamed on internal Qiskit library code, and so will not be shown to users by default. However, test suites running with all warnings treated as errors will also see these warnings, which will mask the one true warning that should be handled by the downstream code. It would be valid for downstream code to filter out these internal warnings, but in order to make it easier for downstream code to do the correct thing immediately, this opts to silence the internal warnings during initialisation. Downstream code running with all warnings enabled will now _only_ see the warning from `import qiskit.opflow` which is the only actionable warning they should have to deal with anyway. These warnings are only filtered during the initialisation and are targetted specifically at opflow-internal deprecation warnings. In general, deprecated code will call other deprecated code and it is not necessary to filter every single usage. This particular commit is a slight compromise to make it slightly easier for downstream libraries to do the right thing. --- qiskit/opflow/operator_globals.py | 44 +++++++++++++++++++------------ 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/qiskit/opflow/operator_globals.py b/qiskit/opflow/operator_globals.py index a5afcd9cf08f..ac0c624c7287 100644 --- a/qiskit/opflow/operator_globals.py +++ b/qiskit/opflow/operator_globals.py @@ -14,6 +14,8 @@ Operator Globals """ +import warnings + from qiskit.quantum_info import Pauli from qiskit.circuit.library import CXGate, SGate, TGate, HGate, SwapGate, CZGate @@ -48,22 +50,30 @@ def make_immutable(obj): return obj -# 1-Qubit Paulis -X = make_immutable(PauliOp(Pauli("X"))) -Y = make_immutable(PauliOp(Pauli("Y"))) -Z = make_immutable(PauliOp(Pauli("Z"))) -I = make_immutable(PauliOp(Pauli("I"))) +# All the deprecation warnings triggered by these object creations correctly blame `qiskit.opflow` +# and so are not shown to users by default. However, since they are eagerly triggered at `import +# qiskit.opflow`, they obscure the one "true" warning of the import when downstream testing code is +# running with all warnings showing. The true warning that really needs attention becomes easy to +# overlook because there's so many that the downstream code didn't explicitly call. +with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning, module=r"qiskit\.opflow\.") + + # 1-Qubit Paulis + X = make_immutable(PauliOp(Pauli("X"))) + Y = make_immutable(PauliOp(Pauli("Y"))) + Z = make_immutable(PauliOp(Pauli("Z"))) + I = make_immutable(PauliOp(Pauli("I"))) -# Clifford+T, and some other common non-parameterized gates -CX = make_immutable(CircuitOp(CXGate())) -S = make_immutable(CircuitOp(SGate())) -H = make_immutable(CircuitOp(HGate())) -T = make_immutable(CircuitOp(TGate())) -Swap = make_immutable(CircuitOp(SwapGate())) -CZ = make_immutable(CircuitOp(CZGate())) + # Clifford+T, and some other common non-parameterized gates + CX = make_immutable(CircuitOp(CXGate())) + S = make_immutable(CircuitOp(SGate())) + H = make_immutable(CircuitOp(HGate())) + T = make_immutable(CircuitOp(TGate())) + Swap = make_immutable(CircuitOp(SwapGate())) + CZ = make_immutable(CircuitOp(CZGate())) -# 1-Qubit states -Zero = make_immutable(DictStateFn("0")) -One = make_immutable(DictStateFn("1")) -Plus = make_immutable(H.compose(Zero)) -Minus = make_immutable(H.compose(X).compose(Zero)) + # 1-Qubit states + Zero = make_immutable(DictStateFn("0")) + One = make_immutable(DictStateFn("1")) + Plus = make_immutable(H.compose(Zero)) + Minus = make_immutable(H.compose(X).compose(Zero)) From 3a49759aaf770f8e61b366d0c932fc6f71b11527 Mon Sep 17 00:00:00 2001 From: "Christopher J. Wood" Date: Thu, 14 Sep 2023 09:54:00 -0400 Subject: [PATCH 10/19] Fix GateDirection pass (#10825) * Fix GateDirection * Fix black * Add note about qubit index assumption to class docstring --------- Co-authored-by: Matthew Treinish --- .../transpiler/passes/utils/gate_direction.py | 9 ++++----- .../fix-gate-direction-d8bc96b62b27dba8.yaml | 7 +++++++ test/python/transpiler/test_gate_direction.py | 20 +++++++++++++++++++ 3 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/fix-gate-direction-d8bc96b62b27dba8.yaml diff --git a/qiskit/transpiler/passes/utils/gate_direction.py b/qiskit/transpiler/passes/utils/gate_direction.py index 262538a2fad0..a259106d3ed4 100644 --- a/qiskit/transpiler/passes/utils/gate_direction.py +++ b/qiskit/transpiler/passes/utils/gate_direction.py @@ -60,6 +60,10 @@ class GateDirection(TransformationPass): │ RZX │ = ├───┤│ RZX │├───┤ q_1: ┤1 ├ q_1: ┤ H ├┤0 ├┤ H ├ └──────┘ └───┘└──────┘└───┘ + + This pass assumes that the positions of the qubits in the :attr:`.DAGCircuit.qubits` attribute + are the physical qubit indicies. For example if ``dag.qubits[0]`` is qubit 0 in the + :class:`.CouplingMap` or :class:`.Target`. """ _KNOWN_REPLACEMENTS = frozenset(["cx", "cz", "ecr", "swap", "rzx", "rxx", "ryy", "rzz"]) @@ -325,11 +329,6 @@ def run(self, dag): cx nodes. """ layout_map = {bit: i for i, bit in enumerate(dag.qubits)} - if len(dag.qregs) > 1: - raise TranspilerError( - "GateDirection expects a single qreg input DAG," - "but input DAG had qregs: {}.".format(dag.qregs) - ) if self.target is None: return self._run_coupling_map(dag, layout_map) return self._run_target(dag, layout_map) diff --git a/releasenotes/notes/fix-gate-direction-d8bc96b62b27dba8.yaml b/releasenotes/notes/fix-gate-direction-d8bc96b62b27dba8.yaml new file mode 100644 index 000000000000..54d29206ccd4 --- /dev/null +++ b/releasenotes/notes/fix-gate-direction-d8bc96b62b27dba8.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixes a bug with the :class:`.GateDirection` transpiler pass where it + unnecessarily raised an exception for input DAGs with more than 1 + quantum register. + Fixed `#10824 `__. diff --git a/test/python/transpiler/test_gate_direction.py b/test/python/transpiler/test_gate_direction.py index a45568efdf58..ef23ea53f5f1 100644 --- a/test/python/transpiler/test_gate_direction.py +++ b/test/python/transpiler/test_gate_direction.py @@ -101,6 +101,26 @@ def test_direction_correct(self): self.assertEqual(dag, after) + def test_multi_register(self): + """The CX is in the right direction + qr0:---(+)--- + | + qr1:----.---- + + CouplingMap map: [0] -> [1] + """ + qr1 = QuantumRegister(1, "qr1") + qr2 = QuantumRegister(1, "qr2") + circuit = QuantumCircuit(qr1, qr2) + circuit.cx(qr1, qr2) + coupling = CouplingMap([[0, 1]]) + dag = circuit_to_dag(circuit) + + pass_ = GateDirection(coupling) + after = pass_.run(dag) + + self.assertEqual(dag, after) + def test_direction_flip(self): """Flip a CX qr0:----.---- From 3aa80c9b3d1a5819c544c413bd9af286af7bc057 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Thu, 14 Sep 2023 18:25:49 +0200 Subject: [PATCH 11/19] Kraus operator documentation formatting (#10772) * Kraus operator documentation formatting * Update qiskit/quantum_info/operators/channel/kraus.py Co-authored-by: Jake Lishman * remove default * can the ambiguitity be resolved like this? * full path * Cyclic import fix --------- Co-authored-by: Jake Lishman --- .../quantum_info/operators/channel/kraus.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/qiskit/quantum_info/operators/channel/kraus.py b/qiskit/quantum_info/operators/channel/kraus.py index ceec196b3621..c898c09f250d 100644 --- a/qiskit/quantum_info/operators/channel/kraus.py +++ b/qiskit/quantum_info/operators/channel/kraus.py @@ -19,6 +19,7 @@ from numbers import Number import numpy as np +from qiskit import circuit from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.instruction import Instruction from qiskit.exceptions import QiskitError @@ -62,30 +63,24 @@ class Kraus(QuantumChannel): def __init__( self, - data: QuantumCircuit | Instruction | BaseOperator | np.ndarray, + data: QuantumCircuit | circuit.instruction.Instruction | BaseOperator | np.ndarray, input_dims: tuple | None = None, output_dims: tuple | None = None, ): """Initialize a quantum channel Kraus operator. Args: - data (QuantumCircuit or - Instruction or - BaseOperator or - matrix): data to initialize superoperator. - input_dims (tuple): the input subsystem dimensions. - [Default: None] - output_dims (tuple): the output subsystem dimensions. - [Default: None] + data: data to initialize superoperator. + input_dims: the input subsystem dimensions. + output_dims: the output subsystem dimensions. Raises: - QiskitError: if input data cannot be initialized as a - a list of Kraus matrices. + QiskitError: if input data cannot be initialized as a list of Kraus matrices. Additional Information: If the input or output dimensions are None, they will be automatically determined from the input data. If the input data is - a list of Numpy arrays of shape (2**N, 2**N) qubit systems will be + a list of Numpy arrays of shape :math:`(2^N,\\,2^N)` qubit systems will be used. If the input does not correspond to an N-qubit channel, it will assign a single subsystem with dimension specified by the shape of the input. From 67021e4c375fdcda76b84802e8f1147d6a76937c Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Fri, 15 Sep 2023 08:50:11 +0100 Subject: [PATCH 12/19] Add fast path to `Parameter.assign` (#10549) This specialises `Parameter.assign` to more efficiently handle parameter assignment via `QuantumCircuit.assign_parameters`. The assignment can either be of `self`, and therefore should just return the value lifted to a `ParameterExpression`, or it's not of `self`, in which case we need to raise an error to match the superclass implementation. The overhead of all the checks that go on during the regular `ParameterExpression.assign` (delegating to `subs` or `bind`) are a sizeable chunk of the cost of assignment in PEC-style circuits. --- qiskit/circuit/parameter.py | 18 ++++++++++++++++++ ...mise-parameter-assign-398ed4f2074ca201.yaml | 6 ++++++ 2 files changed, 24 insertions(+) create mode 100644 releasenotes/notes/optimise-parameter-assign-398ed4f2074ca201.yaml diff --git a/qiskit/circuit/parameter.py b/qiskit/circuit/parameter.py index 75d51ed592d1..3e347b0beac1 100644 --- a/qiskit/circuit/parameter.py +++ b/qiskit/circuit/parameter.py @@ -90,6 +90,24 @@ def __init__(self, name: str): symbol = symengine.Symbol(name) super().__init__(symbol_map={self: symbol}, expr=symbol) + def assign(self, parameter, value): + if parameter != self: + # Corresponds to superclass calls to `subs` and `bind` that would implicitly set + # `allow_unknown_parameters=False`. + raise CircuitError( + f"Cannot bind Parameters ({[str(parameter)]}) not present in expression." + ) + if isinstance(value, ParameterExpression): + # This is the `super().subs` case. + return value + # This is the `super().bind` case, where we're required to return a `ParameterExpression`, + # so we need to lift the given value to a symbolic expression. + if _optionals.HAS_SYMENGINE: + from symengine import sympify + else: + from sympy import sympify + return ParameterExpression({}, sympify(value)) + def subs(self, parameter_map: dict, allow_unknown_parameters: bool = False): """Substitute self with the corresponding parameter in ``parameter_map``.""" if self in parameter_map: diff --git a/releasenotes/notes/optimise-parameter-assign-398ed4f2074ca201.yaml b/releasenotes/notes/optimise-parameter-assign-398ed4f2074ca201.yaml new file mode 100644 index 000000000000..1efa868317c0 --- /dev/null +++ b/releasenotes/notes/optimise-parameter-assign-398ed4f2074ca201.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + A fast path for :meth:`.Parameter.assign` has been added, which improves the performance of + :meth:`.QuantumCircuit.assign_parameters` for the common case of circuits that contain + predominantly "expressions" that are actually just single parameters to be assigned later. From 05e24d51d8714850a966f69eb882d787f78cddae Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Fri, 15 Sep 2023 12:07:49 +0100 Subject: [PATCH 13/19] Optimise `QuantumCircuit.assign_parameters` for single-parameter binding (#10548) When assigning parameters in a heavily parametrised circuit, a significant amount of time was spent constructing a new set of all the `Parameter` instances used in a circuit. This completely dominated the execution time for the case of binding a single parameter out of a many-parameters circuit in place. This modifies the internal-only method `QuantumCircuit._unsorted_parameters` to directly return the set object already constructed by the `ParameterTable` tracking the operations, which makes the function close to free to call, at the cost that internal users of that function must take care not to mutate the output. This is not generally a problem, since no code in Terra outside of `QuantumCircuit` uses that method (nor should it!), and `QuantumCircuit` never needs to mutate that set. --- qiskit/circuit/quantumcircuit.py | 21 +++++++++++++++---- ...-unsorted-parameters-310e5f2420771f49.yaml | 5 +++++ 2 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/optimise-unsorted-parameters-310e5f2420771f49.yaml diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 77660a55889a..6e01f3f32572 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -2523,14 +2523,26 @@ def parameters(self) -> ParameterView: @property def num_parameters(self) -> int: """The number of parameter objects in the circuit.""" + # Avoid a (potential) object creation if we can. + if self._parameters is not None: + return len(self._parameters) return len(self._unsorted_parameters()) def _unsorted_parameters(self) -> set[Parameter]: - """Efficiently get all parameters in the circuit, without any sorting overhead.""" - parameters = set(self._parameter_table) - if isinstance(self.global_phase, ParameterExpression): - parameters.update(self.global_phase.parameters) + """Efficiently get all parameters in the circuit, without any sorting overhead. + + .. warning:: + The returned object may directly view onto the ``ParameterTable`` internals, and so + should not be mutated. This is an internal performance detail. Code outside of this + package should not use this method. + """ + # This should be free, by accessing the actual backing data structure of the table, but that + # means that we need to copy it if adding keys from the global phase. + parameters = self._parameter_table.get_keys() + if isinstance(self.global_phase, ParameterExpression): + # Deliberate copy. + parameters = parameters | self.global_phase.parameters return parameters @overload @@ -2648,6 +2660,7 @@ def assign_parameters( # pylint: disable=missing-raises-doc # 'target' so we can take advantage of any caching we might be doing. if isinstance(parameters, dict): raw_mapping = parameters if flat_input else self._unroll_param_dict(parameters) + # Remember that we _must not_ mutate the output of `_unsorted_parameters`. our_parameters = self._unsorted_parameters() if strict and (extras := raw_mapping.keys() - our_parameters): raise CircuitError( diff --git a/releasenotes/notes/optimise-unsorted-parameters-310e5f2420771f49.yaml b/releasenotes/notes/optimise-unsorted-parameters-310e5f2420771f49.yaml new file mode 100644 index 000000000000..2ab9a49af4f8 --- /dev/null +++ b/releasenotes/notes/optimise-unsorted-parameters-310e5f2420771f49.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + The performance of :meth:`.QuantumCircuit.assign_parameters` when assigning a + single parameter of a circuit that involves many parameters has been improved. From 7d9d32e37e80f86e6f28743e66ad8ed8271cd16f Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Fri, 15 Sep 2023 14:27:21 +0200 Subject: [PATCH 14/19] qreg_creg_descending calls deprecated Bit.index (#10843) * reversing instead of sorting by index in reverse * QCircuitImage calls deprecated Bit.index * revert * Simplifying approach --- Co-authored-by: Jake Lishman --------- Co-authored-by: Jake Lishman --- qiskit/visualization/timeline/layouts.py | 42 ++++-------------------- 1 file changed, 6 insertions(+), 36 deletions(-) diff --git a/qiskit/visualization/timeline/layouts.py b/qiskit/visualization/timeline/layouts.py index 5c5737c6e1f4..d77284e3e021 100644 --- a/qiskit/visualization/timeline/layouts.py +++ b/qiskit/visualization/timeline/layouts.py @@ -48,14 +48,10 @@ def my_layout(time_window: Tuple[int, int]) -> types.HorizontalAxis: Arbitrary layout function satisfying the above format can be accepted. """ - -import warnings - from typing import List, Tuple import numpy as np from qiskit import circuit -from qiskit.visualization.exceptions import VisualizationError from qiskit.visualization.timeline import types @@ -70,23 +66,9 @@ def qreg_creg_ascending(bits: List[types.Bits]) -> List[types.Bits]: Returns: Sorted bits. """ - qregs = [] - cregs = [] - - for bit in bits: - if isinstance(bit, circuit.Qubit): - qregs.append(bit) - elif isinstance(bit, circuit.Clbit): - cregs.append(bit) - else: - raise VisualizationError(f"Unknown bit {bit} is provided.") - - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - qregs = sorted(qregs, key=lambda x: x.index, reverse=False) - cregs = sorted(cregs, key=lambda x: x.index, reverse=False) - - return qregs + cregs + return [x for x in bits if isinstance(x, circuit.Qubit)] + [ + x for x in bits if isinstance(x, circuit.Clbit) + ] def qreg_creg_descending(bits: List[types.Bits]) -> List[types.Bits]: @@ -100,21 +82,9 @@ def qreg_creg_descending(bits: List[types.Bits]) -> List[types.Bits]: Returns: Sorted bits. """ - qregs = [] - cregs = [] - - for bit in bits: - if isinstance(bit, circuit.Qubit): - qregs.append(bit) - elif isinstance(bit, circuit.Clbit): - cregs.append(bit) - else: - raise VisualizationError(f"Unknown bit {bit} is provided.") - - qregs = sorted(qregs, key=lambda x: x.index, reverse=True) - cregs = sorted(cregs, key=lambda x: x.index, reverse=True) - - return qregs + cregs + return [x for x in bits[::-1] if isinstance(x, circuit.Qubit)] + [ + x for x in bits[::-1] if isinstance(x, circuit.Clbit) + ] def time_map_in_dt(time_window: Tuple[int, int]) -> types.HorizontalAxis: From 72f27aad9ed20d6435f9d62c8cbb8e4df8e69247 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Fri, 15 Sep 2023 18:55:23 +0200 Subject: [PATCH 15/19] QCircuitImage calls deprecated Bit.index (#10844) --- qiskit/visualization/circuit/latex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/visualization/circuit/latex.py b/qiskit/visualization/circuit/latex.py index 66e172b1ef85..ad4b8e070e13 100644 --- a/qiskit/visualization/circuit/latex.py +++ b/qiskit/visualization/circuit/latex.py @@ -536,7 +536,7 @@ def _build_measure(self, node, col): register = get_bit_register(self._circuit, node.cargs[0]) if register is not None: wire2 = self._wire_map[register] - idx_str = str(node.cargs[0].index) + idx_str = str(self._circuit.find_bit(node.cargs[0]).registers[0][1]) else: wire2 = self._wire_map[node.cargs[0]] From 4e49a569459733404c33c60c60f914470c38cc32 Mon Sep 17 00:00:00 2001 From: Will Shanks Date: Fri, 15 Sep 2023 19:45:25 -0400 Subject: [PATCH 16/19] Allow dynamical decoupling pass to work on circuits with pulse gates (#10834) The pass was accessing the circuit calibrations incorrectly, resulting in an error when one of the gates in the dynamical decoupling sequence had a pulse gate calibration. --- .../padding/dynamical_decoupling.py | 16 +- .../notes/dd-pg-10833-ddddee68ffd913c4.yaml | 7 + .../transpiler/test_dynamical_decoupling.py | 152 +++++++++++++++++- 3 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/dd-pg-10833-ddddee68ffd913c4.yaml diff --git a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py index bbb1b39bbd72..fb33361a4959 100644 --- a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py @@ -16,7 +16,7 @@ import logging import numpy as np -from qiskit.circuit import Qubit, Gate +from qiskit.circuit import Gate, ParameterExpression, Qubit from qiskit.circuit.delay import Delay from qiskit.circuit.library.standard_gates import IGate, UGate, U3Gate from qiskit.circuit.reset import Reset @@ -227,7 +227,8 @@ def _pre_runhook(self, dag: DAGCircuit): for gate in self._dd_sequence: try: # Check calibration. - gate_length = dag.calibrations[gate.name][(physical_index, gate.params)] + params = self._resolve_params(gate) + gate_length = dag.calibrations[gate.name][((physical_index,), params)].duration if gate_length % self._alignment != 0: # This is necessary to implement lightweight scheduling logic for this pass. # Usually the pulse alignment constraint and pulse data chunk size take @@ -394,6 +395,17 @@ def _constrained_length(values): dag.global_phase = self._mod_2pi(dag.global_phase + sequence_gphase) + @staticmethod + def _resolve_params(gate: Gate) -> tuple: + """Return gate params with any bound parameters replaced with floats""" + params = [] + for p in gate.params: + if isinstance(p, ParameterExpression) and not p.parameters: + params.append(float(p)) + else: + params.append(p) + return tuple(params) + @staticmethod def _mod_2pi(angle: float, atol: float = 0): """Wrap angle into interval [-π,π). If within atol of the endpoint, clamp to -π""" diff --git a/releasenotes/notes/dd-pg-10833-ddddee68ffd913c4.yaml b/releasenotes/notes/dd-pg-10833-ddddee68ffd913c4.yaml new file mode 100644 index 000000000000..cdf23b9ef9a3 --- /dev/null +++ b/releasenotes/notes/dd-pg-10833-ddddee68ffd913c4.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixed a bug in the :class:`.PadDynamicalDecoupling` transpiler pass which + would cause the pass to fail if a circuit contained a pulse gate + calibration for one of the gates in the decoupling sequence. Fixed `#10833 + `_. diff --git a/test/python/transpiler/test_dynamical_decoupling.py b/test/python/transpiler/test_dynamical_decoupling.py index bd8c44ab0973..6ae6a7b3c311 100644 --- a/test/python/transpiler/test_dynamical_decoupling.py +++ b/test/python/transpiler/test_dynamical_decoupling.py @@ -17,7 +17,8 @@ from numpy import pi from ddt import ddt, data -from qiskit.circuit import QuantumCircuit, Delay, Measure, Reset, Parameter +from qiskit import pulse +from qiskit.circuit import Gate, QuantumCircuit, Delay, Measure, Reset, Parameter from qiskit.circuit.library import XGate, YGate, RXGate, UGate, CXGate, HGate from qiskit.quantum_info import Operator from qiskit.transpiler.instruction_durations import InstructionDurations @@ -29,7 +30,6 @@ from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.target import Target, InstructionProperties -from qiskit import pulse from qiskit.test import QiskitTestCase @@ -339,6 +339,154 @@ def test_insert_dd_ghz_everywhere(self): self.assertEqual(ghz4_dd, expected) + def test_insert_dd_with_pulse_gate_calibrations(self): + """Test DD gates are inserted without error when circuit calibrations are used + + ┌───┐ ┌───────────────┐ ┌───┐ » + q_0: ──────┤ H ├─────────■──┤ Delay(75[dt]) ├──────┤ X ├───────» + ┌─────┴───┴─────┐ ┌─┴─┐└───────────────┘┌─────┴───┴──────┐» + q_1: ┤ Delay(50[dt]) ├─┤ X ├────────■────────┤ Delay(300[dt]) ├» + ├───────────────┴┐└───┘ ┌─┴─┐ └────────────────┘» + q_2: ┤ Delay(750[dt]) ├───────────┤ X ├──────────────■─────────» + ├────────────────┤ └───┘ ┌─┴─┐ » + q_3: ┤ Delay(950[dt]) ├────────────────────────────┤ X ├───────» + └────────────────┘ └───┘ » + meas: 4/══════════════════════════════════════════════════════════» + » + « ┌────────────────┐┌───┐┌───────────────┐ ░ ┌─┐ + « q_0: ┤ Delay(150[dt]) ├┤ X ├┤ Delay(75[dt]) ├─░─┤M├───────── + « └────────────────┘└───┘└───────────────┘ ░ └╥┘┌─┐ + « q_1: ─────────────────────────────────────────░──╫─┤M├────── + « ░ ║ └╥┘┌─┐ + « q_2: ─────────────────────────────────────────░──╫──╫─┤M├─── + « ░ ║ ║ └╥┘┌─┐ + « q_3: ─────────────────────────────────────────░──╫──╫──╫─┤M├ + « ░ ║ ║ ║ └╥┘ + «meas: 4/════════════════════════════════════════════╩══╩══╩══╩═ + « 0 1 2 3 + """ + dd_sequence = [XGate(), XGate()] + pm = PassManager( + [ + ALAPScheduleAnalysis(self.durations), + PadDynamicalDecoupling(self.durations, dd_sequence, qubits=[0]), + ] + ) + + # Change duration to 100 from the 50 in self.durations to make sure + # gate duration is used correctly. + with pulse.builder.build() as x_sched: + pulse.builder.delay(100, pulse.DriveChannel(0)) + + circ_in = self.ghz4.measure_all(inplace=False) + circ_in.add_calibration(XGate(), (0,), x_sched) + + ghz4_dd = pm.run(circ_in) + + expected = self.ghz4.copy() + expected = expected.compose(Delay(50), [1], front=True) + expected = expected.compose(Delay(750), [2], front=True) + expected = expected.compose(Delay(950), [3], front=True) + + # Delays different from those of the default case using self.durations + expected = expected.compose(Delay(75), [0]) + expected = expected.compose(XGate(), [0]) + expected = expected.compose(Delay(150), [0]) + expected = expected.compose(XGate(), [0]) + expected = expected.compose(Delay(75), [0]) + + expected = expected.compose(Delay(300), [1]) + + expected.measure_all() + expected.add_calibration(XGate(), (0,), x_sched) + + self.assertEqual(ghz4_dd, expected) + + def test_insert_dd_with_pulse_gate_calibrations_with_parmas(self): + """Test DD gates are inserted without error when parameterized circuit calibrations are used + + ┌───┐ ┌───────────────┐ ┌───┐ » + q_0: ──────┤ H ├─────────■──┤ Delay(75[dt]) ├──────┤ X ├───────» + ┌─────┴───┴─────┐ ┌─┴─┐└───────────────┘┌─────┴───┴──────┐» + q_1: ┤ Delay(50[dt]) ├─┤ X ├────────■────────┤ Delay(300[dt]) ├» + ├───────────────┴┐└───┘ ┌─┴─┐ └────────────────┘» + q_2: ┤ Delay(750[dt]) ├───────────┤ X ├──────────────■─────────» + ├────────────────┤ └───┘ ┌─┴─┐ » + q_3: ┤ Delay(950[dt]) ├────────────────────────────┤ X ├───────» + └────────────────┘ └───┘ » + meas: 4/══════════════════════════════════════════════════════════» + » + « ┌────────────────┐┌───┐┌───────────────┐ ░ ┌─┐ + « q_0: ┤ Delay(150[dt]) ├┤ X ├┤ Delay(75[dt]) ├─░─┤M├───────── + « └────────────────┘└───┘└───────────────┘ ░ └╥┘┌─┐ + « q_1: ─────────────────────────────────────────░──╫─┤M├────── + « ░ ║ └╥┘┌─┐ + « q_2: ─────────────────────────────────────────░──╫──╫─┤M├─── + « ░ ║ ║ └╥┘┌─┐ + « q_3: ─────────────────────────────────────────░──╫──╫──╫─┤M├ + « ░ ║ ║ ║ └╥┘ + «meas: 4/════════════════════════════════════════════╩══╩══╩══╩═ + « 0 1 2 3 + """ + # Change duration to 100 from the 50 in self.durations to make sure + # gate duration is used correctly. + amp = Parameter("amp") + with pulse.builder.build() as sched: + pulse.builder.play( + pulse.Gaussian(100, amp=amp, sigma=10.0), + pulse.DriveChannel(0), + ) + + class Echo(Gate): + """Dummy Gate subclass for testing + + In this test, we use a non-standard gate so we can add parameters + to it, in order to test the handling of parameters by + PadDynamicalDecoupling. PadDynamicalDecoupling checks that the DD + sequence is equivalent to the identity, so we can not use Gate + directly. Here we subclass Gate and add the identity as its matrix + representation to satisfy PadDynamicalDecoupling's check. + """ + + def __array__(self, dtype=None): + return np.eye(2, dtype=dtype) + + # A gate with one unbound and one bound parameter to leave in the final + # circuit. + echo = Echo("echo", 1, [amp, 10.0]) + + circ_in = self.ghz4.measure_all(inplace=False) + circ_in.add_calibration(echo, (0,), sched) + + dd_sequence = [echo, echo] + pm = PassManager( + [ + ALAPScheduleAnalysis(self.durations), + PadDynamicalDecoupling(self.durations, dd_sequence, qubits=[0]), + ] + ) + + ghz4_dd = pm.run(circ_in) + + expected = self.ghz4.copy() + expected = expected.compose(Delay(50), [1], front=True) + expected = expected.compose(Delay(750), [2], front=True) + expected = expected.compose(Delay(950), [3], front=True) + + # Delays different from those of the default case using self.durations + expected = expected.compose(Delay(75), [0]) + expected = expected.compose(echo, [0]) + expected = expected.compose(Delay(150), [0]) + expected = expected.compose(echo, [0]) + expected = expected.compose(Delay(75), [0]) + + expected = expected.compose(Delay(300), [1]) + + expected.measure_all() + expected.add_calibration(echo, (0,), sched) + + self.assertEqual(ghz4_dd, expected) + def test_insert_dd_ghz_xy4(self): """Test XY4 sequence of DD gates. From 2d3632fee633413bb1883e411d1a26f76fc54e92 Mon Sep 17 00:00:00 2001 From: Joonghoon Lee Date: Sun, 17 Sep 2023 05:27:02 +0900 Subject: [PATCH 17/19] Remove deprecated code in 0.19 (#10791) * remove-deprecated-code-in-0.19 * update docs * Apply suggestions from code review Co-authored-by: Luciano Bello * remove unused function --------- Co-authored-by: Luciano Bello --- qiskit/compiler/assembler.py | 4 +-- .../providers/models/backendconfiguration.py | 14 +--------- qiskit/qobj/__init__.py | 12 -------- qiskit/transpiler/instruction_durations.py | 28 +++---------------- qiskit/utils/quantum_instance.py | 4 +-- ...recated-code-in-0.19-a97ccfec62405b9a.yaml | 10 +++++++ 6 files changed, 19 insertions(+), 53 deletions(-) create mode 100644 releasenotes/notes/remove-deprecated-code-in-0.19-a97ccfec62405b9a.yaml diff --git a/qiskit/compiler/assembler.py b/qiskit/compiler/assembler.py index 9ebcb0783526..9ded481b2ecc 100644 --- a/qiskit/compiler/assembler.py +++ b/qiskit/compiler/assembler.py @@ -27,7 +27,7 @@ from qiskit.providers.backend import Backend from qiskit.pulse import Instruction, LoConfig, Schedule, ScheduleBlock from qiskit.pulse.channels import PulseChannel -from qiskit.qobj import Qobj, QobjHeader +from qiskit.qobj import QasmQobj, PulseQobj, QobjHeader from qiskit.qobj.utils import MeasLevel, MeasReturnType logger = logging.getLogger(__name__) @@ -74,7 +74,7 @@ def assemble( parametric_pulses: Optional[List[str]] = None, init_qubits: bool = True, **run_config: Dict, -) -> Qobj: +) -> Union[QasmQobj, PulseQobj]: """Assemble a list of circuits or pulse schedules into a ``Qobj``. This function serializes the payloads, which could be either circuits or schedules, diff --git a/qiskit/providers/models/backendconfiguration.py b/qiskit/providers/models/backendconfiguration.py index 75ee19c1eb35..87150bcf75ff 100644 --- a/qiskit/providers/models/backendconfiguration.py +++ b/qiskit/providers/models/backendconfiguration.py @@ -26,7 +26,6 @@ DriveChannel, MeasureChannel, ) -from qiskit.utils.deprecation import deprecate_arg class GateConfig: @@ -831,22 +830,13 @@ def acquire(self, qubit: int) -> AcquireChannel: raise BackendConfigurationError(f"Invalid index for {qubit}-qubit systems.") return AcquireChannel(qubit) - @deprecate_arg( - "channel", - since="0.19.0", - additional_msg=( - "Instead, use the ``qubits`` argument. This method will now return accurate " - "ControlChannels determined by qubit indices." - ), - ) - def control(self, qubits: Iterable[int] = None, channel: int = None) -> List[ControlChannel]: + def control(self, qubits: Iterable[int] = None) -> List[ControlChannel]: """ Return the secondary drive channel for the given qubit -- typically utilized for controlling multiqubit interactions. This channel is derived from other channels. Args: qubits: Tuple or list of qubits of the form `(control_qubit, target_qubit)`. - channel: Deprecated. Raises: BackendConfigurationError: If the ``qubits`` is not a part of the system or if @@ -855,8 +845,6 @@ def control(self, qubits: Iterable[int] = None, channel: int = None) -> List[Con Returns: List of control channels. """ - if channel is not None: - qubits = [channel] try: if isinstance(qubits, list): qubits = tuple(qubits) diff --git a/qiskit/qobj/__init__.py b/qiskit/qobj/__init__.py index edd6b0099003..5922fdf5dd8b 100644 --- a/qiskit/qobj/__init__.py +++ b/qiskit/qobj/__init__.py @@ -23,7 +23,6 @@ .. autosummary:: :toctree: ../stubs/ - Qobj QobjExperimentHeader QobjHeader @@ -74,14 +73,3 @@ from qiskit.qobj.qasm_qobj import QasmQobjExperiment from qiskit.qobj.qasm_qobj import QasmQobjConfig from qiskit.qobj.qasm_qobj import QasmQobjExperimentConfig - -from qiskit.utils.deprecation import deprecate_func - - -class Qobj(QasmQobj): - """A backwards compat alias for QasmQobj.""" - - @deprecate_func(additional_msg="Instead, use QasmQobj or PulseQobj", since="0.19.0") - def __init__(self, qobj_id=None, config=None, experiments=None, header=None): - """Initialize a Qobj object.""" - super().__init__(qobj_id=qobj_id, config=config, experiments=experiments, header=header) diff --git a/qiskit/transpiler/instruction_durations.py b/qiskit/transpiler/instruction_durations.py index d1611840a040..fc9186a8790a 100644 --- a/qiskit/transpiler/instruction_durations.py +++ b/qiskit/transpiler/instruction_durations.py @@ -16,20 +16,13 @@ import qiskit.circuit from qiskit.circuit import Barrier, Delay -from qiskit.circuit import Instruction, Qubit, ParameterExpression +from qiskit.circuit import Instruction, ParameterExpression from qiskit.circuit.duration import duration_in_dt from qiskit.providers import Backend from qiskit.transpiler.exceptions import TranspilerError -from qiskit.utils.deprecation import deprecate_arg from qiskit.utils.units import apply_prefix -def _is_deprecated_qubits_argument(qubits: Union[int, list[int], Qubit, list[Qubit]]) -> bool: - if isinstance(qubits, (int, Qubit)): - qubits = [qubits] - return isinstance(qubits[0], Qubit) - - class InstructionDurations: """Helper class to provide durations of instructions for scheduling. @@ -130,7 +123,6 @@ def update(self, inst_durations: "InstructionDurationsType" | None, dt: float = ) else: for i, items in enumerate(inst_durations): - if not isinstance(items[-1], str): items = (*items, "dt") # set default unit @@ -170,19 +162,10 @@ def update(self, inst_durations: "InstructionDurationsType" | None, dt: float = return self - @deprecate_arg( - "qubits", - deprecation_description=( - "Using a Qubit or List[Qubit] for the ``qubits`` argument to InstructionDurations.get()" - ), - additional_msg="Instead, use an integer for the qubit index.", - since="0.19.0", - predicate=_is_deprecated_qubits_argument, - ) def get( self, inst: str | qiskit.circuit.Instruction, - qubits: int | list[int] | Qubit | list[Qubit] | list[int | Qubit], + qubits: int | list[int], unit: str = "dt", parameters: list[float] | None = None, ) -> float: @@ -192,7 +175,7 @@ def get( Args: inst: An instruction or its name to be queried. - qubits: Qubits or its indices that the instruction acts on. + qubits: Qubit indices that the instruction acts on. unit: The unit of duration to be returned. It must be 's' or 'dt'. parameters: The value of the parameters of the desired instruction. @@ -212,12 +195,9 @@ def get( else: inst_name = inst - if isinstance(qubits, (int, Qubit)): + if isinstance(qubits, int): qubits = [qubits] - if isinstance(qubits[0], Qubit): - qubits = [q.index for q in qubits] - try: return self._get(inst_name, qubits, unit, parameters) except TranspilerError as ex: diff --git a/qiskit/utils/quantum_instance.py b/qiskit/utils/quantum_instance.py index a4bbb76e1b00..94c1bed535d4 100644 --- a/qiskit/utils/quantum_instance.py +++ b/qiskit/utils/quantum_instance.py @@ -21,7 +21,7 @@ import numpy as np -from qiskit.qobj import Qobj +from qiskit.qobj import QasmQobj, PulseQobj from qiskit.utils import circuit_utils from qiskit.exceptions import QiskitError from qiskit.utils.backend_utils import ( @@ -451,7 +451,7 @@ def transpile(self, circuits, pass_manager=None): return transpiled_circuits - def assemble(self, circuits) -> Qobj: + def assemble(self, circuits) -> Union[QasmQobj, PulseQobj]: """assemble circuits""" # pylint: disable=cyclic-import from qiskit import compiler diff --git a/releasenotes/notes/remove-deprecated-code-in-0.19-a97ccfec62405b9a.yaml b/releasenotes/notes/remove-deprecated-code-in-0.19-a97ccfec62405b9a.yaml new file mode 100644 index 000000000000..eb62402ab3c4 --- /dev/null +++ b/releasenotes/notes/remove-deprecated-code-in-0.19-a97ccfec62405b9a.yaml @@ -0,0 +1,10 @@ +--- +upgrade: + - | + The argument ``qubits`` in the method :meth:`qiskit.transpiler.instruction_durations.InstructionDurations.get`, does not accept :class:`.Qubit` (or a list of them) any more. This functionality was deprecated in Qiskit 0.33 (with Terra 0.19), released on Dec 2021. Instead, use an integer for the qubit indices. + + - | + The argument ``channel`` in the method :meth:`qiskit.providers.models.backendconfiguration.PulseBackendConfiguration.control` is removed. It was deprecated in Qiskit 0.33 (with Terra 0.19), released on Dec 2021. Instead use the ``qubits`` argument. + + - | + The class ``qiskit.qobj.Qobj`` is removed. It was deprecated in Qiskit 0.33 (with Terra 0.19), released on Dec 2021. Instead, use :class:`qiskit.qobj.QasmQobj` or :class:`qiskit.qobj.PulseQobj`. \ No newline at end of file From e62c86bbc2720883270641f1c154743a657cfca8 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 19 Sep 2023 15:41:30 -0400 Subject: [PATCH 18/19] Use singletons for standard library unparameterized, non-controlled gates (#10314) * Use singletons for standard library unparameterized, non-controlled gates This commit adds a new class SingletonGate which is a Gate subclass that reuses a single instance by default for all instances of a particular class. This greatly reduces the memory overhead and significant improves the construction speed for making multiple instances of the same gate. The tradeoff is in the flexibility of use because it precludes having any potentially mutable state in the shared instance. This is a large change to the data model of qiskit because it previously could be assumed that any gate instance was unique and there weren't any unintended side effects from modifying it in isolation (for example doing XGate().label = 'foo' wouldn't potentially break other code). To limit the impact around this instances of SingletonGate do not allow mutation of an existing instance. This can (and likely will) cause unexpected issues as usage of the class is released. Specifically what used to be valid will now raise an exception because it is a shared instance. This is evident from the code modifications necessary to most of the Qiskit code base to enable working with instances of SingletonGates. The knock on effects of this downstream are likely significant and managing how we roll this feature out is going to be equally if not more important than the feature itself. This is why I'm not personally convinced we want to do all this commit includes in a single release. I've opened this as a pull request primarily to start the conversation on how we want to do the roll out to try and minimize and notify downstream users of the potential breakage to avoid issues. The primary issue I have is this doesn't really follow the Qiskit deprecation policy as there is no user facing notification or documentation of this pending change and code that worked in the previously release will not work in the release with this feature. For some aspects of this change (primarily the setters on gate attributes) this can easily be handled by deprecating it in planned singleton standard library gates and waiting the necessary amount of time. But the more fundamental data model changes are hard to announce ahead of time. We can have a release note about it coming in the future but it will end up being very abstract and users will not necessarily be able to act on it ahead of time without concrete examples to test with. This was an issue for me in developing this feature as I couldn't anticipate where API breakages would occur until I switched over all the standard library gates, and there still might be others. Due to the large performance gains this offers and also in the interest of testing the API implications of using singleton gates the unparameterized and non-controlled gates available in qiskit.circuit.library.standard_gates are all updated to be subclasses of singleton gates. In aggregate this is causing construction to be roughly 6x faster and building circuits comprised solely of these gates consume 1/4th the memory as before. But it also exposed a large number of internal changes we needed to make to the wider circuit, QPY, qasm2, dagcircuit, transpiler, converters, and test modules to support working with singleton gates. Besides this there are a couple seemingly unrelated API changes in this PR and it is caused by inconsistencies in the Instruction/Gate API that were preventing this from working. The first which is the ECRGate class was missing a label kwarg in the parent. Similarly all Gate classes and subclasses were missing duration and unit kwargs on their constructors. These are necessary to be able to use these fields on singletons because we need an interface to construct an instance that has the state set so we avoid the use of the global shared instance. In the release notes I labeled these as bugfixes, because in all the cases the parent clases were exposing these interfaces and it primarily is an oversight that they were missing in these places. But personally this does seem more like something we'd normally document as a feature rather than a bugfix. A follow up PR will add a SingletonControlledGate class which will be similar to SingletonGate but will offer two singleton instance based on the value of ctrl_state (and also handle nested labels and other nested mutable state in the base gate). We can then update the standard library gates like CXGate, and CHGate to also be singletons. The ctrl state attribute is primarily why these gates were not included in this commit. * Fix Python 3.8 compatibility There are some differences in how the inspect stdlib module behaves between python 3.8 and newer versions of python. This was causing divergence in the test and qpy behavior where inspect was used to determine different aspects of a gate (either whether label was supported as an arg or find the number of free parameters). This commit fixes this by adjusting the code to handle both newer versions of inspect as well as older ones. * Simplify qpy deserialization label handling * Remove unused classmethod decorator * Fix lint * Fix timeline drawer on output of legacy DD pass * Fix doc build * Add methods to deal with mutability of singleton gates This commit adds two methods to the SingletonGate class, mutable and to_mutable. The mutable() method is a property method that returns whether a particular instance is mutable or a shared singleton instance. The second method to_mutable() returns a mutable copy of the gate. * Disallow custom attribute on singleton instances This commit adds a __setattr__ method to the singleton gate class to ensure that custom attributes are not settable for a shared singleton instance. It prevents addign a custom attribute if the instance is in singleton mode and will raise a NotImplementedError to avoid silently sharing state in the single shared instance. * Fix rebase error * Fix rebase issues * Fix module docstring * Add .mutable and .to_mutable() to Instruction To unify the access patterns between SingletonGates and other instructions this commit adds a common property mutable and method to_mutable() to check if any Instruction (not just singletons) are mutable or not and to get a mutable copy. For things that don't inherit from SingletonGate these are hard coded to `True` and to return a copy as by default every Instruction is mutable, only `SingletonGate` objects are different in this regard. * Unify handling of gates in scheduling passes Previously we were explicitly handling the SingletonGate class in the scheduling passes. But with the introduction of mutable and to_mutable() on Instruction we don't need to condition on gates being singleton or not and we can just handle them in the same manner as other instructions. This commit implements this change. * Remove unnecessary deepcopy in substitute_node_wtih_dag * Fix logic for SingletonGate.copy Co-authored-by: Jake Lishman * Update Singleton Gate class docstring * Remove unused imports * Update release notes * Fix release note typos Co-authored-by: Jake Lishman * Improve setattr performance * Fix deepcopy logic * Add check for kwargs up front * Fix docs typos * Add comment on to_mutable __init__ call * Fix lint * Handle positional initialization arguments in SingletonGate If there are any positional arguments set when initializing a new SingletonGate subclass those were not being handled correctly before. If there is a positional argument being set that would indicate at least a label is being set and indicates a mutable instance should be returned instead of the immutable singleton. This commit adds the missing check to the __new__ logic and also adds a test to verify the behavior is correct. --------- Co-authored-by: Jake Lishman Co-authored-by: Jake Lishman --- qiskit/circuit/__init__.py | 2 + qiskit/circuit/add_control.py | 4 +- qiskit/circuit/gate.py | 12 +- qiskit/circuit/instruction.py | 33 ++- qiskit/circuit/instructionset.py | 2 +- qiskit/circuit/library/standard_gates/dcx.py | 13 +- qiskit/circuit/library/standard_gates/ecr.py | 12 +- qiskit/circuit/library/standard_gates/h.py | 30 ++- qiskit/circuit/library/standard_gates/i.py | 12 +- .../circuit/library/standard_gates/iswap.py | 12 +- qiskit/circuit/library/standard_gates/s.py | 22 +- qiskit/circuit/library/standard_gates/swap.py | 24 +- qiskit/circuit/library/standard_gates/sx.py | 40 ++- qiskit/circuit/library/standard_gates/t.py | 22 +- qiskit/circuit/library/standard_gates/x.py | 97 +++++-- qiskit/circuit/library/standard_gates/y.py | 30 ++- qiskit/circuit/library/standard_gates/z.py | 30 ++- qiskit/circuit/random/utils.py | 2 +- qiskit/circuit/singleton_gate.py | 188 +++++++++++++ qiskit/converters/ast_to_dag.py | 12 +- qiskit/converters/circuit_to_instruction.py | 6 +- qiskit/dagcircuit/dagcircuit.py | 25 +- qiskit/qasm2/parse.py | 3 +- qiskit/qpy/binary_io/circuits.py | 28 +- .../reset_after_measure_simplification.py | 3 +- .../passes/scheduling/dynamical_decoupling.py | 5 +- .../padding/dynamical_decoupling.py | 4 +- .../scheduling/scheduling/base_scheduler.py | 1 + .../passes/scheduling/time_unit_conversion.py | 9 +- .../notes/singletons-83782de8bd062cbc.yaml | 122 +++++++++ test/python/circuit/gate_utils.py | 2 +- .../circuit/test_circuit_load_from_qpy.py | 3 +- test/python/circuit/test_gate_definitions.py | 1 + test/python/circuit/test_instructions.py | 19 +- test/python/circuit/test_singleton_gate.py | 253 ++++++++++++++++++ test/python/dagcircuit/test_dagcircuit.py | 36 +-- 36 files changed, 950 insertions(+), 169 deletions(-) create mode 100644 qiskit/circuit/singleton_gate.py create mode 100644 releasenotes/notes/singletons-83782de8bd062cbc.yaml create mode 100644 test/python/circuit/test_singleton_gate.py diff --git a/qiskit/circuit/__init__.py b/qiskit/circuit/__init__.py index b293fcdd12c2..e2f7936acbcc 100644 --- a/qiskit/circuit/__init__.py +++ b/qiskit/circuit/__init__.py @@ -279,6 +279,7 @@ InstructionSet Operation EquivalenceLibrary + SingletonGate Control Flow Operations ----------------------- @@ -366,6 +367,7 @@ # pylint: disable=cyclic-import from .controlledgate import ControlledGate +from .singleton_gate import SingletonGate from .instruction import Instruction from .instructionset import InstructionSet from .operation import Operation diff --git a/qiskit/circuit/add_control.py b/qiskit/circuit/add_control.py index 9a9837673d1a..46fe2de0c68a 100644 --- a/qiskit/circuit/add_control.py +++ b/qiskit/circuit/add_control.py @@ -55,7 +55,9 @@ def add_control( # attempt decomposition operation._define() cgate = control(operation, num_ctrl_qubits=num_ctrl_qubits, label=label, ctrl_state=ctrl_state) - cgate.base_gate.label = operation.label + if operation.label is not None: + cgate.base_gate = cgate.base_gate.to_mutable() + cgate.base_gate.label = operation.label return cgate diff --git a/qiskit/circuit/gate.py b/qiskit/circuit/gate.py index 3bfd40f15999..65ca210b5958 100644 --- a/qiskit/circuit/gate.py +++ b/qiskit/circuit/gate.py @@ -24,7 +24,15 @@ class Gate(Instruction): """Unitary gate.""" - def __init__(self, name: str, num_qubits: int, params: list, label: str | None = None) -> None: + def __init__( + self, + name: str, + num_qubits: int, + params: list, + label: str | None = None, + duration=None, + unit="dt", + ) -> None: """Create a new gate. Args: @@ -34,7 +42,7 @@ def __init__(self, name: str, num_qubits: int, params: list, label: str | None = label: An optional label for the gate. """ self.definition = None - super().__init__(name, num_qubits, 0, params, label=label) + super().__init__(name, num_qubits, 0, params, label=label, duration=duration, unit=unit) # Set higher priority than Numpy array and matrix classes __array_priority__ = 20 diff --git a/qiskit/circuit/instruction.py b/qiskit/circuit/instruction.py index 6b74dacfaab8..d2fe33d8d3b4 100644 --- a/qiskit/circuit/instruction.py +++ b/qiskit/circuit/instruction.py @@ -94,16 +94,43 @@ def __init__(self, name, num_qubits, num_clbits, params, duration=None, unit="dt self._label = label # tuple (ClassicalRegister, int), tuple (Clbit, bool) or tuple (Clbit, int) # when the instruction has a conditional ("if") - self.condition = None + self._condition = None # list of instructions (and their contexts) that this instruction is composed of # empty definition means opaque or fundamental instruction self._definition = None - self._duration = duration self._unit = unit self.params = params # must be at last (other properties may be required for validation) + @property + def mutable(self) -> bool: + """Is this instance is a mutable unique instance or not. + + If this attribute is ``False`` the gate instance is a shared singleton + and is not mutable. + """ + return True + + def to_mutable(self): + """Return a mutable copy of this gate. + + This method will return a new mutable copy of this gate instance. + If a singleton instance is being used this will be a new unique + instance that can be mutated. If the instance is already mutable it + will be a deepcopy of that instance. + """ + return self.copy() + + @property + def condition(self): + """The classical condition on the instruction.""" + return self._condition + + @condition.setter + def condition(self, condition): + self._condition = condition + def __eq__(self, other): """Two instructions are the same if they have the same name, same dimensions, and same params. @@ -409,7 +436,7 @@ def c_if(self, classical, val): # Casting the conditional value as Boolean when # the classical condition is on a classical bit. val = bool(val) - self.condition = (classical, val) + self._condition = (classical, val) return self def copy(self, name=None): diff --git a/qiskit/circuit/instructionset.py b/qiskit/circuit/instructionset.py index 8ea32f445a7d..2b1a3b756de6 100644 --- a/qiskit/circuit/instructionset.py +++ b/qiskit/circuit/instructionset.py @@ -132,7 +132,7 @@ def c_if(self, classical: Clbit | ClassicalRegister | int, val: int) -> "Instruc if self._requester is not None: classical = self._requester(classical) for instruction in self._instructions: - instruction.operation.c_if(classical, val) + instruction.operation = instruction.operation.c_if(classical, val) return self # Legacy support for properties. Added in Terra 0.21 to support the internal switch in diff --git a/qiskit/circuit/library/standard_gates/dcx.py b/qiskit/circuit/library/standard_gates/dcx.py index 836a81ffa4cd..fed5ae3b442d 100644 --- a/qiskit/circuit/library/standard_gates/dcx.py +++ b/qiskit/circuit/library/standard_gates/dcx.py @@ -12,13 +12,13 @@ """Double-CNOT gate.""" -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array @with_gate_array([[1, 0, 0, 0], [0, 0, 0, 1], [0, 1, 0, 0], [0, 0, 1, 0]]) -class DCXGate(Gate): +class DCXGate(SingletonGate): r"""Double-CNOT gate. A 2-qubit Clifford gate consisting of two back-to-back @@ -48,9 +48,14 @@ class DCXGate(Gate): \end{pmatrix} """ - def __init__(self): + def __init__(self, label=None, duration=None, unit=None, _condition=None): """Create new DCX gate.""" - super().__init__("dcx", 2, []) + if unit is None: + unit = "dt" + + super().__init__( + "dcx", 2, [], label=label, _condition=_condition, duration=duration, unit=unit + ) def _define(self): """ diff --git a/qiskit/circuit/library/standard_gates/ecr.py b/qiskit/circuit/library/standard_gates/ecr.py index cc7b13ecc082..abff2072daa1 100644 --- a/qiskit/circuit/library/standard_gates/ecr.py +++ b/qiskit/circuit/library/standard_gates/ecr.py @@ -15,8 +15,8 @@ import numpy as np from qiskit.circuit._utils import with_gate_array -from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister +from qiskit.circuit.singleton_gate import SingletonGate from .rzx import RZXGate from .x import XGate @@ -24,7 +24,7 @@ @with_gate_array( sqrt(0.5) * np.array([[0, 1, 0, 1.0j], [1, 0, -1.0j, 0], [0, 1.0j, 0, 1], [-1.0j, 0, 1, 0]]) ) -class ECRGate(Gate): +class ECRGate(SingletonGate): r"""An echoed cross-resonance gate. This gate is maximally entangling and is equivalent to a CNOT up to @@ -84,9 +84,13 @@ class ECRGate(Gate): \end{pmatrix} """ - def __init__(self): + def __init__(self, label=None, _condition=None, duration=None, unit=None): """Create new ECR gate.""" - super().__init__("ecr", 2, []) + if unit is None: + unit = "dt" + super().__init__( + "ecr", 2, [], label=label, _condition=_condition, duration=duration, unit=unit + ) def _define(self): """ diff --git a/qiskit/circuit/library/standard_gates/h.py b/qiskit/circuit/library/standard_gates/h.py index 539383436a11..7a75b6094010 100644 --- a/qiskit/circuit/library/standard_gates/h.py +++ b/qiskit/circuit/library/standard_gates/h.py @@ -15,7 +15,7 @@ from typing import Optional, Union import numpy from qiskit.circuit.controlledgate import ControlledGate -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array from .t import TGate, TdgGate @@ -25,7 +25,7 @@ @with_gate_array(_H_ARRAY) -class HGate(Gate): +class HGate(SingletonGate): r"""Single-qubit Hadamard gate. This gate is a \pi rotation about the X+Z axis, and has the effect of @@ -54,9 +54,13 @@ class HGate(Gate): \end{pmatrix} """ - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create new H gate.""" - super().__init__("h", 1, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "h", 1, [], label=label, _condition=_condition, duration=duration, unit=unit + ) def _define(self): """ @@ -94,8 +98,7 @@ def control( ControlledGate: controlled version of this gate. """ if num_ctrl_qubits == 1: - gate = CHGate(label=label, ctrl_state=ctrl_state) - gate.base_gate.label = self.label + gate = CHGate(label=label, ctrl_state=ctrl_state, _base_label=self.label) return gate return super().control(num_ctrl_qubits=num_ctrl_qubits, label=label, ctrl_state=ctrl_state) @@ -162,10 +165,21 @@ class CHGate(ControlledGate): \end{pmatrix} """ - def __init__(self, label: Optional[str] = None, ctrl_state: Optional[Union[int, str]] = None): + def __init__( + self, + label: Optional[str] = None, + ctrl_state: Optional[Union[int, str]] = None, + _base_label=None, + ): """Create new CH gate.""" super().__init__( - "ch", 2, [], num_ctrl_qubits=1, label=label, ctrl_state=ctrl_state, base_gate=HGate() + "ch", + 2, + [], + num_ctrl_qubits=1, + label=label, + ctrl_state=ctrl_state, + base_gate=HGate(label=_base_label), ) def _define(self): diff --git a/qiskit/circuit/library/standard_gates/i.py b/qiskit/circuit/library/standard_gates/i.py index b47571290653..b8742665f66c 100644 --- a/qiskit/circuit/library/standard_gates/i.py +++ b/qiskit/circuit/library/standard_gates/i.py @@ -13,12 +13,12 @@ """Identity gate.""" from typing import Optional -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit._utils import with_gate_array @with_gate_array([[1, 0], [0, 1]]) -class IGate(Gate): +class IGate(SingletonGate): r"""Identity gate. Identity gate corresponds to a single-qubit gate wait cycle, @@ -45,9 +45,13 @@ class IGate(Gate): └───┘ """ - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create new Identity gate.""" - super().__init__("id", 1, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "id", 1, [], label=label, _condition=_condition, duration=duration, unit=unit + ) def inverse(self): """Invert this gate.""" diff --git a/qiskit/circuit/library/standard_gates/iswap.py b/qiskit/circuit/library/standard_gates/iswap.py index a0cca1f484c7..018715ef3d68 100644 --- a/qiskit/circuit/library/standard_gates/iswap.py +++ b/qiskit/circuit/library/standard_gates/iswap.py @@ -16,7 +16,7 @@ import numpy as np -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array @@ -24,7 +24,7 @@ @with_gate_array([[1, 0, 0, 0], [0, 0, 1j, 0], [0, 1j, 0, 0], [0, 0, 0, 1]]) -class iSwapGate(Gate): +class iSwapGate(SingletonGate): r"""iSWAP gate. A 2-qubit XX+YY interaction. @@ -85,9 +85,13 @@ class iSwapGate(Gate): \end{pmatrix} """ - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create new iSwap gate.""" - super().__init__("iswap", 2, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "iswap", 2, [], label=label, _condition=_condition, duration=duration, unit=unit + ) def _define(self): """ diff --git a/qiskit/circuit/library/standard_gates/s.py b/qiskit/circuit/library/standard_gates/s.py index 67b3aafbe786..07349eb96350 100644 --- a/qiskit/circuit/library/standard_gates/s.py +++ b/qiskit/circuit/library/standard_gates/s.py @@ -18,7 +18,7 @@ import numpy from qiskit.circuit.controlledgate import ControlledGate -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.library.standard_gates.p import CPhaseGate, PhaseGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array @@ -29,7 +29,7 @@ @with_gate_array(_S_ARRAY) -class SGate(Gate): +class SGate(SingletonGate): r"""Single qubit S gate (Z**0.5). It induces a :math:`\pi/2` phase, and is sometimes called the P gate (phase). @@ -59,9 +59,13 @@ class SGate(Gate): Equivalent to a :math:`\pi/2` radian rotation about the Z axis. """ - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create new S gate.""" - super().__init__("s", 1, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "s", 1, [], label=label, _condition=_condition, duration=duration, unit=unit + ) def _define(self): """ @@ -90,7 +94,7 @@ def power(self, exponent: float): @with_gate_array(_SDG_ARRAY) -class SdgGate(Gate): +class SdgGate(SingletonGate): r"""Single qubit S-adjoint gate (~Z**0.5). It induces a :math:`-\pi/2` phase. @@ -120,9 +124,13 @@ class SdgGate(Gate): Equivalent to a :math:`-\pi/2` radian rotation about the Z axis. """ - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create new Sdg gate.""" - super().__init__("sdg", 1, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "sdg", 1, [], label=label, _condition=_condition, duration=duration, unit=unit + ) def _define(self): """ diff --git a/qiskit/circuit/library/standard_gates/swap.py b/qiskit/circuit/library/standard_gates/swap.py index 578efc3053fb..5f4cc76a87e1 100644 --- a/qiskit/circuit/library/standard_gates/swap.py +++ b/qiskit/circuit/library/standard_gates/swap.py @@ -15,7 +15,7 @@ from typing import Optional, Union import numpy from qiskit.circuit.controlledgate import ControlledGate -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array @@ -24,7 +24,7 @@ @with_gate_array(_SWAP_ARRAY) -class SwapGate(Gate): +class SwapGate(SingletonGate): r"""The SWAP gate. This is a symmetric and Clifford gate. @@ -59,9 +59,13 @@ class SwapGate(Gate): |a, b\rangle \rightarrow |b, a\rangle """ - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create new SWAP gate.""" - super().__init__("swap", 2, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "swap", 2, [], label=label, _condition=_condition, duration=duration, unit=unit + ) def _define(self): """ @@ -103,8 +107,7 @@ def control( ControlledGate: controlled version of this gate. """ if num_ctrl_qubits == 1: - gate = CSwapGate(label=label, ctrl_state=ctrl_state) - gate.base_gate.label = self.label + gate = CSwapGate(label=label, ctrl_state=ctrl_state, _base_label=self.label) return gate return super().control(num_ctrl_qubits=num_ctrl_qubits, label=label, ctrl_state=ctrl_state) @@ -191,7 +194,12 @@ class CSwapGate(ControlledGate): |1, b, c\rangle \rightarrow |1, c, b\rangle """ - def __init__(self, label: Optional[str] = None, ctrl_state: Optional[Union[str, int]] = None): + def __init__( + self, + label: Optional[str] = None, + ctrl_state: Optional[Union[str, int]] = None, + _base_label=None, + ): """Create new CSWAP gate.""" super().__init__( "cswap", @@ -200,7 +208,7 @@ def __init__(self, label: Optional[str] = None, ctrl_state: Optional[Union[str, num_ctrl_qubits=1, label=label, ctrl_state=ctrl_state, - base_gate=SwapGate(), + base_gate=SwapGate(label=_base_label), ) def _define(self): diff --git a/qiskit/circuit/library/standard_gates/sx.py b/qiskit/circuit/library/standard_gates/sx.py index a15b38787cc1..49fbb11c558c 100644 --- a/qiskit/circuit/library/standard_gates/sx.py +++ b/qiskit/circuit/library/standard_gates/sx.py @@ -16,7 +16,7 @@ from typing import Optional, Union import numpy from qiskit.circuit.controlledgate import ControlledGate -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array @@ -26,7 +26,7 @@ @with_gate_array(_SX_ARRAY) -class SXGate(Gate): +class SXGate(SingletonGate): r"""The single-qubit Sqrt(X) gate (:math:`\sqrt{X}`). Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` @@ -64,9 +64,13 @@ class SXGate(Gate): """ - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create new SX gate.""" - super().__init__("sx", 1, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "sx", 1, [], label=label, _condition=_condition, duration=duration, unit=unit + ) def _define(self): """ @@ -108,14 +112,13 @@ def control( ControlledGate: controlled version of this gate. """ if num_ctrl_qubits == 1: - gate = CSXGate(label=label, ctrl_state=ctrl_state) - gate.base_gate.label = self.label + gate = CSXGate(label=label, ctrl_state=ctrl_state, _base_label=self.label) return gate return super().control(num_ctrl_qubits=num_ctrl_qubits, label=label, ctrl_state=ctrl_state) @with_gate_array(_SXDG_ARRAY) -class SXdgGate(Gate): +class SXdgGate(SingletonGate): r"""The inverse single-qubit Sqrt(X) gate. Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` @@ -148,9 +151,13 @@ class SXdgGate(Gate): ) _ARRAY.setflags(write=False) - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create new SXdg gate.""" - super().__init__("sxdg", 1, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "sxdg", 1, [], label=label, _condition=_condition, duration=duration, unit=unit + ) def _define(self): """ @@ -230,10 +237,21 @@ class CSXGate(ControlledGate): """ - def __init__(self, label: Optional[str] = None, ctrl_state: Optional[Union[str, int]] = None): + def __init__( + self, + label: Optional[str] = None, + ctrl_state: Optional[Union[str, int]] = None, + _base_label=None, + ): """Create new CSX gate.""" super().__init__( - "csx", 2, [], num_ctrl_qubits=1, label=label, ctrl_state=ctrl_state, base_gate=SXGate() + "csx", + 2, + [], + num_ctrl_qubits=1, + label=label, + ctrl_state=ctrl_state, + base_gate=SXGate(label=_base_label), ) def _define(self): diff --git a/qiskit/circuit/library/standard_gates/t.py b/qiskit/circuit/library/standard_gates/t.py index 67a14a12e1c5..e81d33773798 100644 --- a/qiskit/circuit/library/standard_gates/t.py +++ b/qiskit/circuit/library/standard_gates/t.py @@ -17,14 +17,14 @@ import numpy -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.library.standard_gates.p import PhaseGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array @with_gate_array([[1, 0], [0, (1 + 1j) / math.sqrt(2)]]) -class TGate(Gate): +class TGate(SingletonGate): r"""Single qubit T gate (Z**0.25). It induces a :math:`\pi/4` phase, and is sometimes called the pi/8 gate @@ -55,9 +55,13 @@ class TGate(Gate): Equivalent to a :math:`\pi/4` radian rotation about the Z axis. """ - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create new T gate.""" - super().__init__("t", 1, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "t", 1, [], label=label, _condition=_condition, duration=duration, unit=unit + ) def _define(self): """ @@ -86,7 +90,7 @@ def power(self, exponent: float): @with_gate_array([[1, 0], [0, (1 - 1j) / math.sqrt(2)]]) -class TdgGate(Gate): +class TdgGate(SingletonGate): r"""Single qubit T-adjoint gate (~Z**0.25). It induces a :math:`-\pi/4` phase. @@ -116,9 +120,13 @@ class TdgGate(Gate): Equivalent to a :math:`-\pi/4` radian rotation about the Z axis. """ - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create new Tdg gate.""" - super().__init__("tdg", 1, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "tdg", 1, [], label=label, _condition=_condition, duration=duration, unit=unit + ) def _define(self): """ diff --git a/qiskit/circuit/library/standard_gates/x.py b/qiskit/circuit/library/standard_gates/x.py index b48181be8062..cc17b8060ebe 100644 --- a/qiskit/circuit/library/standard_gates/x.py +++ b/qiskit/circuit/library/standard_gates/x.py @@ -17,7 +17,7 @@ import numpy from qiskit.utils.deprecation import deprecate_func from qiskit.circuit.controlledgate import ControlledGate -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import _ctrl_state_to_int, with_gate_array, with_controlled_gate_array from .h import HGate @@ -30,7 +30,7 @@ @with_gate_array(_X_ARRAY) -class XGate(Gate): +class XGate(SingletonGate): r"""The single-qubit Pauli-X gate (:math:`\sigma_x`). Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` @@ -76,9 +76,13 @@ class XGate(Gate): |1\rangle \rightarrow |0\rangle """ - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create new X gate.""" - super().__init__("x", 1, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "x", 1, [], label=label, _condition=_condition, duration=duration, unit=unit + ) def _define(self): """ @@ -115,8 +119,12 @@ def control( Returns: ControlledGate: controlled version of this gate. """ - gate = MCXGate(num_ctrl_qubits=num_ctrl_qubits, label=label, ctrl_state=ctrl_state) - gate.base_gate.label = self.label + gate = MCXGate( + num_ctrl_qubits=num_ctrl_qubits, + label=label, + ctrl_state=ctrl_state, + _base_label=self.label, + ) return gate def inverse(self): @@ -188,10 +196,21 @@ class CXGate(ControlledGate): `|a, b\rangle \rightarrow |a, a \oplus b\rangle` """ - def __init__(self, label: Optional[str] = None, ctrl_state: Optional[Union[str, int]] = None): + def __init__( + self, + label: Optional[str] = None, + ctrl_state: Optional[Union[str, int]] = None, + _base_label=None, + ): """Create new CX gate.""" super().__init__( - "cx", 2, [], num_ctrl_qubits=1, label=label, ctrl_state=ctrl_state, base_gate=XGate() + "cx", + 2, + [], + num_ctrl_qubits=1, + label=label, + ctrl_state=ctrl_state, + base_gate=XGate(label=_base_label), ) def control( @@ -213,8 +232,12 @@ def control( """ ctrl_state = _ctrl_state_to_int(ctrl_state, num_ctrl_qubits) new_ctrl_state = (self.ctrl_state << num_ctrl_qubits) | ctrl_state - gate = MCXGate(num_ctrl_qubits=num_ctrl_qubits + 1, label=label, ctrl_state=new_ctrl_state) - gate.base_gate.label = self.label + gate = MCXGate( + num_ctrl_qubits=num_ctrl_qubits + 1, + label=label, + ctrl_state=new_ctrl_state, + _base_label=self.label, + ) return gate def inverse(self): @@ -291,10 +314,21 @@ class CCXGate(ControlledGate): """ - def __init__(self, label: Optional[str] = None, ctrl_state: Optional[Union[str, int]] = None): + def __init__( + self, + label: Optional[str] = None, + ctrl_state: Optional[Union[str, int]] = None, + _base_label=None, + ): """Create new CCX gate.""" super().__init__( - "ccx", 3, [], num_ctrl_qubits=2, label=label, ctrl_state=ctrl_state, base_gate=XGate() + "ccx", + 3, + [], + num_ctrl_qubits=2, + label=label, + ctrl_state=ctrl_state, + base_gate=XGate(label=_base_label), ) def _define(self): @@ -359,8 +393,12 @@ def control( """ ctrl_state = _ctrl_state_to_int(ctrl_state, num_ctrl_qubits) new_ctrl_state = (self.ctrl_state << num_ctrl_qubits) | ctrl_state - gate = MCXGate(num_ctrl_qubits=num_ctrl_qubits + 2, label=label, ctrl_state=new_ctrl_state) - gate.base_gate.label = self.label + gate = MCXGate( + num_ctrl_qubits=num_ctrl_qubits + 2, + label=label, + ctrl_state=new_ctrl_state, + _base_label=self.label, + ) return gate def inverse(self): @@ -380,7 +418,7 @@ def inverse(self): [0, 0, 0, 1j, 0, 0, 0, 0], ] ) -class RCCXGate(Gate): +class RCCXGate(SingletonGate): """The simplified Toffoli gate, also referred to as Margolus gate. The simplified Toffoli gate implements the Toffoli gate up to relative phases. @@ -396,9 +434,13 @@ class RCCXGate(Gate): with the :meth:`~qiskit.circuit.QuantumCircuit.rccx` method. """ - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create a new simplified CCX gate.""" - super().__init__("rccx", 3, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "rccx", 3, [], label=label, _condition=_condition, duration=duration, unit=unit + ) def _define(self): """ @@ -675,7 +717,7 @@ def inverse(self): [0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0], ] ) -class RC3XGate(Gate): +class RC3XGate(SingletonGate): """The simplified 3-controlled Toffoli gate. The simplified Toffoli gate implements the Toffoli gate up to relative phases. @@ -689,9 +731,13 @@ class RC3XGate(Gate): with the :meth:`~qiskit.circuit.QuantumCircuit.rcccx` method. """ - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create a new RC3X gate.""" - super().__init__("rcccx", 4, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "rcccx", 4, [], label=label, _condition=_condition, duration=duration, unit=unit + ) def _define(self): """ @@ -855,6 +901,7 @@ def __new__( num_ctrl_qubits: Optional[int] = None, label: Optional[str] = None, ctrl_state: Optional[Union[str, int]] = None, + _base_label=None, ): """Create a new MCX instance. @@ -866,9 +913,11 @@ def __new__( explicit: dict[int, Type[ControlledGate]] = {1: CXGate, 2: CCXGate} if num_ctrl_qubits in explicit: gate_class = explicit[num_ctrl_qubits] - gate = gate_class.__new__(gate_class, label=label, ctrl_state=ctrl_state) + gate = gate_class.__new__( + gate_class, label=label, ctrl_state=ctrl_state, _base_label=_base_label + ) # if __new__ does not return the same type as cls, init is not called - gate.__init__(label=label, ctrl_state=ctrl_state) + gate.__init__(label=label, ctrl_state=ctrl_state, _base_label=_base_label) return gate return super().__new__(cls) @@ -878,6 +927,7 @@ def __init__( label: Optional[str] = None, ctrl_state: Optional[Union[str, int]] = None, _name="mcx", + _base_label=None, ): """Create new MCX gate.""" num_ancilla_qubits = self.__class__.get_num_ancilla_qubits(num_ctrl_qubits) @@ -888,7 +938,7 @@ def __init__( num_ctrl_qubits=num_ctrl_qubits, label=label, ctrl_state=ctrl_state, - base_gate=XGate(), + base_gate=XGate(label=_base_label), ) def inverse(self): @@ -963,6 +1013,7 @@ def __new__( num_ctrl_qubits: Optional[int] = None, label: Optional[str] = None, ctrl_state: Optional[Union[str, int]] = None, + _base_label=None, ): """Create a new MCXGrayCode instance""" # if 1 to 4 control qubits, create explicit gates diff --git a/qiskit/circuit/library/standard_gates/y.py b/qiskit/circuit/library/standard_gates/y.py index 65be3087c8bb..b4e9509903e5 100644 --- a/qiskit/circuit/library/standard_gates/y.py +++ b/qiskit/circuit/library/standard_gates/y.py @@ -17,7 +17,7 @@ # pylint: disable=cyclic-import from qiskit.circuit.controlledgate import ControlledGate -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array @@ -25,7 +25,7 @@ @with_gate_array(_Y_ARRAY) -class YGate(Gate): +class YGate(SingletonGate): r"""The single-qubit Pauli-Y gate (:math:`\sigma_y`). Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` @@ -71,9 +71,13 @@ class YGate(Gate): |1\rangle \rightarrow -i|0\rangle """ - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create new Y gate.""" - super().__init__("y", 1, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "y", 1, [], label=label, _condition=_condition, duration=duration, unit=unit + ) def _define(self): # pylint: disable=cyclic-import @@ -108,8 +112,7 @@ def control( ControlledGate: controlled version of this gate. """ if num_ctrl_qubits == 1: - gate = CYGate(label=label, ctrl_state=ctrl_state) - gate.base_gate.label = self.label + gate = CYGate(label=label, ctrl_state=ctrl_state, _base_label=self.label) return gate return super().control(num_ctrl_qubits=num_ctrl_qubits, label=label, ctrl_state=ctrl_state) @@ -175,10 +178,21 @@ class CYGate(ControlledGate): """ - def __init__(self, label: Optional[str] = None, ctrl_state: Optional[Union[str, int]] = None): + def __init__( + self, + label: Optional[str] = None, + ctrl_state: Optional[Union[str, int]] = None, + _base_label=None, + ): """Create new CY gate.""" super().__init__( - "cy", 2, [], num_ctrl_qubits=1, label=label, ctrl_state=ctrl_state, base_gate=YGate() + "cy", + 2, + [], + num_ctrl_qubits=1, + label=label, + ctrl_state=ctrl_state, + base_gate=YGate(label=_base_label), ) def _define(self): diff --git a/qiskit/circuit/library/standard_gates/z.py b/qiskit/circuit/library/standard_gates/z.py index 978688d1908c..9d8ba19d5fb1 100644 --- a/qiskit/circuit/library/standard_gates/z.py +++ b/qiskit/circuit/library/standard_gates/z.py @@ -19,7 +19,7 @@ from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array from qiskit.circuit.controlledgate import ControlledGate -from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.quantumregister import QuantumRegister from .p import PhaseGate @@ -28,7 +28,7 @@ @with_gate_array(_Z_ARRAY) -class ZGate(Gate): +class ZGate(SingletonGate): r"""The single-qubit Pauli-Z gate (:math:`\sigma_z`). Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` @@ -74,9 +74,13 @@ class ZGate(Gate): |1\rangle \rightarrow -|1\rangle """ - def __init__(self, label: Optional[str] = None): + def __init__(self, label: Optional[str] = None, duration=None, unit=None, _condition=None): """Create new Z gate.""" - super().__init__("z", 1, [], label=label) + if unit is None: + unit = "dt" + super().__init__( + "z", 1, [], label=label, _condition=_condition, duration=duration, unit=unit + ) def _define(self): # pylint: disable=cyclic-import @@ -112,8 +116,7 @@ def control( ControlledGate: controlled version of this gate. """ if num_ctrl_qubits == 1: - gate = CZGate(label=label, ctrl_state=ctrl_state) - gate.base_gate.label = self.label + gate = CZGate(label=label, ctrl_state=ctrl_state, _base_label=self.label) return gate return super().control(num_ctrl_qubits=num_ctrl_qubits, label=label, ctrl_state=ctrl_state) @@ -160,10 +163,21 @@ class CZGate(ControlledGate): the target qubit if the control qubit is in the :math:`|1\rangle` state. """ - def __init__(self, label: Optional[str] = None, ctrl_state: Optional[Union[str, int]] = None): + def __init__( + self, + label: Optional[str] = None, + ctrl_state: Optional[Union[str, int]] = None, + _base_label=None, + ): """Create new CZ gate.""" super().__init__( - "cz", 2, [], label=label, num_ctrl_qubits=1, ctrl_state=ctrl_state, base_gate=ZGate() + "cz", + 2, + [], + label=label, + num_ctrl_qubits=1, + ctrl_state=ctrl_state, + base_gate=ZGate(label=_base_label), ) def _define(self): diff --git a/qiskit/circuit/random/utils.py b/qiskit/circuit/random/utils.py index b8ceb36496a7..d8c863a81d6f 100644 --- a/qiskit/circuit/random/utils.py +++ b/qiskit/circuit/random/utils.py @@ -193,7 +193,7 @@ def random_circuit( if is_cond: qc.measure(qc.qubits, cr) # The condition values are required to be bigints, not Numpy's fixed-width type. - operation.condition = (cr, int(condition_values[c_ptr])) + operation = operation.c_if(cr, int(condition_values[c_ptr])) c_ptr += 1 qc._append(CircuitInstruction(operation=operation, qubits=qubits[q_start:q_end])) else: diff --git a/qiskit/circuit/singleton_gate.py b/qiskit/circuit/singleton_gate.py new file mode 100644 index 000000000000..3d24247e31ca --- /dev/null +++ b/qiskit/circuit/singleton_gate.py @@ -0,0 +1,188 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023 +# +# 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. +""" +Singleton gate classes. +""" +import copy + +from qiskit.circuit.gate import Gate +from qiskit.circuit.classicalregister import ClassicalRegister, Clbit +from qiskit.circuit.exceptions import CircuitError + + +SINGLETONGATE_ATTR_SET = frozenset( + ( + "definition", + "unit", + "duration", + "condition", + "label", + "_label", + "_condition", + "_duration", + "_unit", + "_definition", + "_name", + "_num_qubits", + "_num_clbits", + "_params", + "params", + ) +) + + +class SingletonGate(Gate): + """A base class to use for Gate objects that by default are singleton instances + + This class should be used for gate classes that have fixed definitions and + do not contain any unique state. The canonical example of something like + this is :class:`~.HGate` which has an immutable definition and any + instance of :class:`~.HGate` is the same. Using singleton gates + as a base class for these types of gate classes provides a large + advantage in the memory footprint of multiple gates. + + The exception to be aware of with this class though are the :class:`~.Gate` + attributes :attr:`.label`, :attr:`.condition`, :attr:`.duration`, and + :attr:`.unit` which can be set differently for specific instances of gates. + For :class:`~.SingletonGate` usage to be sound setting these attributes + is not available and they can only be set at creation time. If any of these + attributes are used, then instead of using a single shared global instance + of the same gate a new separate instance will be created. + """ + + _instance = None + + def __new__(cls, *args, **kwargs): + if args or ( # pylint: disable=too-many-boolean-expressions + kwargs + and ( + "label" in kwargs + or "_condition" in kwargs + or "duration" in kwargs + or "unit" in kwargs + ) + ): + return super().__new__(cls) + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self, *args, _condition=None, **kwargs): + super().__init__(*args, **kwargs) + self._condition = _condition + + def c_if(self, classical, val): + if not isinstance(classical, (ClassicalRegister, Clbit)): + raise CircuitError("c_if must be used with a classical register or classical bit") + if val < 0: + raise CircuitError("condition value should be non-negative") + if isinstance(classical, Clbit): + # Casting the conditional value as Boolean when + # the classical condition is on a classical bit. + val = bool(val) + instance = type(self)( + label=self.label, _condition=(classical, val), duration=self.duration, unit=self.unit + ) + return instance + + @property + def mutable(self) -> bool: + return self is not self._instance + + def to_mutable(self): + if not self.mutable: + instance = super().__new__(type(self)) + # Coming from a shared singleton none of the arguments to + # __init__ can be set, so this is the correct behavior for + # initializing a new mutable instance + instance.__init__() + return instance + else: + return copy.deepcopy(self) + + @property + def label(self) -> str: + return self._label + + @label.setter + def label(self, label: str): + if self is self._instance: + raise NotImplementedError( + f"This gate class {type(self)} does not support manually setting a " + "label on an instance. Instead you must set the label when instantiating a new object." + ) + self._label = label + + @property + def condition(self): + return self._condition + + @condition.setter + def condition(self, condition): + if self is self._instance: + raise NotImplementedError( + f"This gate class {type(self)} does not support manually setting a " + "condition on an instance. Instead you must set the label when instantiating a new " + "object or via the .c_if() method" + ) + self._condition = condition + + @property + def duration(self): + return self._duration + + @duration.setter + def duration(self, duration): + if self is self._instance: + raise NotImplementedError( + f"This gate class {type(self)} does not support manually setting a " + "duration on an instance. Instead you must set the duration when instantiating a " + "new object." + ) + self._duration = duration + + @property + def unit(self): + return self._unit + + @unit.setter + def unit(self, unit): + if self is self._instance: + raise NotImplementedError( + f"This gate class {type(self)} does not support manually setting a " + "unit on an instance. Instead you must set the unit when instantiating a " + "new object." + ) + self._unit = unit + + def __deepcopy__(self, _memo=None): + if not self.mutable: + return self + else: + return type(self)( + label=self.label, _condition=self.condition, duration=self.duration, unit=self.unit + ) + + def __setattr__(self, name, value): + if self.mutable: + super().__setattr__(name, value) + else: + if name not in SINGLETONGATE_ATTR_SET: + raise NotImplementedError( + "Setting custom attributes is not allowed on a singleton gate" + ) + super().__setattr__(name, value) + + def copy(self, name=None): + if not self.mutable and name is None: + return self + return super().copy(name=name) diff --git a/qiskit/converters/ast_to_dag.py b/qiskit/converters/ast_to_dag.py index 76541d8c89df..9fb3361d9555 100644 --- a/qiskit/converters/ast_to_dag.py +++ b/qiskit/converters/ast_to_dag.py @@ -234,7 +234,8 @@ def _process_cnot(self, node): maxidx = max([len(id0), len(id1)]) for idx in range(maxidx): cx_gate = std.CXGate() - cx_gate.condition = self.condition + if self.condition: + cx_gate = cx_gate.c_if(*self.condition) if len(id0) > 1 and len(id1) > 1: self.dag.apply_operation_back(cx_gate, [id0[idx], id1[idx]], [], check=False) elif len(id0) > 1: @@ -252,7 +253,8 @@ def _process_measure(self, node): ) for idx, idy in zip(id0, id1): meas_gate = Measure() - meas_gate.condition = self.condition + if self.condition: + meas_gate = meas_gate.c_if(*self.condition) self.dag.apply_operation_back(meas_gate, [idx], [idy], check=False) def _process_if(self, node): @@ -341,7 +343,8 @@ def _process_node(self, node): id0 = self._process_bit_id(node.children[0]) for i, _ in enumerate(id0): reset = Reset() - reset.condition = self.condition + if self.condition: + reset = reset.c_if(*self.condition) self.dag.apply_operation_back(reset, [id0[i]], [], check=False) elif node.type == "if": @@ -398,7 +401,8 @@ def _create_dag_op(self, name, params, qargs): QiskitError: if encountering a non-basis opaque gate """ op = self._create_op(name, params) - op.condition = self.condition + if self.condition: + op = op.c_if(*self.condition) self.dag.apply_operation_back(op, qargs, [], check=False) def _create_op(self, name, params): diff --git a/qiskit/converters/circuit_to_instruction.py b/qiskit/converters/circuit_to_instruction.py index 7965e3a474bc..793362281b5e 100644 --- a/qiskit/converters/circuit_to_instruction.py +++ b/qiskit/converters/circuit_to_instruction.py @@ -81,7 +81,7 @@ def circuit_to_instruction(circuit, parameter_map=None, equivalence_library=None params=[*parameter_dict.values()], label=label, ) - out_instruction.condition = None + out_instruction._condition = None target = circuit.assign_parameters(parameter_dict, inplace=False) @@ -114,9 +114,9 @@ def circuit_to_instruction(circuit, parameter_map=None, equivalence_library=None if condition: reg, val = condition if isinstance(reg, Clbit): - rule.operation.condition = (clbit_map[reg], val) + rule.operation = rule.operation.c_if(clbit_map[reg], val) elif reg.size == c.size: - rule.operation.condition = (c, val) + rule.operation = rule.operation.c_if(c, val) else: raise QiskitError( "Cannot convert condition in circuit with " diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index f60223698e02..e11b218efba7 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -833,7 +833,10 @@ def _reject_new_register(reg): m_cargs = [edge_map.get(x, x) for x in nd.cargs] op = nd.op.copy() if (condition := getattr(op, "condition", None)) is not None: - op.condition = variable_mapper.map_condition(condition, allow_reorder=True) + if not isinstance(op, ControlFlowOp): + op = op.c_if(*variable_mapper.map_condition(condition, allow_reorder=True)) + else: + op.condition = variable_mapper.map_condition(condition, allow_reorder=True) elif isinstance(op, SwitchCaseOp): op.target = variable_mapper.map_target(op.target) dag.apply_operation_back(op, m_qargs, m_cargs, check=False) @@ -1272,7 +1275,11 @@ def substitute_node_with_dag(self, node, input_dag, wires=None, propagate_condit "cannot propagate a condition to an element that acts on those bits" ) new_op = copy.copy(in_node.op) - new_op.condition = new_condition + if new_condition: + if not isinstance(new_op, ControlFlowOp): + new_op = new_op.c_if(*new_condition) + else: + new_op.condition = new_condition in_dag.apply_operation_back(new_op, in_node.qargs, in_node.cargs, check=False) else: in_dag = input_dag @@ -1356,8 +1363,13 @@ def edge_weight_map(wire): label=old_node.op.label, ) elif getattr(old_node.op, "condition", None) is not None: - m_op = copy.copy(old_node.op) - m_op.condition = variable_mapper.map_condition(m_op.condition) + m_op = old_node.op + if not isinstance(old_node.op, ControlFlowOp): + new_condition = variable_mapper.map_condition(m_op.condition) + if new_condition is not None: + m_op = m_op.c_if(*new_condition) + else: + m_op.condition = variable_mapper.map_condition(m_op.condition) else: m_op = old_node.op m_qargs = [wire_map[x] for x in old_node.qargs] @@ -1430,7 +1442,10 @@ def substitute_node(self, node, op, inplace=False, propagate_condition=True): if (old_condition := getattr(node.op, "condition", None)) is not None: if not isinstance(op, Instruction): raise DAGCircuitError("Cannot add a condition on a generic Operation.") - op.condition = old_condition + if not isinstance(node.op, ControlFlowOp): + op = op.c_if(*old_condition) + else: + op.condition = old_condition new_wires.update(condition_resources(old_condition).clbits) if new_wires != current_wires: diff --git a/qiskit/qasm2/parse.py b/qiskit/qasm2/parse.py index 6f56d31b0a02..116c6b7c9aa0 100644 --- a/qiskit/qasm2/parse.py +++ b/qiskit/qasm2/parse.py @@ -227,8 +227,7 @@ def from_bytecode(bytecode, custom_instructions: Iterable[CustomInstruction]): ) elif opcode == OpCode.ConditionedGate: gate_id, parameters, op_qubits, creg, value = op.operands - gate = gates[gate_id](*parameters) - gate.condition = (qc.cregs[creg], value) + gate = gates[gate_id](*parameters).c_if(qc.cregs[creg], value) qc._append(CircuitInstruction(gate, [qubits[q] for q in op_qubits])) elif opcode == OpCode.Measure: qubit, clbit = op.operands diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index 5266bbb0346b..91fe62f3e368 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -25,10 +25,11 @@ from qiskit import circuit as circuit_mod from qiskit import extensions -from qiskit.circuit import library, controlflow, CircuitInstruction +from qiskit.circuit import library, controlflow, CircuitInstruction, ControlFlowOp from qiskit.circuit.classical import expr from qiskit.circuit.classicalregister import ClassicalRegister, Clbit from qiskit.circuit.gate import Gate +from qiskit.circuit.singleton_gate import SingletonGate from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.instruction import Instruction from qiskit.circuit.quantumcircuit import QuantumCircuit @@ -272,8 +273,10 @@ def _read_instruction(file_obj, circuit, registers, custom_operations, version, else: raise AttributeError("Invalid instruction type: %s" % gate_name) + if instruction.label_size <= 0: + label = None if gate_name in {"IfElseOp", "WhileLoopOp"}: - gate = gate_class(condition, *params) + gate = gate_class(condition, *params, label=label) elif version >= 5 and issubclass(gate_class, ControlledGate): if gate_name in { "MCPhaseGate", @@ -283,9 +286,9 @@ def _read_instruction(file_obj, circuit, registers, custom_operations, version, "MCXRecursive", "MCXVChain", }: - gate = gate_class(*params, instruction.num_ctrl_qubits) + gate = gate_class(*params, instruction.num_ctrl_qubits, label=label) else: - gate = gate_class(*params) + gate = gate_class(*params, label=label) gate.num_ctrl_qubits = instruction.num_ctrl_qubits gate.ctrl_state = instruction.ctrl_state gate.condition = condition @@ -304,10 +307,19 @@ def _read_instruction(file_obj, circuit, registers, custom_operations, version, params = [len(qargs)] elif gate_name in {"BreakLoopOp", "ContinueLoopOp"}: params = [len(qargs), len(cargs)] - gate = gate_class(*params) - gate.condition = condition - if instruction.label_size > 0: - gate.label = label + if label is not None: + if issubclass(gate_class, SingletonGate): + gate = gate_class(*params, label=label) + else: + gate = gate_class(*params) + gate.label = label + else: + gate = gate_class(*params) + if condition: + if not isinstance(gate, ControlFlowOp): + gate = gate.c_if(*condition) + else: + gate.condition = condition if circuit is None: return gate if not isinstance(gate, Instruction): diff --git a/qiskit/transpiler/passes/optimization/reset_after_measure_simplification.py b/qiskit/transpiler/passes/optimization/reset_after_measure_simplification.py index 4445e878ce6b..d49485784026 100644 --- a/qiskit/transpiler/passes/optimization/reset_after_measure_simplification.py +++ b/qiskit/transpiler/passes/optimization/reset_after_measure_simplification.py @@ -36,8 +36,7 @@ def run(self, dag): for node in dag.op_nodes(Measure): succ = next(dag.quantum_successors(node)) if isinstance(succ, DAGOpNode) and isinstance(succ.op, Reset): - new_x = XGate() - new_x.condition = (node.cargs[0], 1) + new_x = XGate().c_if(node.cargs[0], 1) new_dag = DAGCircuit() new_dag.add_qubits(node.qargs) new_dag.add_clbits(node.cargs) diff --git a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py index 7e5245b2e3fc..bc606a0f7161 100644 --- a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py @@ -192,8 +192,11 @@ def run(self, dag): index_sequence_duration_map = {} for physical_qubit in self._qubits: dd_sequence_duration = 0 - for gate in self._dd_sequence: + for index, gate in enumerate(self._dd_sequence): + gate = gate.to_mutable() + self._dd_sequence[index] = gate gate.duration = self._durations.get(gate, physical_qubit) + dd_sequence_duration += gate.duration index_sequence_duration_map[physical_qubit] = dd_sequence_duration diff --git a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py index fb33361a4959..2d4114f3cfc7 100644 --- a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py @@ -224,7 +224,7 @@ def _pre_runhook(self, dag: DAGCircuit): continue sequence_lengths = [] - for gate in self._dd_sequence: + for index, gate in enumerate(self._dd_sequence): try: # Check calibration. params = self._resolve_params(gate) @@ -246,6 +246,8 @@ def _pre_runhook(self, dag: DAGCircuit): gate_length = self._durations.get(gate, physical_index) sequence_lengths.append(gate_length) # Update gate duration. This is necessary for current timeline drawer, i.e. scheduled. + gate = gate.to_mutable() + self._dd_sequence[index] = gate gate.duration = gate_length self._dd_sequence_lengths[qubit] = sequence_lengths diff --git a/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py index f83aed800009..3792a149fd71 100644 --- a/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py +++ b/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py @@ -70,6 +70,7 @@ def _get_node_duration( duration = dag.calibrations[node.op.name][cal_key].duration # Note that node duration is updated (but this is analysis pass) + node.op = node.op.to_mutable() node.op.duration = duration else: duration = node.op.duration diff --git a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py index a176f0aa5c9a..c75e22f285b8 100644 --- a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py +++ b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py @@ -96,13 +96,14 @@ def run(self, dag: DAGCircuit): # Make units consistent for node in dag.op_nodes(): try: - node.op = node.op.copy() - node.op.duration = self.inst_durations.get( + duration = self.inst_durations.get( node.op, [dag.find_bit(qarg).index for qarg in node.qargs], unit=time_unit ) - node.op.unit = time_unit except TranspilerError: - pass + continue + node.op = node.op.to_mutable() + node.op.duration = duration + node.op.unit = time_unit self.property_set["time_unit"] = time_unit return dag diff --git a/releasenotes/notes/singletons-83782de8bd062cbc.yaml b/releasenotes/notes/singletons-83782de8bd062cbc.yaml new file mode 100644 index 000000000000..db6b761a392a --- /dev/null +++ b/releasenotes/notes/singletons-83782de8bd062cbc.yaml @@ -0,0 +1,122 @@ +--- +features: + - | + Introduced a new class :class:`~.SingletonGate` which is a subclass of + :class:`~.Gate` that uses a single instance for all objects of that type. + The intent behind this class is to minimize the memory and construction + overhead of using multiple gates in a circuit with the tradeoff of having + global shared state. For this reason this class is only applicable to + gates that do not have any unique and/or mutable state stored in an instance. + For example, the best example of this is :class:`.XGate` doesn't contain + any state and could leveerage :class:`~.SingletonGate` (and does starting in + this release), while :class:`~.RXGate` stores an angle parameter in an instance + and thus can not use :class:`~.SingletonGate` because a single shared global + instance can not represent the parameter values. + + The other potential issue to be aware of when using this class is around the + use of the :class:`~.SingletonGate` class is that the :class:`~.Gate` + data model supports some mutable state. Specifically, the + :attr:`~.Gate.label`, :attr:`~.Gate.duration`, :attr:`~.Gate.unit`, and + :attr:`~.Gate.condition` attributes are all accessible and mutable in the + :class:`~.Gate` and its direct subclasses. However, this is incompatible + with having a shared object via :class:`~.SingletonGate`. For instances of + :class:`~.SingletonGate` setting these attributes directly is not allowed + and it will raise an exception. If they are needed for a particular + instance you must set them on the constructor (or via + :meth:`~.SingletonGate.c_if` for :attr:`~.SingletonGate.condition`) when + creating a new object. When this is done the output from the constructor + will be a separate instance with the custom state instead of the globally + shared instance. You can also use the :meth:`.SingletonGate.to_mutable` + method to get a mutable copy of a gate object and then mutate the attributes + like you would on any other :class:`~.circuit.Instruction` object. + - | + The following standard library gates are now instances of + :class:`~.SingletonGate`: + + * :class:`~.DCXGate` + * :class:`~.ECRGate` + * :class:`~.HGate` + * :class:`~.IGate` + * :class:`~.iSwapGate` + * :class:`~.SGate` + * :class:`~.SdgGate` + * :class:`~.SwapGate` + * :class:`~.SXGate` + * :class:`~.SXdgGate` + * :class:`~.TGate` + * :class:`~.TdgGate` + * :class:`~.XGate` + * :class:`~.RCCXGate` + * :class:`~.RC3XGate` + * :class:`~.YGate` + * :class:`~.ZGate` + + This means that unless a ``label``, ``condition``, ``duration``, or ``unit`` + are set on the instance at creation time they will all share a single global + instance whenever a new gate object is created. This results in large reduction + in the memory overhead for > 1 object of these types and significantly faster + object construction time. + - | + Added a new method :meth`.Instruction.to_mutable` and attribute + :attr:`.Instruction.mutable` which is used to get a mutable copy and check whether + an :class:`~.circuit.Instruction` object is mutable. With the introduction + of :class:`~.SingletonGate` these methods can be used to have a unified interface + to deal with the mutablitiy of instruction objects. +upgrade: + - | + The following standard library gates: + + * :class:`~.DCXGate` + * :class:`~.ECRGate` + * :class:`~.HGate` + * :class:`~.IGate` + * :class:`~.iSwapGate` + * :class:`~.SGate` + * :class:`~.SdgGate` + * :class:`~.SwapGate` + * :class:`~.SXGate` + * :class:`~.SXdgGate` + * :class:`~.TGate` + * :class:`~.TdgGate` + * :class:`~.XGate` + * :class:`~.RCCXGate` + * :class:`~.RC3XGate` + * :class:`~.YGate` + * :class:`~.ZGate` + + no longer are able to set :attr:`~.Gate.label`, :attr:`~.Gate.condition`, + :attr:`~.Gate.duration`, or :attr:`~.Gate.unit` after instantiating an object + anymore. You will now only be able to set these attributes as arguments + when creating a new object or in the case of :attr:`~.Gate.condtion` through + the use :meth:`~.Gate.c_if`. Alternatively you can use :meth:`~.Gate.to_mutable` + to get a mutable copy of the instruction and then use the setter on that copy + instead of the original object. This change was necssary as part of converting + these classes to be :class:`~.SingletonGate` types which greatly reduces the + memory footprint of repeated instances of these gates. + - | + For anything that interacts with :class:`~.Gate`, :class:`~.Operation`, + or :class:`~.circuit.Instruction` objects or works with these as part of a + :class:`~.QuantumCircuit` or :class:`~.DAGCircuit` classes it is important + to note that the use of shared references for instances is much more common + now. Previously, it was possible to reuse and share an instance of a + a circuit operation it wasn't very commonly used and a copy would generate + a unique instance. This has changed starting in this release because of + :class:`~.SingletonGate` being made available (and a large number of standard + library gates now built off of it). If your usage of these objects is assuming + unique instances for every circuit operation there are potential issue because + of shared state that will be reused between operations of the same type (that + will persist through copy and deep copies). You can rely on the + :attr:`.Instruction.mutable` attribute to check the mutability of an object or + use :meth:`.Instruction.to_mutable` to get a mutable copy of any instruction. +fixes: + - | + Fixed an oversight in the :class:`~.ECRGate` that prevented setting an + :attr:`.ECRGate.label` attribute at object construction time. All other + :class:`~.Gate` classes and subclasses enable setting a ``label`` keyword + argument in the constructor. + - | + Fixed an oversight in the :class:`~.Gate` (and all its subclasses) constructor + where the :attr:`.Gate.duration` and :attr:`.Gate.unit` attributes could not + be set as keyword arguments during construction. The parent class + :class:`~.circuit.Instruction` supported setting this but :class:`~.Gate` was + previously not exposing this interface correctly. diff --git a/test/python/circuit/gate_utils.py b/test/python/circuit/gate_utils.py index 83e884bb724b..557059c32fb4 100644 --- a/test/python/circuit/gate_utils.py +++ b/test/python/circuit/gate_utils.py @@ -25,7 +25,7 @@ def _get_free_params(fun, ignore=None): Returns: list[str]: The name of the free parameters not listed in ``ignore``. """ - ignore = ignore or [] + ignore = ignore or ["kwargs"] free_params = [] for name, param in signature(fun).parameters.items(): if param.default == Parameter.empty and param.kind != Parameter.VAR_POSITIONAL: diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 1e2f5263ce34..825e46a8ddbc 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -609,8 +609,7 @@ def test_custom_instruction_with_noop_definition(self): def test_standard_gate_with_label(self): """Test a standard gate with a label.""" qc = QuantumCircuit(1) - gate = XGate() - gate.label = "My special X gate" + gate = XGate(label="My special X gate") qc.append(gate, [0]) qpy_file = io.BytesIO() dump(qc, qpy_file) diff --git a/test/python/circuit/test_gate_definitions.py b/test/python/circuit/test_gate_definitions.py index 0c41189fc954..9589a68c7614 100644 --- a/test/python/circuit/test_gate_definitions.py +++ b/test/python/circuit/test_gate_definitions.py @@ -282,6 +282,7 @@ class TestGateEquivalenceEqual(QiskitTestCase): "PermutationGate", "Commuting2qBlock", "PauliEvolutionGate", + "SingletonGate", "_U0Gate", "_DefinedGate", } diff --git a/test/python/circuit/test_instructions.py b/test/python/circuit/test_instructions.py index 8c8e9866050f..f5f5e14d5130 100644 --- a/test/python/circuit/test_instructions.py +++ b/test/python/circuit/test_instructions.py @@ -10,6 +10,8 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. +# pylint: disable=unsubscriptable-object + """Test Qiskit's Instruction class.""" import unittest.mock @@ -22,6 +24,7 @@ from qiskit.circuit import QuantumCircuit from qiskit.circuit import QuantumRegister, ClassicalRegister, Qubit, Clbit from qiskit.circuit.library.standard_gates.h import HGate +from qiskit.circuit.library.standard_gates.rz import RZGate from qiskit.circuit.library.standard_gates.x import CXGate from qiskit.circuit.library.standard_gates.s import SGate from qiskit.circuit.library.standard_gates.t import TGate @@ -539,21 +542,21 @@ def test_instructionset_c_if_with_no_requester(self): arbitrary :obj:`.Clbit` and `:obj:`.ClassicalRegister` instances, but rejects integers.""" with self.subTest("accepts arbitrary register"): - instruction = HGate() + instruction = RZGate(0) instructions = InstructionSet() instructions.add(instruction, [Qubit()], []) register = ClassicalRegister(2) instructions.c_if(register, 0) self.assertIs(instruction.condition[0], register) with self.subTest("accepts arbitrary bit"): - instruction = HGate() + instruction = RZGate(0) instructions = InstructionSet() instructions.add(instruction, [Qubit()], []) bit = Clbit() instructions.c_if(bit, 0) self.assertIs(instruction.condition[0], bit) with self.subTest("rejects index"): - instruction = HGate() + instruction = RZGate(0) instructions = InstructionSet() instructions.add(instruction, [Qubit()], []) with self.assertRaisesRegex(CircuitError, r"Cannot pass an index as a condition .*"): @@ -578,7 +581,7 @@ def dummy_requester(specifier): with self.subTest("calls requester with bit"): dummy_requester.reset_mock() - instruction = HGate() + instruction = RZGate(0) instructions = InstructionSet(resource_requester=dummy_requester) instructions.add(instruction, [Qubit()], []) bit = Clbit() @@ -587,7 +590,7 @@ def dummy_requester(specifier): self.assertIs(instruction.condition[0], sentinel_bit) with self.subTest("calls requester with index"): dummy_requester.reset_mock() - instruction = HGate() + instruction = RZGate(0) instructions = InstructionSet(resource_requester=dummy_requester) instructions.add(instruction, [Qubit()], []) index = 0 @@ -596,7 +599,7 @@ def dummy_requester(specifier): self.assertIs(instruction.condition[0], sentinel_bit) with self.subTest("calls requester with register"): dummy_requester.reset_mock() - instruction = HGate() + instruction = RZGate(0) instructions = InstructionSet(resource_requester=dummy_requester) instructions.add(instruction, [Qubit()], []) register = ClassicalRegister(2) @@ -605,7 +608,7 @@ def dummy_requester(specifier): self.assertIs(instruction.condition[0], sentinel_register) with self.subTest("calls requester only once when broadcast"): dummy_requester.reset_mock() - instruction_list = [HGate(), HGate(), HGate()] + instruction_list = [RZGate(0), RZGate(0), RZGate(0)] instructions = InstructionSet(resource_requester=dummy_requester) for instruction in instruction_list: instructions.add(instruction, [Qubit()], []) @@ -625,7 +628,7 @@ def test_label_type_enforcement(self): Instruction("h", 1, 0, [], label=0) with self.subTest("raises when a non-string label is provided to setter"): with self.assertRaisesRegex(TypeError, r"label expects a string or None"): - instruction = HGate() + instruction = RZGate(0) instruction.label = 0 def test_deprecation_warnings_qasm_methods(self): diff --git a/test/python/circuit/test_singleton_gate.py b/test/python/circuit/test_singleton_gate.py new file mode 100644 index 000000000000..d8c80661d675 --- /dev/null +++ b/test/python/circuit/test_singleton_gate.py @@ -0,0 +1,253 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# 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. + +# pylint: disable=missing-function-docstring + + +""" +Tests for singleton gate behavior +""" + +import copy + +from qiskit.circuit.library import HGate, SXGate +from qiskit.circuit import Clbit, QuantumCircuit, QuantumRegister, ClassicalRegister +from qiskit.converters import dag_to_circuit, circuit_to_dag + +from qiskit.test.base import QiskitTestCase + + +class TestSingletonGate(QiskitTestCase): + """Qiskit SingletonGate tests.""" + + def test_default_singleton(self): + gate = HGate() + new_gate = HGate() + self.assertIs(gate, new_gate) + + def test_label_not_singleton(self): + gate = HGate() + label_gate = HGate(label="special") + self.assertIsNot(gate, label_gate) + + def test_condition_not_singleton(self): + gate = HGate() + condition_gate = HGate().c_if(Clbit(), 0) + self.assertIsNot(gate, condition_gate) + + def test_raise_on_state_mutation(self): + gate = HGate() + with self.assertRaises(NotImplementedError): + gate.label = "foo" + with self.assertRaises(NotImplementedError): + gate.condition = (Clbit(), 0) + + def test_labeled_condition(self): + singleton_gate = HGate() + clbit = Clbit() + gate = HGate(label="conditionally special").c_if(clbit, 0) + self.assertIsNot(singleton_gate, gate) + self.assertEqual(gate.label, "conditionally special") + self.assertEqual(gate.condition, (clbit, 0)) + + def test_default_singleton_copy(self): + gate = HGate() + copied = gate.copy() + self.assertIs(gate, copied) + + def test_label_copy(self): + gate = HGate(label="special") + copied = gate.copy() + self.assertIsNot(gate, copied) + self.assertEqual(gate, copied) + + def test_label_copy_new(self): + gate = HGate() + label_gate = HGate(label="special") + self.assertIsNot(gate, label_gate) + self.assertNotEqual(gate.label, label_gate.label) + copied = gate.copy() + copied_label = label_gate.copy() + self.assertIs(gate, copied) + self.assertIsNot(copied, label_gate) + self.assertIsNot(copied_label, gate) + self.assertIsNot(copied_label, label_gate) + self.assertNotEqual(copied.label, label_gate.label) + self.assertEqual(copied_label, label_gate) + self.assertNotEqual(copied.label, "special") + self.assertEqual(copied_label.label, "special") + + def test_condition_copy(self): + gate = HGate().c_if(Clbit(), 0) + copied = gate.copy() + self.assertIsNot(gate, copied) + self.assertEqual(gate, copied) + + def test_condition_label_copy(self): + clbit = Clbit() + gate = HGate(label="conditionally special").c_if(clbit, 0) + copied = gate.copy() + self.assertIsNot(gate, copied) + self.assertEqual(gate, copied) + self.assertEqual(copied.label, "conditionally special") + self.assertEqual(copied.condition, (clbit, 0)) + + def test_deepcopy(self): + gate = HGate() + copied = copy.deepcopy(gate) + self.assertIs(gate, copied) + + def test_deepcopy_with_label(self): + gate = HGate(label="special") + copied = copy.deepcopy(gate) + self.assertIsNot(gate, copied) + self.assertEqual(gate, copied) + self.assertEqual(copied.label, "special") + + def test_deepcopy_with_condition(self): + gate = HGate().c_if(Clbit(), 0) + copied = copy.deepcopy(gate) + self.assertIsNot(gate, copied) + self.assertEqual(gate, copied) + + def test_condition_label_deepcopy(self): + clbit = Clbit() + gate = HGate(label="conditionally special").c_if(clbit, 0) + copied = copy.deepcopy(gate) + self.assertIsNot(gate, copied) + self.assertEqual(gate, copied) + self.assertEqual(copied.label, "conditionally special") + self.assertEqual(copied.condition, (clbit, 0)) + + def test_label_deepcopy_new(self): + gate = HGate() + label_gate = HGate(label="special") + self.assertIsNot(gate, label_gate) + self.assertNotEqual(gate.label, label_gate.label) + copied = copy.deepcopy(gate) + copied_label = copy.deepcopy(label_gate) + self.assertIs(gate, copied) + self.assertIsNot(copied, label_gate) + self.assertIsNot(copied_label, gate) + self.assertIsNot(copied_label, label_gate) + self.assertNotEqual(copied.label, label_gate.label) + self.assertEqual(copied_label, label_gate) + self.assertNotEqual(copied.label, "special") + self.assertEqual(copied_label.label, "special") + + def test_control_a_singleton(self): + singleton_gate = HGate() + gate = HGate(label="special") + ch = gate.control(label="my_ch") + self.assertEqual(ch.base_gate.label, "special") + self.assertIsNot(ch.base_gate, singleton_gate) + + def test_round_trip_dag_conversion(self): + qc = QuantumCircuit(1) + gate = HGate() + qc.append(gate, [0]) + dag = circuit_to_dag(qc) + out = dag_to_circuit(dag) + self.assertIs(qc.data[0].operation, out.data[0].operation) + + def test_round_trip_dag_conversion_with_label(self): + gate = HGate(label="special") + qc = QuantumCircuit(1) + qc.append(gate, [0]) + dag = circuit_to_dag(qc) + out = dag_to_circuit(dag) + self.assertIsNot(qc.data[0].operation, out.data[0].operation) + self.assertEqual(qc.data[0].operation, out.data[0].operation) + self.assertEqual(out.data[0].operation.label, "special") + + def test_round_trip_dag_conversion_with_condition(self): + qc = QuantumCircuit(1, 1) + gate = HGate().c_if(qc.cregs[0], 0) + qc.append(gate, [0]) + dag = circuit_to_dag(qc) + out = dag_to_circuit(dag) + self.assertIsNot(qc.data[0].operation, out.data[0].operation) + self.assertEqual(qc.data[0].operation, out.data[0].operation) + self.assertEqual(out.data[0].operation.condition, (qc.cregs[0], 0)) + + def test_round_trip_dag_conversion_condition_label(self): + qc = QuantumCircuit(1, 1) + gate = HGate(label="conditionally special").c_if(qc.cregs[0], 0) + qc.append(gate, [0]) + dag = circuit_to_dag(qc) + out = dag_to_circuit(dag) + self.assertIsNot(qc.data[0].operation, out.data[0].operation) + self.assertEqual(qc.data[0].operation, out.data[0].operation) + self.assertEqual(out.data[0].operation.condition, (qc.cregs[0], 0)) + self.assertEqual(out.data[0].operation.label, "conditionally special") + + def test_condition_via_instructionset(self): + gate = HGate() + qr = QuantumRegister(2, "qr") + cr = ClassicalRegister(1, "cr") + circuit = QuantumCircuit(qr, cr) + circuit.h(qr[0]).c_if(cr, 1) + self.assertIsNot(gate, circuit.data[0].operation) + self.assertEqual(circuit.data[0].operation.condition, (cr, 1)) + + def test_is_mutable(self): + gate = HGate() + self.assertFalse(gate.mutable) + label_gate = HGate(label="foo") + self.assertTrue(label_gate.mutable) + self.assertIsNot(gate, label_gate) + + def test_to_mutable(self): + gate = HGate() + self.assertFalse(gate.mutable) + new_gate = gate.to_mutable() + self.assertTrue(new_gate.mutable) + self.assertIsNot(gate, new_gate) + + def test_to_mutable_setter(self): + gate = HGate() + self.assertFalse(gate.mutable) + mutable_gate = gate.to_mutable() + mutable_gate.label = "foo" + mutable_gate.duration = 3 + mutable_gate.unit = "s" + clbit = Clbit() + mutable_gate.condition = (clbit, 0) + self.assertTrue(mutable_gate.mutable) + self.assertIsNot(gate, mutable_gate) + self.assertEqual(mutable_gate.label, "foo") + self.assertEqual(mutable_gate.duration, 3) + self.assertEqual(mutable_gate.unit, "s") + self.assertEqual(mutable_gate.condition, (clbit, 0)) + + def test_to_mutable_of_mutable_instance(self): + gate = HGate(label="foo") + mutable_copy = gate.to_mutable() + self.assertIsNot(gate, mutable_copy) + self.assertEqual(mutable_copy.label, gate.label) + mutable_copy.label = "not foo" + self.assertNotEqual(mutable_copy.label, gate.label) + + def test_set_custom_attr(self): + gate = SXGate() + with self.assertRaises(NotImplementedError): + gate.custom_foo = 12345 + mutable_gate = gate.to_mutable() + self.assertTrue(mutable_gate.mutable) + mutable_gate.custom_foo = 12345 + self.assertEqual(12345, mutable_gate.custom_foo) + + def test_positional_label(self): + gate = SXGate() + label_gate = SXGate("I am a little label") + self.assertIsNot(gate, label_gate) + self.assertEqual(label_gate.label, "I am a little label") diff --git a/test/python/dagcircuit/test_dagcircuit.py b/test/python/dagcircuit/test_dagcircuit.py index 7f7375178bb0..35576d9b4361 100644 --- a/test/python/dagcircuit/test_dagcircuit.py +++ b/test/python/dagcircuit/test_dagcircuit.py @@ -477,8 +477,7 @@ def setUp(self): def test_apply_operation_back(self): """The apply_operation_back() method.""" - x_gate = XGate() - x_gate.condition = self.condition + x_gate = XGate().c_if(*self.condition) self.dag.apply_operation_back(HGate(), [self.qubit0], []) self.dag.apply_operation_back(CXGate(), [self.qubit0, self.qubit1], []) self.dag.apply_operation_back(Measure(), [self.qubit1], [self.clbit1]) @@ -490,8 +489,7 @@ def test_apply_operation_back(self): def test_edges(self): """Test that DAGCircuit.edges() behaves as expected with ops.""" - x_gate = XGate() - x_gate.condition = self.condition + x_gate = XGate().c_if(*self.condition) self.dag.apply_operation_back(HGate(), [self.qubit0], []) self.dag.apply_operation_back(CXGate(), [self.qubit0, self.qubit1], []) self.dag.apply_operation_back(Measure(), [self.qubit1], [self.clbit1]) @@ -509,8 +507,7 @@ def test_apply_operation_back_conditional(self): # Single qubit gate conditional: qc.h(qr[2]).c_if(cr, 3) - h_gate = HGate() - h_gate.condition = self.condition + h_gate = HGate().c_if(*self.condition) h_node = self.dag.apply_operation_back(h_gate, [self.qubit2], []) self.assertEqual(h_node.qargs, (self.qubit2,)) @@ -550,8 +547,7 @@ def test_apply_operation_back_conditional_measure(self): new_creg = ClassicalRegister(1, "cr2") self.dag.add_creg(new_creg) - meas_gate = Measure() - meas_gate.condition = (new_creg, 0) + meas_gate = Measure().c_if(new_creg, 0) meas_node = self.dag.apply_operation_back(meas_gate, [self.qubit0], [self.clbit0]) self.assertEqual(meas_node.qargs, (self.qubit0,)) @@ -596,8 +592,7 @@ def test_apply_operation_back_conditional_measure_to_self(self): # Measure targeting a clbit which _is_ a member of the conditional # register. qc.measure(qr[0], cr[0]).c_if(cr, 3) - meas_gate = Measure() - meas_gate.condition = self.condition + meas_gate = Measure().c_if(*self.condition) meas_node = self.dag.apply_operation_back(meas_gate, [self.qubit1], [self.clbit1]) self.assertEqual(meas_node.qargs, (self.qubit1,)) @@ -1153,8 +1148,7 @@ def test_dag_collect_runs(self): def test_dag_collect_runs_start_with_conditional(self): """Test collect runs with a conditional at the start of the run.""" - h_gate = HGate() - h_gate.condition = self.condition + h_gate = HGate().c_if(*self.condition) self.dag.apply_operation_back(h_gate, [self.qubit0]) self.dag.apply_operation_back(HGate(), [self.qubit0]) self.dag.apply_operation_back(HGate(), [self.qubit0]) @@ -1167,8 +1161,7 @@ def test_dag_collect_runs_start_with_conditional(self): def test_dag_collect_runs_conditional_in_middle(self): """Test collect_runs with a conditional in the middle of a run.""" - h_gate = HGate() - h_gate.condition = self.condition + h_gate = HGate().c_if(*self.condition) self.dag.apply_operation_back(HGate(), [self.qubit0]) self.dag.apply_operation_back(h_gate, [self.qubit0]) self.dag.apply_operation_back(HGate(), [self.qubit0]) @@ -1210,8 +1203,7 @@ def test_dag_collect_1q_runs_start_with_conditional(self): """Test collect 1q runs with a conditional at the start of the run.""" self.dag.apply_operation_back(Reset(), [self.qubit0]) self.dag.apply_operation_back(Delay(100), [self.qubit0]) - h_gate = HGate() - h_gate.condition = self.condition + h_gate = HGate().c_if(*self.condition) self.dag.apply_operation_back(h_gate, [self.qubit0]) self.dag.apply_operation_back(HGate(), [self.qubit0]) self.dag.apply_operation_back(HGate(), [self.qubit0]) @@ -1226,8 +1218,7 @@ def test_dag_collect_1q_runs_conditional_in_middle(self): """Test collect_1q_runs with a conditional in the middle of a run.""" self.dag.apply_operation_back(Reset(), [self.qubit0]) self.dag.apply_operation_back(Delay(100), [self.qubit0]) - h_gate = HGate() - h_gate.condition = self.condition + h_gate = HGate().c_if(*self.condition) self.dag.apply_operation_back(HGate(), [self.qubit0]) self.dag.apply_operation_back(h_gate, [self.qubit0]) self.dag.apply_operation_back(HGate(), [self.qubit0]) @@ -1305,8 +1296,7 @@ def test_layers_basic(self): qubit1 = qreg[1] clbit0 = creg[0] clbit1 = creg[1] - x_gate = XGate() - x_gate.condition = (creg, 3) + x_gate = XGate().c_if(creg, 3) dag = DAGCircuit() dag.add_qreg(qreg) dag.add_creg(creg) @@ -2135,10 +2125,8 @@ def test_substitute_with_provided_wire_map_propagate_condition(self): sub.cx(0, 1) sub.h(0) - conditioned_h = HGate() - conditioned_h.condition = conditioned_cz.condition - conditioned_cx = CXGate() - conditioned_cx.condition = conditioned_cz.condition + conditioned_h = HGate().c_if(*conditioned_cz.condition) + conditioned_cx = CXGate().c_if(*conditioned_cz.condition) expected = DAGCircuit() expected.add_qubits(base_qubits) From 01bb111cfa706658134c4481a354133bc5326f24 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Wed, 20 Sep 2023 10:05:49 +0300 Subject: [PATCH 19/19] adding an option to CouplingMap.reduce to allow disconnected coupling maps (#10863) * adding an option to CouplingMap.reduce to allow disconnected coupling maps * missing 'not' --- qiskit/transpiler/coupling.py | 26 +++++++++---------- ...-coupling-map-reduce-bb19e35ec939570d.yaml | 9 +++++++ test/python/transpiler/test_coupling.py | 16 ++++++++++-- 3 files changed, 35 insertions(+), 16 deletions(-) create mode 100644 releasenotes/notes/extend-coupling-map-reduce-bb19e35ec939570d.yaml diff --git a/qiskit/transpiler/coupling.py b/qiskit/transpiler/coupling.py index 774f79329ff3..614e166050e8 100644 --- a/qiskit/transpiler/coupling.py +++ b/qiskit/transpiler/coupling.py @@ -22,7 +22,6 @@ import math from typing import List -import numpy as np import rustworkx as rx from rustworkx.visualization import graphviz_draw @@ -252,14 +251,16 @@ def _check_symmetry(self): """ return self.graph.is_symmetric() - def reduce(self, mapping): + def reduce(self, mapping, check_if_connected=True): """Returns a reduced coupling map that corresponds to the subgraph of qubits selected in the mapping. Args: mapping (list): A mapping of reduced qubits to device - qubits. + qubits. + check_if_connected (bool): if True, checks that the reduced + coupling map is connected. Returns: CouplingMap: A reduced coupling_map for the selected qubits. @@ -268,9 +269,6 @@ def reduce(self, mapping): CouplingError: Reduced coupling map must be connected. """ - from scipy.sparse import coo_matrix, csgraph - - reduced_qubits = len(mapping) inv_map = [None] * (max(mapping) + 1) for idx, val in enumerate(mapping): inv_map[val] = idx @@ -281,17 +279,17 @@ def reduce(self, mapping): if edge[0] in mapping and edge[1] in mapping: reduced_cmap.append([inv_map[edge[0]], inv_map[edge[1]]]) - # Verify coupling_map is connected - rows = np.array([edge[0] for edge in reduced_cmap], dtype=int) - cols = np.array([edge[1] for edge in reduced_cmap], dtype=int) - data = np.ones_like(rows) - - mat = coo_matrix((data, (rows, cols)), shape=(reduced_qubits, reduced_qubits)).tocsr() + # Note: using reduced_coupling_map.graph is significantly faster + # than calling add_physical_qubit / add_edge. + reduced_coupling_map = CouplingMap() + for node in range(len(mapping)): + reduced_coupling_map.graph.add_node(node) + reduced_coupling_map.graph.extend_from_edge_list([tuple(x) for x in reduced_cmap]) - if csgraph.connected_components(mat)[0] != 1: + if check_if_connected and not reduced_coupling_map.is_connected(): raise CouplingError("coupling_map must be connected.") - return CouplingMap(reduced_cmap) + return reduced_coupling_map @classmethod def from_full(cls, num_qubits, bidirectional=True) -> "CouplingMap": diff --git a/releasenotes/notes/extend-coupling-map-reduce-bb19e35ec939570d.yaml b/releasenotes/notes/extend-coupling-map-reduce-bb19e35ec939570d.yaml new file mode 100644 index 000000000000..5f2d26442409 --- /dev/null +++ b/releasenotes/notes/extend-coupling-map-reduce-bb19e35ec939570d.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + The method :meth:`~qiskit.transpiler.CouplingMap.reduce` now accepts an + additional argument ``check_if_connected``, defaulted to ``True``. This + corresponds to the previous behavior, checking whether the reduced coupling + map remains connected and raising a ``CouplingError`` if not so. When set to + ``False``, the check is skipped, allowing disconnected reduced coupling maps. + diff --git a/test/python/transpiler/test_coupling.py b/test/python/transpiler/test_coupling.py index 7c75e309cb95..7e2caf71babf 100644 --- a/test/python/transpiler/test_coupling.py +++ b/test/python/transpiler/test_coupling.py @@ -111,14 +111,26 @@ def test_successful_reduced_map(self): ans = [(1, 2), (3, 2), (0, 1)] self.assertEqual(set(out), set(ans)) - def test_failed_reduced_map(self): - """Generate a bad disconnected reduced map""" + def test_bad_reduced_map(self): + """Generate disconnected reduced map""" fake = FakeRueschlikon() cmap = fake.configuration().coupling_map coupling_map = CouplingMap(cmap) with self.assertRaises(CouplingError): coupling_map.reduce([12, 11, 10, 3]) + def test_disconnected_reduced_map_allowed(self): + """Generate disconnected reduced map but do not error""" + fake = FakeRueschlikon() + cmap = fake.configuration().coupling_map + coupling_map = CouplingMap(cmap) + reduced_map = coupling_map.reduce([12, 11, 10, 3], check_if_connected=False) + reduced_edges = reduced_map.get_edges() + qubits_expected = [0, 1, 2, 3] + edges_expected = [(0, 1), (1, 2)] + self.assertEqual(qubits_expected, reduced_map.physical_qubits) + self.assertEqual(set(reduced_edges), set(edges_expected)) + def test_symmetric_small_true(self): coupling_list = [[0, 1], [1, 0]] coupling = CouplingMap(coupling_list)