Skip to content

Qmod Tutorial - Part 1

View on GitHub

In this tutorial, we will cover the basics of the Qmod language and its accompanying library. We will learn to use quantum variables, functions, and operators.

Let's begin with a simple code example:

from classiq import *

@qfunc
def foo(q: QBit) -> None:
    X(q)
    H(q)

@qfunc
def main(q: Output[QBit]) -> None:
    allocate(q)
    foo(q)

qmod  = create_model(main)

Function foo takes the quantum parameter q of type QBit and applies X gate to it, followed by H gate. Function main declares a single Output parameter q of type QBit. It first allocates a qubit to q, then calls foo to operate on it. A representation of the model qmod is created based on function main, so that it can later be synthesized and executed.

What results do we expect when executing this model?

  • By calling allocate, q is initialized in the default state \(|0\rangle\).

  • Then, foo is called:

  • It applies X (NOT gate), changing q's state to \(|1\rangle\).

  • Then it applies H (Hadamard gate), resulting in the superposition \(\frac{1}{\sqrt{2}} (|0\rangle - |1\rangle)\).

When executing the model, the output variable q is sampled. We can run the following code and make sure that the states \(|0\rangle\) and \(|1\rangle\) are sampled roughly equally:

job = execute(synthesize(qmod))
job.get_sample_result().parsed_counts
[{'q': 1}: 1032, {'q': 0}: 1016]

Qmod Fundementals

The simple model above demonstrates several features that are essential in every Qmod code:

The @qfunc Decorator

Qmod is a quantum programming language embedded in Python. The decorator @qfunc designates a quantum function, so that it can be processed by the Qmod tool chain. The Python function is executed at a later point to construct the Qmod representation. This decorator is used for every Qmod function definition.

Function main

A complete Qmod model, that is, a description that can be synthesized and executed, must define a function called main. Function main is the quantum entry point - it specifies the inputs and outputs of the quantum program, that is, its interface with the external classical execution logic. Similar to conventional programming languages, function main, can call other functions. Output variables that are declared in main definition are the ones to be measured when the program is executed.

Working with Quantum Variables

Quantum objects are representations of data (boolean values, numbers, arrays of numbers and so on), that are stored in specific qubits. In Qmod, quantum objects are handled and manipulated using quantum variables. Quantum variables must be declared and initialized explicitly. The model above demonstrates two important kinds of declaration:

  • Function foo declares parameter q thus: q: QBit. This declaration means foo expects a pre-existing quantum object.

  • Function main declares parameter q thus: q: Output[QBit]. In this case, q is an output-only parameter - it is initialized inside the scope of main.

Prior to their initialization, local quantum variables and output parameters do not reference any object (this is analogous to null reference in conventional languages). They may be initialized in the following ways:

  • Using allocate to initialize the variable to a new object, with all its qubits in the default \(|0\rangle\) state.

  • Using numeric assignment to initialize the variable to an object representing the result of a quantum expression.

  • Using functions with output parameters (for example, state preparation).

Note: all the variables in main must be declared as Output, as main is the entry point of the model (Think about it: where could a variable be initialized before main was called?). Other functions can declare parameters with or without the Output modifier.

Exercise #0

Rewrite the above model, so that q is initilized inside foo. A solution is provided in the end of the notebook. Hint: it only requires to move one line of code and add the Output modifier in the correct place.

from classiq import *

# Your code here
...

# execute the model to see that we get similar results
qmod  = create_model(main)
qprog = synthesize(qmod)
job = execute(qprog)
job.get_sample_result().parsed_counts
[{'q': 1}: 1066, {'q': 0}: 982]

Quantum Types, Statements and Opertions

Now that we have grasped the principles that are essential for any Qmod code, we can start building up our expressive toolkit, letting us create increasingly sophisticated models. The following exercises introduce some of the most useful variable types, statements and operations that Qmod supports.

Exercise #1 - Quantum Arrays

After we have familiarized with the QBit varible type (which is simply a single qubit), it is a good timing to introduce the quantum array type QArray.

In this exercise, we will prepare the famous \(|+\rangle\) Bell state into a 2-qubit Quantum array.

Recall that \(|+\rangle\) represents the state \(\frac{1}{\sqrt{2}} (|00\rangle + |11\rangle)\).

Instructions:

  1. Declare a quantum variable qarr of type QArray, and initilize it by allocating to it 2 qubits. Don't forget to use the Output modifier.

  2. Apply a Hadamard gate on the first qubit of qarr. Qmod counts from 0, so the first entry of qarr is qarr[0].

  3. Apply CX (controlled-NOT gate), with the control parameter being qarr[0] and the target parameter being qarr[1].

Synthesize and execute your model to assure that \(|00\rangle\) and \(|11\rangle\) are the only states to be measured, and that they are measured roughly equally.

from classiq import *

# Your code here:
...

# execute and inspect the results
qmod  = create_model(main)
qprog = synthesize(qmod)
job = execute(qprog)
job.get_sample_result().parsed_counts

Exercise #2 - The Repeat Statement

Use Qmod's repeat statement to create your own Hadamard Transform - a function that takes a QArray of an unspecified size and applies H to each of its qubits.

Instructions:

  1. Define a function my_hadamard_transform:

  2. It should have a single QArray argument q.

  3. Use repeat to apply H on each of q's qubits.

  4. Note that the iteration block of the repeat statement must use the Python lambda syntax (see repeat documentation).

  5. define a main function that initializes a QArray of length 10, and then passes it to my_hadamard_transform.

The provided code continues by calling show to let you inspect the resulting circuit - make sure that is applies H to each of q's qubits.

from classiq import *

# Your code here:
...

# synthesize the model and show the result
model = create_model(main)
qprog = synthesize(model)
show(qprog)
Opening: https://platform.classiq.io/circuit/2t81iPMifv2HTeqaeYyBTK6csUO?version=0.68.0

Exercise #3 - Power

Raising a quantum operation to an integer power appears in many known algorithms; for example, in Grover search and Quantum Phase Estimation. In the general case the implementation involves repeating the same circuit multiple times.

Sometimes, however, the implementation of the power operation can be simplified, thereby saving computational resources. A simple example is the operation of rotating a single qubit about the X, Y, or Z axis. In this case the rotation gate can be used once with the angle multiplied by the exponent. A similar example is the function unitary - an operation expressed as an explicit unitary matrix (i.e., all n*n matrix terms are given). Raising the operation can be done by raising the matrix to that power via classical computation.

See power operator.

Use the following code to define the value of a Qmod constant named unitary_matrix as a 4x4 (real) unitary:

from typing import List
import numpy as np
from classiq import *

rng = np.random.default_rng(seed=0)
random_matrix = rng.random((4, 4))
qr_unitary, _ = np.linalg.qr(random_matrix)

unitary_matrix = QConstant("unitary_matrix", List[List[float]], qr_unitary.tolist())
  1. Create a model that applies unitary_matrix on a 2-qubit variable three times (e.g. using repeat).

  2. Create another model that applies unitary_matrix raised to the power of 3 on a 2-qubit variable.

  3. Compare the gate count via the Classiq IDE in both cases.

from classiq import *

# Your code here:
...

qmod = create_model(main)
qprog = synthesize(qmod)
show(qprog)

Exercise 4 - User-defined Operators

Create a function that applies a given single-qubit operation to all qubits in its quantum argument (call your function my_apply_to_all). Such a function is also called an operator; i.e., a function that takes another function as an argument (its operand).

See operators.

Follow these guidelines:

  1. Your function declares a parameter of type qubit array and a parameter of a function type with a single qubit parameter.

  2. The body applies the operand to all qubits in the argument.

Now, re-implement my_hadamard_transform from Exercise 2 using this function instead of repeat. Use the same main function from Exercise 2.

from classiq import *

# Your code here:
...

qmod = create_model(main)
qprog = synthesize(qmod)
show(qprog)

Exercise 5 - Quantum Conditionals

Exercise 5a - Control Operator

Use the built-in control operator to create a function that receives two single qubit variables and uses one of them to control an RY gate with a pi/2 angle acting on the other variable (without using the CRY function).

See control.

from classiq import *

# Your code here:
...

qmod = create_model(main)
qprog = synthesize(qmod)
show(qprog)

Exercise 5b - Control Operator with Quantum Expressions

The control operator is the conditional application of some operation, with the condition being that all control qubits are in the state \(|1\rangle\). This notion is generalized in Qmod to other control states, where the condition is specified as a comparison between a quantum numeric variable and a numeric value, similar to a classical if statement. Quantum numeric variables are declared with class QNum.

See numeric types.

  1. Declare a QNum output argument using Output[QNum] and name it x.

  2. Use numeric assignment (the |= operator) to initialize it to 9.

  3. Execute the circuit and observe the results.

  4. Declare another output argument of type QBit and perform a control such that if x is 9, the qubit is flipped. Execute the circuit and observe the results. Repeat for a different condition.

from classiq import *

# Your code here:
...

qmod = create_model(main)
qprog = synthesize(qmod)
show(qprog)

Solutions

Solution - Excercise #0

from classiq import *

# rewrite the model, initializing q inside foo
@qfunc
def foo(q: Output[QBit]) -> None:
    allocate(1,q)
    X(q)
    H(q)

@qfunc
def main(q: Output[QBit]) -> None:
    foo(q)

# execute the model to see that we get similar results
qmod  = create_model(main)
qprog = synthesize(qmod)
job = execute(qprog)
job.get_sample_result().parsed_counts
[{'q': 0}: 1039, {'q': 1}: 1009]

Solution - Exercise #1

from classiq import *


@qfunc
def bell(qarr: QArray[QBit, 2]) -> None:
    H(qarr[0])
    CX(qarr[0], qarr[1])


@qfunc
def main(qarr: Output[QArray]) -> None:
    allocate(2, qarr)
    bell(qarr)

# execute and inspect the results
qmod  = create_model(main)
qprog = synthesize(qmod)
job = execute(qprog)
job.get_sample_result().parsed_counts
[{'qarr': [1, 1]}: 1045, {'qarr': [0, 0]}: 1003]

Solution - Exercise #2

from classiq import *


@qfunc
def my_hadamard_transform(q: QArray[QBit]) -> None:
    repeat(q.len, lambda i: H(q[i]))


@qfunc
def main(q: Output[QArray[QBit]]) -> None:
    allocate(10, q)
    my_hadamard_transform(q)


model = create_model(main)
qprog = synthesize(model)
show(qprog)

Solution - Exercise #3

from typing import List
import numpy as np
from classiq import *

rng = np.random.default_rng(seed=0)
random_matrix = rng.random((4, 4))
qr_unitary, _ = np.linalg.qr(random_matrix)

unitary_matrix = QConstant("unitary_matrix", List[List[float]], qr_unitary.tolist())


@qfunc
def main(q: Output[QArray[QBit]]) -> None:
    allocate(2, q)
    power(3, lambda: unitary(unitary_matrix, q))


model = create_model(main)
qprog = synthesize(model)
show(qprog)

Solution - Exercise #4

from classiq import *


@qfunc
def my_apply_to_all(operand: QCallable[QBit], q: QArray[QBit]) -> None:
    repeat(q.len, lambda i: operand(q[i]))


@qfunc
def my_hadamard_transform(q: QArray[QBit]) -> None:
    my_apply_to_all(lambda t: H(t), q)


@qfunc
def main(q: Output[QArray[QBit]]) -> None:
    allocate(10, q)
    my_hadamard_transform(q)


model = create_model(main)
qprog = synthesize(model)
show(qprog)
Opening: https://platform.classiq.io/circuit/2t84SnF8EYfauWTKi0DG0scuIiV?version=0.68.0

Solution - Exercise #5

Solution - Exercise #5a

from classiq import *
from classiq.qmod.symbolic import pi


@qfunc
def my_controlled_ry(control_bit: QBit, target: QBit) -> None:
    control(control_bit, lambda: RY(pi / 2, target))


@qfunc
def main(control_bit: Output[QBit], target: Output[QBit]) -> None:
    allocate(1, control_bit)
    allocate(1, target)
    my_controlled_ry(control_bit, target)


model = create_model(main)
qprog = synthesize(model)
show(qprog)

Solution - Exercise #5b

from classiq import *


@qfunc
def main(x: Output[QNum], target: Output[QBit]) -> None:
    x |= 9
    allocate(1, target)
    control(x == 9, lambda: X(target))


model = create_model(main)
qprog = synthesize(model)
show(qprog)