diff --git a/qiskit_alice_bob_provider/plugins/state_preparation.py b/qiskit_alice_bob_provider/plugins/state_preparation.py index 8e7e018..f068531 100644 --- a/qiskit_alice_bob_provider/plugins/state_preparation.py +++ b/qiskit_alice_bob_provider/plugins/state_preparation.py @@ -13,14 +13,13 @@ # See the License for the specific language governing permissions and # limitations under the License. ############################################################################## - - +from copy import deepcopy from typing import Any, Callable, Dict import numpy as np from qiskit.circuit import ControlFlowOp, Instruction, Qubit, Reset from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary -from qiskit.circuit.library import Initialize +from qiskit.circuit.library import Initialize, RZGate from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.dagcircuit import DAGCircuit, DAGInNode, DAGOpNode from qiskit.transpiler import ( @@ -30,6 +29,8 @@ TranspilerError, ) from qiskit.transpiler.passes import ( + BasisTranslator, + HighLevelSynthesis, TrivialLayout, UnitarySynthesis, UnrollCustomDefinitions, @@ -40,7 +41,6 @@ ) from qiskit.transpiler.preset_passmanagers.common import ( generate_embed_passmanager, - generate_translation_passmanager, ) from qiskit.transpiler.preset_passmanagers.plugin import PassManagerStagePlugin @@ -49,6 +49,29 @@ def _reset_prep() -> Instruction: return Reset() +def _get_unitary_synthesis(config: PassManagerConfig): + return CustomUnitarySynthesis( + config.basis_gates, + approximation_degree=config.approximation_degree, + coupling_map=config.coupling_map, + backend_props=config.backend_properties, + plugin_config=config.unitary_synthesis_plugin_config, + method=config.unitary_synthesis_method, + target=config.target, + ) + + +def _get_high_level_synthesis(config: PassManagerConfig): + return HighLevelSynthesis( + hls_config=config.hls_config, + coupling_map=config.coupling_map, + target=config.target, + use_qubit_indices=True, + equivalence_library=SessionEquivalenceLibrary, + basis_gates=config.basis_gates, + ) + + class EnsurePreparationPass(TransformationPass): """ A transpilation pass that ensures there is a state preparation at the @@ -305,6 +328,7 @@ def pass_manager( custom_pm.append(IntToLabelInitializePass()) custom_pm.append(BreakDownInitializePass()) + # Can probably be removed (replaced by HighLevelSynthesis?) custom_pm.append( UnrollCustomDefinitions( equivalence_library=SessionEquivalenceLibrary, @@ -313,41 +337,87 @@ def pass_manager( ) ) - default_pm = generate_translation_passmanager( - target=pass_manager_config.target, - basis_gates=pass_manager_config.basis_gates, - method='translator', - approximation_degree=pass_manager_config.approximation_degree, - coupling_map=pass_manager_config.coupling_map, - backend_props=pass_manager_config.backend_properties, - unitary_synthesis_method=( - pass_manager_config.unitary_synthesis_method - ), - unitary_synthesis_plugin_config=( - pass_manager_config.unitary_synthesis_plugin_config + # Replace passes from qiskit generate_translation_passmanager() with + # passes that work for us. + # By default, qiskit returns + # [UnitarySynthesis, HighLevelSynthesis, BasisTranslator] + # In our case, we need + # 1. to replace UnitarySynthesis with our CustomUnitarySynthesis class + # 2. to adapt the passes to handle the "Clifford + T gate basis" case + + if 'rz' in pass_manager_config.target: + # In this case, no need to modify the BasisTranslator pass and to + # force the SK synthesis after. + need_synthesis = False + basis_translator_target = pass_manager_config.target + else: + # In this case, our basis target consists of Clifford + T gate, + # i.e. {cx, h, s, t}. It does not contain unitary rotations + # (e.g. rz). + # + # This is a universal set of gates, but it requires synthesis + # (e.g. with SK algorithm), for transpilation to succeed. This + # synthesis (that transforms rotations into discrete gates of our + # basis target) is done during the CustomUnitarySynthesis pass. + # + # Unfortunately, qiskit BasisTranslator pass does not handle + # synthesis. It tries to match existing gates in the circuit to + # gates in the target basis, but only for exact equivalence (doing + # a graph search). + # + # Therefore, as a workaround, when the target basis does not + # support unitary rotations natively, we + # - add the 'rz' to the BasisTranslator target basis set, to + # trick the algorithm into thinking that rotations are + # supported by the target. + # - add a 2nd CustomUnitarySynthesis pass after that, to get + # rid of any 'rz' pass generated in the pass above. + # + # Example : + # > Input circuit ('cs') + # q_0: ──■── + # ┌─┴─┐ + # q_1: ┤ S ├ + # └───┘ + # + # > output of BasisTranslator, with 'rz' in target : + # ┌─────────┐ + # q_0: ┤ Rz(π/4) ├──■────────────────■───────────── + # └─────────┘┌─┴─┐┌──────────┐┌─┴─┐┌─────────┐ + # q_1: ───────────┤ X ├┤ Rz(-π/4) ├┤ X ├┤ Rz(π/4) ├ + # └───┘└──────────┘└───┘└─────────┘ + # + # > output of BasisTranslator, without 'rz' in target : + # TranspilerError: "Unable to translate the operations..." + # + # > output of the 2nd CustomUnitarySynthesis : + # ┌───┐ + # q_0: ┤ T ├──■───────────■─────── + # └───┘┌─┴─┐┌─────┐┌─┴─┐┌───┐ + # q_1: ─────┤ X ├┤ Tdg ├┤ X ├┤ T ├ + # └───┘└─────┘└───┘└───┘ + need_synthesis = True + + # Ideally, we would use the `target_basis` argument of + # BasisTranslator to specify we want to add the "rz" gate. + # Unfortunately, in the .run method, this argument is ignored in + # favor of the target.target_basis when the target is defined. + # Therefore, we create a copy of the current target and modify + # its set of supported instructions just for this pass. + basis_translator_target = deepcopy(pass_manager_config.target) + basis_translator_target.add_instruction(RZGate(0)) + + custom_pm.append(_get_unitary_synthesis(pass_manager_config)) + custom_pm.append(_get_high_level_synthesis(pass_manager_config)) + custom_pm.append( + BasisTranslator( + SessionEquivalenceLibrary, + pass_manager_config.basis_gates, + basis_translator_target, ), - hls_config=pass_manager_config.hls_config, ) - # pylint: disable=protected-access - for task in default_pm._tasks: - for subtask in task: - # Substitue the default UnitarySynthesis with our - # custom implementation. - if isinstance(subtask, UnitarySynthesis): - custom_pm.append( - # pylint: disable=line-too-long - CustomUnitarySynthesis( - basis_gates=pass_manager_config.basis_gates, - approximation_degree=pass_manager_config.approximation_degree, # noqa: E501 - coupling_map=pass_manager_config.coupling_map, - backend_props=pass_manager_config.backend_properties, # noqa: E501 - method=pass_manager_config.unitary_synthesis_method, # noqa: E501 - plugin_config=pass_manager_config.unitary_synthesis_plugin_config, # noqa: E501 - target=pass_manager_config.target, - ) - ) - continue - custom_pm.append(subtask) + if need_synthesis: + custom_pm.append(_get_unitary_synthesis(pass_manager_config)) custom_pm += generate_embed_passmanager( pass_manager_config.coupling_map diff --git a/tests/local/test_backend.py b/tests/local/test_backend.py index 246e4cb..4b7bd94 100644 --- a/tests/local/test_backend.py +++ b/tests/local/test_backend.py @@ -1,10 +1,13 @@ +import sys from typing import List import numpy as np import pytest from qiskit import QuantumCircuit, transpile -from qiskit.circuit.library import Initialize +from qiskit.circuit import Instruction +from qiskit.circuit.library import Initialize, get_standard_gate_name_mapping +from qiskit_alice_bob_provider import AliceBobLocalProvider from qiskit_alice_bob_provider.local.backend import ProcessorSimulator from qiskit_alice_bob_provider.processor.logical_cat import LogicalCatProcessor from qiskit_alice_bob_provider.processor.physical_cat import ( @@ -116,6 +119,74 @@ def test_synthesize_cz() -> None: assert len(transpiled.get_instructions('h')) == 2 +def test_all_gates(): + """Test transpilation for all basis gates""" + provider = AliceBobLocalProvider() + backend = provider.get_backend('EMU:40Q:LOGICAL_TARGET') + + qiskit_gates = get_standard_gate_name_mapping() + skip_gates = [ + # Trying to access this gates from the circuit attributes returns an + # error "'QuantumCircuit' object has no attribute '...'" + 'c3sx', + 'cu1', + 'cu3', + 'xx_minus_yy', + 'xx_plus_yy', + 'u1', + 'u2', + 'u3', + # This one is not a gate, just a float. + 'global_phase', + ] + if sys.platform == 'darwin': + # For some reason, on macOS we have numerical instabilities with the + # Solovay Kitaev synthesis, with specific angles. + # For instance, a simple 1Q circuit with a RZ(5pi/4) gate fails to + # transpile with our logical backends, and typically raises : + # ValueError('Input matrix is not orthogonal.') + # As a result, the synthesis currently fails for the gates below (this + # needs to be fixed). + skip_gates += [ + 'cry', + 'rccx', + 'rcccx', + ] + + def create_circuit_with_gate(instruction: Instruction): + if instruction.params: + # Most parameters are angles -> use pi/5. + # Except for the delay instruction, which expects an integer for + # param 't' -> use 10. + params = [ + 10 if p.name == 't' else np.pi / 5 for p in instruction.params + ] + else: + params = [] + circuit = QuantumCircuit( + instruction.num_qubits, instruction.num_clbits + ) + args = ( + params + + list(range(instruction.num_qubits)) + + list(range(instruction.num_clbits)) + ) + # apply the gate + getattr(circuit, instruction.name)(*args) + return circuit + + errors = [] + for name, i in qiskit_gates.items(): + if name in skip_gates: + continue + try: + circ = create_circuit_with_gate(i) + _ = transpile(circ, backend=backend) + except Exception as e: # pylint: disable=broad-exception-caught + errors.append((name, e)) + assert not errors + + def test_do_nothing_on_mx() -> None: backend = ProcessorSimulator( SimpleAllToAllProcessor(), translation_stage_plugin='sk_synthesis'