Skip to content

Qmod Workshop - Part 2: Higher-Level Concepts

View on GitHub

This second part of the Qmod workshop covers exercises 6 through 10. Make sure to go through Part 1 before continuing with this notebook.

Exercise 6 - Exponentiation and Pauli Operators

The Qmod language supports different classical types: scalars, arrays, and structs. Structs are objects with member variables or fields.

See classical types.

The built-in PauliTerm struct type is defined as follows:

from dataclasses import dataclass

from classiq import *


@dataclass
class PauliTerm:
    pauli: CArray[Pauli]
    coefficient: CReal

Note that Pauli is an enum for all the Pauli matrices (I, X, Y, Z).

A Pauli-based Hamiltonian can be represented as a list of PauliTerms. A Pauli operator defined this way is the argument to Hamiltonian evolution functions.

This exercise uses the Suzuki-Trotter function to find the evolution of H=0.5XZXX + 0.25YIZI + 0.3 XIZY (captured as a literal value for the Pauli operator), with the evolution coefficient being 3, the order being 2, and using 4 repetitions.

The declaration of the suzuki_trotter function:

@qfunc(external=True)
def suzuki_trotter(
    pauli_operator: CArray[PauliTerm],
    evolution_coefficient: CReal,
    order: CInt,
    repetitions: CInt,
    qbv: QArray[QBit],
) -> None:
    pass

To complete this exercise, allocate q and invoke the suzuki_trotter quantum function:

HINT suzuki_trotter(
 ...,
 evolution_coefficient=3,
 repetitions=4,
 order=2,
 qbv=q,
)
from classiq import *


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


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

Exercise 7 - Basic Arithmetics

This exercise uses quantum numeric variables and calculates expressions over them.

See details on the syntax of numeric types in quantum types. See more on quantum expressions in numeric assignment.

Exercise 7a

Create these quantum programs:

  1. Initialize variables x=2, y=7 and compute res = x + y.
  2. Initialize variables x=2, y=7 and compute res = x * y.
  3. Initialize variables x=2, y=7, z=1 and compute res = x * y - z.

Guidance:

  • Use the |= operators to perform out-of-place assignment of arithmetic expressions.
  • To initialize the variables, use the prepare_int function.
from classiq import *

# Your code here:

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

Exercise 7b

Declare x to be a 2-qubit variable and y a 3-qubit variable.

Add two superposition states such that x is an equal superposition of 0 and 2, and y is an equal superposition of 1, 2, 3, and 6.

  1. Use prepare_state to initialize x and y. Note that prepare_state works with probabilities, not amplitudes. The declaration of the prepare_state function:
@qfunc(external=True)
def prepare_state(
    probabilities: CArray[CReal],
    bound: CReal,
    out: Output[QArray[QBit]],
) -> None:
    pass
HintSet the bound to 0 in your code
  1. Compute res = x + y. Execute the resulting circuit. What did you get?
from classiq import *

# Your code here:

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

Exercise 8 - Within-Apply

The within-apply statement applies the U_dagger V U pattern that appears frequently in quantum computing. It allows you to compute a function V within the context of another function U, and afterward uncompute U to release auxiliary qubits storing intermediate results.

See within apply.

Exercise 8a

This exercise uses within-apply to compute an arithmetic expression in steps.

Use the within_apply operation to calculate res = x + y + z from a two-variable addition building block with these steps:

  1. Add x and y
  2. Add the result to z
  3. Uncompute the result of the first operation

For simplicity, initialize the registers to simple integers: x=3, y=5, z=2.

Hints:

  • Use a temporary variable.
  • Wrap the arithmetic operation in a function.

Execute the circuit and make sure you obtain the expected result.

from classiq import *

# Your code here:

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

Exercise 8b

Why use within-apply and not just write three concatenated functions? To understand the motivation, create another arithmetic circuit. This time, however, set the Classiq synthesis engine to optimize on the circuit’s number of qubits; i.e., its width.

Determine constraints with the set_constraints operation. (See here).

Perform the operation res = w + x + y + z, where w is initialized to 4 and the rest as before:

  1. Add x and y (as part of the within_apply operation)
  2. Add the result to z (as part of the within_apply operation)
  3. Uncompute the result of the first operation (as part of the within_apply operation)
  4. Add the result of the second operation to w. There is no need to perform another uncomputation, as this brings the calculation to an end.

Create the model, optimize on the circuit’s width, and run the circuit. Can you identify where qubits have been released and reused?

from classiq import *

# Your code here:

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

Bonus: Use a Single Arithmetic Expression

What happens when you don't manually decompose this expression?

Use the Classiq arithmetic engine to calculate res |= x + y + z + w and optimize for width. Look at the resulting quantum program. Can you identify the computation and uncomputation blocks? What else do you notice?

from classiq import *

# Your code here:

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

Exercise 9 - In-place Arithmetics

This exercise uses numeric quantum variables that represent fixed-point reals.

Arithmetic expressions can be calculated in place into a target variable, without allocating new qubits to store the result. This is done using the in-place-xor operator.

See numeric assignment.

In-place assignment is often used to nest arithmetic expressions under quantum operators. Note that out-of-place assignment requires its left-value variable to be uninitialized, and therefore cannot be used under an operator if the variable is declared outside its scope. Applying operators to arithmetic expressions is required in many algorithms. One example is the piecewise evaluation of mathematical functions; calculating different expressions over x depending on the subdomain where x falls.

For this exercise, replace the missing parts in the code snippet below to evaluate the result:

\[f(x) = \begin{cases} 2x + 1 & \text{ if } 0 \leq x < 0.5 \\ x + 0.5 & \text{ if } 0.5 \leq x < 1 \end{cases}\]

Notes:

  • You cannot use x directly as the control variable in a control operator because it also occurs in the nested scope. To determine if x is in the lower or upper half of the domain, duplicate the most significant bit (MSB) onto a separate variable called label.

  • In Python, assignment operators cannot be used in lambda expressions, so the computation of the function needs to be factored out to a named Python function (but not necessarily a Qmod function).

Hint dup_msb(x, label)
control(label, ...) # 0.5 <= x < 1.0
X(label)
control(label, ...) # 0.0 <= x < 0.5
from classiq import *


@qfunc
def dup_msb(qba: QArray[QBit], msb: QBit) -> None:
    CX(qba[qba.len - 1], msb)


@qfunc
def main(x: Output[QNum[3, False, 3]], res: Output[QNum[5, False, 3]]) -> None:
    allocate(5, res)
    allocate(3, x)
    hadamard_transform(x)

    label = QArray("label")
    allocate(1, label)
    # Your code here:


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

Exercise 10 - State-preparation Algorithm Using quantum-if

Binding

The bind operation smoothly converts between different quantum types and splits or slices bits when necessary. Here is an example:

from classiq import *


@qfunc
def main(res: Output[QArray[QBit]]) -> None:
    x: QArray[QBit] = QArray("x")
    allocate(3, x)
    ...
    lsb = QBit("lsb")
    msb = QNum("msb", 2, False, 0)
    bind(x, [lsb, msb])
    ...
    bind([lsb, msb], res)


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

The first bind operation splits the 3-qubit register x into the 2-qubit and single-qubit lsb and msb registers, respectively.

After the bind operation:

  1. The lsb and msb registers can be operated on separately.
  2. The x register is consumed and can no longer be used.

The second bind operation concatenates the registers to the res output register.

For this exercise, fill in the missing code parts in the above snippet and use the control statement to manually generate a lovely 3-qubit probability distribution: [1/8, 1/8, 1/8, -sqrt(3)/16, 1/8 + sqrt(3)/16, 1/8, 1/8, 1/8, 1/8].

The following series of gates generate it:

Perform the Hadamard transform on all three qubits.

Apply a rotation on the least-significant bit (LSB) conditioned by the MSB being |0> and the second-to-last MSB being |1>. How would you write this condition using a QNum?

The following series of gates generate it: 1. Perform the Hadamard transform on all three qubits. 2. Apply a pi/3 rotation on the LSB conditioned by the MSB being |0> and the second-to-last MSB being |1>. How would you write this condition using a QNum?

To validate your results without looking at the full solution, compare them to running using the Classiq built-in prepare_state function.

from classiq import *
from classiq.qmod.symbolic import sqrt


@qfunc
def pre_prepared_state(q: QArray[QBit]) -> None:
    prepare_state(
        [
            1 / 8,
            1 / 8,
            1 / 8,
            -sqrt(3) / 16,
            1 / 8 + sqrt(3) / 16,
            1 / 8,
            1 / 8,
            1 / 8,
            1 / 8,
        ],
        0.0,
        q,
    )


# Your code here:

Solutions

Exercise 6

# Solution to Exercise 6:


from classiq import *


@qfunc
def main(q: Output[QArray[QBit]]) -> None:
    allocate(4, q)
    suzuki_trotter(
        [
            PauliTerm([Pauli.X, Pauli.Z, Pauli.X, Pauli.X], 0.5),
            PauliTerm([Pauli.Y, Pauli.I, Pauli.Z, Pauli.I], 0.25),
            PauliTerm([Pauli.X, Pauli.I, Pauli.Z, Pauli.Y], 0.3),
        ],
        evolution_coefficient=3,
        repetitions=4,
        order=2,
        qbv=q,
    )

Exercise 7

# Solution to Exercise 7a:


from classiq import *


@qfunc
def main(res: Output[QNum]) -> None:
    x = QNum("x")
    y = QNum("y")
    z = QNum("z")
    prepare_int(2, x)
    prepare_int(7, y)
    prepare_int(1, z)
    res |= x + y
    # res |= x * y
    # res |= x * y - z
# Solution to Exercise 7b:


from classiq import *


@qfunc
def main(res: Output[QNum]) -> None:
    x = QNum("x")
    y = QNum("y")
    prepare_state([0.5, 0, 0.5, 0.0], 0.0, x)
    prepare_state([0, 0.25, 0.25, 0.25, 0.0, 0.0, 0.25, 0.0], 0.0, y)
    res |= x + y


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

Exercise 8

# Solution to Exercise 8:


from classiq import *


@qfunc
def my_add(x: QNum, y: QNum, res: Output[QNum]) -> None:
    res |= x + y


@qfunc
def main(res: Output[QNum]) -> None:
    x = QNum("x")
    y = QNum("y")
    z = QNum("z")
    prepare_int(3, x)
    prepare_int(5, y)
    prepare_int(2, z)

    temp = QNum("temp")
    within_apply(within=lambda: my_add(x, y, temp), apply=lambda: my_add(temp, z, res))


model = create_model(main)
qprog = synthesize(model)
show(qprog)
# Solution to the advanced part of Exercise 8:


from classiq import *


@qfunc
def my_add(x: QNum, y: QNum, res: Output[QNum]) -> None:
    res |= x + y


@qfunc
def main(res: Output[QNum]) -> None:
    x = QNum("x")
    y = QNum("y")
    z = QNum("z")
    w = QNum("w")
    prepare_int(3, x)
    prepare_int(5, y)
    prepare_int(2, z)
    prepare_int(4, w)

    temp_xy = QNum("temp_xy")
    xyz = QNum("xyz")
    within_apply(
        within=lambda: my_add(x, y, temp_xy), apply=lambda: my_add(temp_xy, z, xyz)
    )
    res |= xyz + w


model = create_model(main)
model = set_constraints(
    model, Constraints(optimization_parameter=OptimizationParameter.WIDTH)
)
qprog = synthesize(model)
show(qprog)

Exercise 9

# Solution to Exercise 9:


from classiq import *


def linear_func(a: float, b: float, x: QNum, res: QNum) -> None:
    res ^= a * x + b


@qfunc
def dup_msb(qba: QArray[QBit], msb: QBit) -> None:
    CX(qba[qba.len - 1], msb)


@qfunc
def main(x: Output[QNum[3, False, 3]], res: Output[QNum[5, False, 3]]) -> None:
    allocate(5, res)
    allocate(3, x)
    hadamard_transform(x)

    label = QArray("label")
    allocate(1, label)

    dup_msb(x, label)
    control(label, lambda: linear_func(1.0, 0.5, x, res))  # 0.5 <= x < 1.0
    X(label)
    control(label, lambda: linear_func(2.0, 1.0, x, res))  # 0.0 <= x < 0.5

Exercise 10

# Solution to Exercise 10:


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


@qfunc
def main(res: Output[QArray[QBit]]) -> None:
    x: QArray[QBit] = QArray("x")
    allocate(3, x)
    hadamard_transform(x)

    lsb = QBit("lsb")
    msb = QNum("msb", 2, False, 0)
    bind(x, [lsb, msb])

    control(msb == 1, lambda: RY(pi / 3, lsb))

    bind([lsb, msb], res)


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