Qiskit / qiskit

Qiskit is an open-source SDK for working with quantum computers at the level of extended quantum circuits, operators, and primitives.
https://www.ibm.com/quantum/qiskit
Apache License 2.0
5.13k stars 2.35k forks source link

Target with supercontrolled gates, but no controlled (only) gates fails to synthesize circuits #9996

Closed jlapeyre closed 1 year ago

jlapeyre commented 1 year ago

Environment

What is happening?

A target whose only entangling gate is an iSwap gate fails to synthesize a circuit. The decomposer XXDecomposer (which can't use iSwap gates) runs anyway and throws an error.

How can we reproduce the issue?

This is fairly minimal, but not super short. (The duration and error on the gates is probably irrelevant and could be omitted)

Code to trigger bug ```python from qiskit.circuit import Parameter from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.circuit import QuantumCircuit from qiskit.transpiler import Target, InstructionProperties from qiskit.transpiler.passes import UnitarySynthesis from qiskit.circuit.library.standard_gates import UGate, CXGate, IGate, RZGate, SXGate, XGate, iSwapGate from qiskit.quantum_info import Operator from qiskit.circuit import Measure # , Reset, Delay def make_target(entangling_gate): """Create a target on 4 qubits whose only entangling gate is `entangling_gate`""" target = Target() theta = Parameter("theta") phi = Parameter("phi") i_props = { (0,): InstructionProperties(duration=35.5e-9, error=0.000413), (1,): InstructionProperties(duration=35.5e-9, error=0.000502), (2,): InstructionProperties(duration=35.5e-9, error=0.0004003), (3,): InstructionProperties(duration=35.5e-9, error=0.000614), (4,): InstructionProperties(duration=35.5e-9, error=0.006149), } target.add_instruction(IGate(), i_props) rz_props = { (0,): InstructionProperties(duration=0, error=0), (1,): InstructionProperties(duration=0, error=0), (2,): InstructionProperties(duration=0, error=0), (3,): InstructionProperties(duration=0, error=0), (4,): InstructionProperties(duration=0, error=0), } target.add_instruction(RZGate(theta), rz_props) sx_props = { (0,): InstructionProperties(duration=35.5e-9, error=0.000413), (1,): InstructionProperties(duration=35.5e-9, error=0.000502), (2,): InstructionProperties(duration=35.5e-9, error=0.0004003), (3,): InstructionProperties(duration=35.5e-9, error=0.000614), (4,): InstructionProperties(duration=35.5e-9, error=0.006149), } target.add_instruction(SXGate(), sx_props) x_props = { (0,): InstructionProperties(duration=35.5e-9, error=0.000413), (1,): InstructionProperties(duration=35.5e-9, error=0.000502), (2,): InstructionProperties(duration=35.5e-9, error=0.0004003), (3,): InstructionProperties(duration=35.5e-9, error=0.000614), (4,): InstructionProperties(duration=35.5e-9, error=0.006149), } target.add_instruction(XGate(), x_props) cx_props = { (3, 4): InstructionProperties(duration=270.22e-9, error=0.00713), (4, 3): InstructionProperties(duration=305.77e-9, error=0.00713), (3, 1): InstructionProperties(duration=462.22e-9, error=0.00929), (1, 3): InstructionProperties(duration=497.77e-9, error=0.00929), (1, 2): InstructionProperties(duration=227.55e-9, error=0.00659), (2, 1): InstructionProperties(duration=263.11e-9, error=0.00659), (0, 1): InstructionProperties(duration=519.11e-9, error=0.01201), (1, 0): InstructionProperties(duration=554.66e-9, error=0.01201), } target.add_instruction(entangling_gate, cx_props) measure_props = { (0,): InstructionProperties(duration=5.813e-6, error=0.0751), (1,): InstructionProperties(duration=5.813e-6, error=0.0225), (2,): InstructionProperties(duration=5.813e-6, error=0.0146), (3,): InstructionProperties(duration=5.813e-6, error=0.0215), (4,): InstructionProperties(duration=5.813e-6, error=0.0333), } target.add_instruction(Measure(), measure_props) return target def make_cx_target(): return make_target(CXGate()) def make_iswap_target(): return make_target(iSwapGate()) def anonymous_cx_circuit(): """Make a dagcircuit with a CX gate converted to a matrix""" qc = QuantumCircuit(2) cxmat = Operator(CXGate()).to_matrix() qc.unitary(cxmat, [0, 1]) # qc.unitary(np.eye(4), [0, 1]) return circuit_to_dag(qc) def try_unitary_synth(target, dag): """Attept unitary synthesis pass on `dag` with `target`.""" unitary_synth_pass = UnitarySynthesis(target=target) result_dag = unitary_synth_pass.run(dag) result_qc = dag_to_circuit(result_dag) return result_qc def try_unitary_synth_with_cx(): target = make_cx_target() dag = anonymous_cx_circuit() try_unitary_synth(target, dag) def try_unitary_synth_with_iswap(): target = make_iswap_target() dag = anonymous_cx_circuit() try_unitary_synth(target, dag) ```

Running code that triggers bug.

Running the function defined above try_unitary_synth_with_iswap() thows IndexError: index out of range. Running try_unitary_synth_with_iswap() runs and exits normally.

What should happen?

Both examples above should successfully synthesize the circuit and exit normally. We have decomposers for both iswap and cx gates.

Comments

This bug is related to #9983 and several other issues and attempted PRs

The circuit in the example above is a a 2q circuit with a single gate, a CX gate that has been converted to a gate of name unitary defined by an explicit matrix. This forces the synthesizers to try to synthesize gates from this matrix.

A list of 2q decomposers is built with one of the variables in the composers being the 2q entangling gates in the target set. Two groups of entangling gates are discriminated: controlled gates and super-controlled gates. In the example above we used a CX gate and an iSwap gate. The CX gate is both controlled and super-controlled, so it appears in both groups. The iSwap gate is super-controlled but not controlled, so it appears in only the super-controlled group, not in the controlled group.

XXDecomposer is a 2q decomposer which only handles basis sets with controlled gates, and not with super-controlled gates.

The 2q composers are invoked with any 2q gates that they do not support filtered out.

The current logic in unitary_synthesis.py will invoke XXDecomposer unconditionally, with an empty basis set if there are no controlled gates in the target basis set. If the circuit to synthesize has any entangling 2q gates, the error mentioned above will be thrown. This prevents a decomposer that can use an iSwap gate from being used in case there is an iSwap in the target basis set.

We could skip running XXDecomposer in the case that there are no controlled gates in the target basis. However, we currently have a test in the test suite that tests a path including XXDecomposer with an empty list of gates synthesizing a nonentangling gate. That is, a gate that doesn't need any entangling basis gates to be sythesized. XXDecomposer correctly handles this. If we always skip XXDecomposer in the case that there are no controlled gates this latter test will fail.

See #9994 and references to discussions there for more details.

One solution that would preserve all existing behavior (except for throwing errors) is to modify the decomposers to return a status flag. If the decomposition failed, you simply ignore the result. Currently XXDecomposer simply errors either graciously with #9994, or ungraciously as before.

jlapeyre commented 1 year ago

I propose (trying) the following. It adds a bit of complexity, but it also removes complexity in the logic. It also breaks anything relying on the existing interface.

The two-qubit synthesizers in quantum_info currently return a circuit. Instead they could return an object that contains the synthesized circuit and also some status or report information. For the first implementation, the report would include only one item, a bool telling whether synthesis succeed or failed... or perhaps a failure message too.

The only "normal" path this would be useful for is this test for synthesizing a trivial circuit for a target with no entangling gates: https://github.com/Qiskit/qiskit-terra/blob/1203a3b18551f06325354208274e61d02d8bd584/test/python/transpiler/test_unitary_synthesis.py#L839-L842

For error paths, it would improve diagnostics. Currently you might throw, say, an index error deep in the algorithm. Instead , the loop that runs all decomposers and compares the results would check the statuses and report that no decomposers succeeded.

EDIT I see one decomposer that flags failure to decompose (probably) using None. https://github.com/Qiskit/qiskit-terra/blob/1203a3b18551f06325354208274e61d02d8bd584/qiskit/transpiler/passes/synthesis/unitary_synthesis.py#L779-L781

I don't think the others do. But you have to dig through the code to see if it could possibly return None. This makes me think an explicit status may be warranted.