Skip to main content

View on GitHub

Open this notebook in GitHub to run it yourself
Welcome back to the Qmod tutorial! Part I introduced the language foundations - quantum variables and types, output parameters, classical and quantum control flow, and quantum arithmetic. Part II (this notebook) builds on those foundations to cover the statements and operators used to express real quantum algorithms. Six sections, each pairing a concept with a hands-on exercise:
  1. Phase statements
  2. Local variables, permutations, and auto-uncomputation
  3. Within-apply
  4. Hamiltonians, Pauli operators, and exponentiation
  5. Higher-order functions
  6. Execution parameters and hybrid execution
Each section has a short concept explanation followed by an exercise. Complete the code where marked with # TODO, then synthesize and run it. All solutions are collected at the end of the notebook - try each exercise before peeking.
Note: If you are working in your own SDK environment, make sure Classiq is installed (pip install -U classiq) and that you have authenticated once by running authenticate() in a Python session. See the registration and installation guide.

  1. Phase Statements
The phase statement applies phases to computational-basis states, where the Z-axis rotation can be a function of quantum numeric variables. In ket notation the operator maps x1,x2,|x_1, x_2, \dots\rangle to eif(x1,x2,)x1,x2,e^{\,i\,f(x_1, x_2, \dots)}\,|x_1, x_2, \dots\rangle, where ff is an expression over the variables x1,x2,x_1, x_2, \dots For example, the following rotates each state by π4x2\frac{\pi}{4}x^2:
If xx is a QNum in the range [0,1,2,3][0, 1, 2, 3] it rotates 1|1\rangle by π/4\pi/4, 2|2\rangle by 4π/44\pi/4 (i.e., half turn), and 3|3\rangle by 9π/49\pi/4 (coming back to π/4\pi/4).
Note: An optional second argument multiplies the expression by a classical coefficient (Part I, Section 3) - typically an execution parameter, as in a QAOA cost layer.
When the phase expression contains no quantum variables, the same phase is applied across all states. Still, this can be restricted to certain states by an enclosing control statement. For example, if xx is a QNum, the following rotates all states other than 0|0\rangle by π/4\pi/4:
Phase statements are the workhorse behind phase oracles (as in Grover’s algorithm) and cost functions (as in QAOA).
Note: Phases are invisible to ordinary sampling - they don’t change measurement probabilities. To “see” them, use the top-level calculate_state_vector(qprog), which returns the amplitude (including phase) of each state.
Exercise A: Put two 2-qubit numbers x and y into uniform superposition, then encode their product x * y into the phase, scaled by a coefficient of 2π/2N=π/82\pi / 2^N = \pi/8 (with N=4N = 4 the total number of qubits). This divides the full turn into 2N=162^N = 16 equal steps - one per unit of the product - so distinct product values land on distinct phases. Use calculate_state_vector to inspect the resulting phases.
Expected result: every basis state has probability 1/16, but the phase column shows a rotation of (xy)π/8(x \cdot y)\,\pi/8 for each state - e.g. x=2, y=1 gives 0.25π0.25\pi, and x=3, y=3 gives 9π/89\pi/8 (shown as 0.88π-0.88\pi, i.e. modulo 2π2\pi).
Exercise B: A phase oracle flips the phase (a rotation by π\pi) of the basis states that satisfy some condition - the marking step at the heart of Grover’s algorithm. Using two 2-qubit numbers a and b in uniform superposition, complete mark_solutions to flip the phase of every state satisfying 3a+b=93a + b = 9 (similar to the exercise in Part I, Section 6). Then inspect the statevector to confirm which states were marked.
Expected result: all 16 basis states keep probability 1/16, but exactly two of them - the assignments satisfying 3a+b=93a+b=9: (a, b) = (3, 0) and (2, 3) - show a phase of π\pi; every other state shows 0.

  1. Local Variables, Permutations, and Auto-Uncomputation
Inside a quantum function you can declare a local quantum variable and use it to hold intermediate results. This is very handy in breaking computations into smaller steps. When a local goes out of scope, Qmod uncomputes it automatically - reversing the operations that set it, so its qubits return to 0|0\rangle and can be reused. For example, the following declares a numeric local variable v and assigns it the computational-basis value
Uncomputation can work as long as the local variable was modified by permutations - operations that map basis states to basis states without creating superposition, such as gate-level functions X and CX, as well as higher level arithmetic (Part I, Section 5). If a local can’t be uncomputed automatically the compiler will issue an error. You can discard the variable explicitly with drop or manually uncompute it and call free. See uncomputation for the full rules. Exercise: Put a 2-qubit number x into uniform superposition. Use a local variable tmp to store 2x+12x+1, and depending on tmp > 3, flip res. tmp is a scratch variable - it is uncomputed automatically at the end of the function, so it never appears in the output.
Expected result: x is uniform over 0-3, and res is 1 exactly when tmp = 2x + 1 > 3 - that is, when x > 1 (x is 2 or 3). The scratch variable tmp was uncomputed automatically, so the output contains only x and res.

  1. Within-Apply
Many quantum routines are based on the conjugation pattern: transform the state with some operation UU, perform an operation VV in that transformed basis, then transform back, with the net effect - UVUU^\dagger V U. The within-apply statement captures this directly. When this pattern is subject to quantum control, only the apply block (the VV operation) actually needs to be controlled. For example, the following applies Z to a qubit q in the Hadamard basis (which is equivalent to applying X to q):
Note: Initializing a variable in the within block implies that its inverse uncomputes and frees it. Hence, such a variable is subject to the same rules as local variables we discussed in Section 8.
Exercise A: A classic example of conjugation is performing addition in the Fourier basis by modifying relative phases. Complete the within_apply so it computes y += x. Variable x starts in uniform superposition and y at the fixed value 2, so each measured y should come out as x + 2.
Expected result: x is uniform over 0-7, and each row’s y equals x + 2 (so y ranges 2-9) - the in-place addition carried out entirely through phase rotations in the Fourier basis.
Exercise B: Now perform the same addition, but only when a control qubit ctrl is 1|1\rangle - put the whole within_apply under a control on ctrl. Then run show(qprog) and inspect the quantum program: notice that the qft and its inverse are not controlled - only the apply (phase) block is.
Expected result: when ctrl is 0, y stays 2; when ctrl is 1, y equals x + 2. In the visualization, the qft and inverse-qft surround the phase block but sit outside the control.

1
  1. Hamiltonians, Pauli Operators, and Exponentiation
The four Pauli matrices I, X, Y, Z are the building blocks of qubit operators. A Hamiltonian - a system’s energy operator - can be represented as a weighted sum of Pauli matrix products. In Qmod this is called Pauli operator, and you build it from the Pauli enum, where Pauli.X(0) is X on qubit 0; multiply to form a product (unmentioned qubits are implicitly I), and add to sum terms. Here is an example Hamiltonian:
A Hamiltonian drives time evolution through U(t)=eiHtU(t) = e^{-iHt}. Exact gate decomposition of this operator can be exponentially large, so the Suzuki-Trotter decomposition approximates it as r repeated short steps over the individual terms. The built-in function suzuki_trotter applies it, given the Hamiltonian, the evolution time, the order, and the repetitions r. A Pauli operator can also serve as an observable - a quantity whose expectation value ψOψ\langle\psi|O|\psi\rangle we measure. Where sample returns the distribution over basis states, the SDK function observe returns the expectation value of a given observable. Exercise A: Use suzuki_trotter to simulate evolution under H=0.5X0X1Z2X3  +  0.25Z1Y3H = 0.5\,X_0 X_1 Z_2 X_3 \;+\; 0.25\,Z_1 Y_3 on 4 qubits, with evolution time t=3t = 3, 2nd order, and r=2r = 2 repetitions.
Tip: Avoid declaring a Python variable with the name H in the global scope, because it would eclipse the function H (the Hadamard function)
Exercise B: Prepare two qubits in ++|+\rangle|+\rangle (with hadamard_transform), then use observe to compute the expectation value of the observable X0+X1X_0 + X_1.
Expected result: observe returns 2.0 - each qubit is in +|+\rangle, the +1+1 eigenstate of X, so X0=X1=1\langle X_0\rangle = \langle X_1\rangle = 1.

1
  1. Higher-Order Functions
A higher-order function is a quantum function that takes another quantum function as an argument. A function-type parameter is declared with type QCallable (the Qmod analog of Python’s Callable). When calling a higher-order function, you pass a function or a lambda expression, whose signature must match the one specified by the QCallable type. For example, the following declares foo with a function-type parameter op; calling foo requires passing a function that takes a classical real and a qubit:
Higher-order functions are very useful for capturing recurring patterns in quantum algorithms. Examples are the Quantum Phase Estimation (QPE) and the Grover double-reflection operator.
Note: Passing a lambda to a higher-order function works just like passing one to a built-in statement - e.g. repeat(qarr.len, lambda i: H(qarr[i])), where i is the lambda’s CInt parameter (Part I, Section 4).
See more under operators. Exercise A: Define my_apply_to_all - a higher-order function that applies a single-qubit operation op to every qubit of qarr. Then use it to apply H to all three qubits, producing a uniform superposition.
Expected result: all eight bit strings appear with roughly equal ~1/8 probability - H was applied to every qubit.
Exercise B: qpe (quantum phase estimation) is a built-in higher-order function: given a unitary U as its operand, it estimates the phase θ\theta of an eigenstate, where Uψ=e2πiθψU|\psi\rangle = e^{2\pi i\theta}|\psi\rangle, and writes it into a QNum. Here 11|11\rangle is an eigenstate of CRZ(pi, state[0], state[1]) with θ=0.25\theta = 0.25. Complete the qpe call to estimate it.
Expected result: theta comes out 0.25 with near-certainty - the phase of CRZ(pi) on its eigenstate 11|11\rangle.

1
  1. Execution Parameters and Hybrid Execution
Many quantum algorithms are hybrid: a classical program drives a quantum one, running it repeatedly at different parameter values and processing the results. This builds on the execution parameters from Part I, Section 3 - classical main parameters (CReal, CArray[CReal], …) that stay symbolic through synthesis. You synthesize once and execute the same quantum program multiple times. The classical logic can be a variational optimizer minimizing a cost (VQE, QAOA), an iterative scheme adapting parameters from measured outcomes (IQAE), or a sweep over values fixed by classical processing (such as Shor). Values are supplied at execution through the parameters argument of sample or observe, a dict mapping each parameter name to its value. For example, if main declares a parameter params of type CArray[CReal, 2], the following runs the synthesized quantum program at specified values:
Use sample for the measured distribution, or observe for an expectation value H\langle H \rangle of a Hamiltonian (Section 10) - the cost a variational algorithm minimizes. See execution for all options.
Note: Execution parameters are ordinary numbers, so use Python floats (e.g. math.pi), not the symbolic pi used inside function bodies.
Exercise A: The parametric program below rotates two qubits by params[0] and params[1], then entangles them (one layer of a typical ansatz). Synthesize it once, then - reusing that same qprog
  • sample it at two different settings, [0.2, 1.0] and [1.5, 0.5], and compare the distributions.
Expected result: two distributions from the same synthesized qprog. [0.2, 1.0] is dominated by 00 (10 next); [1.5, 0.5] splits roughly evenly between 00 and 11.
Exercise B: Using the same qprog from Exercise A, compute the expectation value H\langle H \rangle of H=X0+0.5Z0Z1H = X_0 + 0.5\, Z_0 Z_1 at parameter values [π/4,π/3][\pi/4,\, \pi/3] with observe. Remember to pass the angles as numbers.
Expected result: observe returns a single number, H0.86\langle H\rangle \approx 0.86 - the expectation value on the state prepared at those parameters.

Solutions

Try each exercise before checking the solution below.

Solution 7

  • Phase Statements
Exercise A - encoding a product into the phase
Exercise B - a Grover-style phase oracle

Solution 8

  • Local Variables, Permutations, and Auto-Uncomputation

Solution 9

  • Within-Apply
Exercise A - within-apply addition
Exercise B - the same addition under control

Solution 10

  • Hamiltonians, Pauli Operators, and Exponentiation
**Exercise A
  • Suzuki-Trotter evolution**
Exercise B - measuring an observable with observe

Solution 11

  • Higher-Order Functions
Exercise A - a user-defined higher-order function
Exercise B - using the built-in qpe

Solution 12

  • Execution Parameters and Hybrid Execution
Exercise A - sampling a parametric circuit
Exercise B - an expectation value with observe