diff --git a/.github/workflows/wheels-build.yml b/.github/workflows/wheels-build.yml index 136d0f94b3e6..63fb25678f00 100644 --- a/.github/workflows/wheels-build.yml +++ b/.github/workflows/wheels-build.yml @@ -27,15 +27,6 @@ on: default: "default" required: false - wheels-32bit: - description: >- - The action to take for Tier 1 wheels. - Choose from 'default', 'build' or 'skip'. - This builds multiple artifacts, which all match 'wheels-32bit-*'. - type: string - default: "default" - required: false - wheels-linux-s390x: description: >- The action to take for Linux s390x wheels. @@ -133,33 +124,6 @@ jobs: path: ./wheelhouse/*.whl name: ${{ inputs.artifact-prefix }}wheels-tier-1-${{ matrix.os }} - wheels-32bit: - name: "Wheels / 32bit" - if: (inputs.wheels-32bit == 'default' && inputs.default-action || inputs.wheels-32bit) == 'build' - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: - - ubuntu-latest - - windows-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: ${{ inputs.python-version }} - - uses: dtolnay/rust-toolchain@stable - with: - components: llvm-tools-preview - - name: Build wheels - uses: pypa/cibuildwheel@v2.22.0 - env: - CIBW_SKIP: 'pp* cp36-* cp37-* cp38-* *musllinux* *amd64 *x86_64' - - uses: actions/upload-artifact@v4 - with: - path: ./wheelhouse/*.whl - name: ${{ inputs.artifact-prefix }}wheels-32bit-${{ matrix.os }} - wheels-linux-s390x: name: "Wheels / Linux s390x" if: (inputs.wheels-linux-s390x == 'default' && inputs.default-action || inputs.wheels-linux-s390x) == 'build' diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index b3a5b7cf76aa..42faedb6f271 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -12,7 +12,6 @@ jobs: artifact-prefix: "deploy-core-" default-action: "skip" wheels-tier-1: "build" - wheels-32bit: "build" sdist: "build" upload-core: name: Deploy core @@ -39,7 +38,6 @@ jobs: artifact-prefix: "deploy-others-" default-action: "build" wheels-tier-1: "skip" - wheels-32bit: "skip" sdist: "skip" upload-others: name: Deploy other diff --git a/crates/accelerate/src/circuit_library/pauli_evolution.rs b/crates/accelerate/src/circuit_library/pauli_evolution.rs index 3c5164314c08..b5aa71716341 100644 --- a/crates/accelerate/src/circuit_library/pauli_evolution.rs +++ b/crates/accelerate/src/circuit_library/pauli_evolution.rs @@ -192,22 +192,22 @@ fn multi_qubit_evolution( /// followed by a CX-chain and then a single Pauli-Z rotation on the last qubit. Then the CX-chain /// is uncomputed and the inverse basis transformation applied. E.g. for the evolution under the /// Pauli string XIYZ we have the circuit -/// ┌───┐┌───────┐┌───┐ -/// 0: ─────────────┤ X ├┤ Rz(2) ├┤ X ├─────────── -/// ┌──────┐┌───┐└─┬─┘└───────┘└─┬─┘┌───┐┌────┐ -/// 1: ┤ √Xdg ├┤ X ├──■─────────────■──┤ X ├┤ √X ├ -/// └──────┘└─┬─┘ └─┬─┘└────┘ -/// 2: ──────────┼───────────────────────┼──────── -/// ┌───┐ │ │ ┌───┐ -/// 3: ─┤ H ├────■───────────────────────■──┤ H ├─ -/// └───┘ └───┘ +/// +/// ┌───┐ ┌───┐┌───────┐┌───┐┌───┐ +/// 0: ┤ H ├──────┤ X ├┤ Rz(2) ├┤ X ├┤ H ├──────── +/// └───┘ └─┬─┘└───────┘└─┬─┘└───┘ +/// 1: ─────────────┼─────────────┼─────────────── +/// ┌────┐┌───┐ │ │ ┌───┐┌──────┐ +/// 2: ┤ √X ├┤ X ├──■─────────────■──┤ X ├┤ √Xdg ├ +/// └────┘└─┬─┘ └─┬─┘└──────┘ +/// 3: ────────■───────────────────────■────────── /// /// Args: /// num_qubits: The number of qubits in the Hamiltonian. /// sparse_paulis: The Paulis to implement. Given in a sparse-list format with elements -/// ``(pauli_string, qubit_indices, coefficient)``. An element of the form -/// ``("IXYZ", [0,1,2,3], 0.2)``, for example, is interpreted in terms of qubit indices as -/// I_q0 X_q1 Y_q2 Z_q3 and will use a RZ rotation angle of 0.4. +/// ``(pauli_string, qubit_indices, rz_rotation_angle)``. An element of the form +/// ``("XIYZ", [0,1,2,3], 2)``, for example, is interpreted in terms of qubit indices as +/// X_q0 I_q1 Y_q2 Z_q3 and will use a RZ rotation angle of 2. /// insert_barriers: If ``true``, insert a barrier in between the evolution of individual /// Pauli terms. /// do_fountain: If ``true``, implement the CX propagation as "fountain" shape, where each @@ -244,7 +244,7 @@ pub fn py_pauli_evolution( } paulis.push(pauli); - times.push(time); // note we do not multiply by 2 here, this is done Python side! + times.push(time); // note we do not multiply by 2 here, this is already done Python side! indices.push(tuple.get_item(1)?.extract::>()?) } @@ -266,12 +266,12 @@ pub fn py_pauli_evolution( }, ); - // When handling all-identity Paulis above, we added the time as global phase. - // However, the all-identity Paulis should add a negative phase, as they implement - // exp(-i t I). We apply the negative sign here, to only do a single (-1) multiplication, - // instead of doing it every time we find an all-identity Pauli. + // When handling all-identity Paulis above, we added the RZ rotation angle as global phase, + // meaning that we have implemented of exp(i 2t I). However, what we want it to implement + // exp(-i t I). To only use a single multiplication, we apply a factor of -0.5 here. + // This is faster, in particular as long as the parameter expressions are in Python. if modified_phase { - global_phase = multiply_param(&global_phase, -1.0, py); + global_phase = multiply_param(&global_phase, -0.5, py); } CircuitData::from_packed_operations(py, num_qubits as u32, 0, evos, global_phase) diff --git a/crates/accelerate/src/synthesis/evolution/pauli_network.rs b/crates/accelerate/src/synthesis/evolution/pauli_network.rs index b5a73262aae8..a3a03e2fddbe 100644 --- a/crates/accelerate/src/synthesis/evolution/pauli_network.rs +++ b/crates/accelerate/src/synthesis/evolution/pauli_network.rs @@ -211,7 +211,7 @@ fn inject_rotations( if pauli_support_size == 0 { // in case of an all-identity rotation, update global phase by subtracting // the angle - global_phase = radd_param(global_phase, multiply_param(&angles[i], -1.0, py), py); + global_phase = radd_param(global_phase, multiply_param(&angles[i], -0.5, py), py); hit_paulis[i] = true; dag.remove_node(i); } else if pauli_support_size == 1 && dag.is_front_node(i) { diff --git a/crates/accelerate/src/target_transpiler/mod.rs b/crates/accelerate/src/target_transpiler/mod.rs index f57facf28b74..324876816a68 100644 --- a/crates/accelerate/src/target_transpiler/mod.rs +++ b/crates/accelerate/src/target_transpiler/mod.rs @@ -1151,6 +1151,12 @@ impl Target { /// Checks whether an instruction is supported by the Target based on instruction name and qargs. pub fn instruction_supported(&self, operation_name: &str, qargs: Option<&Qargs>) -> bool { + // Handle case where num_qubits is None by checking globally supported operations + let qargs: Option<&Qargs> = if self.num_qubits.is_none() { + None + } else { + qargs + }; if self.gate_map.contains_key(operation_name) { if let Some(_qargs) = qargs { let qarg_set: HashSet<&PhysicalQubit> = _qargs.iter().collect(); diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index fa5880b1697a..439b9ba80f67 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -545,6 +545,7 @@ fn get_2q_decomposers_from_target( let mut available_2q_props: IndexMap<&str, (Option, Option)> = IndexMap::new(); let mut qubit_gate_map = IndexMap::new(); + match target.operation_names_for_qargs(Some(&qubits)) { Ok(direct_keys) => { qubit_gate_map.insert(&qubits, direct_keys); @@ -597,7 +598,10 @@ fn get_2q_decomposers_from_target( OperationRef::Standard(_) => (), _ => continue, } - + // Filter out non-2q-gate candidates + if op.operation.num_qubits() != 2 { + continue; + } available_2q_basis.insert(key, replace_parametrized_gate(op.clone())); if target.contains_key(key) { diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 444d178c6863..904368e10e20 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -2345,8 +2345,14 @@ pub fn add_param(param: &Param, summand: f64, py: Python) -> Param { } pub fn radd_param(param1: Param, param2: Param, py: Python) -> Param { - match [param1, param2] { + match [¶m1, ¶m2] { [Param::Float(theta), Param::Float(lambda)] => Param::Float(theta + lambda), + [Param::Float(theta), Param::ParameterExpression(_lambda)] => { + add_param(¶m2, *theta, py) + } + [Param::ParameterExpression(_theta), Param::Float(lambda)] => { + add_param(¶m1, *lambda, py) + } [Param::ParameterExpression(theta), Param::ParameterExpression(lambda)] => { Param::ParameterExpression( theta diff --git a/docs/source_images/depth.gif b/docs/source_images/depth.gif deleted file mode 100644 index 4437049fc882..000000000000 Binary files a/docs/source_images/depth.gif and /dev/null differ diff --git a/qiskit/circuit/library/generalized_gates/permutation.py b/qiskit/circuit/library/generalized_gates/permutation.py index aa27a6b20fdc..0dd8e0f9df7f 100644 --- a/qiskit/circuit/library/generalized_gates/permutation.py +++ b/qiskit/circuit/library/generalized_gates/permutation.py @@ -186,7 +186,7 @@ def inverse(self, annotated: bool = False) -> PermutationGate: return PermutationGate(pattern=_inverse_pattern(self.pattern)) - def _qasm2_decomposition(self): + def _qasm_decomposition(self): # pylint: disable=cyclic-import from qiskit.synthesis.permutation import synth_permutation_basic diff --git a/qiskit/circuit/library/generalized_gates/unitary.py b/qiskit/circuit/library/generalized_gates/unitary.py index 40271ed4f594..a1550d1a7acb 100644 --- a/qiskit/circuit/library/generalized_gates/unitary.py +++ b/qiskit/circuit/library/generalized_gates/unitary.py @@ -202,7 +202,7 @@ def control( ) return gate - def _qasm2_decomposition(self): + def _qasm_decomposition(self): """Return an unparameterized version of ourselves, so the OQ2 exporter doesn't choke on the non-standard things in our `params` field.""" out = self.definition.to_gate() diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 5598c3032d14..ab0ad758efb9 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -889,27 +889,6 @@ class QuantumCircuit: assert qc.size() == 19 - A particularly important circuit property is known as the circuit :meth:`depth`. The depth - of a quantum circuit is a measure of how many "layers" of quantum gates, executed in - parallel, it takes to complete the computation defined by the circuit. Because quantum - gates take time to implement, the depth of a circuit roughly corresponds to the amount of - time it takes the quantum computer to execute the circuit. Thus, the depth of a circuit - is one important quantity used to measure if a quantum circuit can be run on a device. - - The depth of a quantum circuit has a mathematical definition as the longest path in a - directed acyclic graph (DAG). However, such a definition is a bit hard to grasp, even for - experts. Fortunately, the depth of a circuit can be easily understood by anyone familiar - with playing `Tetris `_. Lets see how to compute this - graphically: - - .. image:: /source_images/depth.gif - :alt: Rotate the circuit and let each gate fall as far as possible. The gates fall \ - into "layers". The depth of the circuit is the number of layers. - - We can verify our graphical result using :meth:`QuantumCircuit.depth`:: - - assert qc.depth() == 9 - .. automethod:: count_ops .. automethod:: depth .. automethod:: get_instructions @@ -3490,6 +3469,14 @@ def depth( ) -> int: """Return circuit depth (i.e., length of critical path). + The depth of a quantum circuit is a measure of how many + "layers" of quantum gates, executed in parallel, it takes to + complete the computation defined by the circuit. Because + quantum gates take time to implement, the depth of a circuit + roughly corresponds to the amount of time it takes the quantum + computer to execute the circuit. + + .. warning:: This operation is not well defined if the circuit contains control-flow operations. diff --git a/qiskit/qasm2/export.py b/qiskit/qasm2/export.py index 7c655dfd432b..fbe9b533ea9a 100644 --- a/qiskit/qasm2/export.py +++ b/qiskit/qasm2/export.py @@ -314,8 +314,8 @@ def _define_custom_operation(operation, gates_to_define): # definition, but still continue to return the given object as the call-site object. if operation.base_class in known_good_parameterized: parameterized_operation = type(operation)(*_FIXED_PARAMETERS[: len(operation.params)]) - elif hasattr(operation, "_qasm2_decomposition"): - new_op = operation._qasm2_decomposition() + elif hasattr(operation, "_qasm_decomposition"): + new_op = operation._qasm_decomposition() parameterized_operation = operation = new_op.copy(name=_escape_name(new_op.name, "gate_")) else: parameterized_operation = operation diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index 8997770c14e7..b08b2a9719ef 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -1180,13 +1180,16 @@ def build_gate_call(self, instruction: CircuitInstruction): This will also push the gate into the symbol table (if required), including recursively defining the gate blocks.""" - ident = self.symbols.get_gate(instruction.operation) + operation = instruction.operation + if hasattr(operation, "_qasm_decomposition"): + operation = operation._qasm_decomposition() + ident = self.symbols.get_gate(operation) if ident is None: - ident = self.define_gate(instruction.operation) + ident = self.define_gate(operation) qubits = [self._lookup_bit(qubit) for qubit in instruction.qubits] parameters = [ ast.StringifyAndPray(self._rebind_scoped_parameters(param)) - for param in instruction.operation.params + for param in operation.params ] if not self.disable_constants: for parameter in parameters: diff --git a/qiskit/synthesis/evolution/product_formula.py b/qiskit/synthesis/evolution/product_formula.py index b314d5ec5e62..980d8cdfefd9 100644 --- a/qiskit/synthesis/evolution/product_formula.py +++ b/qiskit/synthesis/evolution/product_formula.py @@ -191,12 +191,6 @@ def settings(self) -> dict[str, typing.Any]: "preserve_order": self.preserve_order, } - def _normalize_coefficients( - self, paulis: list[str | list[int], float | complex | ParameterExpression] - ) -> list[str | list[int] | ParameterValueType]: - """Ensure the coefficients are real (or parameter expressions).""" - return [[(op, qubits, real_or_fail(coeff)) for op, qubits, coeff in ops] for ops in paulis] - def _custom_evolution(self, num_qubits, pauli_rotations): """Implement the evolution for the non-standard path. diff --git a/qiskit/synthesis/evolution/suzuki_trotter.py b/qiskit/synthesis/evolution/suzuki_trotter.py index 209f377351a7..194e986aeb0d 100644 --- a/qiskit/synthesis/evolution/suzuki_trotter.py +++ b/qiskit/synthesis/evolution/suzuki_trotter.py @@ -18,7 +18,9 @@ import typing from collections.abc import Callable from itertools import chain +import numpy as np +from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.quantum_info.operators import SparsePauliOp, Pauli from qiskit.utils.deprecation import deprecate_arg @@ -138,7 +140,7 @@ def expand( .. code-block:: text - ("X", [0], t), ("ZZ", [0, 1], 2t), ("X", [0], 2) + ("X", [0], t), ("ZZ", [0, 1], 2t), ("X", [0], t) Note that the rotation angle contains a factor of 2, such that that evolution of a Pauli :math:`P` over time :math:`t`, which is :math:`e^{itP}`, is represented @@ -157,7 +159,10 @@ def expand( time = evolution.time def to_sparse_list(operator): - paulis = (time * (2 / self.reps) * operator).to_sparse_list() + paulis = [ + (pauli, indices, real_or_fail(coeff) * time * 2 / self.reps) + for pauli, indices, coeff in operator.to_sparse_list() + ] if not self.preserve_order: return reorder_paulis(paulis) @@ -171,9 +176,6 @@ def to_sparse_list(operator): # here would be the location to do so. non_commuting = [[op] for op in to_sparse_list(operators)] - # normalize coefficients, i.e. ensure they are float or ParameterExpression - non_commuting = self._normalize_coefficients(non_commuting) - # we're already done here since Lie Trotter does not do any operator repetition product_formula = self._recurse(self.order, non_commuting) flattened = self.reps * list(chain.from_iterable(product_formula)) @@ -213,3 +215,18 @@ def _recurse(order, grouped_paulis): ], ) return outer + inner + outer + + +def real_or_fail(value, tol=100): + """Return real if close, otherwise fail. Unbound parameters are left unchanged. + + Based on NumPy's ``real_if_close``, i.e. ``tol`` is in terms of machine precision for float. + """ + if isinstance(value, ParameterExpression): + return value + + abstol = tol * np.finfo(float).eps + if abs(np.imag(value)) < abstol: + return np.real(value) + + raise ValueError(f"Encountered complex value {value}, but expected real.") diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index 85b0c05d5cae..1e5ca4519864 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -238,7 +238,6 @@ class BasicSwapPassManager(PassManagerStagePlugin): def pass_manager(self, pass_manager_config, optimization_level=None) -> PassManager: """Build routing stage PassManager.""" - seed_transpiler = pass_manager_config.seed_transpiler target = pass_manager_config.target coupling_map = pass_manager_config.coupling_map backend_properties = pass_manager_config.backend_properties @@ -257,7 +256,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana routing_pass, target, coupling_map=coupling_map, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, use_barrier_before_measurement=True, ) if optimization_level == 1: @@ -268,7 +267,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, check_trivial=True, use_barrier_before_measurement=True, ) @@ -280,7 +279,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, use_barrier_before_measurement=True, ) if optimization_level == 3: @@ -291,7 +290,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, use_barrier_before_measurement=True, ) raise TranspilerError(f"Invalid optimization level specified: {optimization_level}") @@ -324,7 +323,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana routing_pass, target, coupling_map=coupling_map, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, use_barrier_before_measurement=True, ) if optimization_level == 1: @@ -335,7 +334,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, check_trivial=True, use_barrier_before_measurement=True, ) @@ -347,7 +346,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, use_barrier_before_measurement=True, ) raise TranspilerError(f"Invalid optimization level specified: {optimization_level}") @@ -358,7 +357,6 @@ class LookaheadSwapPassManager(PassManagerStagePlugin): def pass_manager(self, pass_manager_config, optimization_level=None) -> PassManager: """Build routing stage PassManager.""" - seed_transpiler = pass_manager_config.seed_transpiler target = pass_manager_config.target coupling_map = pass_manager_config.coupling_map coupling_map_routing = target @@ -376,7 +374,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana routing_pass, target, coupling_map=coupling_map, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, use_barrier_before_measurement=True, ) if optimization_level == 1: @@ -388,7 +386,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, check_trivial=True, use_barrier_before_measurement=True, ) @@ -401,7 +399,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, use_barrier_before_measurement=True, ) if optimization_level == 3: @@ -413,7 +411,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, use_barrier_before_measurement=True, ) raise TranspilerError(f"Invalid optimization level specified: {optimization_level}") @@ -448,7 +446,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana routing_pass, target, coupling_map=coupling_map, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, use_barrier_before_measurement=True, ) if optimization_level == 1: @@ -466,7 +464,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, check_trivial=True, use_barrier_before_measurement=True, ) @@ -486,7 +484,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, use_barrier_before_measurement=True, ) if optimization_level == 3: @@ -504,7 +502,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, use_barrier_before_measurement=True, ) raise TranspilerError(f"Invalid optimization level specified: {optimization_level}") @@ -515,7 +513,6 @@ class NoneRoutingPassManager(PassManagerStagePlugin): def pass_manager(self, pass_manager_config, optimization_level=None) -> PassManager: """Build routing stage PassManager.""" - seed_transpiler = pass_manager_config.seed_transpiler target = pass_manager_config.target coupling_map = pass_manager_config.coupling_map routing_pass = Error( @@ -527,7 +524,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana routing_pass, target, coupling_map=coupling_map, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, use_barrier_before_measurement=True, ) @@ -793,7 +790,7 @@ def _swap_mapped(property_set): ) choose_layout_1 = VF2Layout( coupling_map=pass_manager_config.coupling_map, - seed=pass_manager_config.seed_transpiler, + seed=-1, call_limit=int(5e4), # Set call limit to ~100ms with rustworkx 0.10.2 properties=pass_manager_config.backend_properties, target=pass_manager_config.target, @@ -826,7 +823,7 @@ def _swap_mapped(property_set): elif optimization_level == 2: choose_layout_0 = VF2Layout( coupling_map=pass_manager_config.coupling_map, - seed=pass_manager_config.seed_transpiler, + seed=-1, call_limit=int(5e6), # Set call limit to ~10s with rustworkx 0.10.2 properties=pass_manager_config.backend_properties, target=pass_manager_config.target, @@ -861,7 +858,7 @@ def _swap_mapped(property_set): elif optimization_level == 3: choose_layout_0 = VF2Layout( coupling_map=pass_manager_config.coupling_map, - seed=pass_manager_config.seed_transpiler, + seed=-1, call_limit=int(3e7), # Set call limit to ~60s with rustworkx 0.10.2 properties=pass_manager_config.backend_properties, target=pass_manager_config.target, diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index c9bcc9a7904c..431089f657dd 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -281,7 +281,7 @@ def generate_routing_passmanager( coupling_map=None, vf2_call_limit=None, backend_properties=None, - seed_transpiler=None, + seed_transpiler=-1, check_trivial=False, use_barrier_before_measurement=True, vf2_max_trials=None, @@ -300,7 +300,10 @@ def generate_routing_passmanager( backend_properties (BackendProperties): Properties of a backend to synthesize for (e.g. gate fidelities). seed_transpiler (int): Sets random seed for the stochastic parts of - the transpiler. + the transpiler. This is currently only used for :class:`.VF2PostLayout` and the + default value of ``-1`` is strongly recommended (which is no randomization). + If a value of ``None`` is provided this will seed from system + entropy. check_trivial (bool): If set to true this will condition running the :class:`~.VF2PostLayout` pass after routing on whether a trivial layout was tried and was found to not be perfect. This is only @@ -358,7 +361,7 @@ def _swap_condition(property_set): target, coupling_map, backend_properties, - seed_transpiler, + seed=seed_transpiler, call_limit=vf2_call_limit, max_trials=vf2_max_trials, strict_direction=False, diff --git a/releasenotes/notes/64-bit-only-4132f330ec7804b3.yaml b/releasenotes/notes/64-bit-only-4132f330ec7804b3.yaml new file mode 100644 index 000000000000..6632cb914f96 --- /dev/null +++ b/releasenotes/notes/64-bit-only-4132f330ec7804b3.yaml @@ -0,0 +1,16 @@ +--- +upgrade: + - | + Qiskit no longer supports Linux i686 and 32 bit Windows. Starting in Qiskit + 2.0 a 64 bit platform is needed to run Qiskit. The user base for 32bit + architectures is relatively small and as Qiskit continues to focus on + improving performance to handle the increased scale in the complexity + of quantum computing hardware maintaining support for 32 bit platforms + is proving increasingly difficult. This coupled with the larger + scientific/numeric Python community's trend away from 32 bit platform + support maintaining support for 32bit platforms is no longer viable. + Qiskit 1.x will still continue to support 32 bit platforms, but starting + in this 2.0.0 release Qiskit no longer supports these platforms, will + not publish pre-compiled binaries for these platforms any longer, and + there is no longer any guarantee of being able to build from source on + 32 bit platforms. diff --git a/releasenotes/notes/fix-pauli-evo-all-identity-b129acd854d8c391.yaml b/releasenotes/notes/fix-pauli-evo-all-identity-b129acd854d8c391.yaml new file mode 100644 index 000000000000..03cbe8654cb9 --- /dev/null +++ b/releasenotes/notes/fix-pauli-evo-all-identity-b129acd854d8c391.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + The :class:`.PauliEvolutionGate`, if used with a product formula synthesis (this is the default), + did not correctly handle all-identity terms in the operator. The all-identity term + should introduce a global phase equal to ``-evolution_time``, but was off by a factor of 2 + and could break for parameterized times. This behavior is now fixed. + Fixed `#13625 `__. diff --git a/releasenotes/notes/fix-pauli-sympify-ea9acceb2a923aff.yaml b/releasenotes/notes/fix-pauli-sympify-ea9acceb2a923aff.yaml new file mode 100644 index 000000000000..248a09b6f88e --- /dev/null +++ b/releasenotes/notes/fix-pauli-sympify-ea9acceb2a923aff.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + Fixed an inconsistency in the circuit generated by a Pauli evolution synthesis + with :class:`.SuzukiTrotter` or :class:`.LieTrotter` (the default) method. + For parameterized evolution times, the resulting circuits contained parameters + with a spurious, zero complex part, which affected the output of + :meth:`.ParameterExpression.sympify`. The output now correctly is only real. + Fixed `#13642 `__. diff --git a/releasenotes/notes/fix-qasm-3-unitary-2da190be6ba25bbd.yaml b/releasenotes/notes/fix-qasm-3-unitary-2da190be6ba25bbd.yaml new file mode 100644 index 000000000000..3cd59ff5e427 --- /dev/null +++ b/releasenotes/notes/fix-qasm-3-unitary-2da190be6ba25bbd.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fix a bug in :class:`.qasm3.Exporter` that caused the exporter to crash when + handling a unitary gate due to incorrect processing of its ``params`` field. diff --git a/releasenotes/notes/fix-target-instr-supported-900a1caa76e30655.yaml b/releasenotes/notes/fix-target-instr-supported-900a1caa76e30655.yaml new file mode 100644 index 000000000000..5db23e66fed3 --- /dev/null +++ b/releasenotes/notes/fix-target-instr-supported-900a1caa76e30655.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixed a bug in the :meth:`.Target.instruction_supported` method where + targets with ``self.num_qubits==None`` would always return ``false`` + independently of the supported basis set. \ No newline at end of file diff --git a/releasenotes/notes/fix-unitary-synthesis-global-gates-19b93840b28cfcf7.yaml b/releasenotes/notes/fix-unitary-synthesis-global-gates-19b93840b28cfcf7.yaml new file mode 100644 index 000000000000..21310a4729a8 --- /dev/null +++ b/releasenotes/notes/fix-unitary-synthesis-global-gates-19b93840b28cfcf7.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixed a bug in the :class:`.UnitarySynthesis` transpiler pass where + non-2-qubit gates would be included in the available 2 qubit basis, + causing the ``TwoQubitWeylDecomposition`` to panic because of + the dimension mismatch. diff --git a/releasenotes/notes/vf2-order-3ef2b4ca5ebd0588.yaml b/releasenotes/notes/vf2-order-3ef2b4ca5ebd0588.yaml new file mode 100644 index 000000000000..3d573a21901a --- /dev/null +++ b/releasenotes/notes/vf2-order-3ef2b4ca5ebd0588.yaml @@ -0,0 +1,12 @@ +--- +upgrade_transpiler: + - | + The default value for the :func:`.generate_routing_passmanager` argument + ``seed_transpiler`` has changed from ``None`` to ``-1``. This was done + because this flag was only used to configure the :class:`.VF2PostLayout` + transpiler pass in the output, and for that pass in particular the + randomization typically only hurts performance and is not desirable. + If you were relying on the previous default value you can restore this + behavior by explicitly setting the argument ``seed_transpiler=None``. If + you were explicitly setting a seed value for this parameter there is no + change in behavior. diff --git a/test/python/circuit/library/test_evolution_gate.py b/test/python/circuit/library/test_evolution_gate.py index 1b55ea920289..f56da1995e4a 100644 --- a/test/python/circuit/library/test_evolution_gate.py +++ b/test/python/circuit/library/test_evolution_gate.py @@ -479,6 +479,50 @@ def atomic_evolution(pauli, time): decomposed = evo_gate.definition.decompose() self.assertEqual(decomposed.count_ops()["cx"], reps * 3 * 4) + def test_all_identity(self): + """Test circuit with all identity Paulis works correctly.""" + evo = PauliEvolutionGate(I ^ I, time=1).definition + expected = QuantumCircuit(2, global_phase=-1) + self.assertEqual(expected, evo) + + def test_global_phase(self): + """Test a circuit with parameterized global phase terms. + + Regression test of #13625. + """ + pauli = (X ^ X) + (I ^ I) + (I ^ X) + time = Parameter("t") + evo = PauliEvolutionGate(pauli, time=time) + + expected = QuantumCircuit(2, global_phase=-time) + expected.rxx(2 * time, 0, 1) + expected.rx(2 * time, 0) + + with self.subTest(msg="check circuit"): + self.assertEqual(expected, evo.definition) + + # since all terms in the Pauli operator commute, we can compare to an + # exact matrix exponential + time_value = 1.76123 + bound = evo.definition.assign_parameters([time_value]) + exact = scipy.linalg.expm(-1j * time_value * pauli.to_matrix()) + with self.subTest(msg="check correctness"): + self.assertEqual(Operator(exact), Operator(bound)) + + def test_sympify_is_real(self): + """Test converting the parameters to sympy is real. + + Regression test of #13642, where the parameters in the Pauli evolution had a spurious + zero complex part. Even though this is not noticable upon binding or printing the parameter, + it does affect the output of Parameter.sympify. + """ + time = Parameter("t") + evo = PauliEvolutionGate(Z, time=time) + + angle = evo.definition.data[0].operation.params[0] + expected = (2.0 * time).sympify() + self.assertEqual(expected, angle.sympify()) + def exact_atomic_evolution(circuit, pauli, time): """An exact atomic evolution for Suzuki-Trotter. diff --git a/test/python/circuit/library/test_evolved_op_ansatz.py b/test/python/circuit/library/test_evolved_op_ansatz.py index 9c923764f408..2e7865a856c4 100644 --- a/test/python/circuit/library/test_evolved_op_ansatz.py +++ b/test/python/circuit/library/test_evolved_op_ansatz.py @@ -199,6 +199,21 @@ def test_detect_commutation(self): # this Hamiltonian should be split into 2 commuting groups, hence we get 2 parameters self.assertEqual(2, circuit.num_parameters) + def test_evolution_with_identity(self): + """Test a Hamiltonian containing an identity term. + + Regression test of #13644. + """ + hamiltonian = SparsePauliOp(["III", "IZZ", "IXI"]) + ansatz = hamiltonian_variational_ansatz(hamiltonian, reps=1) + bound = ansatz.assign_parameters([1, 1]) # we have two non-commuting groups, hence 2 params + + expected = QuantumCircuit(3, global_phase=-1) + expected.rzz(2, 0, 1) + expected.rx(2, 1) + + self.assertEqual(expected, bound) + def evolve(pauli_string, time): """Get the reference evolution circuit for a single Pauli string.""" diff --git a/test/python/primitives/test_backend_estimator.py b/test/python/primitives/test_backend_estimator.py index 62845461dacf..49d5efa2d782 100644 --- a/test/python/primitives/test_backend_estimator.py +++ b/test/python/primitives/test_backend_estimator.py @@ -440,7 +440,7 @@ def test_layout(self, backend): estimator.set_transpile_options(seed_transpiler=15, optimization_level=1) value = estimator.run(qc, op, shots=10000).result().values[0] if optionals.HAS_AER: - ref_value = -0.9954 if isinstance(backend, GenericBackendV2) else -0.916 + ref_value = -0.9954 if isinstance(backend, GenericBackendV2) else -0.934 else: ref_value = -1 self.assertEqual(value, ref_value) diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index acae1d7d3cd7..1b9c24ca5410 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -2665,6 +2665,23 @@ def test_switch_v1_expr_target(self): test = dumps(qc, experimental=ExperimentalFeatures.SWITCH_CASE_V1) self.assertEqual(test, expected) + def test_circuit_with_unitary(self): + """Test that circuits with `unitary` gate are correctly handled""" + matrix = [[0, 1], [1, 0]] + qc = QuantumCircuit(1) + qc.unitary(matrix, [0]) + expected = """\ +OPENQASM 3.0; +include "stdgates.inc"; +gate unitary _gate_q_0 { + U(pi, -pi, 0) _gate_q_0; +} +qubit[1] q; +unitary q[0]; +""" + test = dumps(qc) + self.assertEqual(test, expected) + @ddt class TestQASM3ExporterFailurePaths(QiskitTestCase): diff --git a/test/python/transpiler/test_target.py b/test/python/transpiler/test_target.py index 86f1953d43a4..f7aae24eff54 100644 --- a/test/python/transpiler/test_target.py +++ b/test/python/transpiler/test_target.py @@ -1169,6 +1169,14 @@ def test_instruction_supported_no_args(self): def test_instruction_supported_no_operation(self): self.assertFalse(self.ibm_target.instruction_supported(qargs=(0,), parameters=[math.pi])) + def test_instruction_supported_no_qubits(self): + """Checks that instruction supported works when target.num_qubits is None.""" + target = Target.from_configuration(["u", "cx", "rxx"]) + self.assertTrue(target.instruction_supported("u", (0,))) + self.assertTrue(target.instruction_supported("cx", (0, 1))) + self.assertTrue(target.instruction_supported("cx", None)) + self.assertTrue(target.instruction_supported("rxx", (2, 3))) + def test_target_serialization_preserve_variadic(self): """Checks that variadics are still seen as variadic after serialization""" diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index 7f2bb2f89911..d7940ab1a763 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -71,8 +71,6 @@ FakeMumbaiFractionalCX, ) -from ..legacy_cmaps import YORKTOWN_CMAP - class FakeBackend2QV2(GenericBackendV2): """A 2-qubit fake backend""" @@ -112,8 +110,8 @@ def __init__(self, bidirectional=True): @ddt -class TestUnitarySynthesis(QiskitTestCase): - """Test UnitarySynthesis pass.""" +class TestUnitarySynthesisBasisGates(QiskitTestCase): + """Test UnitarySynthesis pass with basis gates.""" def test_empty_basis_gates(self): """Verify when basis_gates is None, we do not synthesize unitaries.""" @@ -126,7 +124,6 @@ def test_empty_basis_gates(self): qc.unitary(op_3q.data, [0, 1, 2]) out = UnitarySynthesis(basis_gates=None, min_qubits=2)(qc) - self.assertEqual(out.count_ops(), {"unitary": 3}) @data( @@ -148,81 +145,39 @@ def test_two_qubit_synthesis_to_basis(self, basis_gates): dag = circuit_to_dag(qc) out = UnitarySynthesis(basis_gates).run(dag) - self.assertTrue(set(out.count_ops()).issubset(basis_gates)) - def test_two_qubit_synthesis_to_directional_cx_from_gate_errors(self): + @combine(gate=["unitary", "swap"], natural_direction=[True, False]) + def test_two_qubit_synthesis_to_directional_cx(self, gate, natural_direction): """Verify two qubit unitaries are synthesized to match basis gates.""" # TODO: should make check more explicit e.g. explicitly set gate # direction in test instead of using specific fake backend with self.assertWarns(DeprecationWarning): backend = Fake5QV1() conf = backend.configuration() - qr = QuantumRegister(2) coupling_map = CouplingMap(conf.coupling_map) triv_layout_pass = TrivialLayout(coupling_map) - qc = QuantumCircuit(qr) - qc.unitary(random_unitary(4, seed=12), [0, 1]) - unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, - coupling_map=None, - backend_props=backend.properties(), - pulse_optimize=True, - natural_direction=False, - ) - pm = PassManager([triv_layout_pass, unisynth_pass]) - qc_out = pm.run(qc) - unisynth_pass_nat = UnitarySynthesis( - basis_gates=conf.basis_gates, - coupling_map=None, - backend_props=backend.properties(), - pulse_optimize=True, - natural_direction=True, - ) - - pm_nat = PassManager([triv_layout_pass, unisynth_pass_nat]) - qc_out_nat = pm_nat.run(qc) - self.assertEqual(Operator(qc), Operator(qc_out)) - self.assertEqual(Operator(qc), Operator(qc_out_nat)) - - def test_swap_synthesis_to_directional_cx(self): - """Verify two qubit unitaries are synthesized to match basis gates.""" - # TODO: should make check more explicit e.g. explicitly set gate - # direction in test instead of using specific fake backend - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() qr = QuantumRegister(2) - coupling_map = CouplingMap(conf.coupling_map) - triv_layout_pass = TrivialLayout(coupling_map) qc = QuantumCircuit(qr) - qc.swap(qr[0], qr[1]) + if gate == "unitary": + qc.unitary(random_unitary(4, seed=12), [0, 1]) + elif gate == "swap": + qc.swap(qr[0], qr[1]) + unisynth_pass = UnitarySynthesis( basis_gates=conf.basis_gates, coupling_map=None, backend_props=backend.properties(), pulse_optimize=True, - natural_direction=False, + natural_direction=natural_direction, ) pm = PassManager([triv_layout_pass, unisynth_pass]) qc_out = pm.run(qc) - - unisynth_pass_nat = UnitarySynthesis( - basis_gates=conf.basis_gates, - coupling_map=None, - backend_props=backend.properties(), - pulse_optimize=True, - natural_direction=True, - ) - - pm_nat = PassManager([triv_layout_pass, unisynth_pass_nat]) - qc_out_nat = pm_nat.run(qc) - self.assertEqual(Operator(qc), Operator(qc_out)) - self.assertEqual(Operator(qc), Operator(qc_out_nat)) - def test_two_qubit_synthesis_to_directional_cx_multiple_registers(self): + @data(True, False) + def test_two_qubit_synthesis_to_directional_cx_multiple_registers(self, natural_direction): """Verify two qubit unitaries are synthesized to match basis gates across multiple registers.""" # TODO: should make check more explicit e.g. explicitly set gate @@ -241,25 +196,14 @@ def test_two_qubit_synthesis_to_directional_cx_multiple_registers(self): coupling_map=None, backend_props=backend.properties(), pulse_optimize=True, - natural_direction=False, + natural_direction=natural_direction, ) pm = PassManager([triv_layout_pass, unisynth_pass]) qc_out = pm.run(qc) - - unisynth_pass_nat = UnitarySynthesis( - basis_gates=conf.basis_gates, - coupling_map=None, - backend_props=backend.properties(), - pulse_optimize=True, - natural_direction=True, - ) - - pm_nat = PassManager([triv_layout_pass, unisynth_pass_nat]) - qc_out_nat = pm_nat.run(qc) self.assertEqual(Operator(qc), Operator(qc_out)) - self.assertEqual(Operator(qc), Operator(qc_out_nat)) - def test_two_qubit_synthesis_to_directional_cx_from_coupling_map(self): + @data(True, False, None) + def test_two_qubit_synthesis_to_directional_cx_from_coupling_map(self, natural_direction): """Verify natural cx direction is used when specified in coupling map.""" # TODO: should make check more explicit e.g. explicitly set gate # direction in test instead of using specific fake backend @@ -276,119 +220,22 @@ def test_two_qubit_synthesis_to_directional_cx_from_coupling_map(self): coupling_map=coupling_map, backend_props=backend.properties(), pulse_optimize=True, - natural_direction=False, - ) - pm = PassManager([triv_layout_pass, unisynth_pass]) - qc_out = pm.run(qc) - - unisynth_pass_nat = UnitarySynthesis( - basis_gates=conf.basis_gates, - coupling_map=coupling_map, - backend_props=backend.properties(), - pulse_optimize=True, - natural_direction=True, - ) - - pm_nat = PassManager([triv_layout_pass, unisynth_pass_nat]) - qc_out_nat = pm_nat.run(qc) - # the decomposer defaults to the [1, 0] direction but the coupling - # map specifies a [0, 1] direction. Check that this is respected. - self.assertTrue( - all(((qr[1], qr[0]) == instr.qubits for instr in qc_out.get_instructions("cx"))) - ) - self.assertTrue( - all(((qr[0], qr[1]) == instr.qubits for instr in qc_out_nat.get_instructions("cx"))) - ) - self.assertEqual(Operator(qc), Operator(qc_out)) - self.assertEqual(Operator(qc), Operator(qc_out_nat)) - - def test_two_qubit_synthesis_to_directional_cx_from_coupling_map_natural_none(self): - """Verify natural cx direction is used when specified in coupling map - when natural_direction is None.""" - # TODO: should make check more explicit e.g. explicitly set gate - # direction in test instead of using specific fake backend - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() - qr = QuantumRegister(2) - coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]]) - triv_layout_pass = TrivialLayout(coupling_map) - qc = QuantumCircuit(qr) - qc.unitary(random_unitary(4, seed=12), [0, 1]) - unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, - coupling_map=coupling_map, - backend_props=backend.properties(), - pulse_optimize=True, - natural_direction=False, + natural_direction=natural_direction, ) pm = PassManager([triv_layout_pass, unisynth_pass]) qc_out = pm.run(qc) - unisynth_pass_nat = UnitarySynthesis( - basis_gates=conf.basis_gates, - coupling_map=coupling_map, - backend_props=backend.properties(), - pulse_optimize=True, - natural_direction=None, - ) - - pm_nat = PassManager([triv_layout_pass, unisynth_pass_nat]) - qc_out_nat = pm_nat.run(qc) - # the decomposer defaults to the [1, 0] direction but the coupling - # map specifies a [0, 1] direction. Check that this is respected. - self.assertTrue( - all(((qr[1], qr[0]) == instr.qubits for instr in qc_out.get_instructions("cx"))) - ) - self.assertTrue( - all(((qr[0], qr[1]) == instr.qubits for instr in qc_out_nat.get_instructions("cx"))) - ) - self.assertEqual(Operator(qc), Operator(qc_out)) - self.assertEqual(Operator(qc), Operator(qc_out_nat)) - - def test_two_qubit_synthesis_to_directional_cx_from_coupling_map_natural_false(self): - """Verify natural cx direction is used when specified in coupling map - when natural_direction is None.""" - # TODO: should make check more explicit e.g. explicitly set gate - # direction in test instead of using specific fake backend - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() - qr = QuantumRegister(2) - coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]]) - triv_layout_pass = TrivialLayout(coupling_map) - qc = QuantumCircuit(qr) - qc.unitary(random_unitary(4, seed=12), [0, 1]) - unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, - coupling_map=coupling_map, - backend_props=backend.properties(), - pulse_optimize=True, - natural_direction=False, - ) - pm = PassManager([triv_layout_pass, unisynth_pass]) - qc_out = pm.run(qc) - - unisynth_pass_nat = UnitarySynthesis( - basis_gates=conf.basis_gates, - coupling_map=coupling_map, - backend_props=backend.properties(), - pulse_optimize=True, - natural_direction=False, - ) - - pm_nat = PassManager([triv_layout_pass, unisynth_pass_nat]) - qc_out_nat = pm_nat.run(qc) - # the decomposer defaults to the [1, 0] direction but the coupling - # map specifies a [0, 1] direction. Check that this is respected. - self.assertTrue( - all(((qr[1], qr[0]) == instr.qubits for instr in qc_out.get_instructions("cx"))) - ) - self.assertTrue( - all(((qr[1], qr[0]) == instr.qubits for instr in qc_out_nat.get_instructions("cx"))) - ) + if natural_direction is False: + self.assertTrue( + all(((qr[1], qr[0]) == instr.qubits for instr in qc_out.get_instructions("cx"))) + ) + else: + # the decomposer defaults to the [1, 0] direction but the coupling + # map specifies a [0, 1] direction. Check that this is respected. + self.assertTrue( + all(((qr[0], qr[1]) == instr.qubits for instr in qc_out.get_instructions("cx"))) + ) self.assertEqual(Operator(qc), Operator(qc_out)) - self.assertEqual(Operator(qc), Operator(qc_out_nat)) def test_two_qubit_synthesis_not_pulse_optimal(self): """Verify not attempting pulse optimal decomposition when pulse_optimize==False.""" @@ -432,7 +279,7 @@ def test_two_qubit_pulse_optimal_true_raises(self): with self.assertWarns(DeprecationWarning): backend = Fake5QV1() conf = backend.configuration() - # this assumes iswawp pulse optimal decomposition doesn't exist + # this assumes iswap pulse optimal decomposition doesn't exist conf.basis_gates = [gate if gate != "cx" else "iswap" for gate in conf.basis_gates] qr = QuantumRegister(2) coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]]) @@ -451,12 +298,10 @@ def test_two_qubit_pulse_optimal_true_raises(self): pm.run(qc) def test_two_qubit_natural_direction_true_duration_fallback(self): - """Verify not attempting pulse optimal decomposition when pulse_optimize==False.""" - # this assumes iswawp pulse optimal decomposition doesn't exist + """Verify fallback path when pulse_optimize==True.""" with self.assertWarns(DeprecationWarning): backend = Fake5QV1() conf = backend.configuration() - # conf.basis_gates = [gate if gate != "cx" else "iswap" for gate in conf.basis_gates] qr = QuantumRegister(2) coupling_map = CouplingMap([[0, 1], [1, 0], [1, 2], [1, 3], [3, 4]]) triv_layout_pass = TrivialLayout(coupling_map) @@ -476,8 +321,9 @@ def test_two_qubit_natural_direction_true_duration_fallback(self): ) def test_two_qubit_natural_direction_true_gate_length_raises(self): - """Verify not attempting pulse optimal decomposition when pulse_optimize==False.""" - # this assumes iswawp pulse optimal decomposition doesn't exist + """Verify that error is raised if preferred direction cannot be inferred + from gate lenghts/errors. + """ with self.assertWarns(DeprecationWarning): backend = Fake5QV1() conf = backend.configuration() @@ -501,7 +347,6 @@ def test_two_qubit_natural_direction_true_gate_length_raises(self): def test_two_qubit_pulse_optimal_none_optimal(self): """Verify pulse optimal decomposition when pulse_optimize==None.""" - # this assumes iswawp pulse optimal decomposition doesn't exist with self.assertWarns(DeprecationWarning): backend = Fake5QV1() conf = backend.configuration() @@ -559,7 +404,7 @@ def test_two_qubit_pulse_optimal_none_no_raise(self): self.assertLessEqual(num_ops["sx"], 14) def test_qv_natural(self): - """check that quantum volume circuit compiles for natural direction""" + """Check that quantum volume circuit compiles for natural direction""" qv64 = quantum_volume(5, seed=15) def construct_passmanager(basis_gates, coupling_map, synthesis_fidelity, pulse_optimize): @@ -621,7 +466,7 @@ def construct_passmanager(basis_gates, coupling_map, synthesis_fidelity, pulse_o @data(1, 2, 3) def test_coupling_map_transpile(self, opt): - """test natural_direction works with transpile/execute""" + """test natural_direction works with transpile""" qr = QuantumRegister(2) circ = QuantumCircuit(qr) circ.append(random_unitary(4, seed=1), [0, 1]) @@ -651,6 +496,59 @@ def test_coupling_map_transpile(self, opt): ) ) + def test_if_simple(self): + """Test a simple if statement.""" + basis_gates = {"u", "cx"} + qr = QuantumRegister(2) + cr = ClassicalRegister(2) + + qc_uni = QuantumCircuit(2) + qc_uni.h(0) + qc_uni.cx(0, 1) + qc_uni_mat = Operator(qc_uni) + + qc_true_body = QuantumCircuit(2) + qc_true_body.unitary(qc_uni_mat, [0, 1]) + + qc = QuantumCircuit(qr, cr) + qc.if_test((cr, 1), qc_true_body, [0, 1], []) + dag = circuit_to_dag(qc) + cdag = UnitarySynthesis(basis_gates=basis_gates).run(dag) + cqc = dag_to_circuit(cdag) + cbody = cqc.data[0].operation.params[0] + self.assertEqual(cbody.count_ops().keys(), basis_gates) + self.assertEqual(qc_uni_mat, Operator(cbody)) + + def test_nested_control_flow(self): + """Test unrolling nested control flow blocks.""" + qr = QuantumRegister(2) + cr = ClassicalRegister(1) + qc_uni1 = QuantumCircuit(2) + qc_uni1.swap(0, 1) + qc_uni1_mat = Operator(qc_uni1) + + qc = QuantumCircuit(qr, cr) + with qc.for_loop(range(3)): + with qc.while_loop((cr, 0)): + qc.unitary(qc_uni1_mat, [0, 1]) + dag = circuit_to_dag(qc) + cdag = UnitarySynthesis(basis_gates=["u", "cx"]).run(dag) + cqc = dag_to_circuit(cdag) + cbody = cqc.data[0].operation.params[2].data[0].operation.params[0] + self.assertEqual(cbody.count_ops().keys(), {"u", "cx"}) + self.assertEqual(qc_uni1_mat, Operator(cbody)) + + def test_default_does_not_fail_on_no_syntheses(self): + qc = QuantumCircuit(1) + qc.unitary(np.eye(2), [0]) + pass_ = UnitarySynthesis(["unknown", "gates"]) + self.assertEqual(qc, pass_(qc)) + + +@ddt +class TestUnitarySynthesisTarget(QiskitTestCase): + """Test UnitarySynthesis pass with target/BackendV2.""" + @combine( opt_level=[0, 1, 2, 3], bidirectional=[True, False], @@ -675,38 +573,6 @@ def test_coupling_map_transpile_with_backendv2(self, opt_level, bidirectional): (0, 1), (circ_01_index[instr.qubits[0]], circ_01_index[instr.qubits[1]]) ) - @data(1, 2, 3) - def test_coupling_map_unequal_durations(self, opt): - """Test direction with transpile/execute with backend durations.""" - qr = QuantumRegister(2) - circ = QuantumCircuit(qr) - circ.append(random_unitary(4, seed=1), [1, 0]) - with self.assertWarns(DeprecationWarning): - backend = GenericBackendV2( - num_qubits=5, - coupling_map=YORKTOWN_CMAP, - basis_gates=["id", "rz", "sx", "x", "cx", "reset"], - calibrate_instructions=True, - pulse_channels=True, - seed=42, - ) - tqc = transpile( - circ, - backend=backend, - optimization_level=opt, - translation_method="synthesis", - layout_method="trivial", - ) - tqc_index = {qubit: index for index, qubit in enumerate(tqc.qubits)} - self.assertTrue( - all( - ( - (1, 0) == (tqc_index[instr.qubits[0]], tqc_index[instr.qubits[1]]) - for instr in tqc.get_instructions("cx") - ) - ) - ) - @combine( opt_level=[0, 1, 2, 3], bidirectional=[True, False], @@ -843,48 +709,6 @@ def test_approximation_controlled(self): self.assertGreaterEqual(dag_100.depth(), dag_99.depth()) self.assertEqual(Operator(dag_to_circuit(dag_100)), Operator(circ)) - def test_if_simple(self): - """Test a simple if statement.""" - basis_gates = {"u", "cx"} - qr = QuantumRegister(2) - cr = ClassicalRegister(2) - - qc_uni = QuantumCircuit(2) - qc_uni.h(0) - qc_uni.cx(0, 1) - qc_uni_mat = Operator(qc_uni) - - qc_true_body = QuantumCircuit(2) - qc_true_body.unitary(qc_uni_mat, [0, 1]) - - qc = QuantumCircuit(qr, cr) - qc.if_test((cr, 1), qc_true_body, [0, 1], []) - dag = circuit_to_dag(qc) - cdag = UnitarySynthesis(basis_gates=basis_gates).run(dag) - cqc = dag_to_circuit(cdag) - cbody = cqc.data[0].operation.params[0] - self.assertEqual(cbody.count_ops().keys(), basis_gates) - self.assertEqual(qc_uni_mat, Operator(cbody)) - - def test_nested_control_flow(self): - """Test unrolling nested control flow blocks.""" - qr = QuantumRegister(2) - cr = ClassicalRegister(1) - qc_uni1 = QuantumCircuit(2) - qc_uni1.swap(0, 1) - qc_uni1_mat = Operator(qc_uni1) - - qc = QuantumCircuit(qr, cr) - with qc.for_loop(range(3)): - with qc.while_loop((cr, 0)): - qc.unitary(qc_uni1_mat, [0, 1]) - dag = circuit_to_dag(qc) - cdag = UnitarySynthesis(basis_gates=["u", "cx"]).run(dag) - cqc = dag_to_circuit(cdag) - cbody = cqc.data[0].operation.params[2].data[0].operation.params[0] - self.assertEqual(cbody.count_ops().keys(), {"u", "cx"}) - self.assertEqual(qc_uni1_mat, Operator(cbody)) - def test_mapping_control_flow(self): """Test that inner dags use proper qubit mapping.""" qr = QuantumRegister(3, "q") @@ -974,12 +798,6 @@ def __init__(self): result_qc = dag_to_circuit(result_dag) self.assertEqual(result_qc, qc) - def test_default_does_not_fail_on_no_syntheses(self): - qc = QuantumCircuit(1) - qc.unitary(np.eye(2), [0]) - pass_ = UnitarySynthesis(["unknown", "gates"]) - self.assertEqual(qc, pass_(qc)) - def test_iswap_no_cx_synthesis_succeeds(self): """Test basis set with iswap but no cx can synthesize a circuit""" target = Target() @@ -1101,6 +919,21 @@ def test_3q_measure_all(self): self.assertIn("cx", ops) self.assertIn("measure", ops) + def test_target_with_global_gates(self): + """Test that 2q decomposition can handle a target with global gates.""" + + basis_gates = ["h", "p", "cp", "rz", "cx", "ccx", "swap"] + target = Target.from_configuration(basis_gates=basis_gates) + + bell = QuantumCircuit(2) + bell.h(0) + bell.cx(0, 1) + bell_op = Operator(bell) + qc = QuantumCircuit(2) + qc.unitary(bell_op, [0, 1]) + tqc = transpile(qc, target=target) + self.assertTrue(set(tqc.count_ops()).issubset(basis_gates)) + if __name__ == "__main__": unittest.main()