Quantum Computing Basics
This tutorial introduces the fundamental building blocks of quantum computing: qubits, quantum gates, and measurement. You will learn the mathematical foundations and how to apply them using pyqpanda3.
Prerequisites: Getting Started -- ensure pyqpanda3 is installed and working.
Table of Contents
1. What is a Qubit?
1.1 Classical Bit vs Qubit
A classical bit can be in one of two states: 0 or 1. A quantum bit (qubit) also has two basis states, written in Dirac notation as
| Property | Classical Bit | Qubit |
|---|---|---|
| States | 0 or 1 | |
| Notation | Binary digit | Ket vector |
| Simultaneous states | One at a time | Both at once (superposition) |
| Measurement | Does not change state | Collapses to basis state |
| Representation | Voltage level | Vector in Hilbert space |
1.2 Superposition
A qubit in an arbitrary state is described by:
where
The probability of measuring the qubit in state
In column vector form, the basis states are:
So a general qubit state can be written as:
The key insight is that the qubit holds both possibilities simultaneously until measured. This is not the same as "being in an unknown state" -- the amplitudes
from pyqpanda3 import core
# The |0> state is the default when a qubit is allocated
# Let's verify by measuring a fresh qubit
prog = core.QProg()
prog << core.measure(0, 0)
machine = core.CPUQVM()
machine.run(prog, shots=1000)
counts = machine.result().get_counts()
print(counts)
# Output: {'0': 1000} -- always |0> since no gates were applied1.3 The Bloch Sphere
Any single-qubit pure state can be parameterized by two angles
where
Key points on the Bloch sphere:
- North pole (
): - South pole (
): - Positive X (
): - Negative X (
): - Positive Y (
): - Negative Y (
):
The Bloch vector
pyqpanda3 provides Bloch sphere visualization through the visualization module:
from pyqpanda3.visualization import plot_bloch_vector
# Plot the |+> state (pointing along +X axis)
# Bloch vector: [sin(pi/2)*cos(0), sin(pi/2)*sin(0), cos(pi/2)] = [1, 0, 0]
plot_bloch_vector([1, 0, 0], title="|+> state")2. Quantum Gates
2.1 Unitary Transformations
Quantum gates are unitary transformations that evolve qubit states. A matrix
where
- The total probability is preserved (
) - The gate is reversible (every gate has an inverse)
When a gate
For multi-qubit systems, gates acting on specific qubits are represented by the tensor product of the gate matrix with identity matrices on the other qubits.
2.2 Single-Qubit Gates
pyqpanda3 provides the following single-qubit gates with no parameters:
Pauli X Gate (NOT Gate)
The X gate flips the computational basis states, analogous to a classical NOT gate.
On the Bloch sphere, X is a
Pauli Y Gate
On the Bloch sphere, Y is a
Pauli Z Gate
The Z gate applies a phase flip to
Hadamard Gate (H)
The Hadamard gate creates equal superpositions:
This is the most commonly used gate for creating superposition. Applying H twice returns to the original state:
S Gate (Phase Gate)
The S gate is the square root of Z. It applies a
T Gate ( Gate)
The T gate is the fourth root of Z. It is non-Clifford and essential for universal quantum computation.
I Gate (Identity)
The identity gate leaves the state unchanged. It is useful for padding circuits or as a placeholder.
from pyqpanda3 import core
# Apply single-qubit gates to qubit 0
prog = core.QProg()
prog << core.H(0) # Hadamard on qubit 0
prog << core.X(0) # Pauli-X on qubit 0
prog << core.Y(0) # Pauli-Y on qubit 0
prog << core.Z(0) # Pauli-Z on qubit 0
prog << core.S(0) # S gate on qubit 0
prog << core.T(0) # T gate on qubit 0
prog << core.I(0) # Identity on qubit 0
print(prog)Summary of Non-Parameterized Single-Qubit Gates
| Gate | Matrix | Effect on | Effect on |
|---|---|---|---|
| X | |||
| Y | |||
| Z | |||
| H | |||
| S | |||
| T | |||
| I |
2.3 Rotation Gates
Rotation gates are parameterized by an angle
RX Gate (Rotation around X axis)
RY Gate (Rotation around Y axis)
RZ Gate (Rotation around Z axis)
P Gate (Phase Gate)
Note that
import math
from pyqpanda3 import core
# Rotation gates take (qubit_index, angle_in_radians)
prog = core.QProg()
prog << core.RX(0, math.pi / 4) # Rotate pi/4 around X
prog << core.RY(1, math.pi / 2) # Rotate pi/2 around Y
prog << core.RZ(2, math.pi) # Rotate pi around Z (= Z gate up to phase)
prog << core.P(0, math.pi / 4) # Phase gate with angle pi/4 (= T gate)
# Verify that RX(pi) behaves like X (up to global phase)
# RX(pi) = [[0, -i], [-i, 0]] -- multiplies by -i relative to X
prog_rx = core.QProg()
prog_rx << core.RX(0, math.pi)
prog_rx << core.measure(0, 0)
machine = core.CPUQVM()
machine.run(prog_rx, shots=1000)
print(machine.result().get_counts())
# Output: {'1': 1000} -- same measurement statistics as X|0>2.4 Two-Qubit Gates
Two-qubit gates act on pairs of qubits and can create entanglement, a uniquely quantum phenomenon where the states of two qubits become correlated in a way that cannot be described classically.
CNOT Gate (Controlled-NOT)
The most fundamental two-qubit gate. It flips the target qubit if and only if the control qubit is
Basis state action (first qubit is control, second is target):
In pyqpanda3: core.CNOT(control, target).
CZ Gate (Controlled-Z)
Applies a Z gate to the target qubit if the control is
In pyqpanda3: core.CZ(qubit1, qubit2).
SWAP Gate
Exchanges the states of two qubits.
In pyqpanda3: core.SWAP(qubit1, qubit2).
Parameterized Two-Qubit Gates
pyqpanda3 also provides parameterized two-qubit gates:
| Gate | Syntax | Description |
|---|---|---|
| CRX | core.CRX(control, target, theta) | Controlled RX rotation |
| CRY | core.CRY(control, target, theta) | Controlled RY rotation |
| CRZ | core.CRZ(control, target, theta) | Controlled RZ rotation |
| CP | core.CP(control, target, theta) | Controlled phase |
| CU | core.CU(control, target, theta, phi, lam, gamma) | General controlled unitary |
| RXX | core.RXX(q1, q2, theta) | XX interaction |
| RYY | core.RYY(q1, q2, theta) | YY interaction |
| RZZ | core.RZZ(q1, q2, theta) | ZZ interaction |
| RZX | core.RZX(q1, q2, theta) | ZX interaction |
from pyqpanda3 import core
# Two-qubit gate examples
prog = core.QProg()
prog << core.CNOT(0, 1) # CNOT: control=0, target=1
prog << core.CZ(1, 2) # CZ between qubits 1 and 2
prog << core.SWAP(0, 1) # SWAP qubits 0 and 1
# Parameterized two-qubit gates
import math
prog << core.CRX(0, 1, math.pi / 4) # Controlled-RX with angle pi/4
prog << core.RZZ(0, 1, math.pi / 2) # ZZ rotation with angle pi/2
print(prog)2.5 Multi-Qubit Gates
TOFFOLI Gate (CCNOT)
The TOFFOLI gate is a three-qubit gate with two control qubits and one target. It flips the target if and only if both controls are
In matrix form (8x8), it acts as identity except for swapping the last two rows/columns:
In pyqpanda3: core.TOFFOLI(control1, control2, target).
from pyqpanda3 import core
# TOFFOLI: flips qubit 2 only when qubits 0 and 1 are both |1>
prog = core.QProg()
prog << core.X(0) # Set control qubit 0 to |1>
prog << core.X(1) # Set control qubit 1 to |1>
prog << core.TOFFOLI(0, 1, 2) # Target qubit 2 flips to |1>
prog << core.measure([0, 1, 2], [0, 1, 2])
machine = core.CPUQVM()
machine.run(prog, shots=1000)
print(machine.result().get_counts())
# Output: {'111': 1000} -- all three qubits are |1>Gate Taxonomy
3. Gate Operations in pyqpanda3
pyqpanda3 gates support four powerful operations: adjoint, control, power, and matrix extraction. These operations allow you to construct complex circuits from simple building blocks.
3.1 Adjoint (.dagger())
The .dagger() method returns the adjoint (conjugate transpose) of a gate. For unitary gates, the adjoint is the inverse. This is useful for:
- Uncomputing intermediate results
- Implementing quantum algorithms that require backward steps
- Constructing reflection operators
from pyqpanda3 import core
import math
import numpy as np
# The dagger of H is H itself (H is self-adjoint)
h_gate = core.H(0)
h_dagger = h_gate.dagger()
# Verify: H.dagger() matrix should equal H matrix
h_mat = np.array(h_gate.matrix())
hd_mat = np.array(h_dagger.matrix())
print("H == H.dagger():", np.allclose(h_mat, hd_mat))
# Output: H == H.dagger(): True
# The dagger of S is S^\dagger (which is S^3 or equivalently Z*S)
s_gate = core.S(0)
s_dagger = s_gate.dagger()
s_mat = np.array(s_gate.matrix())
sd_mat = np.array(s_dagger.matrix())
print("S:\n", s_mat)
print("S.dagger():\n", sd_mat)
# S = [[1,0],[0,i]], S.dagger() = [[1,0],[0,-i]]
# Practical use: apply S then uncompute with S.dagger()
prog = core.QProg()
prog << core.H(0) # Create superposition |+>
prog << core.S(0) # Apply S gate
prog << core.S(0).dagger() # Undo S gate (returns to |+>)
prog << core.H(0) # Return to |0>
prog << core.measure(0, 0)
machine = core.CPUQVM()
machine.run(prog, shots=1000)
print(machine.result().get_counts())
# Output: {'0': 1000} -- S and S.dagger() cancel out3.2 Controlled Gates (.control())
The .control(qubit) method transforms any gate into its controlled version. The original gate is applied to its target qubits only if the specified control qubit is in state
This is extremely powerful: you can create controlled versions of any gate, including gates that already have controls.
from pyqpanda3 import core
# Create a controlled-X (equivalent to CNOT) from X
x_gate = core.X(1)
controlled_x = x_gate.control(0) # Control on qubit 0
# Use in a circuit
prog = core.QProg()
prog << core.X(0) # Set control to |1>
prog << controlled_x # X on qubit 1 is activated
prog << core.measure([0, 1], [0, 1])
machine = core.CPUQVM()
machine.run(prog, shots=1000)
print(machine.result().get_counts())
# Output: {'11': 1000}
# Create a controlled-Hadamard
h_gate = core.H(1)
controlled_h = h_gate.control(0)
# Create a controlled-SWAP (Fredkin gate)
swap_gate = core.SWAP(1, 2)
fredkin = swap_gate.control(0) # SWAP qubits 1,2 only if qubit 0 is |1>
# Create a doubly-controlled gate (like TOFFOLI from X)
x_gate = core.X(2)
toffoli = x_gate.control(0).control(1) # X on qubit 2 if qubits 0 AND 1 are |1>
# Verify the doubly-controlled gate
prog2 = core.QProg()
prog2 << core.X(0) # Set first control to |1>
prog2 << core.X(1) # Set second control to |1>
prog2 << toffoli # X on qubit 2 activated
prog2 << core.measure([0, 1, 2], [0, 1, 2])
machine2 = core.CPUQVM()
machine2.run(prog2, shots=1000)
print(machine2.result().get_counts())
# Output: {'111': 1000}3.3 Power (.power())
The .power(k) method raises a gate to the k-th power:
This is especially useful for fractional powers of gates:
from pyqpanda3 import core
import math
import numpy as np
# X^2 = I (applying NOT twice returns to original)
x_gate = core.X(0)
x_squared = x_gate.power(2)
# Verify: X^2 should equal the identity matrix I
identity = np.eye(2, dtype=complex)
print("X^2 == I:", np.allclose(np.array(x_squared.matrix()), identity))
# Output: X^2 == I: True
# Z^{1/2} = S gate
z_gate = core.Z(0)
z_half = z_gate.power(0.5)
# Verify: Z^{1/2} should match S gate matrix
z_half_mat = np.array(z_half.matrix())
s_mat = np.array(core.S(0).matrix())
print("Z^{1/2} == S:", np.allclose(z_half_mat, s_mat))
# Output: Z^{1/2} == S: True
# Z^{1/4} = T gate
z_quarter = z_gate.power(0.25)
z_quarter_mat = np.array(z_quarter.matrix())
t_mat = np.array(core.T(0).matrix())
print("Z^{1/4} == T:", np.allclose(z_quarter_mat, t_mat))
# Output: Z^{1/4} == T: True
# Negative power: U^{-1} = U.dagger()
s_inv = core.S(0).power(-1)
s_dag = core.S(0).dagger()
print("S^{-1} == S.dagger():", np.allclose(
np.array(s_inv.matrix()),
np.array(s_dag.matrix())
))
# Output: S^{-1} == S.dagger(): True3.4 Matrix Representation (.matrix())
The .matrix() method returns the unitary matrix of a gate as a 2D array. This is useful for debugging, verification, and educational purposes.
from pyqpanda3 import core
import numpy as np
# Get the matrix of any gate
h_matrix = core.H(0).matrix()
print("H matrix:")
print(np.array(h_matrix))
# [[ 0.707 0.707]
# [ 0.707 -0.707]]
x_matrix = core.X(0).matrix()
print("\nX matrix:")
print(np.array(x_matrix))
# [[0 1]
# [1 0]]
# Two-qubit gate matrices are 4x4
cnot_matrix = core.CNOT(0, 1).matrix()
print("\nCNOT matrix:")
print(np.array(cnot_matrix))
# [[1 0 0 0]
# [0 1 0 0]
# [0 0 0 1]
# [0 0 1 0]]
# Verify unitarity: U^\dagger U = I
h_arr = np.array(h_matrix)
should_be_identity = h_arr.conj().T @ h_arr
print("\nH^\dagger H (should be I):")
print(np.round(should_be_identity, 10))
# The matrix of a controlled gate
controlled_h = core.H(0).control(2)
ch_matrix = np.array(controlled_h.matrix())
print("\nControlled-H matrix (4x4):")
print(np.round(ch_matrix, 3))
# [[ 0.707 0. 0.707 0. ]
# [ 0. 0.707 0. 0.707]
# [ 0.707 0. -0.707 0. ]
# [ 0. 0.707 0. -0.707]]4. Measurement
4.1 Projective Measurement
Measurement is the process of observing a quantum state, which collapses it from a superposition into a definite classical outcome. In the computational basis, measuring a qubit in state
- Outcome
0with probability - Outcome
1with probability
After measurement, the qubit is in the measured basis state. This collapse is irreversible -- the superposition information is lost.
For an
measuring all qubits yields the bitstring
4.2 Measuring in pyqpanda3
pyqpanda3 provides two forms of measurement:
Single qubit measurement:
core.measure(qubit_index, classical_bit_index)Multi-qubit measurement:
core.measure([qubit_0, qubit_1, ...], [cbit_0, cbit_1, ...])Each qubit measurement result is stored in a corresponding classical bit (cbit).
from pyqpanda3 import core
# Single qubit measurement
prog1 = core.QProg()
prog1 << core.H(0) # Create superposition
prog1 << core.measure(0, 0) # Measure qubit 0 into cbit 0
machine = core.CPUQVM()
machine.run(prog1, shots=1000)
print("Single qubit:", machine.result().get_counts())
# Output: {'0': 498, '1': 502}
# Multi-qubit measurement
prog2 = core.QProg()
prog2 << core.H(0) # Superposition on qubit 0
prog2 << core.X(1) # Qubit 1 in |1>
prog2 << core.measure([0, 1], [0, 1]) # Measure both
machine.run(prog2, shots=1000)
print("Two qubits:", machine.result().get_counts())
# Output: {'01': 502, '11': 498}
# Qubit 0 is random, qubit 1 is always 14.3 Shot-Based Measurement
Quantum measurement is inherently probabilistic. To build up statistics, we run the same circuit many times (called "shots"). The law of large numbers ensures that the observed frequencies converge to the true probabilities as the number of shots increases.
from pyqpanda3 import core
import math
# Demonstrate shot-based measurement convergence
prog = core.QProg()
prog << core.H(0) # Creates |+> = (|0> + |1>)/sqrt(2)
prog << core.measure(0, 0)
machine = core.CPUQVM()
# Few shots: high variance
machine.run(prog, shots=10)
counts_10 = machine.result().get_counts()
print(f"10 shots: {counts_10}")
# Moderate shots
machine.run(prog, shots=100)
counts_100 = machine.result().get_counts()
print(f"100 shots: {counts_100}")
# Many shots: closer to 50/50
machine.run(prog, shots=10000)
counts_10000 = machine.result().get_counts()
print(f"10000 shots: {counts_10000}")
# The probabilities approach 0.5 for each outcome
total = sum(counts_10000.values())
for key, val in sorted(counts_10000.items()):
print(f" P({key}) = {val/total:.4f}")The following diagram illustrates the measurement process:
4.4 Complete Example: Bell State
Let us put everything together to create a Bell state -- one of the simplest entangled states:
The circuit is: H on qubit 0, then CNOT with control=0 and target=1.
from pyqpanda3 import core
# Step 1: Build the Bell state circuit
prog = core.QProg()
prog << core.H(0) # Hadamard creates superposition on qubit 0
prog << core.CNOT(0, 1) # Entangle qubits 0 and 1
prog << core.measure([0, 1], [0, 1]) # Measure both qubits
# Step 2: Run on the CPU simulator
machine = core.CPUQVM()
machine.run(prog, shots=10000)
# Step 3: Get the measurement results
counts = machine.result().get_counts()
print("Bell state results:")
print(counts)
# Expected output: {'00': ~5000, '11': ~5000}
# Note: '01' and '10' should not appear (or very rarely due to numerical precision)
# Step 4: Analyze the results
total = sum(counts.values())
for bitstring in ['00', '01', '10', '11']:
count = counts.get(bitstring, 0)
probability = count / total
print(f" |{bitstring}>: {count} counts ({probability:.4f})")Why only 00 and 11? The H gate puts qubit 0 in
The two qubits are now entangled: measuring one immediately determines the other, regardless of the physical distance between them. This is the resource that powers quantum teleportation, superdense coding, and quantum error correction.
5. Summary
This tutorial covered the three foundational concepts of quantum computing:
| Concept | Key Idea | pyqpanda3 API |
|---|---|---|
| Qubit | Superposition of | Default state when allocated |
| Quantum Gate | Unitary transformation of qubit state | core.H(0), core.X(0), core.CNOT(0,1), etc. |
| Measurement | Collapses superposition to classical bit | core.measure(qubit, cbit) |
Gate operations extend the gate library:
| Operation | Method | Description |
|---|---|---|
| Adjoint | .dagger() | Returns the inverse gate |
| Control | .control(q) | Adds a control qubit |
| Power | .power(k) | Computes |
| Matrix | .matrix() | Returns the unitary matrix |
Next steps:
- Circuit Construction -- learn how to compose gates into complex circuits using
QProg,QCircuit, and the<<operator - Simulation -- explore the different simulators (CPUQVM, DensityMatrixSimulator, Stabilizer)
- Visualization -- learn to draw circuits and plot quantum states on the Bloch sphere
Quick Reference
from pyqpanda3 import core
import math
# ===================
# Single-Qubit Gates
# ===================
core.H(0) # Hadamard
core.X(0) # Pauli-X (NOT)
core.Y(0) # Pauli-Y
core.Z(0) # Pauli-Z
core.S(0) # S gate (sqrt of Z)
core.T(0) # T gate (4th root of Z)
core.I(0) # Identity
# ===================
# Rotation Gates
# ===================
core.RX(0, math.pi / 4) # Rotate around X
core.RY(0, math.pi / 2) # Rotate around Y
core.RZ(0, math.pi) # Rotate around Z
core.P(0, math.pi / 4) # Phase gate
# ===================
# Two-Qubit Gates
# ===================
core.CNOT(0, 1) # Controlled-NOT
core.CZ(0, 1) # Controlled-Z
core.SWAP(0, 1) # SWAP
# ===================
# Multi-Qubit Gates
# ===================
core.TOFFOLI(0, 1, 2) # Doubly-controlled NOT
# ===================
# Gate Operations
# ===================
gate = core.H(0)
gate.dagger() # Adjoint (inverse)
gate.control(2) # Controlled version (control on qubit 2)
gate.power(0.5) # Fractional power
gate.matrix() # Unitary matrix
# ===================
# Measurement
# ===================
core.measure(0, 0) # Single qubit
core.measure([0, 1], [0, 1]) # Multiple qubits
# ===================
# Run Circuit
# ===================
prog = core.QProg()
prog << core.H(0) << core.CNOT(0, 1)
prog << core.measure([0, 1], [0, 1])
machine = core.CPUQVM()
machine.run(prog, shots=1000)
counts = machine.result().get_counts()
print(counts)Knowledge Check
Test your understanding of quantum computing basics with pyqpanda3.
Q1: What is the normalization constraint on a qubit state
A1: The amplitudes must satisfy
Q2: What does the Hadamard gate do to the basis states
A2:
Q3: State the Born rule. If a qubit is in state
A3: The Born rule states
Q4: What is the difference between a product state and an entangled state?
A4: A product state can be factored as
Q5: What is the relationship between the S gate, T gate, and Z gate?
A5:
Q6: Write the matrix for
A6:
Q7: Explain quantum interference in the context of applying two Hadamard gates in sequence.
A7:
Exercise 1: State Preparation
Write a pyqpanda3 program that prepares the state
Solution:
import numpy as np
from pyqpanda3.core import CPUQVM, QProg, RY, measure
qvm = CPUQVM()
# The state cos(θ/2)|0⟩ + sin(θ/2)|1⟩ is prepared by RY(θ)
# We want cos(π/6)|0⟩ + sin(π/6)|1⟩
# So θ/2 = π/6 → θ = π/3
theta = np.pi / 3
prog = QProg()
prog << RY(0, theta)
prog << measure([0], [0])
qvm.run(prog, shots=10000)
result = qvm.result()
counts = result.get_counts()
p0_measured = counts.get("0", 0) / 10000
p1_measured = counts.get("1", 0) / 10000
p0_expected = np.cos(np.pi/6)**2 # 0.75
p1_expected = np.sin(np.pi/6)**2 # 0.25
print(f"P(|0⟩) measured: {p0_measured:.4f}, expected: {p0_expected:.4f}")
print(f"P(|1⟩) measured: {p1_measured:.4f}, expected: {p1_expected:.4f}")
assert abs(p0_measured - p0_expected) < 0.03
assert abs(p1_measured - p1_expected) < 0.03
print("Verification passed!")Exercise 2: Entanglement Verification
Prepare all four Bell states and verify their measurement statistics. The four Bell states are:
Solution:
from pyqpanda3.core import CPUQVM, QProg, H, X, Z, CNOT, measure
qvm = CPUQVM()
shots = 5000
def run_and_print(name, prog):
prog << measure([0, 1], [0, 1])
qvm.run(prog, shots=shots)
counts = qvm.result().get_counts()
print(f"{name}: {counts}")
return counts
# |Φ+⟩ = (|00⟩ + |11⟩) / √2
prog_pp = QProg()
prog_pp << H(0) << CNOT(0, 1)
c1 = run_and_print("|Φ+⟩", prog_pp)
assert c1.get("01", 0) < 100 and c1.get("10", 0) < 100
# |Φ-⟩ = (|00⟩ - |11⟩) / √2
prog_pm = QProg()
prog_pm << H(0) << Z(0) << CNOT(0, 1)
c2 = run_and_print("|Φ-⟩", prog_pm)
assert c2.get("01", 0) < 100 and c2.get("10", 0) < 100
# |Ψ+⟩ = (|01⟩ + |10⟩) / √2
prog_mp = QProg()
prog_mp << H(0) << X(1) << CNOT(0, 1)
c3 = run_and_print("|Ψ+⟩", prog_mp)
assert c3.get("00", 0) < 100 and c3.get("11", 0) < 100
# |Ψ-⟩ = (|01⟩ - |10⟩) / √2
prog_mm = QProg()
prog_mm << X(0) << H(0) << X(1) << CNOT(0, 1)
c4 = run_and_print("|Ψ-⟩", prog_mm)
assert c4.get("00", 0) < 100 and c4.get("11", 0) < 100
print("\nAll four Bell states verified!")Exercise 3: Custom Gate Matrix Verification
Create a custom unitary gate from a
Solution:
import numpy as np
from pyqpanda3.core import CPUQVM, QProg, Oracle, measure
# Define a custom unitary: RY(π/4) matrix
theta = np.pi / 4
matrix = np.array([
[np.cos(theta/2), -np.sin(theta/2)],
[np.sin(theta/2), np.cos(theta/2)]
], dtype=complex)
# Create oracle from matrix and verify
qvm = CPUQVM()
# Apply to |0⟩: expect cos(π/8)|0⟩ + sin(π/8)|1⟩
prog = QProg()
oracle = Oracle([0], matrix)
prog << oracle << measure([0], [0])
qvm.run(prog, shots=10000)
counts = qvm.result().get_counts()
p0 = counts.get("0", 0) / 10000
expected_p0 = np.cos(np.pi/8)**2
print(f"P(|0⟩) measured: {p0:.4f}, expected: {expected_p0:.4f}")
assert abs(p0 - expected_p0) < 0.03
print("Custom gate verified!")