NISQCompiler: a all-in-one blackbox compiler¶

Say you would like to have a compiler that is able to target hardware with limited connectivity and limited gate set, but you don't want to spend time building your own QLM stack using an ad hoc composition of PatternManagers, Nnizers, or LazySynthesis.

Then NISQCompiler is perfect for you. It is simply a high level plugins that utilizes different QLM plugins to:

  • make sure to break down large gates in the input quantum circuits (such as CCNOT gates)
  • compiles and optimizes the circuit for the target connectivity
  • rewrites the circuit to match the target gate set

The supported gate sets encompass:

  • IBM's gate set (CNOT + U3)
  • Trapped ions gate set ($XX(\pi/2)$ + $X(\pi/2)$ + $RZ(\theta)$) and much more.

We will now give two examples: one targetting a IBM like device, and the other a trapped ion like device.

IBM-like device¶

First, let us pick a job to compile. I prepared a job containing a VQE job for LiH.

In [1]:
from qat.core import Job, Batch

job = Job.load('./vqe.qlm')
print('Gates in the circuit:', job.circuit.statistics()['gates'])
Gates in the circuit: {'custom gate': 0, 'X': 2, 'RX': 40, 'H': 40, 'CNOT': 64, 'PH': 12}

Now, we need a target connectivity. We will use IBM's Melbourne template that is already pre-programmed in the QLM:

In [2]:
from qat.devices import IBM_MELBOURNE as device

We are now ready to declare our compiler:

In [3]:
from qat.plugins import NISQCompiler

compiler = NISQCompiler(target_gate_set='IBM')

And, that's it :)

Let's compile our job !

In [4]:
batch = Batch(jobs=[job])

compiled_job = compiler.compile(batch, device).jobs[0]
print('Initial circuit contains:', job.circuit.statistics()['gates'])
print('Final circuit contains:', compiled_job.circuit.statistics()['gates'])
Initial circuit contains: {'custom gate': 0, 'X': 2, 'RX': 40, 'H': 40, 'CNOT': 64, 'PH': 12}
Final circuit contains: {'custom gate': 0, 'U3': 28, 'CNOT': 32}

Note Default compilation args can be retrieved using

NISQCompiler.DEFAULT_COMPILATION_ARGS

Notice how the final circuit is much shorter than the initial one (only 32 CNOTs). We can push it by telling the compilation stage of the plugin to try harder:

In [5]:
my_arguments = {
    'depth': 2, 
    'merge': True, 
    'optimize_initial': True, 
    'bidirectional': True,
    'timeout': 5
}
compiler = NISQCompiler(compiler_options=my_arguments, target_gate_set='IBM')
compiled_job_pushed = compiler.compile(batch, device).jobs[0]
print('Initial circuit contains:', job.circuit.statistics()['gates'])
print('Final circuit contains:', compiled_job.circuit.statistics()['gates'])
print('Final (pushed) circuit contains:', compiled_job_pushed.circuit.statistics()['gates'])
Initial circuit contains: {'custom gate': 0, 'X': 2, 'RX': 40, 'H': 40, 'CNOT': 64, 'PH': 12}
Final circuit contains: {'custom gate': 0, 'U3': 28, 'CNOT': 32}
Final (pushed) circuit contains: {'custom gate': 0, 'U3': 16, 'CNOT': 15}

We went down to 21 CNOTs ! Not bad.

Trapped ion like device¶

Let us define a compiler for a all-to-all connectivity with the gate set of a trapped ion processor.

In [6]:
from qat.devices import AllToAll
compiler = NISQCompiler(target_gate_set=['XX', 'RX+', 'RZ'])
device = AllToAll(job.circuit.nbqbits)
In [7]:
compiled_job = compiler.compile(batch, device).jobs[0]
print('Initial circuit contains:', job.circuit.statistics()['gates'])
print('Final circuit contains:', compiled_job.circuit.statistics()['gates'])
Initial circuit contains: {'custom gate': 0, 'X': 2, 'RX': 40, 'H': 40, 'CNOT': 64, 'PH': 12}
Final circuit contains: {'custom gate': 0, 'RX': 74, 'RZ': 84, 'XX': 17}
In [8]:
my_arguments = {
    'depth': 2, 
    'merge': True, 
    'optimize_initial': True, 
    'bidirectional': True,
    'timeout': 5
}
compiler = NISQCompiler(compiler_options=my_arguments, target_gate_set=['XX', 'RX+', 'RZ'])
compiled_job_pushed = compiler.compile(batch, device).jobs[0]
print('Initial circuit contains:', job.circuit.statistics()['gates'])
print('Final circuit contains:', compiled_job.circuit.statistics()['gates'])
print('Final (pushed) circuit contains:', compiled_job_pushed.circuit.statistics()['gates'])
Initial circuit contains: {'custom gate': 0, 'X': 2, 'RX': 40, 'H': 40, 'CNOT': 64, 'PH': 12}
Final circuit contains: {'custom gate': 0, 'RX': 74, 'RZ': 84, 'XX': 17}
Final (pushed) circuit contains: {'custom gate': 0, 'RX': 56, 'RZ': 70, 'XX': 12}

Since the device has a all-to-all connectivity, the compiler can further optimize the input circuit and bring the entangling gate count down to 12 gates. However, we can observe an increase in the number of single qubit gates since the single qubit gates allowed in this architecture are less expressive.