Branch Optimization

We optimize the conditional branching statements using the following techniques:

  1. Branch Elimination: We evaluate the outcome of the branch condition, if it is known at compile time and does not contain measurement results of qubits. We remove the branch statement and if it evaluates to a true value, attach the corresponding block of code to the main program.
import pyqasm

qasm_code = """
OPENQASM 3.0;
include "stdgates.inc";
qubit[1] q;
bit[1] c;
int[32] a = 0;
if(a > 0){
h q[0];
}
if(a < 0){
x q[0];
}
if(a == 0){
y q[0];
measure q -> c;
}
"""
module = pyqasm.loads(qasm_code)
module.unroll()

print(pyqasm.dumps(module))

  1. Branch Unfolding: If the branch condition contains measurement results of qubits, we unfold the classical registers into individual bits and insert equivalent conditional statements for each bit. This method is particularly useful for systems which do not support multi-bit classical registers in conditional statements.
import pyqasm

qasm_code = """OPENQASM 3.0;
include "stdgates.inc";
qubit[1] q;
bit[4] c;
if(c == 3){
h q[0];
}
if(c >= 3){
h q[0];
} else {
x q[0];
}
if(c <= 3){
h q[0];
} else {
x q[0];
}
if(c < 4){
h q[0];
} else {
x q[0];
}
"""
module = pyqasm.loads(qasm_code)
module.unroll()

print(pyqasm.dumps(module))

Switch Case Optimization

The openqasm reference enforces the switch targets to be of type int and the cases to be unique integer literals or constant integer expressions. This implies that the switch case statements can be optimized at compile time as long as the target variable is not dependent on a measurement result.

Once the target variable is evaluated at compile time, the switch statement is removed and the corresponding switch case code block is attached to the main program.

import pyqasm

qasm_code = """
OPENQASM 3.0;
include "stdgates.inc";

    const int i = 1;
    qubit q;

    switch(i) {
    case 1,3,5,7 {
        int j = 4; // definition inside scope
        switch(j) {
            case 1,3,5,7 {
                x q;
            }
            case 2,4,6,8 {
                j = 5; // assignment inside scope
                y q; // this will be executed
            }
            default {
                z q;
            }
        }
    }
    case 2,4,6,8 {
        y q;
    }
    default {
        z q;
    }
    }

"""
module = pyqasm.loads(qasm_code)
module.unroll()

print(pyqasm.dumps(module))

Subroutine Inlining

Subroutine inlining is the process of replacing a subroutine call with the actual code of the subroutine. Since quantum devices do not support subroutines, it is essential to inline the subroutine code against the call to the subroutine. Here, the formal parameters of the subroutine are replaced with the actual parameters used in the subroutine call. Moreover, the subroutine code is inserted at the location of the subroutine call with careful consideration of the scope of the variables used in the subroutine.

import pyqasm

qasm_code = """OPENQASM 3.0;
include "stdgates.inc";

    gate my_gate(a) q2 {
        rx(a) q2;
    }

    def my_function(qubit a, float[32] b) {
        float[64] c = 2*b;
        my_gate(b) a;
        my_gate(c) a;
        return;
    }
    qubit q;
    float[32] r = 3.14;
    my_function(q, r);

"""
module = pyqasm.loads(qasm_code)
module.unroll()

print(pyqasm.dumps(module))

Loop Unrolling

Loop unrolling is the process of unfolding a loop with a list of equivalent instructions. This is particularly important for quantum programs as quantum computers do not support direct execution of loops. We provide support for unrolling while and for loops -

  1. while Loop
    • We evaluate the loop conditions at compile time and visit the loop body till the condition becomes false.
import pyqasm

qasm_code = """
OPENQASM 3.0;
qubit[4] q;
int i = 0;
while (i < 3) {
h q[i];
cx q[i], q[i+1];
i += 1;
}

"""
module = pyqasm.loads(qasm_code)
module.unroll()

print(pyqasm.dumps(module))

  • To ensure that the unrolling process does not go into an infinite loop, we have introduced a max_loop_iters parameter. It has a default value of 1e9 and allows the user to control the max number of loop iterations -
import pyqasm

qasm_code = """
OPENQASM 3.0;
qubit[100] q;
int i = 0;
while (i < 50) {
h q[i];
cx q[i], q[i+1];
i += 1;
}

"""
module = pyqasm.loads(qasm_code)

# set max loop iterations to 10

module.unroll(max_loop_iters=10)

print(pyqasm.dumps(module))

  • This unrolling process is not applicable in a case when the while condition is dependent on a quantum measurement result. Since the truth value of the condition can only be known at runtime, an exception is raised while unrolling such loops -
import pyqasm

qasm_code = """
OPENQASM 3.0;
qubit q;
bit c;
c = measure q;
while (c) {
h q;
c = measure q;
}

"""
module = pyqasm.loads(qasm_code)
module.unroll()

print(pyqasm.dumps(module))

Unrolling while loops which contain quantum measurements is, in fact, an active area of development. Some researchers have proposed to identify such loops and convert them into a native while loop instructions which can be executed during runtime. However, this approach has not been fully implemented yet.

  1. for Loop
    • The process is similar to the while loop unrolling process, but the loop counter is chosen from the range provided by the user. The loop body is executed as many times as the range value -
import pyqasm

qasm_code = """
OPENQASM 3.0;
include "stdgates.inc";

qubit[4] q;
bit[4] c;

h q;
for int i in [0:2]{
cx q[i], q[i+1];
}
c = measure q;
"""
module = pyqasm.loads(qasm_code)
module.unroll()

print(pyqasm.dumps(module))

Branch Optimization

We optimize the conditional branching statements using the following techniques:

  1. Branch Elimination: We evaluate the outcome of the branch condition, if it is known at compile time and does not contain measurement results of qubits. We remove the branch statement and if it evaluates to a true value, attach the corresponding block of code to the main program.
import pyqasm

qasm_code = """
OPENQASM 3.0;
include "stdgates.inc";
qubit[1] q;
bit[1] c;
int[32] a = 0;
if(a > 0){
h q[0];
}
if(a < 0){
x q[0];
}
if(a == 0){
y q[0];
measure q -> c;
}
"""
module = pyqasm.loads(qasm_code)
module.unroll()

print(pyqasm.dumps(module))

  1. Branch Unfolding: If the branch condition contains measurement results of qubits, we unfold the classical registers into individual bits and insert equivalent conditional statements for each bit. This method is particularly useful for systems which do not support multi-bit classical registers in conditional statements.
import pyqasm

qasm_code = """OPENQASM 3.0;
include "stdgates.inc";
qubit[1] q;
bit[4] c;
if(c == 3){
h q[0];
}
if(c >= 3){
h q[0];
} else {
x q[0];
}
if(c <= 3){
h q[0];
} else {
x q[0];
}
if(c < 4){
h q[0];
} else {
x q[0];
}
"""
module = pyqasm.loads(qasm_code)
module.unroll()

print(pyqasm.dumps(module))

Switch Case Optimization

The openqasm reference enforces the switch targets to be of type int and the cases to be unique integer literals or constant integer expressions. This implies that the switch case statements can be optimized at compile time as long as the target variable is not dependent on a measurement result.

Once the target variable is evaluated at compile time, the switch statement is removed and the corresponding switch case code block is attached to the main program.

import pyqasm

qasm_code = """
OPENQASM 3.0;
include "stdgates.inc";

    const int i = 1;
    qubit q;

    switch(i) {
    case 1,3,5,7 {
        int j = 4; // definition inside scope
        switch(j) {
            case 1,3,5,7 {
                x q;
            }
            case 2,4,6,8 {
                j = 5; // assignment inside scope
                y q; // this will be executed
            }
            default {
                z q;
            }
        }
    }
    case 2,4,6,8 {
        y q;
    }
    default {
        z q;
    }
    }

"""
module = pyqasm.loads(qasm_code)
module.unroll()

print(pyqasm.dumps(module))

Subroutine Inlining

Subroutine inlining is the process of replacing a subroutine call with the actual code of the subroutine. Since quantum devices do not support subroutines, it is essential to inline the subroutine code against the call to the subroutine. Here, the formal parameters of the subroutine are replaced with the actual parameters used in the subroutine call. Moreover, the subroutine code is inserted at the location of the subroutine call with careful consideration of the scope of the variables used in the subroutine.

import pyqasm

qasm_code = """OPENQASM 3.0;
include "stdgates.inc";

    gate my_gate(a) q2 {
        rx(a) q2;
    }

    def my_function(qubit a, float[32] b) {
        float[64] c = 2*b;
        my_gate(b) a;
        my_gate(c) a;
        return;
    }
    qubit q;
    float[32] r = 3.14;
    my_function(q, r);

"""
module = pyqasm.loads(qasm_code)
module.unroll()

print(pyqasm.dumps(module))

Loop Unrolling

Loop unrolling is the process of unfolding a loop with a list of equivalent instructions. This is particularly important for quantum programs as quantum computers do not support direct execution of loops. We provide support for unrolling while and for loops -

  1. while Loop
    • We evaluate the loop conditions at compile time and visit the loop body till the condition becomes false.
import pyqasm

qasm_code = """
OPENQASM 3.0;
qubit[4] q;
int i = 0;
while (i < 3) {
h q[i];
cx q[i], q[i+1];
i += 1;
}

"""
module = pyqasm.loads(qasm_code)
module.unroll()

print(pyqasm.dumps(module))

  • To ensure that the unrolling process does not go into an infinite loop, we have introduced a max_loop_iters parameter. It has a default value of 1e9 and allows the user to control the max number of loop iterations -
import pyqasm

qasm_code = """
OPENQASM 3.0;
qubit[100] q;
int i = 0;
while (i < 50) {
h q[i];
cx q[i], q[i+1];
i += 1;
}

"""
module = pyqasm.loads(qasm_code)

# set max loop iterations to 10

module.unroll(max_loop_iters=10)

print(pyqasm.dumps(module))

  • This unrolling process is not applicable in a case when the while condition is dependent on a quantum measurement result. Since the truth value of the condition can only be known at runtime, an exception is raised while unrolling such loops -
import pyqasm

qasm_code = """
OPENQASM 3.0;
qubit q;
bit c;
c = measure q;
while (c) {
h q;
c = measure q;
}

"""
module = pyqasm.loads(qasm_code)
module.unroll()

print(pyqasm.dumps(module))

Unrolling while loops which contain quantum measurements is, in fact, an active area of development. Some researchers have proposed to identify such loops and convert them into a native while loop instructions which can be executed during runtime. However, this approach has not been fully implemented yet.

  1. for Loop
    • The process is similar to the while loop unrolling process, but the loop counter is chosen from the range provided by the user. The loop body is executed as many times as the range value -
import pyqasm

qasm_code = """
OPENQASM 3.0;
include "stdgates.inc";

qubit[4] q;
bit[4] c;

h q;
for int i in [0:2]{
cx q[i], q[i+1];
}
c = measure q;
"""
module = pyqasm.loads(qasm_code)
module.unroll()

print(pyqasm.dumps(module))