Methodology for Retargetability Assessment of Quantum Compilers
Full text
Study: Investigating Compiler Retargetability Set of provided hints Luke Southall, [email protected] Domenik Eichhorn, [email protected] Joshua Ammermann, [email protected] Rinor Kelmendi, [email protected] Prof. Dr. Ina Schaefer, [email protected] 1 Tket Hint 1: Backend Implementation Guide To get started with your backend implementation, you’ll need to follow the TKET backend implementation guide. This guide contains relevant functions you will need to implement to fulfill the task, as well as the code to the bell test function used later. Resource: TKET Custom Backend Guide Hint 2: List of Necessary Functions The following functions are required for a complete backend implementation: •init •result id type •circuit status •required predicates •default compilation pass •backend info •tk to stim •process circuits •Additionally you will need to set the rebase pass property. Hint 3: TKET to Stim Function This function converts a TKET circuit to a Stim circuit. It maps TKET gate operations to their Stim equivalents. def tk_to_stim (self , tkc : Circuit ) -> stim . Circuit : circuit = stim . Circuit () gate_map = { OpType .X: "X", 1
OpType .Y: "Y", OpType .Z: "Z", OpType .H: "H", OpType .CX : " CNOT ", } for command in tkc: optype = command .op. type if optype in gate_map : qbs = [ tkc . qubits . index ( cast (" Qubit ", arg )) for arg in command . args ] circuit . append ( gate_map [ optype ], qbs ) elif optype == OpType . Measure: qb , cb = command .args circuit . append ("M", [ tkc . qubits . index (qb) ]) else: raise ValueError (f" Cannot convert optype to MyCircuit : { optype }") return circuit Hint 4: Result ID Type Function This property defines the identifier type signature for ResultHandle for this backend. @property def _result_id_type(self) -> _ResultIdTuple: """ Identifier type signature for ResultHandle for this backend . : return : Type signature ( tuple of hashable types ) """ return (str ,) Hint 5: Circuit Status Function This function returns a CircuitStatus reporting the status of the circuit execution corresponding to the ResultHandle. def circuit_status (self , handle : ResultHandle ) -> CircuitStatus : """ Return a CircuitStatus reporting the status of the circuit execution corresponding to the ResultHandle """ if handle in self . _cache: return CircuitStatus ( StatusEnum . COMPLETED ) raise CircuitNotRunError ( handle ) Hint 6: Required Predicates Function This property defines the minimum set of predicates that a circuit must satisfy before it can be successfully run on this backend. @property def required_predicates ( self ) -> list [ Predicate ]: """ The minimum set of predicates that a circuit must satisfy before it can 2
be successfully run on this backend . : return : Required predicates . """ preds = [ GateSetPredicate( { OpType.X, OpType.Y, OpType.Z, OpType.H, OpType .CX , OpType . Measure , } ), ] return preds Hint 7: Rebase Pass The rebase pass is used to convert circuits into the gate set supported by the backend. This implementation defines custom single-qubit and two-qubit gate decompositions. def sq(a, b, c): circ = Circuit (1) if c != 0: circ .Rz (c , 0) if b != 0: circ .Rx (b , 0) if a != 0: circ .Rz (a , 0) return circ rebase = RebaseCustom ({ OpType .X , OpType .Y, OpType .Z, OpType .H, OpType .CX , OpType . Measure }, Circuit (1) , sq) rebase_pass = rebase Hint 8: Default Compilation Pass Function This function provides a suggested compilation pass that guarantees the resulting circuit will be suitable to run on this backend. def default_compilation_pass (self , optimisation_level : int = 1) -> BasePass : seq = [ DecomposeBoxes ()] # Decompose boxes into basic gates seq . append ( self . rebase ) return SequencePass ( seq ) Hint 9: Backend Info Function This property returns metadata about the backend, including its name, version, supported gate set, and architecture. 3
@property def backend_info ( self ) -> BackendInfo : return BackendInfo ( " MyBackend ", "MySimulator", "1.0" , FullyConnected(4), { OpType.X, OpType.Y, OpType.Z, OpType.H, OpType .CX , OpType . Measure , }, supports_midcircuit_measurement=False, misc ={" characterisation ": None}, ) Hint 10: Process Circuits Function This is the core function that submits circuits to the backend for execution and stores results in the cache. def process_circuits( self, circuits : Iterable [ Circuit ], n_shots : Optional [ int] = None , valid_check : bool = True , ** kwargs : KwargTypes , ) -> list[ ResultHandle ]: """ Submit circuits to the backend for running . The results will be stored in the backend ’s result cache to be retrieved by the corresponding get_ <data > method. Use keyword arguments to specify parameters to be used in submitting circuits See specific Backend derived class for available parameters , from the following list: * ‘seed ‘: RNG seed for simulators : param circuits : Circuits to process on the backend . : param n_shots : Number of shots to run per circuit. None is to be used for state / unitary simulators . Defaults to None . : param valid_check : Explicitly check that all circuits satisfy all required predicates to run on the backend. Defaults to True : return : Handles to results for each input circuit , as an interable in the same order as the circuits . """ circuit_list = list ( circuits ) if valid_check : self . _check_all_circuits ( circuit_list ) handle_list = [] for circuit in circuit_list : handle = ResultHandle ( str ( uuid4 ())) mycirc = self. tk_to_stim ( circuit ) tab = stim . Tableau . from_circuit ( mycirc ) 4
state = tab . to_state_vector () state *= np. exp (1j * np. pi * circuit . phase ) implicit_perm = circuit . implicit_qubit_permutation () res_qubits = [ implicit_perm [qb] for qb in sorted ( circuit . qubits , reverse = True )] res = BackendResult ( q_bits = res_qubits , state = state ) self . _cache [ handle ] = {" result ": res } handle_list . append ( handle ) return handle_list Hint 11: Test Backend This test function verifies that the backend correctly implements a Bell state circuit. import numpy as np import pytest def test_bell () -> None: c = Circuit (2) c.H(0) c.CX(0, 1) b = StimStudyBackend() c = b.get_compiled_circuit(c) h = b. process_circuit (c) assert np . allclose ( b. get_result (h).get_state () , np. asarray ([1 , 0, 0, 1]) * 1 / np. sqrt (2) ) if __name__ == ’__main__ ’: test_bell () Hint 12: How to Use Stim This hint explains how to use the Stim library’s Tableau functionality to execute circuits and obtain state vectors. import stim # You can use Tableau , which is compiled from a # stim circuit instance. Tableau supports the # operation to_state_vector () , which effectively # executes the circuit and returns the resulting # state vector . Keep in mind , this does not work # with circuits containing Measure gates . tab = stim . Tableau . from_circuit ( mycirc ) state = tab . to_state_vector () 5
2 Qiskit Hint 1: Backend Implementation Guide To get started with your backend implementation, you’ll need to follow the Qiskit backend implementation guide. This guide contains relevant pointers you will need to implement to fulfill the task. Resource: Qiskit Custom Transpiler Guide Hint 2: List of Necessary Functions The following functions are required for a complete backend implementation: •init •target •max circuits •default options •qiskit to stim circuit •run Hint 3: Qiskit to Stim Function This function converts a Qiskit circuit to a Stim circuit. It maps Qiskit gate operations to their Stim equivalents. def _qiskit_to_stim_circuit (self , qc ): stim_circuit = stim . Circuit () gate_mapping = { ’h’: ’H’, ’x ’: ’X’, ’y ’: ’Y’, ’z ’: ’Z’, ’cx ’: ’CNOT ’ } for instr in qc.data: op_name = instr . operation . name . lower () qubit_ids = [ qc . find_bit (q). index for q in instr . qubits ] if op_name in gate_mapping : stim_circuit . append ( gate_mapping [ op_name ], qubit_ids ) else: raise ValueError (f" Unsupported gate : { op_name }") return stim_circuit Hint 4: Init Function The initialization function sets up the backend with its target gate set and default options. 6
def __init__ ( self ): super (). __init__ ( name=’ stim_simulator ’, description =’ Stim quantum simulator backend ’) self . _target = self. _build_target () self . _options = self. _default_options () def _build_target (self ): target = Target (" Stim simulator target ") target . add_instruction ( HGate () ) target . add_instruction ( XGate () ) target . add_instruction ( YGate () ) target . add_instruction ( ZGate () ) target . add_instruction ( CXGate ()) return target Hint 5: Target Function This property returns the target object that defines the supported gate set. @property def target ( self ): return self . _target Hint 6: Max Circuits Function This property defines the maximum number of circuits that can be run in a single job. Returning None means unlimited. @property def max_circuits ( self ): return None Hint 7: Default Options Function This class method returns the default options for the backend. @classmethod def _default_options ( cls ): return Options () Hint 8: Run Function This is the main execution function that processes circuits and returns a job object. def run (self , run_input , ** options ): if hasattr ( run_input , ’ __iter__ ’) and not isinstance ( run_input , str ): circuits = run_input else: circuits = [ run_input ] job = PrimitiveJob ( self ._run , circuits ) job . _submit () return job 7
def _run (self , circuits ): results = [] for circuit in circuits : stim_circuit = self . _qiskit_to_stim_circuit ( circuit ) tab = stim. Tableau . from_circuit ( stim_circuit ) state = tab . to_state_vector () results . append ( state ) return PrimitiveResult ( results ) Hint 9: Hints for Guide The backend implementation guide is quite overcrowded. It specifies a very specific example with a lot of parameters, which we do not need here. The task is to create a simple implementation. You do not need to provide parameters when adding gates to your target. You do not need to use a shot-based approach. You can pass an empty options object for the default options. You do not need to specify a value for max circuits. Try to write a converter function from Qiskit to stim circuit, run this circuit on stim, and return the state vector result. Hint 10: Run Function Return Type To use the test provided on the task sheet, it is advisable that the run function returns a Job. You can use the PrimitiveJob from qiskit.primitives combined with a private run function to achieve this in the run function. job = PrimitiveJob ( self ._run , ...) job . _submit () return job The private run function should return a Result. For this, you can use the PrimitiveResult from qiskit.primitives.containers. return PrimitiveResult (...) Hint 11: Test Backend This test function verifies that the backend correctly implements a Bell state circuit. from qiskit import QuantumCircuit def test_bell () -> None: backend = StimStudyBackend () qc = QuantumCircuit(2) qc.h(0) qc.cx(0 , 1) job = backend .run (qc) result = job.result() assert np . allclose ( result [0] , np . asarray ([1 , 0, 0, 1]) * 1 / np. sqrt (2) ) or np. allclose ( result , np. asarray ([1 , 0, 0, 1]) * 1 / np. sqrt (2)) if __name__ == ’__main__ ’: test_bell () 8
Hint 12: How to Use Stim This hint explains how to use the Stim library’s Tableau functionality to execute circuits and obtain state vectors. import stim # You can use Tableau , which is compiled from a # stim circuit instance. Tableau supports the # operation to_state_vector () , which effectively # executes the circuit and returns the resulting # state vector . Keep in mind , this does not work # with circuits containing Measure gates . tab = stim . Tableau . from_circuit ( mycirc ) state = tab . to_state_vector () 3 ProjectQ Hint 1: Backend Implementation Guide ProjectQ does not provide a backend implementation guide. Therefore, you may study implemented ProjectQ backends found on the ProjectQ GitHub page. Resource: ProjectQ Backends Hint 2: List of Necessary Functions This is the list of functions used in the solution. Your solution may vary. •init •reset •is available •store •get result •run •receive Hint 3: ProjectQ to Stim Function This function converts ProjectQ commands to Stim circuit operations. It handles gate mapping and circuit construction. def _store ( self , cmd ): if self . _clear : self . _stim_circuit = stim. Circuit () self . _allocated_qubits = set () 9